Go语言圣经 - 第5章 函数 - 5.8 Defered函数

第5章 函数

函数可以让我们将一个语句序列打包成一个单元,然后可以从程序中其他地方多次调用,函数的机制可以让我们把一个大的工作分解成小任务。前面我们已经接触过函数,本章我们将讨论函数的更多特性

5.8 Defered函数

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

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

func title(url string)error {
   resp,err := http.Get(url)
   if err != nil {
      return err
   }
   ct := resp.Header.Get("Content-Type")
   if ct 1!="text/html" && !strings.HasPrefix(ct,"text/html;"){
      resp.Body.Close()
      return fmt.Errorf("parsing %s as HTML:%v",url,err)
   }
   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"&&firstChild != nil{
         fmt.Println(n.firstChild.Data)
      }
   }
   for eachNode(doc,visitNode,nil)
   return nil
}

如下是运行结果

$ ./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

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

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

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

或者是处理互斥锁

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语句后面的圆括号,否则在本该进入时执行的操作会在退出时执行,而本该在退出时执行的将永远不会执行

func bigSlowOperation(){
   defer trace("bigSlowOperation")()
   time.Sleep(10*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))
   }
}

每一次bigSlowOperation的调用,程序都会记录函数的进入和退出以及持续时间(我们用一个time.Sleep来模拟一个耗时操作)

$ ./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)// 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 nil
   }
}

一种解决方法是将循环体中的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()
}

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

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)
   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发生,更有可能接近问题的本质

上一篇:华为的Watch GT 2 ECG Pro 版有望在双12发售哦!:2688人民币


下一篇:1023 组个最小数 (20 分)python3实现