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

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Schneier on Security
Schneier on Security
C
Cisco Blogs
Help Net Security
Help Net Security
I
Intezer
Simon Willison's Weblog
Simon Willison's Weblog
Know Your Adversary
Know Your Adversary
Hacker News: Ask HN
Hacker News: Ask HN
Cisco Talos Blog
Cisco Talos Blog
A
Arctic Wolf
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
WordPress大学
WordPress大学
小众软件
小众软件
Jina AI
Jina AI
量子位
T
Threatpost
Forbes - Security
Forbes - Security
L
LINUX DO - 最新话题
S
Securelist
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
C
Cyber Attacks, Cyber Crime and Cyber Security
有赞技术团队
有赞技术团队
博客园_首页
T
Threat Research - Cisco Blogs
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
The Exploit Database - CXSecurity.com
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Scott Helme
Scott Helme
博客园 - 【当耐特】
IT之家
IT之家
Hugging Face - Blog
Hugging Face - Blog
月光博客
月光博客
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
大猫的无限游戏
大猫的无限游戏
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
The Last Watchdog
The Last Watchdog
Attack and Defense Labs
Attack and Defense Labs
Google DeepMind News
Google DeepMind News
阮一峰的网络日志
阮一峰的网络日志
SecWiki News
SecWiki News
博客园 - 叶小钗
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
罗磊的独立博客
The Cloudflare Blog
S
Schneier on Security
爱范儿
爱范儿
N
News and Events Feed by Topic
NISL@THU
NISL@THU
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

二丫讲梵

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