5.7. 函数

5.7.1. 多值返回

Go语言中函数和方法方法的一个有意思的特性是它们可以同时返回多个值。它可以比C语言 更简洁的处理多个返回值的情况:例如在修改一个参数的同时获取错误返回值 (-1或EOF)。

在传统的C语言中,如果写数据失败的话,会在另外一个地方保存错误标志,而且错误标志很容易被 其他函数产生的错误覆盖。在Go语言中,则可以在返回成功写入的数据数目的同时,也可以返回有意义 的错误信息:“您已经写了一些数据,但不是全部,因为设备在阻塞填充中”。对于os 包中的*File.Write函数,说明如下:

  func (file *File) Write(b []byte) (n int, err Error)

在函数的文档中有函数返回值的描述:返回成功写入的数据长度,如果n != len(b),则同时返回一个非non-nil的错误信息。 这是Go语言中,处理错误的常见方式。在后面的“错误处理”一节,会有更多的描述。

多个返回值还可以用于模拟C语言中通过指针的方式遍历。下面的函数是从一个int数组中获取 一个数据,然后移动到下一个位置。

  func nextInt(b []byte, i int) (int, int) {
      for ; i < len(b) && !isDigit(b[i]); i++ {
      }
      x := 0
      for ; i < len(b) && isDigit(b[i]); i++ {
          x = x*10 + int(b[i])-'0'
      }
      return x, i
  }

你还可以用这个方法来打印一个数组:

      for i := 0; i < len(a); {
          x, i = nextInt(a, i)
          fmt.Println(x)
      }

5.7.2. 命名的结果参数

Go语言中,我们还可以给函数或方法的返回值命名,就像函数的输入参数那样。如果我们命名了返回值, 那么它们将在函数开始的时候被初始化为空。然后,在执行不带参数的return语句时, 命名的返回值变量将被用于返回。

返回值命名并不强制使用,但是有时我们给名返回值命令可以产生更清晰的代码,同时它也可以用于文档。 例如,我们把nextInt的返回值命名:

  func nextInt(b []byte, pos int) (value, nextPos int) {

命名后,返回值会自动初始化,而且不需要在return中显式写出返回参数。下面的io.ReadFull 函数是另一个类似的例子:

  func ReadFull(r Reader, buf []byte) (n int, err os.Error) {
      for len(buf) > 0 && err == nil {
          var nr int
          nr, err = r.Read(buf)
          n += nr
          buf = buf[nr:len(buf)]
      }
      return
  }

5.7.3. Defer

Go 的 defer 语句安排一个函数调用(被 defer 的函数)延迟发生在执行 defer 的函数刚要返回之前。当函数无论怎样返回,某资源必须释放时,可用这种与众不同、但有效的处理方式。传统的例子包括解锁互斥或关闭文件。

  // Contents returns the file's contents as a string.
  func Contents(filename string) (string, os.Error) {
      f, err := os.Open(filename, os.O_RDONLY, 0)
      if err != nil {
          return "", err
      }
      defer f.Close()  // f.Close will run when we're finished.

      var result []byte
      buf := make([]byte, 100)
      for {
          n, err := f.Read(buf[0:])
          result = append(result, buf[0:n]...) // append is discussed later.
          if err != nil {
              if err == os.EOF {
                  break
              }
              return "", err  // f will be closed if we return here.
          }
      }
      return string(result), nil // f will be closed if we return here.
  }

这样延迟一个函数有双重优势:一是你永远不会忘记关闭文件,此错误在你事后编辑函数添加一个返回路径时常常发生。二是关闭和打开靠在一起,比放在函数尾要清晰很多。

延迟函数的参量(包括接受者,如果函数是一个方法)的求值发生在defer 语句执行时,而不是延迟函数调用时。除了不必担心函数执行时变量值的改变外,也意味着同一延迟调用可以延迟多个函数的执行。如下傻例:

  for i := 0; i < 5; i++ {
      defer fmt.Printf("%d ", i)
  }

延迟函数执行顺序为 LIFO,所以上面代码在函数返回时打印 4 3 2 1 0。更可信的例子是跟踪程序中函数执行的一个简单方式。我们可以写些简单的跟踪例程:

  func trace(s string)   { fmt.Println("entering:", s) }
  func untrace(s string) { fmt.Println("leaving:", s) }

  // Use them like this:
  func a() {
      trace("a")
      defer untrace("a")
      // do something....
  }

利用被延迟函数的参量在 defer 执行时得值的特点,跟踪函数可以安排未跟踪函数的参量。

  func trace(s string) string {
      fmt.Println("entering:", s)
      return s
  }

  func un(s string) {
      fmt.Println("leaving:", s)
  }

  func a() {
      defer un(trace("a"))
      fmt.Println("in a")
  }

  func b() {
      defer un(trace("b"))
      fmt.Println("in b")
      a()
  }

  func main() {
      b()
  }

打印:

  entering: b
  in b
  entering: a
  in a
  leaving: a
  leaving: b

对于习惯其它语言的块层次资源管理的程序员,defer 可能比较怪,但它最有趣最强力的应用恰恰来自它不是基于块、而是基于函数。在panic 和 recover 一节我们会看到一个例子。