本文首发于 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
,返回后果都还是文件内容和谬误,开发者应用起来没啥差异,仍然丝滑。