← 返回文章列表

golang學習記錄(8)

Go 基礎學習系列第 8 篇,記錄語法、示例與學習過程。

go語言中的函式支援普通函式、匿名函式、閉包函式、方法等

在go語言中函式是一等公民,函式可以作為引數、函式可以作為返回值、函式可以賦值給變數、函式可以作為閉包函式等

函式可以滿足介面

1、函式的定義

函式的基本定義方式如下:

func 函数名(参数列表) (返回值列表) {
    函数体
}

引數列表:引數列表可以為空,也可以有多個引數,多個引數之間用逗號分隔

返回值列表:返回值列表可以為空,也可以有多個返回值,多個返回值之間用逗號分隔,返回值列表不為空時,函式中必須有return

舉個簡單的例子:

func add(a, b int, c float32) (int, error) {
    return a + b, nil
}

在go語言中引數的傳遞時值傳遞,即傳遞的是值的副本,而不是值的引用,因此在函式中修改引數的值不會影響到原變數的值

但是關於切片的傳遞,由於切片的底層原理,在函式中修改切片的值會影響到原切片的值,因此在go語言中切片的傳遞需要注意。

當且切片發生擴容時,會返回一個新的切片,此時在函式中修改切片的值不會影響到原切片的值

除了上面講到的函式定義的方法,還可以在返回列表中執行返回引數的名稱(相當於定義一個引數名稱),這個引數在函式體中不需要再進行定義

下面是一個例子

func add(a, b int) (sum int, err error) {
    sum = a + b
    return sum, err
    //也可以直接写return
}

2、函式的可變引數

函式在進行引數傳遞的時候沒可能不知道要傳多少個值,因此go語言中函式的引數列表中可以使用可變引數

可變引數的基本定義方式如下:

func 函数名(参数列表...类型) (返回值列表) {
    函数体
}

舉一個簡單的例子:

func add(a ...int)(sum int, err error) {
    for _, value := range a {
        sum += value
    }
}
c := 1
d := 2
sum, _ := add(a, b, 3, 4)
fmt.Println(sum)

其中a是一個int型別的切片,在函式中可以使用a[0]、a[1]等方式訪問切片中的元,也可以透過for迴圈進行訪問

3、函式一等公民特性

函式的一等公民特性,能夠當作引數進行傳遞、作為返回值進行返回、賦值給變數等操作

大大提升了函式的靈活性和複用性

3.1、將函式作為變數賦值

舉個例子:

func add(a ...int)(sum int, err error) {
    for _, value := range a {
        sum += value
    }
}

func main(){
    //这里将add函数作为参数赋值给变量funcVar
    funcVar := add
    c := 1
    d := 2
    sum, _ := funcVar(a, b, 3, 4)
    fmt.Println(sum)
}

3.2、將函式作為返回值

舉個例子:

func cal(op string) func(){
    switch op {
    case "+":
        return func(){
            fmt.Println("这是加法")
        }

    case "-":
        return func(){
            fmt.Println("这是减法")
        }
    default:
        return func(){
            fmt.Println("这不是加法也不是减法")
        }
    }
}

//因为返回的是函数,所以在调用的时候需要加上()
cal("+")()

3.3、將函式作為引數進行傳遞

舉個例子(沒啥意義,只是說明用法):

func add(a, b int) int{
    fmt.Printf("sum is %d\n", a + b)
}
func cal(y int, myfunc func(int, int)) {
    myfunc(y, 2)
}

cal(1, add)

//输出结果为sum is 3
//调用cal函数传递add函数成为myfunc, 然后1+2

3.4、匿名函式

匿名函式就是沒有函式名的函式,匿名函式可以作為引數進行傳遞,也可以作為返回值進行返回

匿名函式是在傳遞或者返回的時候進行定義

//匿名函数作为参数进行传递
func add(a, b int) int{
    fmt.Printf("sum is %d\n", a + b)
}
func cal(y int, myfunc func(int, int)) {
    myfunc(y, 2)
}

cal(1, func(a, b int){
    fmt.Printf("total is %d\n", a + b)
})

//输出结果为total is 3
//调用cal函数传递临时定义的匿名函数函数成为myfunc, 然后1+2


//匿名函数作为变量进行赋值
func cal(y int, myfunc func(int, int)) {
    myfunc(y, 2)
}
localFunc := func(a, b int){
    fmt.Printf("local is %d\n", a + b)
}

cal(1, localFunc)

//输出结果为local is 3
//调用cal函数传递localFunc成为myfunc, 然后1+2,跟上边的效果是一样的

4、go中函式的閉包特性

有一個需求,希望有一個函式每次呼叫返回的結果值都是增加一次之後的值

實現這個需求通常的方法是設定一個全域性變數,每次呼叫函式的時候,將全域性變數的值加1,然後返回全域性變數的值

全域性變數方式實現:

var local int
func add() int{
    local += 1
    return local
}

for i := 0; i < 5; i++ {
    fmt.Println(add())
}

但是這種情況下會出現一個問題,想讓local歸零很難實現,而且被迫宣告瞭一個全集變數

因此go中設計了閉包的特性,下面是一個例子:

func auotoAdd() func() int {
    local := 0
    return func() int {
        return local
    }
}

nextFunc := auotoAdd()
for i := 0; i < 5; i++ {
    fmt.Println(nextFunc())
}
//输出结果为0 1 2 3 4
//想要归零的话,只需要重新调用auotoAdd()函数即可

