golang开发实践之gin框架将前端的dist目录embed到二进制
# 前言
Go 在资源嵌入方面经历了从第三方工具到官方标准化的演进过程:
早期方案(2016年之前)
开发者需依赖第三方库(如go-bindata
、pkger
)将静态文件转换为Go代码。这些工具通过生成代码实现文件嵌入,但存在编译流程复杂、维护成本高等问题。比如之前,我还写过:使用go-bindata将文件编译进二进制 (opens new window)官方标准化(2021年 Go 1.16)
Go 官方在 1.16 版本引入了embed
包,通过//go:embed
指令可以实现编译时的文件嵌入。该方案支持直接嵌入单个文件、目录或模式匹配的多文件,无需额外工具,显著简化了流程。例如://go:embed static/* var staticFiles embed.FS
1
2这一特性成为Go生态中资源嵌入的官方推荐方案,非常适合需要单二进制部署的场景。
# 为何要将dist目录嵌入二进制
部署简化
传统部署需维护dist
目录与二进制文件的路径依赖,而嵌入后仅需分发单个二进制,避免路径错误或者文件丢失。并且单二进制启动也规避了基于 Nginx 配置前后端规则转发的问题,简化配置工作。这对一个开源项目来讲,是十分重要的一个考量点,所有后端是 Go 语言的开源项目,都应该提前向这个方向靠拢。安全性提升
前端资源(如HTML、JS)直接编译到二进制中,可防止被篡改或恶意替换。跨平台兼容性
嵌入资源不受操作系统路径规则限制,例如Windows与Linux的路径分隔符差异无需额外处理。
# Go的embed包入门
# 基本语法
• 嵌入单个文件:声明为string
或[]byte
类型:
//go:embed config.json
var config string
2
• 嵌入目录:使用embed.FS
类型:
//go:embed dist/*
var distFS embed.FS
2
# 路径匹配规则
• 默认排除以.
或_
开头的文件,需添加all:
前缀包含所有文件:
//go:embed all:dist/*
• 支持通配符(*
)匹配多级目录,如dist/**/*.js
。
# 如何使用
可以通过 ReadFile
读取文件内容,或通过 http.FS
转换为HTTP文件系统:
data, _ := distFS.ReadFile("dist/index.html")
http.Handle("/", http.FileServer(http.FS(distFS)))
2
# 实践经验分享
📢 注意:embed 嵌入目录时,不支持使用相对路径嵌入,比如://go:embed ../web/dist
这种写法是不支持的,因此,embed 关键字所在的位置,最顶级也就是在根目录。
于是,在实际实践场景中,有两种方式可用,一种是在 dist 目录所在的同级文件夹下增加一个 dist.go,然后使用 //go:embed dist/*
关键字将 dist 目录嵌入。我的项目示例:static.go (opens new window)
实际代码如下:
package static
import "embed"
//go:embed all:dist
var Static embed.FS
2
3
4
5
6
还有一种方式,可以在 main.go
中定义静态资源的嵌入,在一个子包中定义一个 SetFS()
的方法,然后其他地方就可以调用这个子包来读取静态资源了,简单示例如下:
#=== main.go
package main
import (
"embed"
"godemo/public/pkged"
)
//go:embed conf/config.yaml
var embedFS embed.FS
func main() {
// 设置静态资源
pkged.SetFS(embedFS)
}
#=== public/pkged/pkged.go
package pkged
import "io/fs"
var f fs.FS
func SetFS(embedFs fs.FS) {
f = embedFs
}
func FS() fs.FS {
return f
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
然后其他地方就可以通过调用 pkged.FS()
来使用嵌入的 conf/config.yaml
这个对象。这种设计思路使用更为优雅,也能解决掉 embed
不支持相对路径嵌入的问题。
# Gin框架集成embed的完整流程
这里建议的项目目录规划如下(仅保留主要内容作为示意):
.
├── middleware
│ └── EmbedMiddleware.go
├── public
│ └── pkged
├── routes
│ └── a_enter.go
├── ui
│ └── dist
├── go.mod
├── go.sum
└── main.go
2
3
4
5
6
7
8
9
10
11
12
说明:其中 main.go
和 pkged.go
内容与上一步中的一致。并且静态资源构建在 ui/dist
目录下。
接下来主要是在 routes/a_enter.go
里引用,并借助 EmbedMiddleware.go
来解决一些路由上的问题。
EmbedMiddleware.go
package middleware
import (
"io/fs"
"net/http"
"os"
"path"
"strings"
"github.com/gin-gonic/gin"
)
const INDEX = "index.html"
type ServeFileSystem interface {
http.FileSystem
Exists(prefix string, path string) bool
}
type localFileSystem struct {
http.FileSystem
root string
indexes bool
}
func LocalFile(root string, indexes bool) *localFileSystem {
return &localFileSystem{
FileSystem: gin.Dir(root, indexes),
root: root,
indexes: indexes,
}
}
func (l *localFileSystem) Exists(prefix string, filepath string) bool {
if p := strings.TrimPrefix(filepath, prefix); len(p) < len(filepath) {
name := path.Join(l.root, p)
stats, err := os.Stat(name)
if err != nil {
return false
}
if stats.IsDir() {
if !l.indexes {
index := path.Join(name, INDEX)
_, err := os.Stat(index)
if err != nil {
return false
}
}
}
return true
}
return false
}
func ServeRoot(urlPrefix, root string) gin.HandlerFunc {
return Serve(urlPrefix, LocalFile(root, false))
}
// Static returns a middleware handler that serves static files in the given directory.
func Serve(urlPrefix string, fs ServeFileSystem) gin.HandlerFunc {
fileserver := http.FileServer(fs)
if urlPrefix != "" {
fileserver = http.StripPrefix(urlPrefix, fileserver)
}
return func(c *gin.Context) {
if fs.Exists(urlPrefix, c.Request.URL.Path) {
fileserver.ServeHTTP(c.Writer, c.Request)
c.Abort()
}
}
}
type embedFileSystem struct {
http.FileSystem
}
func (e embedFileSystem) Exists(prefix string, path string) bool {
_, err := e.Open(path)
return err == nil
}
func EmbedFolder(fsEmbed fs.FS, targetPath string) ServeFileSystem {
fsys, err := fs.Sub(fsEmbed, targetPath)
if err != nil {
panic(err)
}
return embedFileSystem{
FileSystem: http.FS(fsys),
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
routes/a_enter.go
package routes
import (
"fmt"
"io"
"net/http"
"time"
"github.com/eryajf/xirang/middleware"
"github.com/eryajf/xirang/public/pkged"
"github.com/gin-gonic/gin"
)
var RouterGroupApp = new(RouterGroup)
// 初始化
func InitRoutes() *gin.Engine {
// 日志与恢复中间件
r := gin.Default()
// 静态资源中间件
r.Use(middleware.Serve("/", middleware.EmbedFolder(pkged.FS(), "ui/dist")))
r.NoRoute(func(c *gin.Context) {
fileObj, err := pkged.FS().Open("ui/dist/index.html")
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
data, err := io.ReadAll(fileObj)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Data(http.StatusOK, "text/html; charset=utf-8", data)
})
......
return r
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
如此,即可实现后端启动之后,访问端口默认进入前端 index.html 路由,请求接口则调用后端自身暴漏的接口。此处需要注意,后端接口不要监听在根路由。
# 最后
通过Go的 embed
包与Gin框架结合,开发者可实现前后端资源的无缝整合,显著提升部署效率和安全性。针对开源项目,非常建议使用这种嵌入方案,对于项目的部署体验步骤,极大简化,能够让人快速拉起体验,抓住初相识的前五分钟,对一个项目的影响力扩展是非常重要的。

