Go 語言支持普通函數、匿名函數和閉包,從設計上對函數進行了優化和改進,讓函數使用起來更加方便。
Go 語言的函數屬于“一等公民”(first-class),也就是說:
- 函數本身可以作為值進行傳遞。
- 支持匿名函數和閉包(closure)。
- 函數可以滿足接口。
函數定義:
func function_name( [parameter list] ) [return_types] {函數體
}
- func:函數由 func 開始聲明
- function_name:函數名稱,函數名和參數列表一起構成了函數簽名。
- parameter list:參數列表,參數就像一個占位符,當函數被調用時,你可以將值傳遞給參數,這個值被稱為實際參數。參數列表指定的是參數類型、順序、及參數個數。參數是可選的,也就是說函數也可以不包含參數。
- return_types:返回類型,函數返回一列值。return_types 是該列值的數據類型。有些功能不需要返回值,這種情況下 return_types 不是必須的。
- 函數體:函數定義的代碼集合。
返回值可以為多個:
func test(x, y int, s string) (int, string) {// 類型相同的相鄰參數,參數類型可合并。 多返回值必須用括號。n := x + y return n, fmt.Sprintf(s, n)
}
1.1 函數做為參數
函數做為一等公民,可以做為參數傳遞。
func fn() int {return 300
}
func test(fn func() int) int {return fn()
}
func main() {//這是直接使用匿名函數s := test(func() int { return 200 })fmt.Println(s) //200//傳入一個函數s2 := test(fn)fmt.Println(s2) //300
}
在將函數做為參數的時候,我們可以使用類型定義,將函數定義為類型,這樣便于閱讀
type myFunc func(s string, x, y int) stringfunc format(fn myFunc, s string, x, y int) string {return fn(s, x, y)
}func formatFunc(s string, x, y int) string {return fmt.Sprintf(s, x, y)
}
func main() {s2 := format(formatFunc, "%d %d", 10, 20)fmt.Println(s2)
}
1.2 函數返回值
函數返回值可以有多個,同時Go支持對返回值命名
func main() {var a, b = 10, 20fmt.Println(sum(a, b))
}
//多個返回值 用括號擴起來
func sum(a, b int) (int, int) {return a, b
}
.3 參數
函數定義時指出,函數定義時有參數,該變量可稱為函數的形參。
形參就像定義在函數體內的局部變量。
但當調用函數,傳遞過來的變量就是函數的實參,函數可以通過兩種方式來傳遞參數:
- 值傳遞:指在調用函數時將實際參數復制一份傳遞到函數中,這樣在函數中如果對參數進行修改,將不會影響到實際參數。
func swap(x, y int) int {... ...}
- 引用傳遞:是指在調用函數時將實際參數的地址傳遞到函數中,那么在函數中對參數所進行的修改,將影響到實際參數。
package mainimport ("fmt"
)/* 定義相互交換值的函數 */
func swap(x, y *int) {*x,*y = *y,*x
}func main() {var a, b int = 1, 2/*調用 swap() 函數&a 指向 a 指針,a 變量的地址&b 指向 b 指針,b 變量的地址*/swap(&a, &b)fmt.Println(a, b)
}
map、slice、chan、指針、interface默認以引用的方式傳遞。
不定參數傳值
不定參數傳值 就是函數的參數不是固定的,后面的類型是固定的。(可變參數)
Golang 可變參數本質上就是 slice。只能有一個,且必須是最后一個。
在參數賦值時可以不用用一個一個的賦值,可以直接傳遞一個數組或者切片。
格式:
func test1(args ...int) { //0個或多個參數}func test2(a int, args ...int) int { //1個或多個參數}func test3(a int, b int, args ...int) int { //2個或多個參數}
注意:其中args是一個slice,我們可以通過arg[index]依次訪問所有參數,通過len(arg)來判斷傳遞參數的個數.
func main() {a := []int{10, 20, 10}fmt.Println(test("sum: %d", a...)) // slice... 展開slice
}func test(s string, n ...int) string {var x = 0for _, v := range n {x += v}return fmt.Sprintf(s, x)
}
2. 匿名函數
匿名函數是指不需要定義函數名的一種函數實現方式。
在Go里面,函數可以像普通變量一樣被傳遞或使用,Go語言支持隨時在代碼里定義匿名函數。
匿名函數由一個不帶函數名的函數聲明和函數體組成。匿名函數的優越性在于可以直接使用函數內的變量,不必聲明。
匿名函數的定義格式如下:
func(參數列表)(返回參數列表){函數體
}
func main() {//這里將一個函數當做一個變量一樣的操作。getSqrt := func(a float64) float64 { return math.Sqrt(a)}fmt.Println(getSqrt(4.123))
}
在定義時調用匿名函數
匿名函數可以在聲明后調用,例如:
func main() {func(a int) {fmt.Println(a)}(100) //(100),表示對匿名函數進行調用,傳遞參數為 100。
}
返回多個匿名函數
package mainimport "fmt"func FGen(x, y int) (func() int, func(int) int) {//求和的匿名函sum := func() int {return x + y}// (x+y) *z 的匿名函數avg := func(z int) int {return (x + y) * z}return sum, avg
}func main() {f1, f2 := FGen(1, 2)fmt.Println(f1())fmt.Println(f2(3))
}
3. 閉包
所謂“閉包”,指的是一個擁有許多變量和綁定了這些變量的環境的表達式(通常是一個函數),因而這些變量也是該表達式的一部分。
閉包=函數+引用環境
package mainimport "fmt"func main() {// 創建一個玩家生成器generator := playGen("碼神")// 返回玩家的名字和血量name, hp := generator()// 打印值fmt.Println(name, hp)generator1 := playGen2()name1, hp1 := generator1("碼神")// 打印值fmt.Println(name1, hp1)
}// 創建一個玩家生成器, 輸入名稱, 輸出生成器
func playGen(s string) func() (string, int) {// 血量一直為150hp := 150// 返回創建的閉包return func() (string, int) {// 將變量引用到閉包中return s, hp}
}// 創建一個玩家生成器, 輸入名稱, 輸出生成器
func playGen2() func(name string) (string, int) {hp := 150return func(name string) (string, int) {return name, hp}
}
. 延遲調用
Go語言的 defer 語句會將其后面跟隨的語句進行延遲處理
defer特性:
- 關鍵字 defer 用于注冊延遲調用。
- 這些調用直到 return 前才被執。因此,可以用來做資源清理。
- 多個defer語句,按先進后出的方式執行。
- defer語句中的變量,在defer聲明時就決定了。
defer的用途:
- 關閉文件句柄
- 鎖資源釋放
- 數據庫連接釋放
go 語言的defer功能強大,對于資源管理非常方便,但是如果沒用好,也會有陷阱。
package mainimport ("log""time"
)func main() {start := time.Now()log.Printf("開始時間為:%v", start)defer log.Printf("時間差:%v", time.Since(start)) // Now()此時已經copy進去了//不受這3秒睡眠的影響time.Sleep(3 * time.Second)log.Printf("函數結束")
}
- Go 語言中所有的函數調用都是傳值的
- 調用 defer 關鍵字會立刻拷貝函數中引用的外部參數 ,包括start 和time.Since中的Now
- defer的函數在壓棧的時候也會保存參數的值,并非在執行時取值。
如何解決上述問題:使用defer func()
func main() {start := time.Now()defer func() {log.Printf("開始調用defer")log.Printf("時間差:%v", time.Since(start))log.Printf("結束調用defer")}()time.Sleep(3 * time.Second)log.Printf("函數結束")
}
因為拷貝的是函數指針,函數屬于引用傳遞
另一個問題:
func main() {var whatever = [5]int{1, 2, 3, 4, 5}for i, _ := range whatever {//函數正常執行,由于閉包用到的變量 i 在執行的時候已經變成4,所以輸出全都是4.defer func() { fmt.Println(i) }()}
}
解決:
func main() {var whatever = [5]int{1, 2, 3, 4, 5}for i, _ := range whatever {i := idefer func() { fmt.Println(i) }()}
}
5. 異常處理
Go語言中使用 panic 拋出錯誤,recover 捕獲錯誤。
異常的使用場景簡單描述:Go中可以拋出一個panic的異常,然后在defer中通過recover捕獲這個異常,然后正常處理。
panic:
- 內置函數
- 假如函數F中書寫了panic語句,會終止其后要執行的代碼,在panic所在函數F內如果存在要執行的defer函數列表,按照defer的逆序執行
- 返回函數F的調用者G,在G中,調用函數F語句之后的代碼不會執行,假如函數G中存在要執行的defer函數列表,按照defer的逆序執行
- 直到goroutine整個退出,并報告錯誤
recover:
- 內置函數
- 用來捕獲panic,從而影響應用的行為
golang 的錯誤處理流程:當一個函數在執行過程中出現了異常或遇到 panic(),正常語句就會立即終止,然后執行 defer 語句,再報告異常信息,最后退出 goroutine。如果在 defer 中使用了 recover() 函數,則會捕獲錯誤信息,使該錯誤信息終止報告。
注意:
- 利用recover處理panic指令,defer 必須放在 panic 之前定義,另外 recover 只有在 defer 調用的函數中才有效。否則當panic時,recover無法捕獲到panic,無法防止panic擴散。
- recover 處理異常后,邏輯并不會恢復到 panic 那個點去,函數跑到 defer 之后的那個點。
- 多個 defer 會形成 defer 棧,后定義的 defer 語句會被最先調用。
func main() {test()
}func test() {defer func() {if err := recover(); err != nil {log.Println(err) //2024/02/28 23:03:38 print out panic}}()panic("print out panic")}
由于 panic、recover 參數類型為 interface{},因此可拋出任何類型對象。
func panic(v interface{})func recover() interface{}
延遲調用中引發的錯誤,可被后續延遲調用捕獲,但僅最后一個錯誤可被捕獲:
func test() {defer func() {// defer panic 會打印fmt.Println(recover()) //defer panic}()defer func() {panic("defer panic")}()panic("test panic")
}func main() {test()
}
除用 panic 引發中斷性錯誤外,還可返回 error 類型錯誤對象來表示函數調用狀態:
type error interface {Error() string
}
標準庫 errors.New 和 fmt.Errorf 函數用于創建實現 error 接口的錯誤對象。通過判斷錯誤對象實例來確定具體錯誤類型。
package mainimport ("errors""fmt""log"
)var divError = errors.New("div error:分母不能為0")func div(x, y int) (int, error) {if y == 0 {return 0, divError }return x / y, nil
}func main() {defer func() {if err := recover(); err != nil {log.Fatalln(err) //2024/02/28 23:17:14 div error:分母不能為0}}()res, err := div(8, 0)switch err {case nil:fmt.Println(res)case divError:panic(err)}}