gopl-zh.github.com/ch5/ch5-08.md
Asakijz f9ccbec80e
Update ch5-08.md
同上次一样,结尾加粗样式丢失
2022-09-05 11:25:18 +08:00

8.4 KiB
Raw Blame History

5.8. Deferred函数

在findLinks的例子中我们用http.Get的输出作为html.Parse的输入。只有url的内容的确是HTML格式的html.Parse才可以正常工作但实际上url指向的内容很丰富可能是图片纯文本或是其他。将这些格式的内容传递给html.parse会产生不良后果。

下面的例子获取HTML页面并输出页面的标题。title函数会检查服务器返回的Content-Type字段如果发现页面不是HTML将终止函数运行返回错误。

gopl.io/ch5/title1

func title(url string) error {
	resp, err := http.Get(url)
	if err != nil {
		return err
	}
	// Check Content-Type is HTML (e.g., "text/html;charset=utf-8").
	ct := resp.Header.Get("Content-Type")
	if ct != "text/html" && !strings.HasPrefix(ct,"text/html;") {
		resp.Body.Close()
		return fmt.Errorf("%s has type %s, not text/html",url, ct)
	}
	doc, err := html.Parse(resp.Body)
	resp.Body.Close()
	if err != nil {
		return fmt.Errorf("parsing %s as HTML: %v", url,err)
	}
	visitNode := func(n *html.Node) {
		if n.Type == html.ElementNode && n.Data == "title"&&n.FirstChild != nil {
			fmt.Println(n.FirstChild.Data)
		}
	}
	forEachNode(doc, visitNode, nil)
	return nil
}

下面展示了运行效果:

$ go build gopl.io/ch5/title1
$ ./title1 http://gopl.io
The Go Programming Language
$ ./title1 https://golang.org/doc/effective_go.html
Effective Go - The Go Programming Language
$ ./title1 https://golang.org/doc/gopher/frontpage.png
title1: https://golang.org/doc/gopher/frontpage.png has type image/png, not text/html

resp.Body.close调用了多次这是为了确保title在所有执行路径下即使函数运行失败都关闭了网络连接。随着函数变得复杂需要处理的错误也变多维护清理逻辑变得越来越困难。而Go语言独有的defer机制可以让事情变得简单。

你只需要在调用普通函数或方法前加上关键字defer就完成了defer所需要的语法。当执行到该条语句时函数和参数表达式得到计算但直到包含该defer语句的函数执行完毕时defer后的函数才会被执行不论包含defer语句的函数是通过return正常结束还是由于panic导致的异常结束。你可以在一个函数中执行多条defer语句它们的执行顺序与声明顺序相反。

defer语句经常被用于处理成对的操作如打开、关闭、连接、断开连接、加锁、释放锁。通过defer机制不论函数逻辑多复杂都能保证在任何执行路径下资源被释放。释放资源的defer应该直接跟在请求资源的语句后。在下面的代码中一条defer语句替代了之前的所有resp.Body.Close

gopl.io/ch5/title2

func title(url string) error {
	resp, err := http.Get(url)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	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("parsing %s as HTML: %v", url,err)
	}
	// ...print doc's title element…
	return nil
}

在处理其他资源时也可以采用defer机制比如对文件的操作

io/ioutil

package ioutil
func ReadFile(filename string) ([]byte, error) {
	f, err := os.Open(filename)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	return ReadAll(f)
}

或是处理互斥锁9.2章)

var mu sync.Mutex
var m = make(map[string]int)
func lookup(key string) int {
	mu.Lock()
	defer mu.Unlock()
	return m[key]
}

调试复杂程序时defer机制也常被用于记录何时进入和退出函数。下例中的bigSlowOperation函数直接调用trace记录函数的被调情况。bigSlowOperation被调时trace会返回一个函数值该函数值会在bigSlowOperation退出时被调用。通过这种方式 我们可以只通过一条语句控制函数的入口和所有的出口甚至可以记录函数的运行时间如例子中的start。需要注意一点不要忘记defer语句后的圆括号否则本该在进入时执行的操作会在退出时执行而本该在退出时执行的永远不会被执行。

