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

Tags: