本文介绍如何在 Golang 中整合静态资源文件,将静态资源文件编译到二进制可执行文件中,不管是图片还是其它的文件,实际上都可以当成资源文件,都可以嵌入到可执行文件中。这与其它程序的打包可能是一个概念,也可能不是,后续有空研究再补充。
起因
大概10年前,即2011年,研究了如何将图片嵌入到可执行文件中的方法,当时主要使用 C 语言实现,在 ARM 板子上测试。 那篇文章如下图:
当时对技术的兴趣比较浓厚,没想过房子车子的事,现在经常想房子车子,但也被迫对技术感兴趣。现在再使用 Golang 语言重新研究一下。
实践
经查,有2个类似的工具:go-bindata 和 go-bindata-assetfs。两者可以将文件转换成 golang 语言代码,后者似乎依赖于前者,本着使用的目的,暂未研究细节,看了一下生成的 golang 代码,有对外提供的接口,有文件映射表,有真正存储文件的字节流。
安装
使用go get
命令安装:
goget-ugithub.com/go-bindata/go-bindata/...goget-ugithub.com/elazarl/go-bindata-assetfs/...
输入对应的命令验证:
go-bindatago-bindata-assetfs
生成
为适合项目目录,本文约定使用 static 目录存放静态资源文件——即需要打包到可执行程序中的文件,生成的代码,存放到 bindata 目录,且其包名亦为 bindata。经研究发现似乎 go-bindata-assetfs 更好一些,因此本文使用该工具,生成命令如下:
go-bindata-assetfs-o=bindata/bindata.go-pkg=bindata-ignore="README.md"-prefix=staticstatic/...
-o
指定了输出文件,-pkg
指定包名(一般与前者保持一致),-ignore
指定需忽略的文件,-prefix
指定文件路径前缀(本例中,指定了前缀,不需在代码中使用static
前缀)。如果不需要如此复杂,可将其生成的文件与包 main 在同一目录,包名亦为 main,可用于简单测试:
go-bindata-assetfs-o=bindata.go-ignore="README.md"-prefix=staticstatic/...
为了调试方便——即不需要每次更新文件都要重新编译代码,则可以添加-debug
参数,命令如下:
go-bindata-assetfs-debug-o=bindata.go-ignore="README.md"-prefix=staticstatic/...
添加-debug
选项后,当修改了原资源文件后,重新运行程序,获取的内容会发生变化,不需要重新生成,方便调试。内部实现原理:在调用 bindataRead 读取文件时,添加文件的绝对路径。如果是非 debug 版本,则不加路径。
测试
资源文件目录 static 如下:
$treestatic/static/|--conf|`--config.toml|--html|`--foo.html`--libfoo.so2directories,3files
主要使用的接口如下:
//获取所有的文件名称filenames:=bindata.AssetNames()//读取某一文件的内容filename="html/foo.html"content,err=bindata.Asset(filename)
指定的文件,以static
为根目录,其形式与一般的路径无差异。
完整测试代码如下:
packagemainimport("fmt""strings""io/ioutil""bindata_test2/bindata")funcmain(){fmt.Println("bindatatest..");//遍历所有文件,打印文件名,并输出html的内容filenames:=bindata.AssetNames()for_,item:=rangefilenames{fmt.Println("gotfile:",item)if!strings.HasSuffix(item,".tmpl")&&!strings.HasSuffix(item,".html"){continue}content,err:=bindata.Asset(item)iferr!=nil{fmt.Printf("notfoundfile%s:%s\n",item,err.Error())}fmt.Println(string(content))fmt.Println("-----------------------------------\n")}//单独测试filename:="assets/foo.html"content,err:=bindata.Asset(filename)iferr!=nil{fmt.Printf("notfoundfile%s:%s\n",filename,err.Error())}filename="foo.html"content,err=bindata.Asset(filename)iferr!=nil{fmt.Printf("notfoundfile%s:%s\n",filename,err.Error())}filename="html/foo.html"content,err=bindata.Asset(filename)iferr!=nil{fmt.Printf("notfoundfile%s:%s\n",filename,err.Error())}//content为二进制buf,怎么用?filename="conf/config.toml"content,err=bindata.Asset(filename)iferr!=nil{fmt.Printf("notfoundfile%s:%s\n",filename,err.Error())}fmt.Println(string(content))//读取so并保存filename="libfoo.so"content,err=bindata.Asset(filename)iferr!=nil{fmt.Printf("notfoundfile%s:%s\n",filename,err.Error())return}//filename="libfoo.so"err=ioutil.WriteFile(filename,content,0755)iferr!=nil{fmt.Println("writefileerror:",err)return}fmt.Printf("writefile%sok\n",filename)}
以 libfoo.so 文件为例,原文件和保存的文件对比如下:
$md5sum.exestatic/libfoo.solibfoo.so9416ab261b2867d9acbb563690116885*static/libfoo.so9416ab261b2867d9acbb563690116885*libfoo.so
两者内容是相同的。
扩展
本文所述方法,有一定范围内可以使用,对于大型项目或多人协作项目,不建议使用。 针对该方法,笔者认为可以进行的事有: 1、将 web 服务有关的 css、js、html 等整合到可执行二进制文件中,方便部署。在笔者即将实现的 web 服务中,由于功能唯一,又是内部使用,且还只是由笔者个人实现,因此对技术栈拥有完全自主的决定权,通俗地讲,同事和上头不管技术细节,能实现功能即可,为了方便自己,故如此设计。 2、动态库整合,如果涉及动态库文件的使用,则可以将动态库打包到可执行文件,在运行时读取并保存到指定目录,再加载。此法将二者绑定一起,无法做到只更新动态库文件,因此需慎重。 3、配置文件整合,对于需配置文件的程序而言,在部署时需自带配置文件,或默认首次运行时生成。对于后者,有的直接在代码中固定配置,根据情况写到指定目录,使用本文,则直接将配置文件打包到二进制文件,如不存在,则再写到指定目录。 4、其它待探索发现并实施。