0.1、索引

https://waterflow.link/articles/1666090530880

1、概念

1.1、源文件里的代码执行程序

init 函数是用于初始化应用程序状态的函数。 它不承受任何参数并且不返回任何后果(一个 func() 函数)。 初始化包时,将初始化包中的所有常量和变量申明。 而后,执行初始化函数。 上面是一个初始化主包的例子:

package mainimport "fmt"// 1var a = func() int {    fmt.Println("var a")    return 0}()// 2func init()  {    fmt.Println("init")}// 1var b = func() int {    fmt.Println("var b")    return 1}()// 3func main() {    fmt.Println("main")}

下面代码的初始化程序是:

  1. 初始化常量/变量(尽管b在init函数前面,然而会首先初始化)
  2. 初始化init函数
  3. 执行main函数

咱们看下打印的后果:

go run 2.govar avar binitmain

1.2、不同包的init函数执行程序

初始化包时会执行一个 init 函数。 在上面的例子中,咱们定义了两个包,main 和 redis,其中 main 依赖于 redis。 首先, 2 .go 是主包:

package mainimport (    "fmt"    "go-demo/100gomistakes/2/redis")// 2func init()  {    fmt.Println("main init")}// 3func main() {    err := redis.Store("ni", "hao")    fmt.Println(err)}

咱们能够看到main包中调用了redis包的办法。

咱们再看下redis包中的内容:

package redisimport "fmt"// 1func 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函数。

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

下图阐明了这个问题:

1.4、同一个文件中的init函数

当然,咱们还能够在同一个源文件中定义多个初始化函数:

package mainimport (    "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.goredis2 initredis initmain init1main init2<nil>

1.5、以副作用形式执行的init函数

咱们开发的时候常常会应用gorm查询数据库,所以咱们常常会看到在初始化db连贯的时候会有上面的代码:

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

这种状况就是说,咱们没有强依赖mysql包(没有间接应用mysql的公共函数)。然而咱们须要初始化mysql包里的一些数据,也就是执行mysql包里的init函数。这个时候咱们就能够在这个包的后面加上_

须要留神的是init函数不能被当作一般函数调用,会编译报错

2、应用场景

首先,让咱们看一个应用 init 函数可能被认为不适合的示例:持有数据库连接池。 在示例的 init 函数中,咱们应用 sql.Open 关上一个数据库。 这个全局变量db会被其余函数调用:

package mainimport (    "database/sql"    "fmt"    _ "github.com/go-sql-driver/mysql"    "log")var db *sql.DBfunc init() {    d, err := sql.Open("mysql",        "root:[email protected](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:[email protected](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{})}

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