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

huangapple go评论91阅读模式
英文:

What's the best way to bundle static resources in a Go program?

问题

我正在使用Go开发一个小型的Web应用程序,旨在作为开发者机器上的工具来帮助调试他们的应用程序/ Web服务。程序的界面是一个包含HTML、一些JavaScript(用于功能)、图像和CSS(用于样式)的网页。我计划将这个应用程序开源,所以用户应该能够运行一个Makefile,并且所有资源都会放在它们需要的位置。然而,我也希望能够简单地分发一个可执行文件,尽量减少文件/依赖的数量。有没有一种好的方法将HTML/CSS/JS与可执行文件捆绑在一起,这样用户只需要下载和关心一个文件?


目前,在我的应用程序中,提供静态文件的代码看起来有点像这样:

// 通过http.ListenAndServe调用
func switchboard(w http.ResponseWriter, r *http.Request) {

    // 省略动态路由...

	// 查找静态资源
	uri := r.URL.RequestURI()
	if fp, err := os.Open("static" + uri); err == nil {
		defer fp.Close()
		staticHandler(w, r, fp)
		return
	}

    // 省略黑洞路由
}

所以很简单:如果请求的文件存在于我的静态目录中,就调用处理程序,它只是打开文件并尝试设置一个良好的Content-Type然后提供服务。我的想法是,这没有必要基于真实的文件系统:如果有编译的资源,我可以简单地按请求URI索引它们并提供服务。

如果没有好的方法来做这个,或者我试图做这个的方式不对,请告诉我。我只是觉得最终用户会喜欢尽可能少的文件来管理。

如果有比[tag:go]更合适的标签,请随意添加或告诉我。

英文:

I'm working on a small web application in Go that's meant to be used as a tool on a developer's machine to help debug their applications/web services. The interface to the program is a web page that includes not only the HTML but some JavaScript (for functionality), images, and CSS (for styling). I'm planning on open-sourcing this application, so users should be able to run a Makefile, and all the resources will go where they need to go. However, I'd also like to be able to simply distribute an executable with as few files/dependencies as possible. Is there a good way to bundle the HTML/CSS/JS with the executable, so users only have to download and worry about one file?


Right now, in my app, serving a static file looks a little like this:

// 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
}

So it's pretty simple: if the requested file exists in my static directory, invoke the handler, which simply opens the file and tries to set a good Content-Type before serving. My thought was that there's no reason this needs to be based on the real filesystem: if there were compiled resources, I could simply index them by request URI and serve them as such.

Let me know if there's not a good way to do this or I'm barking up the wrong tree by trying to do this. I just figured the end-user would appreciate as few files as possible to manage.

<sup>If there are more appropriate tags than [tag:go], please feel free to add them or let me know.</sup>

答案1

得分: 115

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

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

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

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保存我们的静态Web服务器内容。
//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>
`

// 发送:
w.Write([]byte(html))  // w是io.Writer

优化提示:

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

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

// 发送:
w.Write(html)  // w是io.Writer

唯一需要注意的是原始字符串字面量不能包含反引号字符(`)。原始字符串字面量不能包含序列(与解释字符串字面量不同),因此如果要嵌入的文本包含反引号,则必须将原始字符串字面量分割并将反引号连接为解释字符串字面量,如以下示例所示:

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

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

嵌入二进制文件

存储为字节切片

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

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

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的字节,则示例输出如下(在Go Playground上尝试):

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

存储为Base64字符串

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

将(二进制)文件转换为Base64字符串就像这样简单:

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的类型为[]byte

存储为带引号的字符串

比存储为Base64更高效,但在源代码中可能更长的是存储_带引号的_字符串字面量的二进制数据。我们可以使用strconv.Quote()函数获取任何字符串的带引号形式:

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

对于包含值从0到64的二进制数据,输出如下(在Go Playground上尝试):

"\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:;<=>?")
英文:

Starting with Go 1.16 the go tool has support for embedding static files directly in the executable binary.

You have to import the embed package, and use the //go:embed directive to mark what files you want to embed and into which variable you want to store them.

3 ways to embed a hello.txt file into the executable:

import &quot;embed&quot;

//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(&quot;hello.txt&quot;)
print(string(data))

Using the embed.FS type for the variable you can even include multiple files into a variable that will provide a simple file-system interface:

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

The net/http has support to serve files from a value of embed.FS using http.FS() like this:

http.Handle(&quot;/static/&quot;, http.StripPrefix(&quot;/static/&quot;, http.FileServer(http.FS(content))))

The template packages can also parse templates using text/template.ParseFS(), html/template.ParseFS() functions and text/template.Template.ParseFS(), html/template.Template.ParseFS() methods:

template.ParseFS(content, &quot;*.tmpl&quot;)

The following of the answer lists your old options (prior to Go 1.16).


Embedding Text Files

If we're talking about text files, they can easily be embedded in the source code itself. Just use the back quotes to declare the string literal like this:

