Go 内嵌网页
参考项目:D:\code\go-homework
记住这条链路:前端 build → Go embed → fs.Sub → HTTP 提供网页 → go build → 运行程序、浏览器访问。
1. 目录结构
myapp/
├── go.mod
├── main.go
└── frontend/
├── package.json
├── src/
└── dist/ # pnpm build 生成
├── index.html
└── assets/
2. 前端调用同源接口
fetch('/api/hello')
生产环境不要写死 http://localhost:8080。网页由哪个地址打开,相对路径就请求那个地址。
开发时前端和 Go 分别启动,用 Vite 代理:
// vite.config.ts:合并到现有配置中
server: {
proxy: {
'/api': 'http://localhost:8080',
},
},
3. Go 嵌入并提供网页
下面使用标准库,方便直接复用。现有项目用 Gin 时原理相同。
package main
import (
"embed"
"io/fs"
"log"
"net/http"
)
// 路径相对于当前 Go 文件,编译前必须存在构建产物。
//go:embed frontend/dist
var assets embed.FS
func main() {
// 去掉 frontend/dist 前缀,让 index.html 成为文件系统根目录下的文件。
frontend, err := fs.Sub(assets, "frontend/dist")
if err != nil {
log.Fatal(err)
}
mux := http.NewServeMux()
// API 和网页由同一个服务提供。
mux.HandleFunc("/api/hello", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"message":"hello"}`))
})
mux.HandleFunc("/api/", http.NotFound)
// 从嵌入资源读取网页,不依赖运行目录下的 dist 文件夹。
mux.Handle("/", http.FileServer(http.FS(frontend)))
log.Println("打开 http://localhost:8080")
log.Fatal(http.ListenAndServe("127.0.0.1:8080", mux))
}
这个例子只监听本机。部署到服务器、需要其他机器访问时,按需改为 :8080。
前端使用 Hash 路由(如 /#/todos)可以直接配合上述代码。使用 History 路由(如 /todos)时,需要额外为页面路由返回 index.html;不要把不存在的 API、JS、CSS 也返回成首页。
4. 按顺序构建
首次创建项目时,在根目录执行一次 go mod init example.com/myapp。
每次发布,在项目根目录执行:
cd frontend
pnpm install
pnpm build
cd ..
go build -ldflags="-s -w" -o myapp.exe .
必须先构建前端,再编译 Go。 Go 编译时会把当时的 dist 内容装进程序;前端改完后,必须重新执行这两步。
5. 运行和交付
.\myapp.exe
浏览器打开 http://localhost:8080。
网页资源随可执行文件交付,运行时不需要 Node.js 或单独的 dist 目录。数据库、上传文件、运行配置等仍按业务需求单独处理。
对照现有项目
| 通用示例 | go-homework 中的位置 |
|---|---|
frontend/dist |
static/fe-project/dist |
embed 和 fs.Sub |
main.go |
| HTTP 提供网页 | routers/index.go 的 frontendHandler |
| 先前端、后 Go 的构建顺序 | taskfile.yml 的 task build |
一句话记忆:embed 装进去,fs.Sub 去前缀,HTTP 送出来;先打前端,再编 Go。
优化
对你这个 Gin 项目,最贴合的是 gin-contrib/static,配合 Go 自带的 embed 使用。
| 方案 | 做什么 | 适用场景 |
|---|---|---|
| gin-contrib/static | 把嵌入资源接到 Gin,省去自己写静态文件读取逻辑 | 你现在这种“运行 exe,浏览器访问” |
| Wails | 整合前端构建、Go 编译、资源嵌入和桌面窗口 | 希望网页变成独立桌面软件 |
| go.rice | 通过生成 Go 代码或向二进制追加资源完成打包 | 需要这类特殊资源打包方式 |
用 gin-contrib/static,核心代码可以简化为:
import (
"embed"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
)
//go:embed static/fe-project/dist
var assets embed.FS
func main() {
r := gin.Default()
frontend, err := static.EmbedFolder(
assets, "static/fe-project/dist",
)
if err != nil {
panic(err)
}
r.Use(static.Serve("/", frontend))
// 在这里注册 /api 路由
if err := r.Run(":8080"); err != nil {
panic(err)
}
}
安装:
go get github.com/gin-contrib/static
构建仍然是 先 pnpm build,再 go build。这个库负责提供静态网页,真正把网页装进可执行文件的仍然是 embed。History 路由的首页兜底需要另外处理。官方示例
你现在可以用 embed + gin-contrib/static;如果只是想少记代码,也可以把现有实现保存成项目模板,无需增加依赖。

浙公网安备 33010602011771号