Golang 基礎之函式使用 (二)

帽兒山的槍手發表於2022-06-01

大家好,今天將梳理出的 Go語言函式用法內容,分享給大家。 請多多指教,謝謝。

本次《Go語言函式使用》內容共分為三個章節,本文為第二章節。

本章節內容

  • 匿名函式
  • 閉包
  • 遞迴函式
  • 延遲呼叫 (defer)

匿名函式

介紹

在Go語言中,函式可以像普通變數一樣被傳遞或使用,支援隨時在程式碼裡定義匿名函式。

匿名函式由一個不帶函式名的函式宣告和函式體組成。匿名函式的優越性在於可以直接使用函式內的變數,不必申明。

匿名函式有動態建立的特性,該特性使得匿名函式不用通過引數傳遞的方式,就可以直接引用外部的變數。

使用

第一種用法:將匿名函式賦給變數,在通過變數呼叫匿名函式

sum := func(a, b int) int {
  return a + b
}
fmt.Println(sum(1, 2)) // 輸出 3

第二種用法:在定義匿名函式的時候直接使用,這種方式只能使用一次傳參

sum := func(a, b int) int {
    return a + b
}(1,2) // 傳入引數
fmt.Println(sum) // 輸出 3

第三種用法:將匿名函式賦給一個全域性變數,匿名函式在當前程式中都可以使用

package main

import "fmt"

var (
  // 全域性變數必須首字母大寫
    Sum = func(a, b int) int {
        return a + b
    }
)

func main() {
    sum := Sum(1, 2)
    fmt.Println("sum=", sum) // 輸出 sum= 3
}

閉包

介紹

所謂“閉包”,指的是一個擁有許多變數和繫結了這些變數的環境的表示式(通常是一個函式),因而這些變數也是該表示式的一部分。

閉包(Closure),是引用了自由變數的函式。這個被引用的自由變數將和這個函式一同存在,即使已經離開了創造它的環境也不例外。所以,有另一種說法認為閉包是由函式和與其相關的引用環境組合而成的實體。閉包在執行時可以有多個例項,不同的引用環境和相同的函式組合可以產生不同的例項。

可以理解為:閉包是匿名函式與匿名函式所引用環境的組合,類似常規函式引用全域性變數處於一個包的環境。

閉包的優點

  • 變數可以常駐記憶體
  • 變數不汙染全域性
閉包裡作用域返回的區域性變數不會被立刻銷燬回收,可能會佔用更多記憶體過度使用閉包會導致效能下降。

使用

package main

import "fmt"

func main() {
    n := 0
    count := func() int {  // 這就是一個閉包
        n += 1
        return n
    }
    fmt.Println(count()) // 輸出 1
    fmt.Println(count()) // 輸出 2
}

常規函式、匿名函式 + 全域性變數 + 包就等同於閉包, count不僅儲存了函式的返回值,還儲存了閉包的狀態。

閉包被返回賦予一個同型別的變數時,同時賦值的是整個閉包的狀態,該狀態會一直存在外部被賦值的變數count中,直到count被銷燬,整個閉包生命週期結束。

也可以寫成下列形式

package main

import "fmt"

func Count() func() int { // 返回函式
    n := 0
    return func() int {
        n++
        return n
    }
}

func main() {
    count := Count()
    fmt.Println(count()) // 輸出 1
    fmt.Println(count()) // 輸出 2
}
高階閉包特性,比如併發中的閉包。 將放到後面章節為大家介紹。

遞迴函式

介紹

遞迴,就是在執行的過程中呼叫自己。

一個函式呼叫自己, 就叫做遞迴函式。

構成遞迴具備的條件:

  1. 子問題需要與原始問題為同樣的事,且更為簡單。
  2. 不能無限制地呼叫本身,須有個出口,化簡為非遞迴狀況處理。

使用

舉例:數字階乘

一個正整數的階乘是所有小於及等於該數的正整數的積,並且0的階乘為1。

package main

import "fmt"

