golang中的init初始化函数

0.1、索引

https://waterflow.link/articles/1666090530880

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函数。

所以我们不应该依赖包中初始化函数的顺序。 实际上,这可能很危险,因为可以重命名源文件,从而可能影响执行顺序。

下图说明了这个问题:
[图片上传失败...(image-79883e-1667261741418)]

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{})
}

上面的例子,通过注册提供的驱动名称使数据库驱动程序可用。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,284评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,115评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,614评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,671评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,699评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,562评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,309评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,223评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,668评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,859评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,981评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,705评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,310评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,904评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,023评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,146评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,933评论 2 355

推荐阅读更多精彩内容