趁着五一放假,趁着有时间,把欠的一些技术集中研究研究,写写文章,好给自己一个交待。
本文介绍如何在 Golang 中整合静态资源文件,将静态资源文件编译到二进制可执行文件中,这与其它程序的打包可能是一个概念,也可能不是,后续有空研究再补充。

起因

大概10年前,即2011年,也研究一下这方面的内容,主要针对 C 语言,使用 ARM 板子测试。 那篇文章如下图:
在这里插入图片描述

当时对技术的兴趣比较浓厚,没想过房子车子的事,现在经常想房子车子,但也被迫对技术感兴趣。因此,使用 Golang 语言重新研究一下。

实践

经查,有2个类似的工具:go-bindata 和 go-bindata-assetfs。两者可以将文件转换成 golang 语言代码,后者似乎依赖于前者,本着使用的目的,暂未研究细节,看了一下生成的 golang 代码,有对外提供的接口,有文件映射表,有真正存储文件的字节流。

安装

go get
go get -u github.com/go-bindata/go-bindata/...
go get -u github.com/elazarl/go-bindata-assetfs/...

输入对应的命令验证:

go-bindata
go-bindata-assetfs

生成

为适合项目目录,本文约定使用 static 目录存放静态资源文件——即需要打包到可执行程序中的文件,生成的代码,存放到 bindata 目录,且其包名亦为 bindata。经研究发现似乎 go-bindata-assetfs 更好一些,因此本文使用该工具,生成命令如下:

go-bindata-assetfs -o=bindata/bindata.go -pkg=bindata -ignore="README.md" -prefix=static static/... 
-o-pkg-ignore-prefixstatic
go-bindata-assetfs -o=bindata.go -ignore="README.md" -prefix=static static/...
-debug
go-bindata-assetfs -debug -o=bindata.go -ignore="README.md" -prefix=static static/...

该参数是在代码中指定静态资源文件绝对路径,因此,修改原文件无需重新编译即可生效,有兴趣可研读生成的代码。

测试

资源文件目录 static 如下:

$ tree static/
static/
|-- conf
|   `-- config.toml
|-- html
|   `-- foo.html
`-- libfoo.so

2 directories, 3 files

主要使用的接口如下:

// 获取所有的文件名称
filenames := bindata.AssetNames()

// 读取某一文件的内容
filename = "html/foo.html"
content, err = bindata.Asset(filename)
static

完整测试代码如下:

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") {
			continue
		}
        
        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")
        
	}
    
    // 单独测试
    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 文件为例,原文件和保存的文件对比如下:

$ 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