10.5. 匿名导入

如果只是导入一个包而并不使用导入的包是一个编译错误. 但是有时候我们只是想利用导入包产生的副作用: 它会计算包级变量的初始化表达式和执行导入包的 init 初始化函数 (§2.6.2). 这时候我们需要抑制“未使用的导入”错误是合理的, 我们可以用下划线 _ 来重命名导入的包. 像往常一样, 下划线 _ 为空白标识符, 并不能被访问.

import _ "image/png" // register PNG decoder

这个被称为匿名导入. 它通常是用来实现一个编译时机制, 然后通过在main主程序入口选择性地导入附加的包. 首先, 让我们看看如何使用它, 然后再看看它是如何工作的:

标准库的 image 图像包导入了一个 Decode 函数, 用于从 io.Reader 接口读取数据并解码图像, 它调用底层注册的图像解码器工作, 然后返回 image.Image 类型的图像. 使用 image.Decode 很容易编写一个图像格式的转换工具, 读取一种格式的图像, 然后编码为另一种图像格式:

gopl.io/ch10/jpeg
// The jpeg command reads a PNG image from the standard input
// and writes it as a JPEG image to the standard output.
package main

import (
    "fmt"
    "image"
    "image/jpeg"
    _ "image/png" // register PNG decoder
    "io"
    "os"
)

func main() {
    if err := toJPEG(os.Stdin, os.Stdout); err != nil {
        fmt.Fprintf(os.Stderr, "jpeg: %v\n", err)
        os.Exit(1)
    }
}

func toJPEG(in io.Reader, out io.Writer) error {
    img, kind, err := image.Decode(in)
    if err != nil {
        return err
    }
    fmt.Fprintln(os.Stderr, "Input format =", kind)
    return jpeg.Encode(out, img, &jpeg.Options{Quality: 95})
}

如果我们将 gopl.io/ch3/mandelbrot (§3.3) 的输出导入到这个工具的输入, 它将解码输入的PNG格式图像, 然后转换为JPEG格式的图像(图3.3).

$ go build gopl.io/ch3/mandelbrot
$ go build gopl.io/ch10/jpeg
$ ./mandelbrot | ./jpeg >mandelbrot.jpg
Input format = png

要注意 image/png 包的匿名导入语句. 如果没有这一行语句, 依然可以编译和运行, 但是它将不能识别 PNG 格式的图像:

$ go build gopl.io/ch10/jpeg
$ ./mandelbrot | ./jpeg >mandelbrot.jpg
jpeg: image: unknown format

下面的代码演示了它的工作机制. 标准库提供了GIF, PNG, 和 JPEG 格式图像的解码器, 用户也可以提供自己的解码器, 但是为了保存程序体积较小, 很多解码器并没有被包含尽量, 除非是明确需要支持的格式. image.Decode 函数会查询支持的格式列表. 列表的每个入口指定了四件事情: 格式的名称; 一个用于描述这种图像数据开头部分模式的字符串, 用于解码器检测识别; 一个 Decode 函数 用于解码图像; 一个 DecodeConfig 函数用于解码图像的大小和颜色空间的信息. 每个入口是通过调用 image.RegisterFormat 函数注册, 一般是在每个格式包的初始化函数中调用, 例如 image/png 包是这样的:

package png // image/png

func Decode(r io.Reader) (image.Image, error)
func DecodeConfig(r io.Reader) (image.Config, error)

func init() {
    const pngHeader = "\x89PNG\r\n\x1a\n"
    image.RegisterFormat("png", pngHeader, Decode, DecodeConfig)
}

最终的效果是, 主程序值需要匿名导入需要 image.Decode 支持的格式对应解码包就可以解码图像了.

数据库包 database/sql 也是采用了类似的技术, 让用户可以根据自己需要选择导入必要的数据库驱动. 例如:

import (
    "database/mysql"
    _ "github.com/lib/pq"              // enable support for Postgres
    _ "github.com/go-sql-driver/mysql" // enable support for MySQL
)

db, err = sql.Open("postgres", dbname) // OK
db, err = sql.Open("mysql", dbname)    // OK
db, err = sql.Open("sqlite3", dbname)  // returns error: unknown driver "sqlite3"

练习 10.1: 扩展 jpeg 程序, 支持任意图像格式之间的相互转换, 使用 image.Decode 检测支持的格式类型, 然后同步 flag 命令行标志参数选择输出的格式.

练习 10.2: 设计一个通用的压缩文件读取框架, 用来读取 ZIP(archive/zip) 和 POSIX tar(archive/tar) 格式压缩的文档. 使用类似上面的注册机制来扩展支持不同的压缩格式, 然后根据需要通过匿名导入选择支持的格式.