在init函数中实现“注册模式”

在init函数中实现“注册模式”

在init函数中实现“注册模式” #

为了让你更好地理解,首先我们来看一段使用 lib/pq 包访问 PostgreSQL 数据库的代码示例:

import (
    "database/sql"
    _ "github.com/lib/pq"
)
func main() {
    db, err := sql.Open("postgres",
                        "user=pqgotest dbname=pqgotest sslmode=verify-full")
    if err != nil {
        log.Fatal(err)
    }

    age := 21
    rows, err := db.Query("SELECT name FROM users WHERE age = $1", age)
    ...
}

其实,这是一段“神奇”的代码,你可以看到示例代码是以空导入的方式导入 lib/pq 包的,main 函数中没有使用 pq 包的任何变量、函数或方法,这样就实现了对 PostgreSQL 数据库的访问。而这一切的奥秘,全在 pq 包的 init 函数中:

func init() {
    sql.Register("postgres", &Driver{})
}

这个奥秘就在,我们其实是利用了用空导入的方式导入 lib/pq 包时产生的一个“副作用”,也就是 lib/pq 包作为 main 包的依赖包,它的 init 函数会在 pq 包初始化的时候得以执行。

从上面代码中,我们可以看到在 pq 包的 init 函数中,pq 包将自己实现的 sql 驱动注册到了 sql 包中。这样只要应用层代码在 Open 数据库的时候,传入驱动的名字(这里是“postgres”),那么通过 sql.Open 函数,返回的数据库实例句柄对数据库进行的操作,实际上调用的都是 pq 包中相应的驱动实现。

实际上,这种通过在 init 函数中注册自己的实现的模式,就有效降低了 Go 包对外的直接暴露,尤其是包级变量的暴露,从而避免了外部通过包级变量对包状态的改动。

另外,从标准库 database/sql 包的角度来看,这种“注册模式”实质是一种工厂设计模式的实现,sql.Open 函数就是这个模式中的工厂方法,它根据外部传入的驱动名称“生产”出不同类别的数据库实例句柄。

这种“注册模式”在标准库的其他包中也有广泛应用,比如说,使用标准库 image 包获取各种格式图片的宽和高:

package main
import (
    "fmt"
    "image"
    _ "image/gif" // 以空导入方式注入gif图片格式驱动
    _ "image/jpeg" // 以空导入方式注入jpeg图片格式驱动
    _ "image/png" // 以空导入方式注入png图片格式驱动
    "os"
)
func main() {
    // 支持png, jpeg, gif
    width, height, err := imageSize(os.Args[1]) // 获取传入的图片文件的宽与高
    if err != nil {
        fmt.Println("get image size error:", err)
        return
    }
    fmt.Printf("image size: [%d, %d]\n", width, height)
}
func imageSize(imageFile string) (int, int, error) {
    f, _ := os.Open(imageFile) // 打开图文文件
    defer f.Close()
    img, _, err := image.Decode(f) // 对文件进行解码,得到图片实例
    if err != nil {
        return 0, 0, err
    }
    b := img.Bounds() // 返回图片区域
    return b.Max.X, b.Max.Y, nil
}

你可以看到,上面这个示例程序支持 png、jpeg、gif 三种格式的图片,而达成这一目标的原因,正是 image/png、image/jpeg 和 image/gif 包都在各自的 init 函数中,将自己“注册”到 image 的支持格式列表中了,你可以看看下面这个代码:

// $GOROOT/src/image/png/reader.go
func init() {
    image.RegisterFormat("png", pngHeader, Decode, DecodeConfig)
}
// $GOROOT/src/image/jpeg/reader.go
func init() {
    image.RegisterFormat("jpeg", "\xff\xd8", Decode, DecodeConfig)
}
// $GOROOT/src/image/gif/reader.go
func init() {
    image.RegisterFormat("gif", "GIF8?a", Decode, DecodeConfig)
}

那么,现在我们了解了 init 函数的常见用途。init 函数之所以可以胜任这些工作,恰恰是因为它在 Go 应用初始化次序中的特殊“位次”,也就是 main 函数之前,常量和变量初始化之后。

Viewpoint #

From #