func factorial(i int) int {  // 解讀為 5*4*3*2*1=120 
    if i <= 1 {
        return 1
    }
    return i * factorial(i-1)
}

func main() {
    var i int = 5
    fmt.Printf("%d\n", factorial((i))) // 120
}
1808年,基斯頓·卡曼引進這個表示法。

舉例:裴波那契數列(Fibonacci)

這個數列從第3項開始,每一項都等於前兩項之和。

package main

import "fmt"

func fibonaci(i int) int {    
    if i == 0 {
        return 0    
    }
    if i == 1 {
        return 1
    }    
    return fibonaci(i-1) + fibonaci(i-2)
}

func main() {
    var i int
    for i = 0; i < 10; i++ {
        fmt.Printf("%d ", fibonaci(i)) // 0 1 1 2 3 5 8 13 21 34
    }
}

延遲呼叫 (defer)

介紹

在基礎語法中已經介紹了defer延遲呼叫的使用,今天深入瞭解使用一下defer機制。

defer特性

  1. 關鍵字 defer 用於註冊延遲呼叫。
  2. 這些呼叫直到 return 前才被執。因此,可以用來做資源清理。
  3. 多個defer語句,按先進後出的方式執行。
  4. defer語句中的變數,在defer宣告時就決定了。
  5. 某個延遲呼叫發生錯誤,這些呼叫依舊會被執行。
defer後面的語句在執行的時候,函式呼叫的引數會被儲存起來,但是不執行。也就是複製了一份。

defer用途

  1. 關閉檔案控制程式碼
  2. 鎖資源釋放
  3. 資料庫連線釋放

使用

多個 defer 註冊,按 FILO 次序執行 ( 先進後出 ) 原則。

package main

func main() {
    defer println("1") // 先進來, 最後出去
    defer println("2")
    defer println("3") // 最後進來, 先出去
}

輸出

3
2
1

延遲呼叫引數在註冊時求值或複製,可以用指標或閉包 “延遲” 讀取。

package main

import "fmt"

func main() {
    x, y := 10, 100
    defer func(i int) {
        fmt.Printf("defer x = %v, y = %v\n", i, y) // y 閉包引用
    }(x) // x 被複制

    x += 10
    y += 20
    println("x = ", x, "y = ", y)
}

輸出

x =  20 y =  120
defer x = 10, y = 120

defer和return兩者執行順序

  1. 有名返回值 (函式返回值為已經命名的返回值)
package main

import "fmt"

func foo() (i int) { // 3.return i 值
    i = 0
    defer func() {
    fmt.Println(i) // 2.讀取臨時變數地址(返回值)
    }()
    return 2 // 1.返回值賦值,寫入臨時變數
}

func main() {
    foo()
}

輸出

2

foo() 返回值的函式中 (這裡返回值為 i),執行 return 2 的時候實際上已經將i 的值重新賦值為2。 所以defer closure輸出結果為2。

解析:

return 的機制:1.首先將返回值放到一個臨時變數中(為返回值賦值) 2. 然後將返回值返回到被呼叫處。

而defer函式恰在return的兩個操作之間執行。

執行順序是: 先為返回值賦值,即將返回值放到一個臨時變數中,然後執行defer,然後return到函式被呼叫處。

b0wWvD.png

  1. 無名返回值 (即函式返回值為沒有命名的函式返回值)
package main

import "fmt"

func foo() int {
    var i int
    defer func() {
        i++ // 這個地方 i,不是臨時變數
        fmt.Println("defer = ", i) // 輸出 defer = 1
    }()
    return i // 返回值賦值,寫入臨時變數
}

func main() {
    fmt.Println("return = ", foo()) // 輸出 return = 0
}

解析:

return 先把返回值放到一個臨時變數中,defer函式無法獲取到這個臨時變數地址 (沒有函式返回值),所以無論defer函式做任何操作,都不會對最終返回值造成任何變動。

技術文章持續更新,請大家多多關注呀~~

搜尋微信公眾號【 帽兒山的槍手 】,關注我

相關文章