在 Go 程序中捆绑静态资源的最佳方法是什么?

我正在使用 Go 开发一个小型的 web 应用程序,它可以作为开发人员机器上的一个工具来帮助调试他们的应用程序/web 服务。该程序的界面是一个网页,不仅包括 HTML,还包括一些 JavaScript (用于功能)、图像和 CSS (用于样式)。我计划开放这个应用程序的外包,这样用户就可以运行 Makefile,所有的资源就可以到达他们需要去的地方。但是,我还希望能够简单地分发一个可执行文件,其中尽可能少地包含文件/依赖项。有没有一种好的方法可以将 HTML/CSS/JS 与可执行文件捆绑在一起,这样用户只需要下载一个文件就可以了?


现在,在我的应用程序中,提供静态文件有点像这样:

// called via http.ListenAndServe
func switchboard(w http.ResponseWriter, r *http.Request) {


// snipped dynamic routing...


// look for static resource
uri := r.URL.RequestURI()
if fp, err := os.Open("static" + uri); err == nil {
defer fp.Close()
staticHandler(w, r, fp)
return
}


// snipped blackhole route
}
    

所以它非常简单: 如果请求的文件存在于我的静态目录中,调用处理程序,它只是打开文件并尝试在服务之前设置一个良好的 Content-Type。我的想法是,没有理由需要基于真正的文件系统: 如果有已编译的资源,我可以简单地通过请求 URI 对它们进行索引,并以这种方式提供服务。

如果没有好办法,请告诉我否则我就找错人了。我只是觉得最终用户会希望管理的文件越少越好。

如果有比 更合适的标签,请随时添加或让我知道。

52270 次浏览

Go-bindata 包看起来可能是您感兴趣的内容。

Https://github.com/go-bindata/go-bindata

它将允许您将任何静态文件转换为可以嵌入到代码中的函数调用,并在调用时返回文件内容的一个字节片。

从 Go 1.16开始,Go 工具支持将静态文件直接嵌入到可执行二进制文件中。

您必须导入 embed包,并使用 //go:embed指令来标记要嵌入的文件和要存储它们的变量。

hello.txt文件嵌入可执行文件的3种方法:

import "embed"


//go:embed hello.txt
var s string
print(s)


//go:embed hello.txt
var b []byte
print(string(b))


//go:embed hello.txt
var f embed.FS
data, _ := f.ReadFile("hello.txt")
print(string(data))

对变量使用 embed.FS类型,您甚至可以将多个文件包含到一个变量中,该变量将提供一个简单的文件系统接口:

// content holds our static web server content.
//go:embed image/* template/*
//go:embed html/index.html
var content embed.FS

net/http支持使用如下 http.FS()从值 embed.FS提供文件:

http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(content))))

模板包还可以使用 text/template.ParseFS()html/template.ParseFS()函数和 text/template.Template.ParseFS()html/template.Template.ParseFS()方法解析模板:

template.ParseFS(content, "*.tmpl")

下面的答案列出了您的旧选项(在 Go 1.16之前)。


嵌入文本文件

如果我们讨论的是文本文件,那么它们可以很容易地嵌入到源代码中。只要使用反引号像下面这样声明 string文字:

const html = `
<html>
<body>Example embedded HTML content.</body>
</html>
`


// Sending it:
w.Write([]byte(html))  // w is an io.Writer

优化提示:

由于大多数情况下,您只需要将资源写入 io.Writer,因此您还可以存储 []byte转换的结果:

var html = []byte(`
<html><body>Example...</body></html>
`)


// Sending it:
w.Write(html)  // w is an io.Writer

唯一需要注意的是原始字符串文字不能包含反引号字符(‘)。原始字符串文字不能包含序列(不同于解释后的字符串文字) ,所以如果您想嵌入的文本包含反引号,您必须将原始字符串文字中断,并将反引号连接为解释后的字符串文字,如下例所示:

var html = `<p>This is a back quote followed by a dot: ` + "`" + `.</p>`

性能不受影响,因为这些连接将由编译器执行。

嵌入二进制文件

以字节片的形式存储

