本文介绍如何在 Golang 中整合静态资源文件,将静态资源文件编译到二进制可执行文件中,不管是图片还是其它的文件,实际上都可以当成资源文件,都可以嵌入到可执行文件中。这与其它程序的打包可能是一个概念,也可能不是,后续有空研究再补充。
起因
大概10年前,即2011年,研究了如何将图片嵌入到可执行文件中的方法,当时主要使用 C 语言实现,在 ARM 板子上测试。 那篇文章如下图:

当时对技术的兴趣比较浓厚,没想过房子车子的事,现在经常想房子车子,但也被迫对技术感兴趣。现在再使用 Golang 语言重新研究一下。
实践
经查,有2个类似的工具:go-bindata 和 go-bindata-assetfs。两者可以将文件转换成 golang 语言代码,后者似乎依赖于前者,本着使用的目的,暂未研究细节,看了一下生成的 golang 代码,有对外提供的接口,有文件映射表,有真正存储文件的字节流。
安装
使用go get
命令安装:
1 2
| go get -u github.com/go-bindata/go-bindata/... go get -u github.com/elazarl/go-bindata-assetfs/...
|
输入对应的命令验证:
1 2
| go-bindata go-bindata-assetfs
|
生成
为适合项目目录,本文约定使用 static 目录存放静态资源文件——即需要打包到可执行程序中的文件,生成的代码,存放到 bindata 目录,且其包名亦为 bindata。经研究发现似乎 go-bindata-assetfs 更好一些,因此本文使用该工具,生成命令如下:
1
| go-bindata-assetfs -o=bindata/bindata.go -pkg=bindata -ignore="README.md" -prefix=static static/...
|
-o
指定了输出文件,-pkg
指定包名(一般与前者保持一致),-ignore
指定需忽略的文件,-prefix
指定文件路径前缀(本例中,指定了前缀,不需在代码中使用static
前缀)。如果不需要如此复杂,可将其生成的文件与包 main 在同一目录,包名亦为 main,可用于简单测试:
1
| go-bindata-assetfs -o=bindata.go -ignore="README.md" -prefix=static static/...
|
为了调试方便——即不需要每次更新文件都要重新编译代码,则可以添加-debug
参数,命令如下:
1
| go-bindata-assetfs -debug -o=bindata.go -ignore="README.md" -prefix=static static/...
|
添加-debug
选项后,当修改了原资源文件后,重新运行程序,获取的内容会发生变化,不需要重新生成,方便调试。内部实现原理:在调用 bindataRead 读取文件时,添加文件的绝对路径。如果是非 debug 版本,则不加路径。
测试
资源文件目录 static 如下:
1 2 3 4 5 6 7 8 9 10
| $ tree static/ static/ |-- conf | `-- config.toml |-- html | `-- foo.html `-- libfoo.so
2 directories, 3 files
|
主要使用的接口如下:
1 2 3 4 5 6
| // 获取所有的文件名称 filenames := bindata.AssetNames()
// 读取某一文件的内容 filename = "html/foo.html" content, err = bindata.Asset(filename)
|
指定的文件,以static
为根目录,其形式与一般的路径无差异。
完整测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| package main
import ( "fmt" "strings" "io/ioutil" "bindata_test2/bindata" )
func main() { fmt.Println("bindata test.."); // 遍历所有文件,打印文件名,并输出html的内容 filenames := bindata.AssetNames() for _, item := range filenames { fmt.Println("got file: ", item)
if !strings.HasSuffix(item, ".tmpl") && !strings.HasSuffix(item, ".html") { truetruetruecontinue truetrue} content, err := bindata.Asset(item) if err != nil { fmt.Printf("not found file %s: %s\n", item, err.Error()) } fmt.Println(string(content)) fmt.Println("-----------------------------------\n") true} // 单独测试 filename := "assets/foo.html" content, err := bindata.Asset(filename) if err != nil { fmt.Printf("not found file %s: %s\n", filename, err.Error()) } filename = "foo.html" content, err = bindata.Asset(filename) if err != nil { fmt.Printf("not found file %s: %s\n", filename, err.Error()) } filename = "html/foo.html" content, err = bindata.Asset(filename) if err != nil { fmt.Printf("not found file %s: %s\n", filename, err.Error()) } // content 为二进制buf,怎么用? filename = "conf/config.toml" content, err = bindata.Asset(filename) if err != nil { fmt.Printf("not found file %s: %s\n", filename, err.Error()) }
fmt.Println(string(content)) // 读取so并保存 filename = "libfoo.so" content, err = bindata.Asset(filename) if err != nil { fmt.Printf("not found file %s: %s\n", filename, err.Error()) return } //filename = "libfoo.so" err = ioutil.WriteFile(filename, content, 0755) if err != nil { fmt.Println("write file error: ", err) return } fmt.Printf("write file %s ok\n", filename) }
|
以 libfoo.so 文件为例,原文件和保存的文件对比如下:
1 2 3
| $ md5sum.exe static/libfoo.so libfoo.so 9416ab261b2867d9acbb563690116885 *static/libfoo.so 9416ab261b2867d9acbb563690116885 *libfoo.so
|
两者内容是相同的。
扩展
本文所述方法,有一定范围内可以使用,对于大型项目或多人协作项目,不建议使用。
针对该方法,笔者认为可以进行的事有:
1、将 web 服务有关的 css、js、html 等整合到可执行二进制文件中,方便部署。在笔者即将实现的 web 服务中,由于功能唯一,又是内部使用,且还只是由笔者个人实现,因此对技术栈拥有完全自主的决定权,通俗地讲,同事和上头不管技术细节,能实现功能即可,为了方便自己,故如此设计。
2、动态库整合,如果涉及动态库文件的使用,则可以将动态库打包到可执行文件,在运行时读取并保存到指定目录,再加载。此法将二者绑定一起,无法做到只更新动态库文件,因此需慎重。
3、配置文件整合,对于需配置文件的程序而言,在部署时需自带配置文件,或默认首次运行时生成。对于后者,有的直接在代码中固定配置,根据情况写到指定目录,使用本文,则直接将配置文件打包到二进制文件,如不存在,则再写到指定目录。
4、其它待探索发现并实施。
李迟 2021.5.5