gopl.io/ch5/trace

func bigSlowOperation() {
	defer trace("bigSlowOperation")() // don't forget the extra parentheses
	// ...lots of work…
	time.Sleep(10 * time.Second) // simulate slow operation by sleeping
}
func trace(msg string) func() {
	start := time.Now()
	log.Printf("enter %s", msg)
	return func() { 
		log.Printf("exit %s (%s)", msg,time.Since(start)) 
	}
}

每一次bigSlowOperation被调用程序都会记录函数的进入退出持续时间。我们用time.Sleep模拟一个耗时的操作

$ go build gopl.io/ch5/trace
$ ./trace
2015/11/18 09:53:26 enter bigSlowOperation
2015/11/18 09:53:36 exit bigSlowOperation (10.000589217s)

我们知道defer语句中的函数会在return语句更新返回值变量后再执行又因为在函数中定义的匿名函数可以访问该函数包括返回值变量在内的所有变量所以对匿名函数采用defer机制可以使其观察函数的返回值。

以double函数为例

func double(x int) int {
	return x + x
}

我们只需要首先命名double的返回值再增加defer语句我们就可以在double每次被调用时输出参数以及返回值。

func double(x int) (result int) {
	defer func() { fmt.Printf("double(%d) = %d\n", x,result) }()
	return x + x
}
_ = double(4)
// Output:
// "double(4) = 8"

可能double函数过于简单看不出这个小技巧的作用但对于有许多return语句的函数而言这个技巧很有用。

被延迟执行的匿名函数甚至可以修改函数返回给调用者的返回值:

func triple(x int) (result int) {
	defer func() { result += x }()
	return double(x)
}
fmt.Println(triple(4)) // "12"

在循环体中的defer语句需要特别注意因为只有在函数执行完毕后这些被延迟的函数才会执行。下面的代码会导致系统的文件描述符耗尽因为在所有文件都被处理之前没有文件会被关闭。

for _, filename := range filenames {
	f, err := os.Open(filename)
	if err != nil {
		return err
	}
	defer f.Close() // NOTE: risky; could run out of file descriptors
	// ...process f…
}

一种解决方法是将循环体中的defer语句移至另外一个函数。在每次循环时调用这个函数。

for _, filename := range filenames {
	if err := doFile(filename); err != nil {
		return err
	}
}
func doFile(filename string) error {
	f, err := os.Open(filename)
	if err != nil {
		return err
	}
	defer f.Close()
	// ...process f…
}

下面的代码是fetch1.5节的改进版我们将http响应信息写入本地文件而不是从标准输出流输出。我们通过path.Base提出url路径的最后一段作为文件名。

gopl.io/ch5/fetch

// Fetch downloads the URL and returns the
// name and length of the local file.
func fetch(url string) (filename string, n int64, err error) {
	resp, err := http.Get(url)
	if err != nil {
		return "", 0, err
	}
	defer resp.Body.Close()
	local := path.Base(resp.Request.URL.Path)
	if local == "/" {
		local = "index.html"
	}
	f, err := os.Create(local)
	if err != nil {
		return "", 0, err
	}
	n, err = io.Copy(f, resp.Body)
	// Close file, but prefer error from Copy, if any.
	if closeErr := f.Close(); err == nil {
		err = closeErr
	}
	return local, n, err
}

对resp.Body.Close延迟调用我们已经见过了在此不做解释。上例中通过os.Create打开文件进行写入在关闭文件时我们没有对f.close采用defer机制因为这会产生一些微妙的错误。许多文件系统尤其是NFS写入文件时发生的错误会被延迟到文件关闭时反馈。如果没有检查文件关闭时的反馈信息可能会导致数据丢失而我们还误以为写入操作成功。如果io.Copy和f.close都失败了我们倾向于将io.Copy的错误信息反馈给调用者因为它先于f.close发生更有可能接近问题的本质。

练习5.18 不修改fetch的行为重写fetch函数要求使用defer机制关闭文件。