Go 函數詳解
一、函數基礎
- 函數由函數聲明關鍵字 func、函數名、參數列表、返回列表、函數體組成
- 函數是一種類型。函數類型變數可以像其他類型變數一樣使用,可以作為其他函數的參數或返回值,也可以直接調用執行
- 函數名首字母大小寫決定了其包可見性
- 參數和返回值需用
()
包裹,如果返回值是一個非命名的參數,則可省略。函數體使用{}
包裹,且{
必須位於同行行尾
1. 基本使用
// 1. 可以沒有輸入參數,也可以沒有返回值(默認返回 0)
func A() {
...
}
// 2. 多個相鄰的相同類型參數可以使用簡寫模式
func B(a, b int) int {
return a + b
}
// 3. 支援有名的返回值
func C(a, b int) (sum int) {
// sum 相當於函數體內的局部變數,初始化為零值
sum = a + b
return // 可以不帶 sum
}
// 4. 不支援默認值參數
// 5. 不支援函數重載
// 6. 不支援函數嵌套定義,但支援嵌套匿名函數
func D(a, b int) (sum int) {
E := func(x, y int) int {
return x + y
}
return E(a, b)
}
// 7. 支援多值返回(一般將錯誤類型作為最後一個返回值)
func F(a, b int) (int, int) {
return b, a
}
// 8. 函數實參到形參的傳遞永遠是**值拷貝**
func G(a *int) { // a 是實參指針變數的副本,和實參指向同一個地址
*a += 1
}
2. 不定參數
// 1. 不定參數類型相同
// 2. 不定參數必須是函數的最後一個參數
// 3. 不定參數在函數體內相當於切片
func sum(arr ...int) (sum int) {
for _, v := range arr { // arr 相當於切片,可使用 range訪問
sum += v
}
return
}
// 4. 可以將切片傳遞給不定參數
array := [...]int{1, 2, 3, 4} // 不能將數組傳遞給不定參數
slice := []int{1, 2, 3, 4}
sum(slice...) // 切片名後要加 ...
// 5. 形參為不定參數的函數和形參為切片的函數類型不同
func suma(arr ...int) (sum int) {
for v := range arr {
sum += v
}
return
}
func sumb(arr []int) (sum int) {
for v := range arr {
sum += v
}
return
}
fmt.Printf("%T\n", suma) // func(...int) int
fmt.Printf("%T", sumb) // func([]int) int
3. 函數類型
函數類型又叫函數簽名:函數定義行去掉函數名、參數名和 {
func add(a, b int) int { return a + b }
func sub(x int, y int) (c int) { c = x - y; return }
fmt.Printf("%T", add) // func(int, int) int
fmt.Printf("%T", sub) // func(int, int) int
可以使用 type 定義函數類型。函數類型變數和函數名都可以看做指針變數,該指針指向函數程式碼的開始位置
func add(a, b int) int { return a + b }
func sub(a, b int) int { return a - b }
type Op func(int, int) int // 定義一個函數類型:輸入兩個 int,返回一個 int
func do(f Op, a, b int) int {
t := f
return t(a, b)
}
fmt.Println(do(add, 1, 2)) // 3
fmt.Println(do(sub, 1, 2)) // -1
4. 匿名函數
// 1. 直接賦值給函數變數
var sum = func(a, b int) int {
return a + b
}
func do(f func(int, int) int, a, b int) int {
return f(a, b)
}
// 2. 作為返回值
func getAdd() func(int, int) int {
return func(a, b int) int {
return a + b
}
}
func main() {
// 3. 直接被調用
defer func() {
if err:= recover(); err != nil {
fmt.Println(err)
}
}()
sum(1, 2)
getAdd()(1 , 2)
// 4. 作為實參
do(func(x, y int) int { return x + y }, 1, 2)
}
二、函數高級
1. defer
可註冊多個延遲調用函數,以先進後出的順序執行。常用於保證資源最終得到回收釋放
func main() {
// defer 後跟函數或方法調用,不能是語句
defer func() {
println("first")
}()
defer func() {
println("second")
}()
println("main")
}
// main
// second
// first
defer 函數的實參在註冊時傳遞,後續變更無影響
func f() int {
a := 1
defer func(i int) {
println("defer i =", i)
}(a)
a++
return a
}
print(f())
// defer i = 1
// 2
defer 若位於 return 後,則不會執行
func main() {
println("main")
return
defer func() {
println("first")
}()
}
// main
若主動調用os.Exit(int)
退出進程,則不會執行 defer
func main() {
defer func() {
println("first")
}()
println("main")
os.Exit(1)
}
// main
關閉資源例子
func CopyFile(dst, src string) (w int64, err error) {
srcFile, err := os.Open(src)
if err != nil {
return
}
// defer 一般放在錯誤檢查語句後面。若位置不當可能造成 panic
defer srcFile.Close()
dstFile, err := os.Create(dst)
if err != nil {
return
}
defer dstFile.Close()
w, err = io.Copy(dstFile, srcFile)
return
}
defer 使用注意事項:
- defer 會延遲資源的釋放
- 盡量不要放在循環語句中
- defer 相對於普通函數調用需要間接的數據結構支援,有一定性能損耗
- defer 中最好不要對有名返回值進行操作
2. 閉包
- 閉包是由函數及其相關引用環境組合成的實體。一般通過在匿名函數中引用外部函數的局部變數或包全局變數構成
- 閉包對閉包外的環境引入是直接引用:編譯器檢測到閉包,會將閉包引用的外部變數分配到堆上
- 閉包是為了減少全局變數,在函數調用的過程中隱式地傳遞共享變數。但不夠清晰,一般不建議用
- 對象是附有行為的數據,而閉包是附有數據的行為。類在定義時已經顯式地集中定義了行為,但閉包中的數據沒有顯式地集中聲明的地方
// fa 返回的是一個閉包:形參a + 匿名函數
func fa(a int) func(i int) int {
return func(i int) int {
println(&a, a)
a = a + i
return a
}
}
func main() {
f := fa(1) // f 使用的 a 是 0xc0000200f0
g := fa(1) // g 使用的 a 是 0xc0000200f8
// f、g 引用的閉包環境中的 a 是函數調用產生的副本:每次調用都會為局部變數分配記憶體
println(f(1))
println(f(1)) // 閉包共享外部引用,因此修改的是同一個副本
println(g(1))
println(g(1))
}
// 0xc0000200f0 1
// 2
// 0xc0000200f0 2
// 3
// 0xc0000200f8 1
// 2
// 0xc0000200f8 2
// 3
閉包引用全局變數(不推薦)
var a = 0
// fa 返回的是一個閉包:全局變數a + 匿名函數
func fa() func(i int) int {
return func(i int) int {
println(&a, a)
a = a + i
return a
}
}
func main() {
f := fa()
g := fa()
// f、g 引用的閉包環境中的 a 是同一個
println(f(1))
println(g(1))
println(f(1))
println(g(1))
}
// 0x511020 0
// 1
// 0x511020 1
// 2
// 0x511020 2
// 3
// 0x511020 3
// 4
同一個函數返回的多個閉包共享該函數的局部變數
func fa(a int) (func(int) int, func(int) int) {
println(&a, a)
add := func(i int) int {
a += i
println(&a, a)
return a
}
sub := func(i int) int {
a -= i
println(&a, a)
return a
}
return add, sub
}
func main() {
f, g := fa(0) // f、g 使用的 a 都是 0xc0000200f0
s, k := fa(0) // s、k 使用的 a 都是 0xc0000200f8
println(f(1), g(2))
println(s(1), k(2))
}
// 0xc0000200f0 0
// 0xc0000200f8 0
// 0xc0000200f0 1
// 0xc0000200f0 -1
// 1 -1
// 0xc0000200f8 1
// 0xc0000200f8 -1
// 1 -1
三、錯誤處理
1. 錯誤和異常
- 廣義的錯誤:發生非期望的行為
- 狹義的錯誤:發生非期望的己知行為
- 這裡的己知是指錯誤類型是預料並定義好的
- 異常:發生非期待的未知行為,又被稱為未捕獲的錯誤
- 這裡的未知是指錯誤的類型不在預先定義的範圍內
- 程式在執行時發生未預先定義的錯誤,程式編譯器和運行時都沒有及時將其捕獲處理,而是由作業系統進行異常處理。如 C 語言的 Segmentation Fault
Go 不會出現 untrapped error,只需處理 runtime errors 和程式邏輯錯誤
Go 提供兩種錯誤處理機制
- 通過 panic 列印程式調用棧,終止程式來處理錯誤
- 通過函數返回錯誤類型的值來處理錯誤
Go 是靜態強類型語言,程式的大部分錯誤是可以在編譯器檢測到的,但有些錯誤行為需要在運行期才能檢測出來,此種錯誤行為將導致程式異常退出。建議:
- 若程式發生的錯誤導致程式不能繼續執行,此時程式應該主動調用 panic
- 若程式發生的錯誤能夠容錯繼續執行,此時應該使用 error 返回值的方式處理,或在非關鍵分支上使用 recover 捕獲 panic
2. panic 和 recover
panic(i interface{}) // 主動拋出錯誤
recover() interface{} // 捕獲拋出的錯誤
- 引發panic情況:①主動調用panic;②程式運行時檢測拋出運行時錯誤
- panic 後,程式會從當前位置返回,逐層向上執行 defer 語句,逐層列印函數調用棧,直到被 recover 捕獲或運行到最外層函數退出
- 參數為空介面類型,可以傳遞任意類型變數
- defer 中也可以 panic,能被後續 defer 捕獲
- recover 只有在 defer 函數體內被調用才能捕獲 panic,否則返回 nil
// 以下場景捕獲失敗
defer recover()
defer fmt.Println(recover())
defer func() {
func() { // 兩層嵌套
println("defer inner")
recover()
}()
}()
// 以下場景捕獲成功
defer func() {
println("defer inner")
recover()
}()
func except() {
recover()
}
func test() {
defer except()
painc("test panic")
}
可以同時有多個 panic(只會出現在 defer 里),但只有最後一次 panic 能被捕獲
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
fmt.Println(recover())
}()
defer func() {
panic("first defer panic")
}()
defer func() {
panic("second defer panic")
}()
panic("main panic")
}
// first defer panic
// <nil>
包中 init 函數引發的 panic 只能在 init 函數中捕獲(init 先於 main 執行)
函數不能捕獲內部新啟動的 goroutine 拋出的 panic
func do() {
// 不能捕獲 da 中的 panic
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
go da()
time.Sleep(3 * time.Second)
}
func da() {
panic("panic da")
}
3. error
Go 內置錯誤介面類型 error。任何類型只要實現Error() string
方法,都可以傳遞 error 介面類型變數???
type error interface {
Error() string
}
使用 error:
- 在多個返回值的函數中,error 作為函數最後一個返回值
- 若函數返回 error 類型變數,先處理
error != nil
的異常場景,再處理其他流程 - defer 放在 error 判斷的後面
四、底層實現
TODO