天天看點

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 函數詳解

Go 不會出現 untrapped error,隻需處理 runtime errors 和程式邏輯錯誤

Go 提供兩種錯誤處理機制

  • 通過 panic 列印程式調用棧,終止程式來處理錯誤
  • 通過函數傳回錯誤類型的值來處理錯誤

Go 是靜态強類型語言,程式的大部分錯誤是可以在編譯器檢測到的,但有些錯誤行為需要在運作期才能檢測出來,此種錯誤行為将導緻程式異常退出。建議:

  • 若程式發生的錯誤導緻程式不能繼續執行,此時程式應該主動調用 panic
  • 若程式發生的錯誤能夠容錯繼續執行,此時應該使用 error 傳回值的方式處理,或在非關鍵分支上使用 recover 捕獲 panic
Go 函數詳解

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