对于二进制文件(例如图像) ,最紧凑(关于生成的本机二进制文件)和最有效的方法是在源代码中将文件的内容作为 []byte。这可以由第三方工具/库(如 转到 Bindata)生成。

如果你不想使用第三方库,这里有一个简单的代码片段,读取一个二进制文件,并输出 Go 源代码,它声明了一个类型为 []byte的变量,这个变量将用文件的确切内容进行初始化:

imgdata, err := ioutil.ReadFile("someimage.png")
if err != nil {
panic(err)
}


fmt.Print("var imgdata = []byte{")
for i, v := range imgdata {
if i > 0 {
fmt.Print(", ")
}
fmt.Print(v)
}
fmt.Println("}")

如果文件包含从0到16的字节,输出示例(在 去游乐场上尝试) :

var imgdata = []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}

存储为基地64 string

如果文件不是“太大”(大多数图片/图标符合条件) ,还有其他可行的选择。可以将文件内容转换为 Base64string并将其存储在源代码中。在应用程序启动时(func init())或需要时,可以将其解码为原始的 []byte内容。Go 在 encoding/base64包中对 Base64编码有很好的支持。

将一个(二进制)文件转换为 base64string非常简单:

data, err := ioutil.ReadFile("someimage.png")
if err != nil {
panic(err)
}
fmt.Println(base64.StdEncoding.EncodeToString(data))

将结果 base64字符串存储在源代码中,例如 const

解码它只是一个函数调用:

const imgBase64 = "<insert base64 string here>"


data, err := base64.StdEncoding.DecodeString(imgBase64) // data is of type []byte

按报价 string存储

存储二进制数据的 引用字符串文字比存储 base64更有效,但在源代码中可能更长。我们可以使用 strconv.Quote()函数获得任何字符串的引号形式:

data, err := ioutil.ReadFile("someimage.png")
if err != nil {
panic(err)
}
fmt.Println(strconv.Quote(string(data))

对于包含从0到64值的二进制数据,下面是输出的样子(在 去游乐场上试一下) :

"\x00\x01\x02\x03\x04\x05\x06\a\b\t\n\v\f\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>?"

(请注意,strconv.Quote()在后面加了一个引号。)

您可以在源代码中直接使用这个带引号的字符串,例如:

const imgdata = "\x00\x01\x02\x03\x04\x05\x06\a\b\t\n\v\f\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>?"

它已经可以使用了,不需要解码; 取消引号是由 Go 编译器在编译时完成的。

如果需要的话,您也可以将它存储为一个字节片:

var imgdata = []byte("\x00\x01\x02\x03\x04\x05\x06\a\b\t\n\v\f\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>?")

还有一些奇特的方法-我使用 Maven 插件构建 GoLang 项目,它允许使用 JCP 预处理器将二进制块和文本文件嵌入到源代码中。在 case 中,代码看起来就像下面的行(在这里可以找到一些例子)

var imageArray = []uint8{/*$binfile("./image.png","uint8[]")$*/}

作为另一个答案中提到的 go-bindata的流行替代品,Mjibson/esc也嵌入任意文件,但是处理目录树特别方便。

捆绑反应应用程序

例如,您的生成输出如下所示:

build/favicon.ico
build/index.html
build/asset-manifest.json
build/static/css/**
build/static/js/**
build/manifest.json

当您像这样使用 go:embed时,它将把内容作为 http://localhost:port/build/index.html提供,而这并不是我们想要的(意外的 /build)。

//go:embed build/*
var static embed.FS


// ...
http.Handle("/", http.FileServer(http.FS(static)))


实际上,我们还需要再进一步使用 fs.Sub使其按预期工作:

package main


import (
"embed"
"io/fs"
"log"
"net/http"
)


//go:embed build/*
var static embed.FS


func main() {
contentStatic, _ := fs.Sub(static, "build")
http.Handle("/", http.FileServer(http.FS(contentStatic)))
log.Fatal(http.ListenAndServe("localhost:8080", nil))
}


现在,http://localhost:8080应该按照预期服务您的 Web 应用程序。

归功于 阿米特 · 米塔尔

注意: go:embed要求达到1.16或更高。