前言
Go 编译出的二进制程序可以很方便的进行部署,但是如果在程序中引用了静态文件,则部署的时候还要带上静态文件。从 Go 1.16 开始,编译器提供将静态文件嵌入二进制程序中的功能。
使用方法
将一个文件嵌入字符串中
例子1.1
import _ "embed"
//go:embed hello.txt
var s string
print(s)
将一个文件嵌入 []byte
例子1.2
import _ "embed"
//go:embed hello.txt
var b []byte
print(string(b))
将一个或多个文件嵌入文件系统中 (type FS)
例子1.3
import "embed"
//go:embed hello.txt
var f embed.FS
data, _ := f.ReadFile("hello.txt")
print(string(data))
指令
在变量定义的上方加入 //go:embed <相对路径> 指明要嵌入的文件的路径或路径匹配参数。相对路径只能是与当前 go 文件同级的路径,不能使用 ../
返回到上一级。
指令必须紧跟在包含单个变量声明的行之前。指令和声明之间只允许空行和//
行注释。
这个变量的类型只能是 string, []byte, FS (或 type FS)。
例子2.1
package server
import "embed"
// content holds our static web server content.
//go:embed image/* template/*
//go:embed html/index.html
var content embed.FS
为了使代码更加清晰,//go:embed
指令接受多个空格分隔的路径参数,也可以使用多行 //go:embed
以避免出现太长的行。路径分割符是正斜杠 /
,在Windows上也是如此。路径参数不能包含 .
、 ..
或空路径参数,也就是他们不能以斜杠作为路径的开始或结束。
要匹配目录中的所有内容,请使用 *
而非 .
。
如果文件名中带有空格,可以使用双引号或后引号将路径括起来。
package server
import "embed"
//go:embed "text file" `or this`
var content string
如果路径参数是是一个目录名,则这个目录树下的所有文件都会被递归地嵌入,除了文件名以 .
和 _
开头的的文件。
所以例子2.1也可以被下面的样子
// content is our static web server content.
//go:embed image template html/index.html
var content embed.FS
在大多数情况下他们是等价的。区别在于 image/*
嵌入的目录包含 image/.tmp
、image/dir/.tmp
但 image
不包含。
//go:embed
指令既可以用于导出的变量(exported variables)也可以用于未导出的变量(unexported variables),这取决于你是否允许其他包访问该内容。他只能是包内变量(package scope),不能是代码块、函数块内的局部变量(local variables)
如果路径参数无效或匹配失败,则将无法通过编译。
字符串或[]byte
字符串或 []byte 类型的变量上方的 //go:embed 行只能有一个路径参数,且该参数只能匹配一个文件,字符串和 []byte 将使用文件中的内容进行初始化。
在使用字符串和 []byte 时的 //go:embed 指令要求导入 “embed” 包。但由于没有在代码中用到 embed 包内的变量或方法,所以这里要求空白导入 (_ “embed”) ,如例子1.1和1.2。
文件系统
对于单一文件的嵌入,字符串或 []byte 类型的变量通常是最好的。FS 类型允许嵌入一个文件树,例如一个静态 Web 服务器内容的目录,如例子1.3所示。
FS 实现了 io/fs 包内的 FS 接口,因此他可以用于任何可调用文件系统(type FS)的包,例如 net/http, text/template, and html/template。以下是一个例子。
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(content))))
template.ParseFS(content, "*.tmpl")
应用
在 Nginx UI 项目中v1.2版本起,我们将前端编译后的产物嵌入 Go 编译出的二进制文件,这样将会极大地简化 Nginx UI 的部署。
我们这里使用了 gin-contrib/static 作为静态文件的处理器,当没有命中 route 内的任何规则时,会转到静态文件的处理器中进行下一步处理,如果文件系统中也没有找到该文件,则转到 NoRoute 进行处理。
r.Use(static.Serve("/", mustFS("")))
mustFS 的实现如下,实现了手动实现了 embed.FS 到 static.ServeFileSystem 的转换。
package router
import (
"github.com/0xJacky/Nginx-UI/frontend"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
"io/fs"
"log"
"net/http"
)
type serverFileSystemType struct {
http.FileSystem
}
func (f serverFileSystemType) Exists(prefix string, _path string) bool {
_, err := f.Open(path.Join(prefix, _path))
return err == nil
}
func mustFS(dir string) (serverFileSystem static.ServeFileSystem) {
sub, err := fs.Sub(frontend.DistFS, path.Join("dist", dir))
if err != nil {
log.Println(err)
return
}
serverFileSystem = serverFileSystemType{
http.FS(sub),
}
return
}
因为原来的前端是使用 Vue-Router,且采用 history mode
,按照之前的部署方式,需要在 Nginx 配置如要使用如下配置,以作为伪静态将所有前端的请求放到 index.html 中处理。
location / {
try_files $uri $uri/ /index.html;
}
到了Go这边,则需要重写 Gin 的 NoRoute 方法,此操作在于当请求没有命中 Gin Route 内的任何规则时,都将返回 index.html 的内容,由客户端进行进一步的处理,如果该请求也没有命中 Vue Route 内的任何规则,将会跳转到前端的 404 页面。
r.NoRoute(func(c *gin.Context) {
accept := c.Request.Header.Get("Accept")
if strings.Contains(accept, "text/html") {
file, _ := mustFS("").Open("index.html")
defer file.Close()
stat, _ := file.Stat()
c.DataFromReader(http.StatusOK, stat.Size(), "text/html",
bufio.NewReader(file), nil)
}
})
最后,为了实现在浏览器缓存 js,我设计了如下中间件。当浏览器发送了 If-Modified-Since 头并且它的值与 settings.LastModified 相同时,则返回 304 Not Modified 的空响应。经测试,浏览器可以很好的缓存到静态文件。
func cacheJs() gin.HandlerFunc {
return func(c *gin.Context) {
if strings.Contains(c.Request.URL.String(), "js") {
c.Header("Cache-Control", "max-age: 1296000")
if c.Request.Header.Get("If-Modified-Since") == settings.LastModified {
c.AbortWithStatus(http.StatusNotModified)
}
c.Header("Last-Modified", settings.LastModified)
}
}
}
参考资料
[1] embed, https://pkg.go.dev/embed [2] Go embed 简明教程, https://colobu.com/2021/01/17/go-embed-tutorial/文章最后修订于 2022年8月10日
评论 (0)