本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 [署名 4.0 国际 (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/deed.zh) 本文作者: 苏洋 创建时间: 2024年01月03日 统计字数: 6357字 阅读时间: 13分钟阅读 本文链接: https://soulteary.com/2024/01/03/golang-gin-static-middleware-improves.html ----- # 完善 Golang Gin 框架的静态中间件:Gin-Static Gin 是 Golang 生态中目前最受用户欢迎和关注的 Web 框架,但是生态中的 `Static` 中间件使用起来却一直很不顺手。 所以,我顺手改了它,然后把这个改良版开源了。 ## 写在前面 ![soulteary/gin-static](https://attachment.soulteary.com/2024/01/03/gin-static.jpg) Gin-static 的改良版,我开源在了 [soulteary/gin-static](https://github.com/soulteary/gin-static),也发布在了 Go 软件包市场:[pkg.go.dev/github.com/soulteary/gin-static](https://pkg.go.dev/github.com/soulteary/gin-static),有需要可以自取。 提到改良优化,那么就不得不提 Go-Gin 和原版的 Gin-Static 对于静态文件的处理。 ### 关于 Go-Gin 和 Gin 社区的静态文件处理 在 Gin 的官方文档中,关于如何使用 Gin 来处理“[静态文件相关请求](https://gin-gonic.com/docs/examples/serving-static-files/)” 写的很清楚: ```go func main() { router := gin.Default() router.Static("/assets", "./assets") router.StaticFS("/more_static", http.Dir("my_file_system")) router.StaticFile("/favicon.ico", "./resources/favicon.ico") // Listen and serve on 0.0.0.0:8080 router.Run(":8080") } ``` 不过,这个例子中,官方只考虑到了静态资源都存放于二级目录,并且静态资源目录只存在静态资源的情况。 如果我们的静态资源需要使用 `/` 根目录,或者在静态目录所在的 `/assets/*` 中,存在需要 Golang后端程序要进行处理的“动态逻辑”,或者我们希望使用通配符来处理某些静态文件路由,这个玩法就失效了。而这个情况,在很多前端比较重的应用中非常常见,尤其是我们希望用 Golang 来优化 Node 或者纯前端实现的项目时。 这个问题在社区的反馈中有提到过,“[\#21,不能够在 `/` 根目录使用静态文件](https://github.com/gin-gonic/gin/issues/75)”、“[\#360,通配符和静态文件冲突](https://github.com/gin-gonic/gin/issues/360)”。 所以,在八年前 gin-contrib 社区出现了一个专注于处理静态程序的中间件:[gin-contrib/static ](https://github.com/gin-contrib/static/commit/b0491c78a9ed6170dc00e31890d8d19374023194),帮助我们解决了这个问题,使用的方法也很简单: ```go package main import ( "github.com/gin-contrib/static" "github.com/gin-gonic/gin" ) func main() { r := gin.Default() // ... r.Use(static.Serve("/", static.LocalFile("/tmp", false))) // ... } ``` 不过,当基础功能完备后,这个插件就陷入了沉睡状态,版本号停留在 0.0.1 直至现在。 时过境迁,Golang 的版本已经升到了 1.21,这个中间件中引用的一些软件也变的陈旧,甚至被废弃,社区中也挂起了一些很好的功能实现(比如,“[\#19,Go 原生文件嵌入实现](https://github.com/gin-contrib/static/issues/19)”),但是因为作者比较忙碌或者没有相同的痛点,所以 PR 一直未能合并。 在若干年后批判古早的代码毫无意义,所以我们就不扯出代码一行行审阅了,我个人认为相对靠谱的动作是帮助它解决问题。 在早些时候,《[深入浅出 Golang 资源嵌入方案:前篇](https://soulteary.com/2022/01/15/explain-the-golang-resource-embedding-solution-part-1.html)》、《[深入浅出 Golang 资源嵌入方案:go-bindata篇](https://soulteary.com/2022/01/16/explain-the-golang-resource-embedding-solution-go-bindata.html)》这两篇文章中,我提到过的 Golang 官方和社区排名靠前的资源嵌入方案,对于制作性能靠谱、方便分发的单文件应用非常有价值。 所以,结合社区里存在的 PR 提交([feat: Implement embed folder and a better organisation](https://github.com/gin-contrib/static/pull/20)),我提交了[一个新的 PR(#46)](https://github.com/gin-contrib/static/pull/46),对之前的程序和 PR 实现的代码都做了一些完善,并且确保这个中间件测试覆盖率是 100%,使用起来能够更安心。 ## 下载 gin-static 优化版 和其他社区软件一样,使用下面的一句话命令,可以完成 gin-static 的下载了: ```bash go get github.com/soulteary/gin-static ``` 如果你是全新使用,在你的在程序中添加下面的引用内容即可: ```go import "github.com/soulteary/gin-static" // 或 import ( static "github.com/soulteary/gin-static" ) ``` 如果你已经使用了社区的 `github.com/gin-gonic/gin-static` 软件包,并且不想修改已有程序的引用和行为,那么我们可以用另外一种方法。 在你的 `go.mod` 文件中,我们应该能够看到类似下面的内容: ```go module your-project go 1.21.2 require ( github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin-static v0.0.1 ) ``` 我们只需要在 `require` 之前,添加一条依赖替换规则即可: ```go module your-project go 1.21.2 replace ( github.com/gin-gonic/gin-static v0.0.1 => github.com/soulteary/gin-static v0.0.5 ) require ( github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin-static v0.0.1 ) ``` 完成内容添加后,我们执行 `go mod tidy`,完成依赖的更新即可。不论是哪一种使用方式,当你执行完命令后,我们就能够使用支持 Go 原生嵌入文件使用啦。 ## 使用 gin-static 优化版 在项目的[示例目录中](https://github.com/soulteary/gin-static/tree/main/example),我提交了两个使用示例程序,分别包含“基础使用(simple)” 和 支持“文件嵌入”的例子(embed): ```go ├── embed │   ├── go.mod │   ├── go.sum │   ├── main.go │   └── public │   └── page └── simple ├── go.mod ├── go.sum ├── main.go └── public └── index.html ``` ### 基础使用 程序的基础使用,和之前社区版本的接口一致,如果我们想在程序中直接使用本地的静态文件: ```go package main import ( "log" "github.com/gin-gonic/gin" static "github.com/soulteary/gin-static" ) func main() { r := gin.Default() // 静态文件在默认根路径 r.Use(static.Serve("/", static.LocalFile("./public", false))) // 其他路径 /other-place // r.Use(static.Serve("/other-place", static.LocalFile("./public", false))) r.GET("/ping", func(c *gin.Context) { c.String(200, "test") }) // Listen and Server in 0.0.0.0:8080 if err := r.Run(":8080"); err != nil { log.Fatal(err) } } ``` 实际使用过程中,我们还可以对根目录做一些额外的逻辑,使用 `r.[Method]` 来覆盖默认的静态文件路由: ```go // 将静态资源注册到根目录,使用本地的 Public 作为“数据源” r.Use(static.Serve("/", static.LocalFile("public", false))) // 允许添加其他的路由规则处理根目录 r.GET("/", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/somewhere") }) ``` ### 文件嵌入 在早些时候,《[深入浅出 Golang 资源嵌入方案:前篇](https://soulteary.com/2022/01/15/explain-the-golang-resource-embedding-solution-part-1.html)》、《[深入浅出 Golang 资源嵌入方案:go-bindata篇](https://soulteary.com/2022/01/16/explain-the-golang-resource-embedding-solution-go-bindata.html)》这两篇文章中,我提到过的 Golang 官方和社区排名靠前的资源嵌入方案,对于制作性能靠谱、方便分发的单文件应用非常有价值。 使用 `gin-static` 来处理嵌入文件非常简单,并且支持多种用法: ```go package main import ( "embed" "fmt" "net/http" "github.com/gin-gonic/gin" ) //go:embed public var EmbedFS embed.FS func main() { r := gin.Default() // Method 1: use as Gin Router // trim embedfs path `public/page`, and use it as url path `/` r.GET("/", static.ServeEmbed("public/page", EmbedFS)) // OR, Method 2: use as middleware // trim embedfs path `public/page`, the embedfs path start with `/` r.Use(static.ServeEmbed("public/page", EmbedFS)) // OR, Method 2.1: use as middleware // trim embedfs path `public/page`, the embedfs path start with `/public/page` r.Use(static.ServeEmbed("", EmbedFS)) // OR, Method 3: use as manual // trim embedfs path `public/page`, the embedfs path start with `/public/page` // staticFiles, err := static.EmbedFolder(EmbedFS, "public/page") // if err != nil { // log.Fatalln("initialization of embed folder failed:", err) // } else { // r.Use(static.Serve("/", staticFiles)) // } r.GET("/ping", func(c *gin.Context) { c.String(200, "test") }) r.NoRoute(func(c *gin.Context) { fmt.Printf("%s doesn't exists, redirect on /\n", c.Request.URL.Path) c.Redirect(http.StatusMovedPermanently, "/") }) // Listen and Server in 0.0.0.0:8080 r.Run(":8080") } ``` 上面的代码中,我们首先使用 `//go:embed public` 将本地的 `public` 目录读入 Golang 程序中,转换为程序可以访问的对象。然后你就可以根据你自己的具体情况,使用上面程序中的任意一种用法了。 当我们使用 `go build` 构建程序后,就能够得到一个包含了所有依赖静态文件的单一可执行文件啦。 ### 个人倾向用法 我个人在使用的过程中,倾向于将上面两种用法合并在一起,当我们在开发的时候,使用本地文件系统(前者),而当我们构建的时候,则使用 Go 内嵌文件系统(后者)。 这样可以确保我们在玩的时候,静态文件支持所见即所得的修改立即生效,下面是我个人喜欢的用法示例: ```go if debugMode { r.Use(static.Serve("/", static.LocalFile("public", false))) } else { r.NoRoute( // 例如,对存在的具体目录进行一些特殊逻辑处理 func(c *gin.Context) { if c.Request.URL.Path == "/somewhere/" { c.Data(http.StatusOK, "text/html; charset=utf-8", []byte("custom as you like")) c.Abort() } }, static.ServeEmbed("public", EmbedFS), ) // 或者,不需要额外处理和拦截存在的静态文件 // r.NoRoute(static.ServeEmbed("public", EmbedFS)) } ``` 在上面的代码里,我们将本地的静态文件,在开发时默认挂载在 `/` 根目录,用于“兜底访问(fallback)”,这些文件允许被各种其他的路由覆盖。当我们进行构建或设置 `debugMode=false` 的时候,我们将静态文件挂载低优先级的 `NoRoute` 路由中,用于“兜底访问(fallback)”,如果我们需要调整或覆盖一些真实存在的静态文件,那么我们需要在路由前做额外的处理。 ## 最后 好了,这个中间件就是这么简单,我们已经聊完了 80% 相关的内容啦。有机会我们在聊聊更有趣的 Embed 文件优化的故事。 --EOF