温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

gopl 函数

发布时间:2020-07-27 20:53:45 来源:网络 阅读:564 作者:骑士救兵 栏目:编程语言

裸返回

一个函数如果有命名的返回值,可以省略 return 语句的操作数,这称为裸返回
在一个函数中如果存在许多返回语句且有多个返回结果,裸返回可以消除重复代码,但是并不能使代码更加易于理解。比如,对于这种方式,在第一眼看来,不能直观地看出返回的值具体是什么。如果之前一直没有使用过返回值的变量名,返回变量的零值,如果赋过值了,则返回新的值,这就有可能会看漏。鉴于这个原因,应该保守使用裸返回。

图的遍历

在下面的例子中,变量 prereqs 的 map 提供了很多课程(key),以及学习该课程的前置条件(value):

var prereqs = map[string][]string{ "algorithems": {"data structures"}, "calculus": {"linear algebra"}, "compilers": { "data structures", "formal languages", "computer organization", }, "data structures": {"discrete math"}, "databases": {"data structures"}, "discrete math": {"intro to programming"}, "formal languages": {"discrete math"}, "networks": {"operating systems"}, "operating systems": {"data structures", "computer organization"}, "programming languages": {"data structures", "computer organization"}, }


这样的问题是一种拓扑排序。概念上,先决条件的内容构成了一张有向图,每一个节点代表一门课程。每一条边代表一门课程所依赖的另一门课程的关系。
图是无环的:没有节点可以通过图上的路径回到它自己。

可以使用深度优先的搜索计算得到合法的学习路径,代码入下所示:

