本文首发于 IEG增长中台技术团队公众号。

起因

上周部署一个新项目,项目的配置文件跟随项目目录存放,还未放到配置中心。构建镜像运行时,因为没有拷贝配置文件,项目编译后的二进制文件读不到配置,服务起不来。

这是很多Go开发者或多或少都会遇到的经典文件路径问题。

这时要么写个部署脚本,每次构建镜像,把配置文件跟编译后的二进制文件放在一起。

但这时我想到了Go embed,今年Go发布了1.16,1.17两个版本,其中1.16里边的Go embed及io/fs就解决了这个经典文件路径问题。

image.png

Go embed使用

传统的文件读取代码,配置文件需要跟随二进制文件发布:

func main() {
    fPath := "conf/conf.ini"
    c, err := ioutil.ReadFile(fPath)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(string(c))
}

image.png

换成Go embed的方式,配置文件会编译进最终的二进制文件,发布不需要拷贝配置文件,只需注解声明一个embed配置和定义一个embed变量:

//go:embed conf/conf.ini
var f embed.FS

func main() {
    fPath := "conf/conf.ini"
    data, err := f.ReadFile(fPath)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(string(data))
}

image.png

源码实现

通过上面的例子,我们可以发现这个Go embed是编译期的行为,而且还是以注解的方式嵌入,有点新奇。Go的注解,目前我能想到的只有两个,分别是go:generatego:build,后者还是1.17新推出的注解,这感觉是Go逐渐Java化?

获取注解

执行go build -n查看embed方式代码的编译过程,下面是整个过程的部分截图。Go编译的核心有三个命令:compile/buildid/link(不在截图中),在compile动作之前,我们可以看到跟embed注解相关的内容就是红框内的embedcfg文件,这里会将注解后面的文件路径"conf/conf.ini"信息结构化写到embedcfg文件。
image.png

Go编译期间的代码基本都集中在src/cmd/compile/internal/gc目录下,我们在Main函数里边先找到go:embed注解的获取,就是上面的embedcfg:
image.png

一顿readEmbedCfg操作把刚刚的文件Unmarshal成正经的embedCfg struct

image.png

embedcfg的使用

embedcfg结构化之后,还是compile的Main函数,里面会调用一个initEmbed的方法检查embedcfg的文件信息,并读取该路径对应的文件,将文件内容读取出来,最终跟compile期间的其他所有内容写到一个中间文件_pkg_.a,检查和读取文件的关键子方法为下图的fileStringSym,这里可以看到我们熟悉的文件系统调用open跟stat。

image.png
image.png

Go embed背后的设计

上文我们大致了解了go:embed这个注解的使用和实现,但为什么要弄一个这种东西呢?难道就是为了解决配置文件部署问题吗🐶?当然不是,go:embed的出现是为了引人一个全新的文件系统库io/fs

网上冲浪了解到,Go的创始人之一--Rob Pike在2016年就弄了一个提案,1.16之前的os.File并不是个接口,文件系统设计不抽象,只能用来表示系统文件。

image.png

而我们都知道世界上最好的文件系统抽象就是Unix,Unix的设计哲学正是"一切都是文件"。事实上Rob Pike也曾是贝尔实验室(Bell Labs)的Unix团队和Plan 9操作系统计划的成员。

从这个设计思想出发,我们先看go:embed关联的FS对象
image.png

从注释说明中我们也可以看到这个FS对象实现了io/fs包里的FS接口
image.png

这个接口提供的Open方法返回了一个File接口,这个File接口正是文件系统的抽象所在
image.png

有了这个File接口,我们就可以为所欲为,再也不用像以前那样,只能用os.File
image.png

当然,我们也看到,目前的FS对象是个只读对象,不能写,File接口也没有提供Write的方法,是个残缺的文件系统抽象。事实上,也有人在go:embed出来不久就提出了Write方法的提案,参考Write提案,相信这个文件系统会逐渐齐全。

结语

事实上,除了把这个文件系统抽象之外,Go官方为了保障os/File的向前兼容性,还做了很多工作,但Go语言本身的接口设计思想很好地保障兼容改动。比如本文中文件配置的例子,以前是用ioutil.ReadFile,现在是embed.FS.ReadFile,返回结果都还是文件内容和错误,开发者使用起来没啥差别,依然丝滑。

image.png


Kyrie
144 声望12 粉丝