💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
[TOC] ## 多个返回值 Go的其中一个不同寻常的特点是,函数和方法可以返回多个值。这种形式可以用来改进C程序中几个笨拙的语言风格:返回一个错误,例如`-1`对应于`EOF`,同时修改一个由地址传递的参数。 在C中,一个写错误是由一个负的计数和一个隐藏在易变位置(a volatile location)的错误代码来表示的。在Go中,`Write`可以返回一个计数*和*一个错误:“是的,你写了一些字节,但并没有全部写完,由于设备已经被填满了”。在程序包`os`的文件中,`Write`方法的签名是: ~~~ func (file *File) Write(b []byte) (n int, err error) ~~~ 正如文档所言,其返回写入的字节数和一个非零的`error`,当`n``!=` `len(b)`的时候。这是一种常见的风格;更多的例子可以参见错误处理章节。 类似的方法使得不再需要传递一个返回值指针来模拟一个引用参数。这里有一个非常简单的函数,用来从字节切片中的一个位置抓取一个数,返回该数和下一个位置。 ~~~ 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 } ~~~ ## 延期执行 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`这样的函数调用进行延期,有两个好处。首先,其确保了你不会忘记关闭文件,如果你之后修改了函数增加一个新的返回路径,会很容易犯这样的错。其次,这意味着关闭操作紧挨着打开操作,这比将其放在函数结尾更加清晰。 被延期执行的函数,它的参数(包括接收者,如果函数是一个方法)是在*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`执行的时候被求值这个事实,我们可以做的更好些。trace程序可以为untrace程序建立参数。这个例子: ~~~ 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`章节中看到它另一个可能的例子。