函数
多个返回值
Go的一个不同寻常的特征就是它的函数和方法可以有多个返回值。这种机制可以用来改进大量C语言中笨拙的语言习惯:返回一个错误,例如 -1
对应于 EOF
,同时修改一个通过地址传递的参数。
在C中,一个写错误是由一个负数和隐蔽在易变位置的错误代码所表示。在 Go 中,Write
方法会返回一个计数和一个错误:“是的,你写了一些字节但不是所有的,因为你的设备已经满了。”在程序包 os
中,Write
方法的签名是:
func (file *File) Write(b []byte) (n int, err 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
}
你可以用它来扫描输入切片中的数字,像这样:
for i := 0; i < len(b); {
x, i = nextInt(b, i)
fmt.Println(x)
}
命名的结果参数
Go函数中的返回或结果参数可以被命名并当做普通变量使用,就像输入参数一样。在命名后,它们在函数起始处被初始化成它们类型的0值。如果函数执行了没有参数的返回语句,结果参数的当前值将会作为返回值。
命名并不是强制的,但是却可以让代码更加简短和整洁:它们也是文档。如果我们命名了结果参数 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.
}
推迟像 Defer
这样的函数调用有两个优点。第一点,它保证了你将不会忘记关闭文件,如果你之后编辑函数并加上新的返回路径将会很容易犯这个错误。第二点,这意味着关闭操作紧挨着打开,这将比放在函数末尾更加清晰。
传递给被延迟的函数的参数(包括接收者,如果这个函数是一个方法)将会在 defer
执行的时候被确定,而不是函数调用的时候被执行。除了避免随着函数执行,变量的值会改变之外,这也意味着一个延迟调用点将会延迟多个函数执行。这里有一个简单的例子。
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
延迟函数安装后进先出的顺序执行,所以当该函数返回时,这段代码会让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
程序可以给 un
程序设置参数。这个例子:
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
章节中,我们将会看到关于它的可能性的别的例子。