[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`章節中看到它另一個可能的例子。