## 前言 假設一個場景,服務端部署在內網,客戶端需要通過暴露在公網的nginx與服務端進行通信。為了避免在公網進行 http 明文通信造成的信息泄露,nginx與客戶端之間的通信應當使用 https 協議,並且nginx也要驗證客戶端的身份,也就是mTLS雙向加密認證通信。 這條通信鏈路有三個角色 ...
概述
在任何語言中函數都是極其重要的內容,業務功能都是由一個或多個函數組合完成。go
語言是函數式編程語言,函數是一等公民,可以被傳遞、有函數類型,go
語言有三種類型的函數,普通函數、匿名函數(Lambda函數)、方法函數。go
語言函數有獨特屬性,可以有多個返回值,需要使用多個變數接收、函數也是一種類型,函數簽名是函數類型、函數不能被重載
基本使用
聲明函數時必須包含參數類型、返回值類型。單個返回值可省略括弧
fun sum(x int, y int) int { return x+y }
調用函數
n := sum(10, 20) // 30
兩個返回值,多個返回值必須使用括弧。
fun sum(x int, y int) (int, int) { return x+y, x-y }
使用兩個變數接收
n, m := sum(x, y)
返回值變數也可以在聲明中定義
func sum(x int, y int) (a int, b int) { a = x + y b = x - y return }
在函數聲明定義返回類型和返回值變數,使用return
時可省略返回對象。
兩個返回值一般用於錯誤處理,一個表示結果,一個表示錯誤。很多庫函數都有類似的應用
func Open(name string) (file *File, err error)
第一個返回值file
表示文件指針,第二個返回值err
表示打開文件異常,所以一般先判斷是否有異常
file, err := open("test.txt") if err != nil { fmt.Pringln("打開文件失敗, ", err) return }
使用_
表示忽略返回值
file, _ := open("test.txt")
go
語言函數也支持可變參數,註意可變參數必須在最後位置,固定是切片類型。
func sum(x int, args... int) int { for _, arg := range args { x += arg } return x } // 使用 i := sum(10, 20, 30, 40)
當可變參數是一個空介面類型時,調用者是否解包可變參數會導致不同的結果:
func Print(a ...interface{}) { fmt.Println(a...) } func main() { var a = []interface{}{123, "abc"} Print(a...) // 123 abc Print(a) // [123 abc] }
第一個 Print 調用時傳入的參數是 a...
,等價於直接調用 Print(123, "abc")
。第二個 Print 調用傳入的是未解包的 a
,等價於直接調用 Print([]interface{}{123, "abc"})
。
Go 語言函數還可以直接或間接地調用自己,也就是支持遞歸調用。相比其他語言 Go 語言函數的遞歸調用深度邏輯上沒有限制,函數調用的棧不會出現溢出錯誤,因為 Go 語言運行時會根據需要動態地調整函數棧的大小。每個 goroutine 剛啟動時只會分配很小的棧(4 或 8KB,具體依賴實現),根據需要動態調整棧的大小,棧最大可以達到 GB 級(依賴具體實現,在目前的實現中,32 位體繫結構為 250MB,64 位體繫結構為 1GB)
func f(x int) { if x > 1 { f(x-1) } else { fmt.Println(x) } }
因為Go 語言函數的棧會自動調整大小,所以程式員很少需要關心棧的運行機制的,在 Go 語言規範中甚至故意沒有講到棧和堆的概念。我們無法知道函數參數或局部變數到底是保存在棧中還是堆中,只需要知道它們能夠正常工作就可以了
func f(x int) *int { return &x } func g() *int { i := 20 return i }
有C/C++經驗的程式員會驚訝這兩個函數有bug,因為參數變數在棧上維護,函數返回之後棧變數就失效了,返回的地址自然也應該失效了,返回的是野指針。在Go語言中可以正常工作,Go 編譯器會保證指針指向的變數在合適的地方,不用關心 Go 語言中函數棧和堆的問題,編譯器和運行時會幫我們搞定;同樣不要假設變數在記憶體中的位置是固定不變的,指針隨時可能會變化,特別是在你不期望它變化的時候。
函數類型
函數也是一種類型,即函數類型,也稱為函數簽名。
func sum(x int, y int) int { return x + y } func main() { fmt.Printf("%T\n", sum) // 輸出:func(int, int) int }
註意,簽名中不包括函數名稱
和C
語言一樣,go
的函數名是只讀指針,指向函數的首地址,所以go
函數是引用類型。
把函數當參數傳遞時,複製出來的新指針,也指向相同的函數地址。註意不是複製函數,是複製函數指針,還有多種引用類型,如slice、map、chan、interface等。
func main() { fmt.Println(sum) // 0x108ef60 fmt.Printf("%v\n", sum) // 0x108ef60 fmt.Printf("%v\n", &sum) // err }
兩條語句結果一樣,都列印函數的起始記憶體地址。註意無法使用地址符獲取函數地址,這也說明不存在函數指針,更無法通過指針調用函數,這與C語言有區別,C語言回調函數經常使用這招兒。
在go
語言中函數是一等公民,可以被傳遞、調用,這時就依賴函數類型
func handler(sum func(int, int, ) int, x int, y int) int { return sum(x, y) }
第一個參數是函數類型,接收函數做為參數,使用函數簽名定義。
也可自定義類型,簡單理解就是定義別名,簡化寫法
// 自定義類型 type sum func(int, int) int // 接收自定義類型 func handler(plus sum, x int, y int) int { return plus(x, y) }
匿名函數
與JavaScript
一樣go
也支持匿名函數,聲明函數時不寫名稱
func main() { f := func(x int, y int) int { return x + y } fmt.Printf("%T\n", f) // func(int, int) int fmt.Println(f(1, 2)) // 3 }
與普通函數一樣使用、傳遞,匿名函數也有函數簽名,也可自定義類型
// 自定義類型 type sum func(int, int) int func main() { // 聲明變數 var f sum = func(x int, y int) int { return x + y } fmt.Printf("%T\n", f) fmt.Println(f(1, 2)) }
函數閉包
閉包是函數式編程語言的招牌功能之一,go
當然也支持閉包。簡單來說就是函數可記住誕生時的環境信息,也稱為記憶效應。
var str string = "hello" func func1() { fmt.Println(str) }
函數func1
應用引用了外部變數str
,註意是“引用”,而非值傳遞,兩者會相互影響,這與函數調用傳參有本質區別。
func1() // hello str = "world" func1() // world
更多使用動態生成匿名函數的方式,如下
func Accumulate(value int) func() int { // 返回一個閉包 return func() int { // 引入外部變數value並累加 value++ // 返回一個累加值 return value } }
返回值是一個函數,並且引用了外部變數value
,該變數被會記錄在函數內部,外部環境被銷毀也不受影響,有點類似Python的裝飾器。
// 創建一個累加器, 初始值為1 accumulator := Accumulate(1) fmt.Println(accumulator()) // 2 fmt.Println(accumulator()) // 3
每次調用value
都會被累加,有點Java中lombda
的感覺,
延遲執行
這是go
特有的技能,函數內部被defer
修飾的語句總是最後執行,有點類似java中finally
的特性。
func main() { fmt.Println("defer begin") defer fmt.Println(1) defer fmt.Println(2) defer fmt.Println(3) fmt.Println("defer end") }
可以有多條defer
修飾,與棧的數據結構一致,先進後出,輸出如下
defer begin defer end 3 2 1
註意,被defer
修飾的預計總是最後執行,不再是順序執行,與語句所在的位置無關。
func main() { fmt.Println("start time: ", time.Now()) defer fmt.Println("end time: ", time.Now()) time.Sleep(time.Duration(rand.Intn(1000))) fmt.Println("ok") return }
不受return
影響,end time
預計總是在最後列印,輸出如下
start time: 2023-07-19 23:45:51.551994 +0800 CST m=+0.000133126 ok end time: 2023-07-19 23:45:51.552634 +0800 CST m=+0.000773430
也不受panic
的影響,程式崩潰前也會執行被defer
修飾的語句
主要使用場景是異常捕獲、回收資源、釋放互斥鎖等,因為被defer
修飾的語句一定會在最後執行
func main() { fp, err := os.Open(filename) if err != nil { fmt.Println("open file error", err) return } // 最後一定會關閉文件 defer f.Close() // 對文件指針fp進行操作 ... }
也可以用於申請和釋放鎖
func main(key string) int { // 申請鎖 sync.Mutex.Lock() // 釋放鎖,延遲到函數結束時執行 defer sync.Mutex.Unlock() // 業務邏輯 ... }
特殊函數
go
語言中有兩個比較特殊的函數,在固定場景下使用
main
函數,同C
一樣是程式的入口函數,只能有一個main
函數,程式從這裡開始執行,註意main
函數只能屬於main
包。只有當代碼包含有main
函數時才可編譯出可執行文件。
package main // main包 import "fmt" func main() { // main函數 fmt.Println("hello world") }
init
函數,也稱為初始化函數,會在main
函數之前被自動調用,只要被import
導入,該包所有init
函數都被會自動執行,多次導入只執行一次。import
是鏈式導入,init
執行也是鏈式執行,如下圖。
go
執行順序是:常量定義(const) -> 全局變數定義(var) -> 初始化函數(init) -> 程式入口(main)
要註意的是,在 main 函數執行之前所有代碼都運行在同一個 Goroutine 中,也是運行在程式的主系統線程中。如果某個 init 函數內部用 go 關鍵字啟動了新的 Goroutine 的話,新的 Goroutine 和 main.main 函數是併發執行的