#Go函數
是時候討論一下Go的函數定義了。
**什么是函數**
函數,簡單來講就是一段將`輸入數據`轉換為`輸出數據`的`公用代碼塊`。當然有的時候函數的返回值為空,那么就是說輸出數據為空。而真正的處理過程在函數內部已經完成了。
想一想我們為什么需要函數,最直接的需求就是代碼中有太多的重復代碼了,為了代碼的可讀性和可維護性,將這些重復代碼重構為函數也是必要的。
**函數定義**
先看一個例子
package main
import (
"fmt"
)
func slice_sum(arr []int) int {
sum := 0
for _, elem := range arr {
sum += elem
}
return sum
}
func main() {
var arr1 = []int{1, 3, 2, 3, 2}
var arr2 = []int{3, 2, 3, 1, 6, 4, 8, 9}
fmt.Println(slice_sum(arr1))
fmt.Println(slice_sum(arr2))
}
在上面的例子中,我們需要分別計算兩個切片的元素和。如果我們把計算切片元素的和的代碼分別為兩個切片展開,那么代碼就失去了簡潔性和一致性。假設你預想實現同樣功能的代碼在拷貝粘貼的過程中發生了錯誤,比如忘記改變量名之類的,到時候debug到崩潰吧。因為這時很有可能你就先入為主了,因為模板代碼沒有錯啊,是不是。所以函數就是這個用處。
我們再仔細看一下上面的函數定義:
首先是關鍵字`func`,然后后面是`函數名稱`,`參數列表`,最后是`返回值列表`。當然如果函數沒有參數列表或者返回值,那么這兩項都是可選的。其中返回值兩邊的括號在只聲明一個返回值類型的時候可以省略。
**命名返回值**
Go的函數很有趣,你甚至可以為返回值預先定義一個名稱,在函數結束的時候,直接一個return就可以返回所有的預定義返回值。例如上面的例子,我們將sum作為命名返回值。
package main
import (
"fmt"
)
func slice_sum(arr []int) (sum int) {
sum = 0
for _, elem := range arr {
sum += elem
}
return
}
func main() {
var arr1 = []int{1, 3, 2, 3, 2}
var arr2 = []int{3, 2, 3, 1, 6, 4, 8, 9}
fmt.Println(slice_sum(arr1))
fmt.Println(slice_sum(arr2))
}
這里要注意的是,如果你定義了命名返回值,那么在函數內部你將不能再重復定義一個同樣名稱的變量。比如第一個例子中我們用`sum:=0`來定義和初始化變量sum,而在第二個例子中,我們只能用`sum=0`初始化這個變量了。因為`:=`表示的是定義并且初始化變量。
**實參數和虛參數**
可能你聽說過函數的實參數和虛參數。其實所謂的`實參數就是函數調用的時候傳入的參數`。在上面的例子中,實參就是`arr1`和`arr2`,而`虛參數就是函數定義的時候表示函數需要傳入哪些參數的占位參數`。在上面的例子中,虛參就是`arr`。`實參和虛參的名字不必是一樣的。即使是一樣的,也互不影響。`因為虛參是函數的內部變量。而實參則是另一個函數的內部變量或者是全局變量。它們的作用域不同。如果一個函數的虛參碰巧和一個全局變量名稱相同,那么函數使用的也是虛參。例如我們再修改一下上面的例子。
package main
import (
"fmt"
)
var arr = []int{1, 3, 2, 3, 2}
func slice_sum(arr []int) (sum int) {
sum = 0
for _, elem := range arr {
sum += elem
}
return
}
func main() {
var arr2 = []int{3, 2, 3, 1, 6, 4, 8, 9}
fmt.Println(slice_sum(arr))
fmt.Println(slice_sum(arr2))
}
在上面的例子中,我們定義了全局變量arr并且初始化值,而我們的slice_sum函數的虛參也是arr,但是程序同樣正常工作。
**函數多返回值**
記不記得你在java或者c里面需要返回多個值時還得去定義一個對象或者結構體的呢?在Go里面,你不需要這么做了。Go函數支持你返回多個值。
其實函數的多返回值,我們在上面遇見過很多次了。那就是`range`函數。這個函數用來迭代數組或者切片的時候返回的是兩個值,一個是數組或切片元素的索引,另外一個是數組或切片元素。在上面的例子中,因為我們不需要元素的索引,所以我們用一個特殊的忽略返回值符號`下劃線(_)`來忽略索引。
假設上面的例子我們除了返回切片的元素和,還想返回切片元素的平均值,那么我們修改一下代碼。
package main
import (
"fmt"
)
func slice_sum(arr []int) (int, float64) {
sum := 0
avg := 0.0
for _, elem := range arr {
sum += elem
}
avg = float64(sum) / float64(len(arr))
return sum, avg
}
func main() {
var arr1 = []int{3, 2, 3, 1, 6, 4, 8, 9}
fmt.Println(slice_sum(arr1))
}
很簡單吧,當然我們還可以將上面的參數定義為命名參數
package main
import (
"fmt"
)
func slice_sum(arr []int) (sum int, avg float64) {
sum = 0
avg = 0.0
for _, elem := range arr {
sum += elem
}
avg = float64(sum) / float64(len(arr))
//return sum, avg
return
}
func main() {
var arr1 = []int{3, 2, 3, 1, 6, 4, 8, 9}
fmt.Println(slice_sum(arr1))
}
在上面的代碼里面,將`return sum, avg`給注釋了而直接使用`return`。其實這兩種返回方式都可以。
**變長參數**
想一想我們的fmt包里面的Println函數,它怎么知道你傳入的參數個數呢?
package main
import (
"fmt"
)
func main() {
fmt.Println(1)
fmt.Println(1, 2)
fmt.Println(1, 2, 3)
}
這個要歸功于Go的一大特性,支持可變長參數列表。
首先我們來看一個例子
package main
import (
"fmt"
)
func sum(arr ...int) int {
sum := 0
for _, val := range arr {
sum += val
}
return sum
}
func main() {
fmt.Println(sum(1))
fmt.Println(sum(1, 2))
fmt.Println(sum(1, 2, 3))
}
在上面的例子中,我們將原來的切片參數修改為可變長參數,然后使用range函數迭代這些參數,并求和。
從這里我們可以看出至少一點那就是`可變長參數列表里面的參數類型都是相同的`(*如果你對這句話表示懷疑,可能是因為你看到Println函數恰恰可以輸出不同類型的可變參數,這個問題的答案要等到我們介紹完Go的接口后才行*)。
另外還有一點需要注意,那就是`可變長參數定義只能是函數的最后一個參數`。比如下面的例子:
package main
import (
"fmt"
)
func sum(base int, arr ...int) int {
sum := base
for _, val := range arr {
sum += val
}
return sum
}
func main() {
fmt.Println(sum(100, 1))
fmt.Println(sum(200, 1, 2))
fmt.Println(sum(300, 1, 2, 3))
}
這里不知道你是否覺得這個例子其實和那個切片的例子很像啊,在哪里呢?
package main
import (
"fmt"
)
func sum(base int, arr ...int) int {
sum := base
for _, val := range arr {
sum += val
}
return sum
}
func main() {
var arr1 = []int{1, 2, 3, 4, 5}
fmt.Println(sum(300, arr1...))
}
呵呵,就是把切片“啪,啪,啪”三個耳光打碎了,傳遞過去啊!:-P
**閉包函數**
曾經使用python和javascript的時候就在想,如果有一天可以把這兩種語言的特性做個并集該有多好。
這一天終于來了,Go支持閉包函數。
首先看一個閉包函數的例子。所謂閉包函數就是將整個函數的定義一氣呵成寫好并賦值給一個變量。然后用這個變量名作為函數名去調用函數體。
我們將剛剛的例子修改一下:
package main
import (
"fmt"
)
func main() {
var arr1 = []int{1, 2, 3, 4, 5}
var sum = func(arr ...int) int {
total_sum := 0
for _, val := range arr {
total_sum += val
}
return total_sum
}
fmt.Println(sum(arr1...))
}
從這里我們可以看出,其實閉包函數也沒有什么特別之處。因為Go不支持在一個函數的內部再定義一個嵌套函數,所以使用閉包函數能夠實現在一個函數內部定義另一個函數的目的。
這里我們需要注意的一個問題是,閉包函數對它外層的函數中的變量具有`訪問`和`修改`的權限。例如:
package main
import (
"fmt"
)
func main() {
var arr1 = []int{1, 2, 3, 4, 5}
var base = 300
var sum = func(arr ...int) int {
total_sum := 0
total_sum += base
for _, val := range arr {
total_sum += val
}
return total_sum
}
fmt.Println(sum(arr1...))
}
這個例子,輸出315,因為total_sum加上了base的值。
package main
import (
"fmt"
)
func main() {
var base = 0
inc := func() {
base += 1
}
fmt.Println(base)
inc()
fmt.Println(base)
}
在上面的例子中,閉包函數修改了main函數的局部變量base。
最后我們來看一個閉包的示例,生成偶數序列。
package main
import (
"fmt"
)
func createEvenGenerator() func() uint {
i := uint(0)
return func() (retVal uint) {
retVal = i
i += 2
return
}
}
func main() {
nextEven := createEvenGenerator()
fmt.Println(nextEven())
fmt.Println(nextEven())
fmt.Println(nextEven())
}
這個例子很有意思的,因為我們定義了一個`返回函數定義`的函數。而所返回的函數定義就是`在這個函數的內部定義的閉包函數`。這個閉包函數在外層函數調用的時候,每次都生成一個新的偶數(加2操作)然后返回閉包函數定義。
其中`func() uint`就是函數createEvenGenerator的返回值。在createEvenGenerator中,這個返回值是return返回的閉包函數定義。
func() (retVal uint) {
retVal = i
i += 2
return
}
因為createEvenGenerator函數返回的是一個函數定義,所以我們再把它賦值給一個代表函數的變量,然后用這個代表閉包函數的變量去調用函數執行。
**遞歸函數**
每次談到遞歸函數,必然繞不開階乘和斐波拉切數列。
階乘
package main
/**
n!=1*2*3*...*n
*/
import (
"fmt"
)
func factorial(x uint) uint {
if x == 0 {
return 1
}
return x * factorial(x-1)
}
func main() {
fmt.Println(factorial(5))
}
如果x為0,那么返回1,因為0!=1。如果x是1,那么f(1)=1*f(0),如果x是2,那么f(2)=2*f(1)=2*1*f(0),依次推斷f(x)=x*(x-1)*...*2*1*f(0)。
從上面看出所謂遞歸,就是在函數的內部重復調用一個函數的過程。需要注意的是這個函數必須能夠一層一層分解,并且有出口。上面的例子出口就是0。
斐波拉切數列
求第N個斐波拉切元素
package main
/**
f(1)=1
f(2)=2
f(n)=f(n-2)+f(n-1)
*/
import (
"fmt"
)
func fibonacci(n int) int {
var retVal = 0
if n == 1 {
retVal = 1
} else if n == 2 {
retVal = 2
} else {
retVal = fibonacci(n-2) + fibonacci(n-1)
}
return retVal
}
func main() {
fmt.Println(fibonacci(5))
}
斐波拉切第一個元素是1,第二個元素是2,后面的元素依次是前兩個元素的和。
其實對于遞歸函數來講,只要知道了函數的出口,后面的不過是讓計算機去不斷地推斷,一直推斷到這個出口。理解了這一點,遞歸就很好理解了。
**異常處理**
當你讀取文件失敗而退出的時候是否擔心文件句柄是否已經關閉?抑或是你對于try...catch...finally的結構中finally里面的代碼和try里面的return代碼那個先執行這樣的問題痛苦不已?
一切都結束了。一門完美的語言必須有一個清晰的無歧義的執行邏輯。
好,來看看Go提供的異常處理。
*defer*
package main
import (
"fmt"
)
func first() {
fmt.Println("first func run")
}
func second() {
fmt.Println("second func run")
}
func main() {
defer second()
first()
}
Go語言提供了關鍵字`defer`來在函數運行結束的時候運行一段代碼或調用一個清理函數。上面的例子中,雖然second()函數寫在first()函數前面,但是由于使用了defer標注,所以它是在main函數執行結束的時候才調用的。
所以輸出結果
first func run
second func run
`defer`用途最多的在于釋放各種資源。比如我們讀取一個文件,讀完之后需要釋放文件句柄。
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
fname := "D:\\Temp\\test.txt"
f, err := os.Open(fname)
defer f.Close()
if err != nil {
os.Exit(1)
}
bReader := bufio.NewReader(f)
for {
line, ok := bReader.ReadString('\n')
if ok != nil {
break
}
fmt.Println(strings.Trim(line, "\r\n"))
}
}
在上面的例子中,我們按行讀取文件,并且輸出。從代碼中,我們可以看到在使用os包中的Open方法打開文件后,立馬跟著一個defer語句用來關閉文件句柄。這樣就保證了該文件句柄在main函數運行結束的時候或者異常終止的時候一定能夠被釋放。而且由于緊跟著Open語句,一旦養成了習慣,就不會忘記去關閉文件句柄了。
*panic* & *recover*
>當你周末走在林蔭道上,聽著小歌,哼著小曲,很是愜意。突然之間,從天而降瓢潑大雨,你頓時慌張(panic)起來,沒有帶傘啊,淋著雨感冒就不好了。于是你四下張望,忽然發現自己離地鐵站很近,那里有很多賣傘的,心中頓時又安定了下來(recover),于是你飛奔過去買了一把傘(defer)。
好了,panic和recover是Go語言提供的用以處理異常的關鍵字。`panic用來觸發異常`,而`recover用來終止異常并且返回傳遞給panic的值`。(注意`recover并不能處理異常`,而且`recover只能在defer里面使用,否則無效`。)
先瞧個小例子
package main
import (
"fmt"
)
func main() {
fmt.Println("I am walking and singing...")
panic("It starts to rain cats and dogs")
msg := recover()
fmt.Println(msg)
}
看看輸出結果
runtime.panic(0x48d380, 0xc084003210)
C:/Users/ADMINI~1/AppData/Local/Temp/2/bindist667667715/go/src/pkg/runtime/panic.c:266 +0xc8
main.main()
D:/JemyGraw/Creation/Go/freebook_go/func_d1.go:9 +0xea
exit status 2
咦?怎么沒有輸出recover獲取的錯誤信息呢?
這是因為在運行到panic語句的時候,程序已經異常終止了,后面的代碼就不運行了。
那么如何才能阻止程序異常終止呢?這個時候要使用defer。因為`defer一定是在函數執行結束的時候運行的。不管是正常結束還是異常終止`。
修改一下代碼
package main
import (
"fmt"
)
func main() {
defer func() {
msg := recover()
fmt.Println(msg)
}()
fmt.Println("I am walking and singing...")
panic("It starts to rain cats and dogs")
}
好了,看下輸出
I am walking and singing...
It starts to rain cats and dogs
小結:
panic觸發的異常通常是運行時錯誤。比如試圖訪問的索引超出了數組邊界,忘記初始化字典或者任何無法輕易恢復到正常執行的錯誤。