惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Security Archives - TechRepublic
Security Archives - TechRepublic
T
The Exploit Database - CXSecurity.com
P
Proofpoint News Feed
Scott Helme
Scott Helme
NISL@THU
NISL@THU
Cisco Talos Blog
Cisco Talos Blog
C
Cybersecurity and Infrastructure Security Agency CISA
AWS News Blog
AWS News Blog
V
Vulnerabilities – Threatpost
J
Java Code Geeks
U
Unit 42
The GitHub Blog
The GitHub Blog
H
Help Net Security
T
Tenable Blog
aimingoo的专栏
aimingoo的专栏
Jina AI
Jina AI
Spread Privacy
Spread Privacy
Apple Machine Learning Research
Apple Machine Learning Research
人人都是产品经理
人人都是产品经理
L
Lohrmann on Cybersecurity
T
Threatpost
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Engineering at Meta
Engineering at Meta
A
About on SuperTechFans
I
InfoQ
Microsoft Azure Blog
Microsoft Azure Blog
B
Blog
L
LINUX DO - 最新话题
K
Kaspersky official blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
T
Threat Research - Cisco Blogs
C
Check Point Blog
T
The Blog of Author Tim Ferriss
有赞技术团队
有赞技术团队
宝玉的分享
宝玉的分享
Help Net Security
Help Net Security
Google DeepMind News
Google DeepMind News
A
Arctic Wolf
Y
Y Combinator Blog
N
News | PayPal Newsroom
M
MIT News - Artificial intelligence
Latest news
Latest news
H
Hacker News: Front Page
Blog — PlanetScale
Blog — PlanetScale
腾讯CDC
I
Intezer
爱范儿
爱范儿
F
Fortinet All Blogs
P
Palo Alto Networks Blog
C
CERT Recently Published Vulnerability Notes

二丫讲梵

学习周刊-总第258期-2026年第15周 学习周刊-总第257期-2026年第14周 学习周刊-总第256期-2026年第13周 学习周刊-总第255期-2026年第12周 学习周刊-总第254期-2026年第11周 学习周刊-总第253期-2026年第10周 临时插播一条羊毛,免费领取450元大模型API代金券 学习周刊-总第252期-2026年第09周 学习周刊-总第251期-2026年第08周 学习周刊-总第250期-2026年第07周 学习周刊-总第249期-2026年第06周 诚邀评论,聊聊你所知欲知的我 学习周刊-总第248期-2026年第05周 我的QQ动态之2015年 我的QQ动态之2014年 我的QQ动态之2013年 我的QQ动态之2012年 我的QQ动态-2010-2011年 我的QQ动态-创栏小叙 学习周刊-总第247期-2026年第04周 Nexus社区版权益阉割--一文告诉你有哪些版本可以选择 学习周刊-总第246期-2026年第03周 学习周刊-总第245期-2026年第02周 学习周刊-总第244期-2026年第01周 学习周刊-总第243期-2025年第52周 学习周刊-总第242期-2025年第51周 开源项目ZenOps:带你领略禅意运维 学习周刊-总第241期-2025年第50周 用京东金融,享负债人生 学习周刊-总第240期-2025年第49周 学习周刊-总第239期-2025年第48周 整理我在静态服务透明代理上的极致求索之路,最后一个你绝想不到 学习周刊-总第238期-2025年第47周 我们输了,但收获颇多 太多了,太多了 学习周刊-总第237期-2025年第46周 学习周刊-总第236期-2025年第45周 学习周刊-总第235期-2025年第44周 二五年国庆二三事 学习周刊-总第234期-2025年第43周 学习周刊-总第233期-2025年第42周 学习周刊-总第232期-2025年第41周 学习周刊-总第231期-2025年第40周 学习周刊-总第230期-2025年第39周 西湖毅行 2025年开源世界逸闻三则 学习周刊-总第229期-2025年第38周 手抄《与妻书》 CNB开发与构建基于docker-cache缓存复用的配置实践心得 学习周刊-总第228期-2025年第37周 父亲善行录 学习周刊-总第227期-2025年第36周 带你认识我之看看我的高中同学录(其二) 学习周刊-总第226期-2025年第35周 带你认识我之看看我的高中同学录(其一) 认识神级MCP工具系列--用anyquery和数据库交互 学习周刊-总第225期-2025年第34周 学习周刊-总第224期-2025年第33周 学习周刊-总第223期-2025年第32周 学习周刊-总第222期-2025年第31周 学习周刊-总第221期-2025年第30周 CNB云原生开发环境届的瑞士军刀,详解qifei项目 vuepress-vdoing主题配置自建不蒜子统计 学习周刊-总第220期-2025年第29周 写在博客发表文章1000篇的节点 从claude cli的体验聊聊最大的敌人是我们自己的成见 学习周刊-总第219期-2025年第28周 学习周刊-总第218期-2025年第27周 学习周刊-总第217期-2025年第26周 理论正确,事实错误 学习周刊-总第216期-2025年第25周 学习周刊-总第215期-2025年第24周 学习周刊-总第214期-2025年第23周 学习周刊-总第213期-2025年第22周 学习周刊-总第212期-2025年第21周 从赵心童世锦赛夺冠聊聊我的斯诺克情缘 学习周刊-总第211期-2025年第20周 记录二五年五一之短暂回归家庭 学习周刊-总第210期-2025年第19周 学习周刊-总第209期-2025年第18周 学习周刊-总第208期-2025年第17周 学习周刊-总第207期-2025年第16周 近期关于cobra库的一些实践心得总结 学习周刊-总第206期-2025年第15周 我的嗜好--嗑瓜子 记大宝参加幼儿园组织的清明烈士陵园扫墓活动 学习周刊-总第205期-2025年第14周 学习周刊-总第204期-2025年第13周 前端开发小笔记--在数组中给字段添加值与删除字段的操作 学习周刊-总第203期-2025年第12周 人生实苦,何以自渡 学习周刊-总第202期-2025年第11周 近期发现的一些优秀的工具提名(一) 学习周刊-总第201期-2025年第10周 学习周刊-总第200期-2025年第09周 记一次女友喝醉使我忍术破功 爱情风波--一次分手又复合的经历 学习周刊-总第199期-2025年第08周 整理我的信息源 学习周刊-总第198期-2025年第07周
Go开发实践之Gin框架将前端的dist目录embed到二进制
二丫讲梵 · 2025-04-23 · via 二丫讲梵

# 前言

Go 在资源嵌入方面经历了从第三方工具到官方标准化的演进过程:

  • 早期方案(2016年之前)

    开发者需依赖第三方库(如 go-bindatapkger)将静态文件转换为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

1
2

嵌入目录:使用embed.FS类型:

//go:embed dist/*
var distFS embed.FS

1
2

# 路径匹配规则

默认排除以._开头的文件,需添加all:前缀包含所有文件:

支持通配符(*)匹配多级目录,如dist/**/*.js

# 如何使用

可以通过 ReadFile 读取文件内容,或通过 http.FS 转换为HTTP文件系统:

data, _ := distFS.ReadFile("dist/index.html")
http.Handle("/", http.FileServer(http.FS(distFS)))

1
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

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

1
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

1
2
3
4
5
6
7
8
9
10
11
12

说明:其中 main.gopkged.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),
    }
}

1
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
91

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
}

1
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框架结合,开发者可实现前后端资源的无缝整合,显著提升部署效率和安全性。针对开源项目,非常建议使用这种嵌入方案,对于项目的部署体验步骤,极大简化,能够让人快速拉起体验,抓住初相识的前五分钟,对一个项目的影响力扩展是非常重要的。