乐趣区

关于go: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 函数。

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

下图阐明了这个问题:

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

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

退出移动版