func main() { for i, course := range topoSort(prereqs) { fmt.Printf("%d:\t%s\n", i+1, course) } } func topoSort(m map[string][]string) []string { // 闭包的部分 var order []string seen := make(map[string]bool) var visitAll func(items []string) visitAll = func(items []string) { for _, item := range items { if !seen[item] { seen[item] = true visitAll(m[item]) order = append(order, item) } } } // 主体 var keys []string for key := range m { keys = append(keys, key) } sort.Strings(keys) visitAll(keys) return order }

当一个匿名函数需要进行递归,必须先声明一个变量然后将匿名函数赋给这个变量。如果将两个步骤合并成一个声明,函数字面量将不会存在于该匿名函数的作用域中,这样就不能递归地调用自己了。
下面是拓扑排序的程序输出,它是确定的结果,就是每次执行都一样。这里输出时调用的是切片而不是 map,所以迭代的顺序是确定的并且在调用最初的 map 之前是对它的 key 进行了排序的。

PS H:\Go\src\gopl\ch6\toposort> go run main.go 1: intro to programming 2: discrete math 3: data structures 4: algorithems 5: linear algebra 6: calculus 7: formal languages 8: computer organization 9: compilers 10: databases 11: operating systems 12: networks 13: programming languages PS H:\Go\src\gopl\ch6\toposort>

警告:捕获迭代变量

首先,看下面的代码:

package main import "fmt" func main() { var shows []func() for _, v := range []int{1, 2, 3, 4, 5} { shows = append(shows, func() { fmt.Println(v) }) } for _, f := range shows { f() } }

这里的期望是依次打印每个数。但实际打印出来的全部都是5。
在for循环引进的一个块作用域内声明了变量v,然后到了循环里使用的这类变量共享相同的变量,即一个可访问的存储位置,而不是固定的值。v的值在不断地迭代中更新,因此当之后调用打印的时候,v变量已经被每一次的for循环更新多次。所以打印出来的是最后一次迭代时的值。
这里可以通过引入一个内部变量来解决这个问题,可以换个名字,也可以使用一样的变量名:

func main() { var shows []func() for _, v := range []int{1, 2, 3, 4, 5} { v := v // 这句是关键 shows = append(shows, func() { fmt.Println(v) }) } for _, f := range shows { f() } }

看起来奇怪,但却是一个关键性的声明。for循环内也可以随意定义一个不一样的变量名,这样看着更好理解一些。
也可以用匿名函数(闭包)来理解,这里确实是一个闭包,匿名函数内引用了外部变量。第一个示例中,变量v会在for循环的每次迭戈中更新。第二个示例,匿名函数引用的变量v是在for循环内部声明的,不会随着迭代而更新,并且在for循环内部也没有变化过。
这样的隐患不仅仅存在于使用range的for循环里。在 for i := 0; i < 10; i++ {} 这样的循环里作用域也是同样的,这里的变量i也是会有同样的问题,需要避免。
另外在go语句和derfer语句的使用当中,迭代变量捕获的问题是最频繁的,这是因为这两个逻辑都会推迟函数的执行时机,直到循环结束。但是这个问题并不是有go或者defer语句造成的。

goroutine 中同样的问题

下面的用法是错误的:

for _, f := range names { go func() { call(f) // 注意:不正确 } }

需要作为一个字面量函数的显式参数传递 f,而不是在 for 循环中声明 f。正确的做法如下:

for _, f := range names { go func(f string) { call(f) }(f) // 显式的传递 f 给函数 }

像上面这样,通过添加显式参数,可以确保当 go 语句执行的时候,使用 f 的当前值。

延迟函数调用(defer)

defer 语句也可以用来调试一个复杂的函数,即在函数的“入口”和“出口”处设置调试行为。下面的 bigSlowOperation 函数在开头调用 trace 函数,在函数刚进入的时候执行输出,然后返回一个函数变量,当其被调用的时候执行退出函数的操作。以这种方式推迟返回函数的调用,就可以使一个语句在函数入口和所有出口添加处理,甚至可以传递一些有用的值,比如每个操作的开始时间:

package main import ( "log" "time" ) func bigSlowOperation() { defer trace("bigSlowOperation")() // 这个小括号很重要 // ...这里假设有一些操作... time.Sleep(3 * time.Second) // 模拟慢操作 } func trace(msg string) func() { start := time.Now() log.Printf("enter %s", msg) return func() { log.Printf("exit %s (%s)", msg, time.Since(start)) } } func main() { bigSlowOperation() }

通常的defer语句提供一个函数,会在函数退出时再调用。
上面的defer语句,最后面有两个小括号。trace函数调用后会返回一个匿名函数,加上后面的小括号才是延迟调用执行的部分。而trace函数本身则会在当前位置就执行,并且返回匿名函数给defer语句。在trace函数获取返回值的过程中,也就是trace函数里,会先执行两行语句,获取start变量的值以及输出一行信息,这个是在函数开头就执行的。最后函数返回的匿名函数是提供给defer语句在退出的时候进行延迟调用的。

Panic异常

Go 语言的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等。这些运行时错误会引起painc异常。

主动调用 panic

可以直接调用内置的 panic 函数。如果碰到“不可能发生”的状况,panic 是最好的处理方式,比如语句执行到逻辑上不可能到达的地方时。

转储栈信息

runtime 包提供了转储栈的方法是程序员可以诊断错误,下面的代码在 main 函数中延迟 printStack 的执行:

package main import ( "fmt" "os" "runtime" ) func f(x int) { fmt.Printf("f(%d)\n", x+0/x) defer fmt.Printf("defer %d\n", x) f(x - 1) } func printStack() { var buf [4096]byte n := runtime.Stack(buf[:], false) os.Stdout.WriteString("Stack 中的内容:\n") os.Stdout.Write(buf[:n]) os.Stdout.WriteString("Stack 结束...\n") } func main() { defer printStack() f(3) }

Panic之后,在退出前会调用 defer 的内容,输出 buf 中的栈信息。最后还会输出宕机消息到标准输出流。
runtime.Stack 能够输出函数栈信息,在其他语言中,此时函数栈的信息应该已经不存在了。但是 Go 语言的宕机机制让延迟执行的函数在栈清理之前调用。

Recover捕获异常

退出程序通常是正常的处理panic异常的方式。但有时需要从异常中恢复,至少可以在程序崩溃前做一些操作。

recover函数

将内置的 recover 函数在延迟函数的内部调用,当定义了该 defer 语句的函数发生了 panic 异常,recover 就会终止当前的 panic 状态并且返回 panic value。函数不会从之前 panic 的地方继续运行而是正常返回。在未发生 panic 时调用 recover 则没有任何效果并且返回 nil。

举例说明

假设有一个语言解析器。即使看起来运行正常,但考虑到工作的复杂性,还是会存在只在特殊情况下发生的 bug。此时我们更希望返回一个错误 error 而不是导致程序崩溃 panic。所以 panic 发生后,不要立即终止运行,而是将一些有用的附加消息提供给用户来报告这个bug。下面是使用 recover 部分的代码:

func Parse(input string) (s *Syntax, err error) { defer func() { if p := recover(); p != nil { err = fmt.Errorf("internal error: %v", p) } }() // ...parser... }

恢复的原则

对于 panic 采用无差别的恢复措施是不可靠的。
从同一个包内发生的 panic 进行恢复有助于简化处理复杂和未知的错误,但一般的原则是,不应该尝试去恢复从另一个包内发生的 panic。公共的 API 应该直接报告错误。同样,也不应该恢复一个 panic,而这段代码却不是由你来维护的,比如调用这提供的回调函数,因为你不清楚这样做是否安全。
有时也很难完全遵循规范,举个例子,net\/http包中提供了一个web服务器,将收到的请求分发给用户提供的处理函数。很显然,我们不能因为某个处理函数引发的panic异常,影响整个进程导致退出。web服务器遇到处理函数导致的panic时会调用recover,输出堆栈信息,继续运行。这样的做法在实践中很便捷,但也会有一定的风险,比如导致资源泄漏或是因为recover操作,导致其他问题。
所以,最安全的做法就是选择性地使用 recover。当 panic 之后需要进行恢复的情况本来就不多。为了标识某个 panic 是否应该被恢复,我们可以将 panic value 设置成特殊类型。在 recover 时对 panic value 进行检查,如果发现 panic value 是特殊类型,就将这个 panic 作为 errror 处理。如果不是,则按照正常的 panic 进行处理。
下面示例代码中的 soleTitle 函数就是一个这样的例子:

package main import ( "fmt" "net/http" "os" "strings" "golang.org/x/net/html" ) func forEachNode(n *html.Node, pre, post func(n *html.Node)) { if pre != nil { pre(n) } for c := n.FirstChild; c != nil; c = c.NextSibling { forEachNode(c, pre, post) } if post != nil { post(n) } } // soleTitle 返回文档中一个非空标题元素 // 如果没有标题则返回错误 func soleTitle(doc *html.Node) (title string, err error) { type bailout struct{} defer func() { switch p := recover(); p { case nil: // 没有宕机 case bailout{}: // 预期的宕机 err = fmt.Errorf("multiple title elements") default: panic(p) // 未预期的宕机,继续宕机过程 } }() // 如果发现多余一个非空标题,退出递归 forEachNode(doc, func(n *html.Node) { if n.Type == html.ElementNode && n.Data == "title" && n.FirstChild != nil { if title != "" { panic(bailout{}) // 多个标题元素 } title = n.FirstChild.Data } }, nil) if title == "" { return "", fmt.Errorf("no title element") } return title, nil } func title(url string) error { resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() // 检查返回的页面是HTML通过判断Content-Type,比如:Content-Type: text/html; charset=utf-8 ct := resp.Header.Get("Content-Type") if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") { return fmt.Errorf("%s has type %s, not text/html", url, ct) } doc, err := html.Parse(resp.Body) if err != nil { return fmt.Errorf("parseing %s as HTML: %v", url, err) } title, err := soleTitle(doc) if err != nil { return err } fmt.Println(title) return nil } func main() { for _, arg := range os.Args[1:] { if err := title(arg); err != nil { fmt.Fprintf(os.Stderr, "title: %v\n", err) } } }

defer 调用 recover,检查 panic value,如果该值是 bailout{} 则返回一个普通的错误。所有其他非空的值都是预料外的 panic,这时继续使用 panic value 的值作为参数调用 panic。

这个示例里,违反了 panic 不处理"预期"错误的建议,但是这里是为了展示这种处理 panic 的机制:

if title != "" { panic(bailout{}) // 多个标题元素 }

对于一个预期的错误,比如这里标题为空的情况。正常编写程序的时候,不应该调用panic,而是进行处理,比如返回 error。

有些情况下是没有恢复动作的。比如,内存耗尽会使 Go 运行时发生严重错误而直接终止进程。

练习

使用 panic 和 recover 写一个函数,它没有 return 语句,但是能够返回一个非零的值。

package main import "fmt" func main() { s := noRet() fmt.Println(s) } func noRet() (s string) { defer func() { p := recover() s = fmt.Sprint(p) }() panic("Hello") }
向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI