本文是Effective Go的第四篇翻译,对应的章节为:Functions
第一篇翻译在这里,对应的章节为:Introduction, Formatting, Commentary.
第二篇翻译在这里,对应的章节为:Names, Semicolons.
第三篇翻译在这里,对应的章节为:Control structures.
第五篇翻译在这里,对应的章节为:Data.
第六篇翻译在这里,对应的章节为:Initialization, Methods.
第七篇翻译在这里,对应的章节为:Interfaces and other types,The blank identifier.
第八篇翻译在这里,对应的章节为:Embedding.

函数

多返回值

Go的一个特性是函数和方法可以有多个返回值。这样可以改进C程序中一些笨拙的习惯用法:将-1和EOF的错误返回以及修改引用传参的变量(即指针)。
在C中,写入操作发生的错误会由一个负数标记,并隐藏在某个不确定的位置。在Go中,写入操作可以返回写入的字节数和一个错误,类似于:是的,你已经写入了一些字节,但没有完全写入,因为设备已满。os包中的Write方法的函数签名是这样的:
func (file *File) Write(b []byte) (n int, err error)
正如文档所写,它返回已写入的字节数,当n != len(b)的时候,返回一个非空的错误。这是一个通用的风格,可以参见error章节查看更多的示例。
类似的方法避免了需要传递指针给返回值以访问引用的参数。下面是一个简单的函数,可从字节切片中的某个位置获取一个数字,然后返回该数字和下一个位置:

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
}

可以用它扫描输入的切片b,来获取数字:

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

命名返回值

Go函数的返回值或称返回参数可以进行命名,并像入参一样作为常规变量使用。命名后,当函数开始执行,它们会被初始化为对应类型的零值;如果函数执行不带参数的return语句,则返回参数的现值会作为返回值返回。
命名不是强制性的,但它们可以让代码更短更清晰:就像文档一样,正如我们命名了nextInt的返回值参数,那我们就能很明白返回的int代表的是哪个。
func nextInt(b []byte, pos int) (value, nextPos int)
由于返回值已初始化,并且return语句并没有返回值,所以可以简化,清晰化代码。下面是一个io.ReadFull的使用示例:

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

延迟处理函数(Defer)

Go的defer语句会安排一个函数调用(延迟处理的函数),defer会在函数返回之前运行。这是一种特殊但是很高效的处理方式。例如无论函数最后返回什么,都必须释放资源的情况。典型的例子就是解锁互斥锁或关闭文件。

// Contents returns the file's contents as a string.
func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    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 == io.EOF {
                break
            }
            return "", err  // f will be closed if we return here.
        }
    }
    return string(result), nil // f will be closed if we return here.
}

Close这样的延迟处理函数有两个优点。一,可以确保不会忘记去关闭文件,如果在return之前编辑close,则很容易忘记;二,这意味着close在open附近,比放置在函数的末尾要清晰得多。
延迟处理函数(如果是方法,则包括接收方)的参数会在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

对于使用其他语言进行块级资源管理的程序员来说,延迟似乎很奇特,但是它最有趣和最强大的应用正是基于它不是基于块而是基于函数的特性。在panic和recover章节会看到其他不一样的示例。