根據上面的例子可以得出閉包的定義:是指一個函式能夠訪問其外部作用域中的變數,即使外部函式已經結束執行。

比如:在一個函式中的匿名函式能夠訪問這個函式中的區域性變數,這個匿名函式稱為閉包。

5、defer的應用場景

defer可以理解為其它語言中的finally,在函式執行完畢之後執行,通常用於釋放資源、關閉檔案、關閉資料庫連線等操作

連線資料庫、開啟檔案、開始鎖等場景下,無論執行是否成功都要記得進行關閉操作,否則會造成資源洩漏等問題

// 一个在Java中的例子
try {
    // 可能会发生异常的代码
} catch (Exception e) {
    // 异常处理代码
} finally {
    // 无论是否发生异常,都会执行的代码
}

在Java中try和finally之間的距離可能很遠,容易忘記關閉資源,而在go中可以使用defer來解決這個問題

在go中defer的使用方式如下:

var mu sync.Mutex

mu.lock()
defer mu.unlock() //defer后面的代码会在return之前执行

defer語句成對出現,可以防止忘記關閉資源,機制更好用,程式碼更簡潔

defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
fmt.Println("4")
//输出结果为4 3 2 1

defer的執行順序是先進後出的,類似於棧的概念

defer的應用可以修改返回值

// 在这个例子中,defer 推迟执行的是一个匿名函数
// deferReturn 函数被调用,准备返回值 10。
// 在返回之前,defer 推迟执行的匿名函数被执行。
// 匿名函数中,result 被增加 1,因此 result 的值从 10 变为 11。
// deferReturn 函数返回修改后的 result 值,即 11。
// 因此,尽管 deferReturn 函数的返回语句是 return 10,但由于 defer 推迟执行的函数在返回之前修改了 result 的值,所以最终的输出结果是 11。这也展示了在 Go 语言中,defer 可以修改函数的返回值。

// 搞不懂就看源码

func deferReturn() (result int){
    defer func(){
        result++
    }()
    return 10
}

ret := deferReturn()
fmt.Println(ret) //输出结果为11

6、go的error、recover和panic

在go語言中關於出錯處理最重要的概念有三個:

1、error

2、panic

3、recover

go中關於語言錯誤處理的理念:不應該使用異常處理控制流程,而應該使用錯誤值。

其他語言在使用異常處理控制流程時,通常會使用try catch來捕獲異常,類似於包住函式 {: .prompt-tip }

在go語言中開發函式的人需要有一個返回值告訴呼叫者是否成功,go的設計者要求我們必須處理這個err,在go的程式碼中,會大量出現”if err != nil”這樣的語句

go的設計者認為必須處理這個err,繼續防禦性程式設計

所以go中使用panic來處理錯誤,panic會中斷當前的函式執行,然後在呼叫函式中查詢是否有recover,如果有recover則會執行recover,否則會一直向上查詢,直到找到recover或者程式崩潰

6.1、error

在go中error有專門的包,下面舉個例子:

import (
    "fmt"
    "errors"
)

func A() (int, error) {
    return 1, errors.New("这是一个错误")
}

func main() {
    if _, err := A(); err!= nil {
        fmt.Println(err)
    }
}

6.2、panic

panic是go中的內建函式,這個函式會導致程式退出,使用的場景不是很多

再舉個例子:

import (
    "fmt"
    "errors"
)

func A() (int, error) {
    panic("this is a panic")
    fmt.Println("能执行到这吗?")
    return 1, errors.New("这是一个错误")
}

func main() {
    if _, err := A(); err!= nil {
        fmt.Println(err)
    }
}

// 在上面的例子中,panic会导致程序退出,所以fmt.Println("能执行到这吗?")不会被执行,只会打印错误栈

panic會導致程式的退出,注意平時開發中不能隨便使用,下面是它的一些應用場景:

在一個服務啟動的過程中,開始必須要準備好一些依賴服務,日誌檔案是否存在、資料庫是否能連線、配置檔案有沒有問題等等,準備好後才能啟動服務

如果在進行服務啟動檢查的過程中,發現任何一項需求不能被滿足,就主動呼叫panic,讓程式退出

一旦伺服器啟動了,某行程式碼中不小心呼叫了panic,程式掛了就是非常嚴重的事故

在go中一些地方也會被動觸發panic:

1、陣列越界

2、空指標

……

為了能夠應對被動觸發panic的情況,go中提供了recover函式,recover函式可以捕獲panic,並且讓程式繼續執行,下一節介紹recover函式

panic後會返回一個interface{}型別的值,這個值就是panic的值

6.3、recover

下面是一個recover使用的例子:

// 在go中map的使用需要进行初始化,否则会报错,这是一个会被动触发panic的场景

func A() {
    defer func(){
        if err := recover(); err!= nil {
            fmt.Println("recoverd is A(): ", err)
        }
    }()
    var names map[string]string
    names["xiaoming"] = "go工程师"
    return 0, errors.New("这是一个错误")
}
// 上面的代码会输出recoverd is A(): assignment to entry nil map

還有一些使用的注意事項:

1、recover只能在defer函式中使用才能生效

2、defer需要在panic之前進行定義

3、recover處理異常後,邏輯並不會恢復到panic的點繼續執行

4、多個defer會形成棧,後定義的defer會先執行

關於這篇文章的日期

保留舊站原始文章標註的日期;實際開始寫作的時間可能更早。

原始日期: 2024-10-18 15:56:31 +0800

同一系列 · Go 基礎學習

上一篇golang學習記錄(7) ←下一篇golang學習記錄(9) →