《Go語言聖經》學習筆記:5.函數

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表示失敗。

處理錯誤的五種策略

運行中的錯誤有很多種,對於不同的錯誤應該採取不同的策略

  1. 最常用的方式是傳播錯誤。也就是從底層函數不斷的返回,並且在這個過程中給錯誤加上一些信息,確保錯誤最後傳回main函數的時候能夠分析出錯誤從哪裏傳導出來的。
  2. 偶然錯誤,進行重試。 如果錯誤的發生是偶然性的,或由不可預知的問題導致的。一個明智的選擇是重新嘗試失敗的操作。在重試時,我們需要限制重試的時間間隔或重試的次數,防止無限制的重試。
  3. 程序無法繼續運行,輸出錯誤信息並結束程序。 需要注意的是,這種策略只應在main中執行。對庫函數而言,應僅向上傳播錯誤,除非該錯誤意味着程序內部包含不一致性,即遇到了bug,才能在庫函數中結束程序。
  4. 有時,我們只需要輸出錯誤信息就足夠了,不需要中斷程序的運行。我們可以通過log包提供函數
  5. 直接忽略掉錯誤。用於即便有錯誤也不會影響程序的整體情況。

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
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章