const html = `
&lt;html&gt;
&lt;body&gt;Example embedded HTML content.&lt;/body&gt;
&lt;/html&gt;
`

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

Optimization tip:

Since most of the times you will only need to write the resource to an io.Writer, you can also store the result of a []byte conversion:

var html = []byte(`
&lt;html&gt;&lt;body&gt;Example...&lt;/body&gt;&lt;/html&gt;
`)

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

Only thing you have to be careful about is that raw string literals cannot contain the back quote character (`). Raw string literals cannot contain sequences (unlike the interpreted string literals), so if the text you want to embed does contain back quotes, you have to break the raw string literal and concatenate back quotes as interpreted string literals, like in this example:

var html = `&lt;p&gt;This is a back quote followed by a dot: ` + &quot;`&quot; + `.&lt;/p&gt;`

Performance is not affected, as these concatenations will be executed by the compiler.

Embedding Binary Files

Storing as a byte slice

For binary files (e.g. images) most compact (regarding the resulting native binary) and most efficient would be to have the content of the file as a []byte in your source code. This can be generated by 3rd party toos/libraries like go-bindata.

If you don't want to use a 3rd party library for this, here's a simple code snippet that reads a binary file, and outputs Go source code that declares a variable of type []byte that will be initialized with the exact content of the file:

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

fmt.Print(&quot;var imgdata = []byte{&quot;)
for i, v := range imgdata {
	if i &gt; 0 {
		fmt.Print(&quot;, &quot;)
	}
	fmt.Print(v)
}
fmt.Println(&quot;}&quot;)

Example output if the file would contain bytes from 0 to 16 (try it on the Go Playground):

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

Storing as base64 string

If the file is not "too large" (most images/icons qualify), there are other viable options too. You can convert the content of the file to a Base64 string and store that in your source code. On application startup (func init()) or when needed, you can decode it to the original []byte content. Go has nice support for Base64 encoding in the encoding/base64 package.

Converting a (binary) file to base64 string is as simple as:

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

Store the result base64 string in your source code, e.g. as a const.

Decoding it is just one function call:

const imgBase64 = &quot;&lt;insert base64 string here&gt;&quot;

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

Storing as quoted string

More efficient than storing as base64, but may be longer in source code is storing the quoted string literal of the binary data. We can obtain the quoted form of any string using the strconv.Quote() function:

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

For binary data containing values from 0 up to 64 this is how the output would look like (try it on the Go Playground):

&quot;\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 !\&quot;#$%&amp;&#39;()*+,-./0123456789:;&lt;=&gt;?&quot;

(Note that strconv.Quote() appends and prepends a quotation mark to it.)

You can directly use this quoted string in your source code, for example:

const imgdata = &quot;\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 !\&quot;#$%&amp;&#39;()*+,-./0123456789:;&lt;=&gt;?&quot;

It is ready to use, no need to decode it; the unquoting is done by the Go compiler, at compile time.

You may also store it as a byte slice should you need it like that:

var imgdata = []byte(&quot;\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 !\&quot;#$%&amp;&#39;()*+,-./0123456789:;&lt;=&gt;?&quot;)

答案2

得分: 81

go-bindata包看起来可能是你感兴趣的东西。

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

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

英文:

The go-bindata package looks like it might be what you're interested in.

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

It will allow you to convert any static file into a function call that can be embedded in your code and will return a byte slice of the file content when called.

答案3

得分: 8

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

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

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))
}

Credit to Amit Mittal.

Note: go:embed requires go 1.16 or higher.

英文:

Bundle React application

For example, you have a build output from react like the following:

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

When you use go:embed like this, it will serve the contents as http://localhost:port/build/index.html which is not what we want (unexpected /build).

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

// ...
http.Handle(&quot;/&quot;, http.FileServer(http.FS(static)))

In fact, we will need to take one more step to make it works as expected by using fs.Sub:

package main

import (
	&quot;embed&quot;
	&quot;io/fs&quot;
	&quot;log&quot;
	&quot;net/http&quot;
)

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

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

Now, http://localhost:8080 should serve your web application as expected.

Credit to Amit Mittal.

Note: go:embed requires go 1.16 or higher.

答案4

得分: 2

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

var imageArray = []uint8{/*$binfile(&quot;./image.png&quot;,&quot;uint8[]&quot;)$*/}
英文:

also there is some exotic way - I use maven plugin to build GoLang projects and it allows to use JCP preprocessor to embed binary blocks and text files into sources. In the case code just look like line below (and some example can be found here)

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

答案5

得分: 1

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

英文:

As a popular alternative to go-bindata mentioned in another answer, mjibson/esc also embeds arbitrary files, but handles directory trees particularly conveniently.

huangapple
  • 本文由 发表于 2012年12月17日 02:52:32
  • 转载请务必保留本文链接:https://go.coder-hub.com/13904441.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定