Golang实践录:静态资源文件整合:初步使用

趁着五一放假,趁着有时间,把欠的一些技术集中研究研究,写写文章,好给自己一个交待。
本文介绍如何在 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