Golang 1.6 新特性:embed 及运用其打包 Web 程序中的静态文件的方法

前言

最近有点闲,寻思着过了暑假冬天也快要来了,于是决定把先前越挖越大的坑给姑且填一填。在顺手把项目从go 1.5升级到go 1.6的时候,发现 Golang 在 1.6 更新了一个名为embed的新特性,似乎原生支持了如vfsgen等第三方工具实现的将静态文件嵌入到最终生成的二进制文件中的功能。于是本着既然都更新了那就试一试的心态果断踩进去。

使用 Go embed

关于 go embed 以及 go 1.6 更新了啥,可以去下面这些网站看看:

大概就是…你从 1.6 开始可以把各种玩意以字符串、字节切片、文件的方式嵌入到最终生成的二进制文件中。不过这里我仅使用了以文件的方式嵌入,其他的暂时还用不上。

这里我用的代码片段取自最近在深坑回填的项目,因为最开始并没有用到其他的第三方工具将静态文件嵌入二进制文件,因此修改起来挺简单的。不过最开始还是有遇到一些问题所以稍微记录一下。

对原有代码进行更改

1
2
3
4
5
6
7
8
9
10
11
12
// server\webService\webService.go
// ...
func Start() {
r := mux.NewRouter()
spa := spaHandler{staticPath: "./web/dist", indexPath: "index.html"}
proxy := spaHandler{staticPath: "", indexPath: ""}

r.PathPrefix("/api").Handler(proxy)
r.PathPrefix("/").Handler(spa)
// ...
}
// ...

这是我原先用于启动 web 服务的部分代码,因为需要在一个端口上挂载 http 和 grpc 两个服务,因此使用了gorilla/mux进行路由。可以看见原先前端部分对应的静态文件位于./web/dist目录下,这个路径是基于最后生成的二进制文件的。

而如果我们需要用 go embed 将前端的静态文件打包进二进制文件,所涉及的修改非常少。只需要在文件中加入如下的两行代码即可

1
2
//go:embed [需要打包的路径]
var [随便起一个名] embed.FS

但需要注意的是,这个go:embed后面跟随着的打包路径是基于写下这句话的.go 文件,并且只能访问到该层目录下的文件的,因此之前的我傻 fufu 的在server\webService\webService.go下各种姿势写路径,怎么都报错no matching files found,说找不到这个文件夹。最后在找资料看这个路径怎么写才标准的时候看见了这一篇文章

才恍(万)然(马)大(奔)悟(腾)。于是再捣鼓捣鼓,得到了第一次修改后的代码:

1
2
3
4
5
6
7
8
9
10
11
// main.go
// ...
//go:embed web/dist/*
var WebFlies embed.FS

func main() {
// ...
webService.WebFlies = WebFlies
go webService.Start()
// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// server\webService\webService.go
// ...
var WebFlies embed.FS

func Start() {
r := mux.NewRouter()
// spa := spaHandler{staticPath: "./web/dist", indexPath: "index.html"}
proxy := spaHandler{staticPath: "", indexPath: ""}

r.PathPrefix("/api").Handler(proxy)
// r.PathPrefix("/").Handler(spa)
r.PathPrefix("/").Handler(http.StripPrefix("/", http.FileServer(http.FS(WebFlies))))

// ...
}

在写这里的代码的时候参考了下面的文章:

修改后的代码中,将 go embed 声明放在了 main.go 文件中,同时又因为使用的位置在 webService.go 中,因此在 webService.go 下也声明了一个 embed.FS 类型的全局变量,并在 main.go 中传值过去。

修改好后试了一下,诶编译成功了!于是运行程序用浏览器打开前端的地址瞅瞅,结果看到了冰冷冷的目录列表:

image-20210715101221010

这时候才猛然发现,这玩意会连着我前面定义的目录结构也一起存进去,而不是只存储了我指定的那个文件。于是我们需要把我们使用的那个文件系统的根目录变为./web/dist。在查阅相关的文档后,发现 Go 在 1.6 后提供了一个名为 io/fs 的包,该包中提供了一个 Sub 函数

1
func Sub(fsys FS, dir string) (FS, error)

通过这个函数可以获得一个子文件系统,并且返回的子文件系统的根目录由第二个参数 dir 指定。那这样就非常简单了,我们稍加修改后代码变成了这样子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// server\webService\webService.go
// ...
var WebFlies embed.FS

func Start() {
r := mux.NewRouter()
// spa := spaHandler{staticPath: "./web/dist", indexPath: "index.html"}
proxy := spaHandler{staticPath: "", indexPath: ""}

r.PathPrefix("/api").Handler(proxy)
// r.PathPrefix("/").Handler(spa)
web, _ := fs.Sub(WebFlies, "web/dist")
r.PathPrefix("/").Handler(http.StripPrefix("/", http.FileServer(http.FS(web))))

// ...
}

再次修改完成后,编译运行,可以看见正常的网页了:

image-20210715102930176

总结

  • go embed 只能读取到它所在.go 文件所属的目录及其子目录的内容,无法读取上级目录
  • go embed 所生成的文件系统会连着原文件的目录结构一起存入,若需要将指定文件夹作为根目录使用,则可以使用 Sub 函数来生成一个子文件系统。