5. 函數
5.1 特性
- 不支持:重載、嵌套函數和默認參數
- 支持:無需聲明原型、不定長度變參、多返回值、命名返回值參數、匿名函數和閉包
5.2 多返回值
在Go中,一個函數可以返回多個值。
一個函數內部可以將另一個有多返回值的函數作爲返回值。
可以將一個返回多參數的函數作爲該函數的參數。
如果一個函數將所有的返回值都顯示的變量名,那麼該函數的return語句可以省略操作數。這稱之爲bare return。
func add(a, b, c int) (d, e int) {
d = a+b
e = b+e
return
}
上述代碼中,如果d,e如果沒有被修改,那麼就返回零值。
5.3 錯誤
很難保證一個函數能夠沒有錯誤的運行,所以一定要將可能預見的錯誤進行返回。
如果導致失敗的原因只有一個,額外的返回值可以是一個布爾值,通常被命名爲ok。
導致失敗的原因不止一種,尤其是對I/O操作而言,用戶需要了解更多的錯誤信息。因此,額外的返回值不再是簡單的布爾類型,而是error類型。error類型可能是nil或者non-nil。nil意味着函數運行成功,non-nil表示失敗。
處理錯誤的五種策略
運行中的錯誤有很多種,對於不同的錯誤應該採取不同的策略
- 最常用的方式是傳播錯誤。也就是從底層函數不斷的返回,並且在這個過程中給錯誤加上一些信息,確保錯誤最後傳回main函數的時候能夠分析出錯誤從哪裏傳導出來的。
- 偶然錯誤,進行重試。 如果錯誤的發生是偶然性的,或由不可預知的問題導致的。一個明智的選擇是重新嘗試失敗的操作。在重試時,我們需要限制重試的時間間隔或重試的次數,防止無限制的重試。
- 程序無法繼續運行,輸出錯誤信息並結束程序。 需要注意的是,這種策略只應在main中執行。對庫函數而言,應僅向上傳播錯誤,除非該錯誤意味着程序內部包含不一致性,即遇到了bug,才能在庫函數中結束程序。
- 有時,我們只需要輸出錯誤信息就足夠了,不需要中斷程序的運行。我們可以通過log包提供函數
- 直接忽略掉錯誤。用於即便有錯誤也不會影響程序的整體情況。
5.4 函數值
函數像其他值一樣,擁有類型,可以被賦值給其他變量,傳遞給函數,從函數返回。
函數類型的零值是nil。調用值爲nil的函數值會引起panic錯誤
函數值之間是不可比較的,也不能用函數值作爲map的key。
舉個簡單有趣的例子
func add(x, y int) int { return x+y}
func sub(x, y int) int { return x-y}
func op(x, y int, f func(int, int) int) int {
x++
y--
return f(x, y)
}
func main() {
x, y := 10, 7
fmt.Println(op(x, y, add))
fmt.Println(op(x, y, sub))
}
在上面函數op中f就是一個函數值。當然函數值也是可以像下面一樣操作的。
func square(n int) int { return n * n }
func negative(n int) int { return -n }
func product(m, n int) int { return m * n }
f := square
fmt.Println(f(3)) // "9"
f = negative
fmt.Println(f(3)) // "-3"
fmt.Printf("%T\n", f) // "func(int) int"
f = product // compile error: can't assign func(int, int) int to func(int) int
5.5 匿名函數
匿名函數從名字上的意義來看就是沒有名字的函數。它的一個重要的應用場景就是閉包:比如函數A中定義了匿名函數B,並將匿名函數B做參數返回,其中B可以對A中的變量進行操作。所以在函數A外部接收到B,便可以通過B來對A中變量進行操作。
// squares返回一個匿名函數。
// 該匿名函數每次被調用時都會返回下一個數的平方。
func squares() func() int {
var x int
return func() int {
x++
return x * x
}
}
func main() {
f := squares()
fmt.Println(f()) // "1"
fmt.Println(f()) // "4"
fmt.Println(f()) // "9"
fmt.Println(f()) // "16"
}
一個更難但更有趣的閉包:
func A(x int) (func(int) int, func(int) int) {
A := func(i int) int {
x += i
return x
}
B := func(i int) int {
x -= i
return x
}
return A, B
}
func main() {
add, sub := A(10)
fmt.Println(add(10)) // 10+10=20
fmt.Println(sub(2)) // 20-2=18
}
捕獲迭代變量
慮這個樣一個問題:你被要求首先創建一些目錄,再將目錄刪除。在下面的例子中我們用函數值來完成刪除操作。下面的示例代碼需要引入os包。爲了使代碼簡單,我們忽略了所有的異常處理。
var rmdirs []func()
for _, d := range tempDirs() {
dir := d // NOTE: necessary!
os.MkdirAll(dir, 0755) // creates parent directories too
rmdirs = append(rmdirs, func() {
os.RemoveAll(dir)
})
}
// ...do some work…
for _, rmdir := range rmdirs {
rmdir() // clean up
}
5.6 可變參數
參數數量可變的函數稱爲爲可變參數函數。
可變參數一定要在函數參數列表中的最後一個。
func dsum(d int, args ...int) int {
result := 0
for _, v := range args {
result += v
}
return d * result
}
func main() {
arr := []int{1, 2, 3, 4, 5}
fmt.Println(dsum(2, arr...)) // 30
// 對於數組可以利用切片生成切片
arr1 := [...]int{1, 2, 3, 4, 5}
fmt.Println(dsum(2, arr1[:]...)) // 30
}
5.7 defer
特性:
- 執行的方式類似於其他語言的析構函數,在函數體執行結束後,按照調用的順序的相反順序逐個執行
- 即使函數發生嚴重錯誤也會執行
- 支持匿名函數的調用
- 常用於資源清理,文件關閉,解鎖以及記錄時間等操作
- 通過與匿名函數結合可在return之後修改函數計算結果
- 如果函數體內某個變量作爲defer時匿名函數的參數,則在定義defer即已經獲得拷貝,否則則是引用了某個變量的地址
func main() {
for i:=0; i<3; i++ {
defer fmt.Println(i)
}
}
結果爲:2,1,0.符合上面所述特性的第一點
不過使用defer有一個問題,就是上面所提到的最後一點,可以看看下面的程序,嘗試下想想可能會發生的結果:
func main() {
for i:=0; i<3; i++ {
defer func() {
fmt.Println(i)
}()
}
for i:=0; i<3; i++ {
defer func(i int) {
fmt.Println(i)
}(i) // 傳參
}
}
結果:
2
1
0
3
3
3
首先,在結果中輸出的前三個變量是第二個for的結果,後面是三個是第一個for的結果。(不明白的回去看上面的例子,結合特性的第一點)
第一個for之所以會輸出3個3,是因爲這裏產生了閉包,使用的i是func外的變量i,所以地址並沒有改變。
而第二個for中定義的函數其實自己聲明瞭變量,所以將外部的變量i傳參給func的時候是進行了值拷貝,所以func內部的i和外部的i是兩個地址。
分析一下下面函數,看看是不是和自己預期的一樣:
func main() {
var fs = [4]func(){}
for i:=0; i<4; i++ {
defer fmt.Println("defer i = ", i)
defer func() {fmt.Println("defer_closure i = ", i)}()
fs[i] = func() {fmt.Println("closure i = ", i)}
}
for _ , f := range fs {
f()
}
}
結果:
closure i = 4
closure i = 4
closure i = 4
closure i = 4
closure i = 4
defer i = 3
closure i = 4
defer i = 2
closure i = 4
defer i = 1
closure i = 4
defer i = 0
5.8 panic和recover
go語言中使用panic來觸發錯誤。一旦觸發panic,之後所有的操作都會暫停。
func A() {
fmt.Println("A")
}
func B() {
fmt.Println("B")
panic("panic in B")
}
func C() {
fmt.Println("C")
}
func main() {
A()
B()
C()
}
結果如下:可以看到C函數並不會被執行
A
B
panic: panic in B
在觸發panic函數之前有defer函數了,那麼被defer的函數能夠在觸發panic之後正常運行完。
例如將上面的函數B改寫爲以下:
func B() {
fmt.Println("B")
defer func() {
fmt.Println("Func in B")
}()
panic("panic in B")
}
結果如下:
A
B
Func in B
panic: panic in B
可以看到defer的匿名函數會在panic之前執行,C函數並不會被執行
在編程中,我們能夠預想到一些錯誤,這些錯誤能通過處理後能夠保證程序繼續正確運行而不會中斷,那麼可以使用recover,使即便觸發panic程序依然能夠繼續執行。
繼續修改上面的函數B:
func B() {
fmt.Println("B")
defer func() {
if err := recover(); err!=nil {
fmt.Println("recover from panic:", err)
}
}()
panic("panic in B")
}
上述代碼中, err是panic的內容,整個程序運行結果如下:
A
B
recover from panic: panic in B
C
可以看到程序恢復正常運行,並且運行完整個流程了。
主要參考資料:《Go語言聖經》
撩我?
我的公衆號:Kyda