本文首发于 IEG增长中台技术团队公众号。
起因
上周部署一个新项目,项目的配置文件跟随项目目录存放,还未放到配置中心。构建镜像运行时,因为没有拷贝配置文件,项目编译后的二进制文件读不到配置,服务起不来。
这是很多Go开发者或多或少都会遇到的经典文件路径问题。
这时要么写个部署脚本,每次构建镜像,把配置文件跟编译后的二进制文件放在一起。
但这时我想到了Go embed,今年Go发布了1.16,1.17两个版本,其中1.16里边的Go embed及io/fs就解决了这个经典文件路径问题。
Go embed使用
传统的文件读取代码,配置文件需要跟随二进制文件发布:
func main() {
fPath := "conf/conf.ini"
c, err := ioutil.ReadFile(fPath)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(c))
}
换成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))
}
源码实现
通过上面的例子,我们可以发现这个Go embed是编译期的行为,而且还是以注解的方式嵌入,有点新奇。Go的注解,目前我能想到的只有两个,分别是go:generate跟go:build,后者还是1.17新推出的注解,这感觉是Go逐渐Java化?
获取注解
执行go build -n
查看embed方式代码的编译过程,下面是整个过程的部分截图。Go编译的核心有三个命令:compile/buildid/link(不在截图中),在compile动作之前,我们可以看到跟embed注解相关的内容就是红框内的embedcfg文件,这里会将注解后面的文件路径"conf/conf.ini"信息结构化写到embedcfg文件。
Go编译期间的代码基本都集中在src/cmd/compile/internal/gc目录下,我们在Main函数里边先找到go:embed注解的获取,就是上面的embedcfg:
一顿readEmbedCfg操作把刚刚的文件Unmarshal成正经的embedCfg struct:
embedcfg的使用
embedcfg结构化之后,还是compile的Main函数,里面会调用一个initEmbed
的方法检查embedcfg的文件信息,并读取该路径对应的文件,将文件内容读取出来,最终跟compile期间的其他所有内容写到一个中间文件_pkg_.a,检查和读取文件的关键子方法为下图的fileStringSym
,这里可以看到我们熟悉的文件系统调用open跟stat。
Go embed背后的设计
上文我们大致了解了go:embed这个注解的使用和实现,但为什么要弄一个这种东西呢?难道就是为了解决配置文件部署问题吗🐶?当然不是,go:embed的出现是为了引人一个全新的文件系统库io/fs。
网上冲浪了解到,Go的创始人之一--Rob Pike在2016年就弄了一个提案,1.16之前的os.File并不是个接口,文件系统设计不抽象,只能用来表示系统文件。
而我们都知道世界上最好的文件系统抽象就是Unix,Unix的设计哲学正是"一切都是文件"。事实上Rob Pike也曾是贝尔实验室(Bell Labs)的Unix团队和Plan 9操作系统计划的成员。
从这个设计思想出发,我们先看go:embed关联的FS对象:
从注释说明中我们也可以看到这个FS对象实现了io/fs包里的FS接口:
这个接口提供的Open方法返回了一个File接口,这个File接口正是文件系统的抽象所在:
有了这个File接口,我们就可以为所欲为,再也不用像以前那样,只能用os.File:
当然,我们也看到,目前的FS对象是个只读对象,不能写,File接口也没有提供Write的方法,是个残缺的文件系统抽象。事实上,也有人在go:embed出来不久就提出了Write方法的提案,参考Write提案,相信这个文件系统会逐渐齐全。
结语
事实上,除了把这个文件系统抽象之外,Go官方为了保障os/File的向前兼容性,还做了很多工作,但Go语言本身的接口设计思想很好地保障兼容改动。比如本文中文件配置的例子,以前是用ioutil.ReadFile
,现在是embed.FS.ReadFile
,返回结果都还是文件内容和错误,开发者使用起来没啥差别,依然丝滑。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。