golang中的init初始化函式

slowquery發表於2022-10-18

0.1、索引

waterflow.link/articles/1666090530...

1、概念

1.1、原始檔裡的程式碼執行順序

init 函式是用於初始化應用程式狀態的函式。 它不接受任何引數並且不返回任何結果(一個 func() 函式)。 初始化包時,將初始化包中的所有常量和變數宣告。 然後,執行初始化函式。 下面是一個初始化主包的例子:

package main

import "fmt"

// 1
var a = func() int {
    fmt.Println("var a")
    return 0
}()

// 2
func init()  {
    fmt.Println("init")
}

// 1
var b = func() int {
    fmt.Println("var b")
    return 1
}()

// 3
func main() {
    fmt.Println("main")
}

上面程式碼的初始化順序是:

  1. 初始化常量/變數(雖然b在init函式後面,但是會首先初始化)
  2. 初始化init函式
  3. 執行main函式

我們看下列印的結果:

go run 2.go
var a
var b
init
main

1.2、不同包的init函式執行順序

初始化包時會執行一個 init 函式。 在下面的例子中,我們定義了兩個包,main 和 redis,其中 main 依賴於 redis。 首先, 2 .go 是主包:

package main

import (
    "fmt"
    "go-demo/100gomistakes/2/redis"
)

// 2
func init()  {
    fmt.Println("main init")
}

// 3
func main() {
    err := redis.Store("ni", "hao")
    fmt.Println(err)
}

我們可以看到main包中呼叫了redis包的方法。

我們再看下redis包中的內容:

package redis

import "fmt"

// 1
func init()  {
    fmt.Println("redis init")
}

func Store(key, value string) error {
    return nil
}

因為main依賴redis,所以先執行redis包的init函式,然後是main包的init,然後是main函式本身。上面的程式碼中標明瞭執行順序。

1.3、同一個包不同檔案init執行順序

我們可以為每個包定義多個初始化函式。 當我們這樣做時,包內的 init 函式的執行順序是基於原始檔的字母順序。 例如,如果一個包包含一個 a.go 檔案和一個 b.go 檔案,並且都有一個 init 函式,則首先執行 a.go init 函式。

但是如果我們把檔案a.go改為ca.go,則會先執行b.go的init函式。

所以我們不應該依賴包中初始化函式的順序。 實際上,這可能很危險,因為可以重新命名原始檔,從而可能影響執行順序。

下圖說明了這個問題:
https://i.iter01.com/images/6140bd8fdce6c422d39787fc8966608eff40eb29c5ea4bdcb15dbbeb36175398.png

1.4、同一個檔案中的init函式

當然,我們還可以在同一個原始檔中定義多個初始化函式:

package main

import (
    "fmt"
    "go-demo/100gomistakes/2/redis"
)


func init()  {
    fmt.Println("main init1")
}

func init()  {
    fmt.Println("main init2")
}


func main() {
    err := redis.Store("ni", "hao")
    fmt.Println(err)
}

執行順序是按照原始檔順序執行的,我們看下列印的結果:

go run 2.go
redis2 init
redis init
main init1
main init2
<nil>

1.5、以副作用方式執行的init函式

我們開發的時候經常會使用gorm查詢資料庫,所以我們經常會看到在初始化db連線的時候會有下面的程式碼:

package models

import (
    _ "github.com/go-sql-driver/mysql"
    "github.com/jinzhu/gorm"
)

這種情況就是說,我們沒有強依賴mysql包(沒有直接使用mysql的公共函式)。但是我們需要初始化mysql包裡的一些資料,也就是執行mysql包裡的init函式。這個時候我們就可以在這個包的前面加上_

需要注意的是init函式不能被當作普通函式呼叫,會編譯報錯

2、使用場景

首先,讓我們看一個使用 init 函式可能被認為不合適的示例:持有資料庫連線池。 在示例的 init 函式中,我們使用 sql.Open 開啟一個資料庫。 這個全域性變數db會被其他函式呼叫:

package main

import (
    "database/sql"
    "fmt"
    _ "github.com/go-sql-driver/mysql"
    "log"
)

var db *sql.DB

func init() {
    d, err := sql.Open("mysql",
        "root:liufutian@tcp(127.0.0.1:3306)/test")
    if err != nil {
        log.Panic(err)
    }

  err := db.Ping()
    if err != nil {
        log.Panic(err)
    }

    db = d
}

func main() {


}

在本例中,我們開啟資料庫,檢查是否可以 ping 通,然後將其分配給全域性變數。

但是這種實現會帶來一些問題:

  1. init 函式中的錯誤管理是有限的。 實際上,由於 init 函式不返回錯誤,因此發出錯誤訊號的唯一方法之一就是panic,導致應用程式停止。 在我們的示例中,如果開啟資料庫失敗,無論如何都會停止應用程式。 但是,不一定要由包本身來決定是否停止應用程式。 有可能呼叫者想實現重試或使用回退機制。 在這種情況下,在 init 函式中開啟資料庫會阻止客戶端包實現其錯誤處理邏輯。
  2. 如果我們向這個檔案新增測試,init 函式將在執行測試用例之前執行,但是我們想要的是建立db連線的時候需要測試。 因此,此示例中的 init 函式使編寫單元測試變得複雜。
  3. 該示例需要將資料庫連線池分配給全域性變數。 全域性變數有一些嚴重的缺點; 例如:
    • 任何函式都可以更改包中的全域性變數
    • 單元測試可能會更復雜,因為依賴於全域性變數的函式將不再被隔離

在大多數情況下,我們應該傾向於封裝一個變數而不是讓它保持全域性。

由於這些原因,之前的初始化可能應該封裝到一個函式中處理,如下所示:

func createDB() (*sql.DB, error) {
    d, err := sql.Open("mysql",
        "root:liufutian@tcp(127.0.0.1:3306)/test")
    if err != nil {
        return nil, err
    }
    err = db.Ping()
    if err != nil {
        return nil, err
    }
    return d, nil
}

這樣寫的話,我們解決了之前討論的主要缺點:

  • 是否處理錯誤留給呼叫者
  • 可以建立一個整合測試來檢查此功能是否有效
  • 連線池封裝在函式中

但是這樣就是不能使用init函式了麼?在我們上面的引入mysql驅動的例子中,說明使用init還是有幫助的:

func init() {
    sql.Register("mysql", &MySQLDriver{})
}

上面的例子,透過註冊提供的驅動名稱使資料庫驅動程式可用。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章