這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
函式宣告
文法:
func name(parameter-list) (result-list) { body}
如果函數沒有傳回值, 或者僅僅有一個匿名的傳回值的話, 那麼 result-list 的圓括弧可以省略, 例如:
func sayHello() () { fmt.Println("Hello, world!")}
可以寫為:
func sayHello() { fmt.Println("Hello, world!")}
除了匿名的傳回值外(即僅僅給出傳回值的類型, 而沒有名字), Go 還支援命名的傳回值, 例如:
func max(x, y int) (z int) { if x > y { z = x } else { z = y } return}
上面的函數中, 傳回值是命名的, 它的名字是 "z", 並且這個傳回值在函數內可見. 因此我們可以在函數中對這個傳回值變數進程操作, 最後直接使用一個 return 語句返回即可, 此時函數返回的就是變數 z 的值了.
函數變數
在 Go 中, 函數是一等公民, 因此像其他類型一樣, 函數也有類型, 並且它們可以賦值給一個變數, 將函數變數作為一個參數傳遞給另一個函數甚至函數可以作為傳回值返回.
例如:
func add(x, y int) int { return x + y}func doSomething(x, y int, f func(int, int) int) { fmt.Println(f(x, y))}func main() { f := add fmt.Println(f(1, 2)) doSomething(10, 20, f)}
上面的例子中, 我們首先將函數 add 賦值給變數 f, 因此可以直接以 f 作為函數名調用.
接著我們將函數變數 f 作為參數傳遞給 doSomething 函數, 並且在 doSomething 中調用了它.
其中 doSomething 參數 "f func(int, int) int" 含義是接受一個函數變數, 其類型是 func(int, int) int, 即接受一個帶有兩個 int 參數, 並且傳回值是 int 類型的函數.
匿名函數
對於命名函數來說, 它們必須要在包範圍上定義
, 但是我們可以利用函數字面量在任意的運算式中定義一個函數.
一個函數字面量的文法和函式宣告類似, 但是它沒有函數名. 並且一個函數字面量是一個運算式, 這個運算式的值被稱為 匿名函數.
通過匿名函數, 我們就可以實現閉包:
func increment(x int) func() int { return func () int { x++ return x }}func main() { f := increment(10) fmt.Println(f()) fmt.Println(f()) fmt.Println(f()) fmt.Println(f())}
上面的 increment 函數接收一個 int 變數, 然後返回一個 func() int 類型的函數. 注意到, 在返回的匿名函數中, 使用到了 increment 的局部變數, 每次調用返回的這個函數, 那麼 x 的值就自增一.
由上面的例子, 我們看到, 一個函數不僅僅是由代碼構成的, 它還有內部狀態(在這裡, 函數 f 的狀態就是 x 的值, 它捕獲了外部變數 x, 每次調用 f 時, x 的值都改變, 因此 f 的狀態也就改變了). 正因為函數內部維護有隱藏的狀態, 因此我們才規定一個函數類型是一個參考型別, 並且函數類型是不可比較的.
如上面 increment 函數返回的匿名函數類似, 如果一個匿名函數捕獲了外部的一個局部變數, 那麼我們就稱這個匿名函數為閉包(closure)
變參函數
一個函數如果可以接收的函數個數不定, 則稱為變參函數.
一個變參函數最後一個參數的參數名和類型需要添加 ..., 表示此參數是變參, 例如:
func oper(name string, vals ...int) { total := 0 if (name == "add") { for _, val := range vals { total += val } } else { for _, val := range vals { total -= val } } fmt.Printf("Operation %s, result: %d\n", name, total) fmt.Printf("vals type: %T\n", vals)}func main() { oper("add", 1, 2, 3, 4, 5)}
oper 函數的第一個參數是一個字串, 剩下的參數是一個或多個 int 變數.
Go 實現變參函數的原理是: 首先分配一個數組, 然後將參數拷貝到數組中, 接著將整個數組的切片作為參數傳遞給函數.
因此上面 "oper("add", 1, 2, 3, 4, 4)" 調用可以等效為:
arr := []int{1, 2, 3, 4, 5}oper("add", arr[:]...)// 也可以直接傳遞數組oper("add", arr...)
不過上面的例子有一個需要特別注意的地方, 如果參數已經是 slice 的了, 那麼調用變參函數時, 參數後需要添加**...**
雖然在函數內部, 變參是一個 slice, 但是變參函數和接收 slice 的函數是不同類型的.
例如:
func f(...int) {}func g([]int) {}func main() { fmt.Printf("%T\n", f) // "func(...int)" fmt.Printf("%T\n", g) // "func([]int)"}
關於 panic 和 recover
panic 類似於 Java 中的異常, 當程式運行時出現了致命錯誤時, 會產生一個 panic.
當然我們也可以手動觸發一個 panic, 例如:
func test() { panic("some error")}
我們直接調用內建的 panic 函數就可以收到發出一個 panic 了, 類似於 Java 中的 new Exception() 一樣.
如果產生了一個 panic 的話, 通常來說程式會立即終止. 但是有時候我們並不希望發生 panic 時造成整個程式的崩潰, 此時可以使用 recover 函數來捕獲這個 panic (相當於 Java 的 try ... catch). 例如:
func test() { defer func() { if p := recover(); p != nil { fmt.Printf("We got error: %v\n", p) } }() panic("some error")}func main() { test() fmt.Println("OK")}
使用 recover 有兩點需要注意的是:
- recover 調用必須在 defer 函數中.
- 包裹 recover 的 defer 函數需求在潛在的 panic 代碼前調用.
例如:
func test() { panic("some error") defer func() { if p := recover(); p != nil { fmt.Printf("We got error: %v\n", p) } }() }
上面的代碼直接就導致了程式的崩潰, 而不會觸發 recover.