..

在 Go 使用 embed 当中嵌入父目录中的文件

自 Go 1.16 版本开始,Go 提供了将二进制文件打包进入到 Binary 文件当中的机制:`//go:embed。不过,我看到的示例大多数都是嵌入当前文件夹下的子文件夹的示例。并没有嵌入父一级文件夹的示例。

为什么会需要嵌入父目录中的文件?

这是因为不同的项目的构建规则不同。一些小型项目可能只有一个 main.go 或同级目录下几个其他的 go source 文件即可,但对于更大型的项目,合理的项目拆分是有助于帮助提升项目的可维护性的。

以我为例,我的目录结构如下:biz 目录下是我的核心逻辑,也是我日常写代码的地方;conf 则放置了各种配置,比如各种 API Key,ral 则是网络访问层,比如我比较喜欢用 gin 来做底层的网络访问层;script 放置了开发所需要的各种脚本文件;static 则放置了前端所需要的 JS / CSS 文件。在开发一些偏内容向的页面时,我会习惯使用 Server Side Render,所以我在 biz 目录下还有一个 template 目录。而主要的逻辑则放在 handler。

├── biz
│   ├── dal
│   ├── handler
│   ├── logic
│   ├── service
│   └── template
├── conf
├── ral
│   └── gin
├── script
└── static
    └── public

在日常开发时,一个非常常见的操作是在 handler 里处理基本的逻辑,并将 template 中的模板渲染出来,并返回给用户,这个时候就需要在 handler 里使用上一级目录的文件了。

  • 错误的尝试1:使用 .. 来引入

    作为文件系统的常规配置,我自然知道可以通过 .. 来代表上一级目录,因此我试着使用如下的语法来引入文件

    //go:embed ../template/* 结果报了个错误 invalid pattern syntax。看得出来,是因为我的语法出现了问题。

    通过搜索,我找到了这个 Github issue,才得知,由于避免跨模块的访问,go embed 阻止了 .. 语法的使用

    Because embed.Files implements fs.FS, it cannot provide access to files with names beginning with .., so files in parent directories are also disallowed entirely, even when the parent directory named by .. does happen to be in the same module.

    注: https://go.googlesource.com/proposal/+/master/design/draft-embed.md

  • 错误的尝试2: 使用 go:generate 来执行复制

    在上面的 issue 当中,我注意到还有另外一个方式,可以实现类似的效果。可以借助 go:generate来实现在执行 go generate 方法时,复制文件到本地,这样就可以实现在 Build 二进制文件之前,把文件复制到本地,即规避了跨模块的问题,也避免了将 template 文件放在本地,污染代码目录的问题。

    //go:generate cp -r ../../assets ./local-asset-dir
    //go:embed local-asset-dir
    var assetFs embed.FS
    

    这个方法在运行的时候没有效果,不过在其他人可能是有效果的(我猜是因为我开发时很少直接用 go generate,都是直接 go run 了)。不过,这个方法也有个问题,因为是写在代码本身当中的,很有可能后续因为某些原因,这些代码被误删除或修改了,导致整个系统的运行不正常。而散落在各文件的描述也会使得修复十分复杂。

  • 正确的做法

    在看这个 issue 的最后,我注意到了正确的用法,在另外一个 issue 当中,开发者 tomasf 提到了,我们应该在 asset 目录下创建一个 embed.go 来完成 embedFs 的建立,并在其他文件直接引入这个文件,完成定义。

    You can create a embed.go file in your assets directory and make the assets available as it’s own package to the rest of your program.

    这个说法一出,豁然开朗!尝试一下,果然有效。我在 template 目录下创建了一个 embed.go 文件,并添加了如下代码。

    package template
    
    import "embed"
    
    //go:embed *
    var TemplateFs embed.FS
    

    并在另外一个文件当中使用 template.TemplateFs.ReadFile("index.tmpl") 来完成模板文件的引用。这样既不违背 golang 的跨模块,也不会使得代码不可维护,非常好。

参考阅读