defer
go
复制代码
func main() {
for i := 0; i < 3; i++ {
defer func(){ println(i) } ()
}
}
因为是闭包,在 for 迭代语句中,每个 defer 语句延迟执行的函数引用的都是同一个 i 迭代变 量,在循环结束后这个变量的值为3,因此最终输出的都是3。修复的思路是在每轮迭代中为每个 defer 函数生成独有的变量defer 从下往上执行。
切片
在go语言,中任何可以通过函数参数修改调用参数的情形,都是 因为函数参数中显式或隐式传入了指针参数。函数参数传值的规范更准确说是只针对数据结构中固定的 部分传值,例如字符串或切片对应结构体中的指针和字符串长度结构体传值,但是并不包含指针间接指 向的内容。 因为切片中的底层数组部分是通过隐式指针传递(指针本身依然是传值的,但是指针指向的却是同一份 的数据),所以被调用函数是可以通过指针修改掉调用参数切片中的数据。除了数据之外,切片结构还 包含了切片长度和切片容量信息,这2个信息也是传值的。如果被调用函数中修改了 Len 或 Cap 信 息的话,就无法反映到调用参数的切片中,这时候我们一般会通过返回修改后的切片来更新之前的切 片。这也是为何内置的 append 必须要返回一个切片的原因。
异常处理
Go的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用 等。这些运行时错误会引起painc异常。 一般而言,当panic异常发生时,程序会中断运行,并立即执行在该goroutine中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信息。日志信息包括 panic value和函数调用的堆栈跟踪信息。panic value通常是某种错误信息。对于每个goroutine,日志 信息中都会有与之相对的,发生panic时的函数调用堆栈跟踪信息。通常,我们不需要再次运行程序去定 位问题,日志信息已经提供了足够的诊断依据。因此,在我们填写问题报告时,一般会将panic异常和日 志信息一并记录。 通常来说,不应该对panic异常做任何处理,但有时,也许我们可以从异常中恢复,至少我们可以在程序 崩溃前,做一些操作。如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常, recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常 返回。在未发生panic时调用recover,recover会返回nil。通常来说,恢复应该被恢复的panic异常,此外, 这些异常所占的比例应该尽可能的低。为了标识某个panic是否应该被恢复,我们可以将panic value设 置成特殊类型。在recover时对panic value进行检查,如果发现panic value是特殊类型,就将这个 panic作为errror处理,如果不是,则按照正常的panic进行处理。
go
复制代码
func soleTitle(doc *html.Node) (title string, err error) {
type bailout struct{}
defer func() {
switch p := recover(); p {
case nil:
// no panic
case bailout{}:
// "expected" panic
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{}) // multiple titleelements
}
title = n.FirstChild.Data
}
}, nil)
if title == "" {
return "", fmt.Errorf("no title element")
}
return title, nil
}
在上例中,deferred函数调用recover,并检查panic value。当panic value是bailout{}类型时, deferred函数生成一个error返回给调用者。当panic value是其他non-nil值时,表示发生了未知的 panic异常,deferred函数将调用panic函数并将当前的panic value作为参数传入;此时,等同于 recover没有做任何操作。