二丫讲梵 二丫讲梵
首页
  • 最佳实践
  • 迎刃而解
  • Nginx
  • Php
  • Zabbix
  • AWS
  • Prometheus
  • Grafana
  • CentOS
  • Systemd
  • Docker
  • Rancher
  • Ansible
  • Ldap
  • Gitlab
  • GitHub
  • Etcd
  • Consul
  • RabbitMQ
  • Kafka
  • MySql
  • MongoDB
  • OpenVPN
  • KVM
  • VMware
  • Other
  • ELK
  • K8S
  • LLM
  • Nexus
  • Jenkins
  • 随写编年
  • 家人物语
  • 追忆青春
  • 父亲的朋友圈
  • 电影音乐
  • 效率工具
  • 博客相关
  • Shell
  • 前端实践
  • Vue学习笔记
  • Golang学习笔记
  • Golang编程技巧
  • 学习周刊
  • Obsidian插件周刊
关于
友链
  • 本站索引

    • 分类
    • 标签
    • 归档
  • 本站页面

    • 导航
    • 打赏
  • 我的工具

    • 备忘录清单 (opens new window)
    • json2go (opens new window)
    • gopher (opens new window)
    • 微信MD编辑 (opens new window)
    • 国内镜像 (opens new window)
    • 出口IP查询 (opens new window)
    • 代码高亮工具 (opens new window)
  • 外站页面

    • 开往 (opens new window)
    • ldapdoc (opens new window)
    • HowToStartOpenSource (opens new window)
    • vdoing-template (opens new window)
GitHub (opens new window)

二丫讲梵

行者常至,为者常成
首页
  • 最佳实践
  • 迎刃而解
  • Nginx
  • Php
  • Zabbix
  • AWS
  • Prometheus
  • Grafana
  • CentOS
  • Systemd
  • Docker
  • Rancher
  • Ansible
  • Ldap
  • Gitlab
  • GitHub
  • Etcd
  • Consul
  • RabbitMQ
  • Kafka
  • MySql
  • MongoDB
  • OpenVPN
  • KVM
  • VMware
  • Other
  • ELK
  • K8S
  • LLM
  • Nexus
  • Jenkins
  • 随写编年
  • 家人物语
  • 追忆青春
  • 父亲的朋友圈
  • 电影音乐
  • 效率工具
  • 博客相关
  • Shell
  • 前端实践
  • Vue学习笔记
  • Golang学习笔记
  • Golang编程技巧
  • 学习周刊
  • Obsidian插件周刊
关于
友链
  • 本站索引

    • 分类
    • 标签
    • 归档
  • 本站页面

    • 导航
    • 打赏
  • 我的工具

    • 备忘录清单 (opens new window)
    • json2go (opens new window)
    • gopher (opens new window)
    • 微信MD编辑 (opens new window)
    • 国内镜像 (opens new window)
    • 出口IP查询 (opens new window)
    • 代码高亮工具 (opens new window)
  • 外站页面

    • 开往 (opens new window)
    • ldapdoc (opens new window)
    • HowToStartOpenSource (opens new window)
    • vdoing-template (opens new window)
GitHub (opens new window)
  • Shell编程

  • Go编程笔记

    • 开发技巧

      • go日常开发代码片段
      • golang交叉编译
      • 两个切片内容相减的几种方法
      • golang以结构体中某个字段进行排序
      • vscode开发golang报黄提示composite literal uses unkeyed fields
      • golang使用$in或$nin查询MongoDB是否在数组内的数据
      • golang使用$push和$addToSet往数组添加字段的异同
      • MongoDB自增ID在golang中的实践
      • golang数据类型转换汇总
      • 记录VSCode中写Go代码切换Sqlite无CGO依赖版本的过程以及遇到的五个问题
      • 企业微信自建应用-golang校验回调
      • 对接腾讯云未集成到SDK的接口开发实践小记
      • Go开发实践之Gin框架将前端的dist目录embed到二进制
        • 前言
        • 为何要将dist目录嵌入二进制
        • Go的embed包入门
          • 基本语法
          • 路径匹配规则
          • 如何使用
          • 实践经验分享
        • Gin框架集成embed的完整流程
        • 最后
    • 库包研究

    • 个人项目

  • 前端编程笔记

  • Go学习笔记

  • Vue-21年学习笔记

  • Vue-22年重学笔记

  • 编程世界
  • Go编程笔记
  • 开发技巧
二丫讲梵
2025-04-22
目录

Go开发实践之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
1
2

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

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

# 路径匹配规则

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

//go:embed all:dist/*
1

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

微信 支付宝
#go开发技巧
上次更新: 2025/04/23, 11:07:50
对接腾讯云未集成到SDK的接口开发实践小记
使用gorm进行联合查询的整理总结

← 对接腾讯云未集成到SDK的接口开发实践小记 使用gorm进行联合查询的整理总结→

最近更新
01
记录二五年五一之短暂回归家庭
05-09
02
学习周刊-总第210期-2025年第19周
05-09
03
学习周刊-总第209期-2025年第18周
05-03
更多文章>
Theme by Vdoing | Copyright © 2017-2025 | 点击查看十年之约 | 浙ICP备18057030号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式