原文链接:https://xiets.blog.csdn.net/article/details/144357170
版权声明:原创文章禁止转载
专栏目录:Golang 专栏(总目录)
文章目录
Gin 是一个用 Go(Golang)编写的 HTTP Web 框架。它采用了类似 Martini 的 API,但性能比 Martini 快 40 倍。
相关网站:
- Gin GitHub:https://github.com/gin-gonic/gin
- Gin 文档:https://gin-gonic.com/zh-cn/docs/
- API 文档:https://pkg.go.dev/github.com/gin-gonic/gin
1. 快速入门
安装 Gin:
$ go get -u github.com/gin-gonic/gin
基础示例:
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
r := gin.Default()
r.GET("/ping", func(ctx *gin.Context) {
// gin.H 的底层类型是 map[string]any
ctx.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
err := r.Run() // listen and serve on "0.0.0.0:8080" (default)
if err != nil {
panic(err)
}
}
// 浏览器访问: http://localhost:8080/ping
gin.Context
是一个包含了 Request
和 ResponseWriter
等信息的结构体,*gin.Context
实现了 context.Context
接口。
2 响应渲染器
gin.Context 是 Gin 最重要的部分。它允许我们在中间件之间传递变量、管理流程、验证请求的 JSON 并呈现 JSON 响应。请求处理器函数的参数就是一个 gin.Context
实例,形参通常记作 ctx
。
2.1 JSON/JSON/YAML/ProtoBuf 渲染器
gin.Context
提供了许多方便使用的渲染器方法,它可以把一个 结构体实例 或 map/slice 自动序列化为你所需要的数据类型写入响应 Body,并自动添加对应的 Content-Type
到响应头。
渲染器方法包括:
ctx.JSON(code int, resBody)
ctx.AsciiJSON(code int, obj any)
ctx.YAML(code int, resBody)
ctx.TOML(code int, resBody)
ctx.XML(code int, resBody)
ctx.ProtoBuf(code int, resBody)
以上方法实际内部调用的是一个渲染方法
ctx.Render(code int, r render.Render)
,通过内置的各种渲染器实现自动序列化,也可以自定义渲染器。
渲染器方法示例:
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
// 创建一个 gin.Engine 实例,其中已附加 gin.Logger() 和 gin.Recovery() 中间件。不使用默认中间件,可以通过 gin.New() 创建实例。
router := gin.Default()
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
// GET /get_json, 相当于: r.Handle("GET", path, handlers)
router.GET("/get_json", func(ctx *gin.Context) {
p := &Person{
Name: "tom",
Age: 18,
}
// 将给定的结构体序列化为 JSON 写入响应 Body 中,并设置响应头 "Content-Type: application/json; charset=utf-8"
ctx.JSON(http.StatusOK, p)
// ctx.AsciiJSON(code int, obj any) // 非 ASCII 字符以 \u 的形式表示
// 其他自动序列化对象响应的方法
// ctx.YAML(code int, obj any) // Content-Type: application/yaml; charset=utf-8
// ctx.TOML(code int, obj any) // Content-Type: application/toml; charset=utf-8
// ctx.XML(code int, obj any) // Content-Type: application/xml; charset=utf-8
// ctx.ProtoBuf(http.StatusOK, resBody) // Content-Type: application/x-protobuf
})
err := router.Run(":8080")
if err != nil {
panic(err)
}
}
2.2 通用响应数据
ctx.String()
方法用于将字符串写入到 Body,并设置 Content-Type: text/plain; charset=utf-8
响应头。
ctx.Data()
和 ctx.DataFromReader()
方法则用于将任意二进制数据写入响应 Body,并自定义 Content-Type
。
package main
import (
"bytes"
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
router := gin.Default()
// GET /get_info
router.GET("/get_info", func(ctx *gin.Context) {
resBody := bytes.NewBuffer(nil)
resBody.WriteString("RemoteIP: " + ctx.RemoteIP() + "\n")
resBody.WriteString("FullPath: " + ctx.FullPath() + "\n")
resBody.WriteString(fmt.Sprintf("http.Request: %+v", ctx.Request)) // ctx.Request 是 *http.Request 类型
// 文本响应:"Content-Type: text/plain; charset=utf-8"
ctx.String(http.StatusOK, resBody.String())
// 其他任意数据响应
// ctx.Data(code int, contentType string, data []byte)
// ctx.DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string)
})
err := router.Run(":8080")
if err != nil {
panic(err)
}
}
3. 路由参数与自动重定向
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
router := gin.Default()
// 路径参数:路径中带有 name 和 id 两个参数
router.GET("/user/:name/:id", func(ctx *gin.Context) {
// 遍历路径中的所有参数
for _, param := range ctx.Params {
fmt.Printf("Params: %s=%s\n", param.Key, param.Value)
}
// 路径中的指定参数 (参数值均以字符串返回, 如果不存在则返回空字符串)
fmt.Printf("id=%s\n", ctx.Param("id"))
fmt.Printf("name=%s\n", ctx.Param("name"))
// 路径中的参数绑定到结构体中
type Person struct {
ID int64 `uri:"id" binding:"required"`
Name string `uri:"name" binding:"required"`
}
p := &Person{}
err := ctx.ShouldBindUri(p) // 如果参数值无法解析为字段需要的类型 或者 不符合required约束, 则返回 error
if err != nil {
panic(err)
}
fmt.Printf("p=%+v\n", p)
ctx.String(http.StatusOK, "succeed")
})
// 路径泛匹配:以 "/home/*" 开头的路径都会匹配到此路由,action 用于接收 "/home" 后面的路径(以"/"开头)
router.GET("/home/*action", func(ctx *gin.Context) {
action := ctx.Param("action")
ctx.String(http.StatusOK, action)
})
// 自动重定向:如果注册了 "/hello",但没注册 "/hello/",则后者会自动重定向到前者
router.GET("/hello", func(ctx *gin.Context) {
ctx.String(http.StatusOK, "/hello")
})
// 自动重定向:如果注册了 "/world/",但没注册 "/world",则后者会自动重定向到前者
router.GET("/world/", func(ctx *gin.Context) {
ctx.String(http.StatusOK, "/world")
})
err := router.Run(":8080")
if err != nil {
panic(err)
}
}
测试访问:
http://localhost:8080/user/abc/123
http://localhost:8080/home/abc/def/123
http://localhost:8080/hello/
http://localhost:8080/world
4. Query 参数
*gin.Context
中获取 Query 参数值的相关方法:
ctx.Query(key string) (value string)
:获取指定名称的 Query 参数值,没有则返回空字符串。ctx.GetQuery(key string) (string, bool)
:获取指定名称的 Query 参数值,返回(参数值, 是否存在)
。ctx.DefaultQuery(key, defaultValue string) string
:获取指定名称的 Query 参数值,如果不存在则返回defaultValue
。
以数组/Map的方式获取 Query 参数值:
ctx.QueryArray(key string) (values []string)
ctx.GetQueryArray(key string) (values []string, ok bool)
Query 参数绑定到对象:
ctx.ShouldBindQuery(obj any) error
Query 参数获取示例:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
router := gin.Default()
router.GET("/get", func(ctx *gin.Context) {
name := ctx.Query("name") // 参数值自动 UrlDecode
age := ctx.Query("age")
fmt.Printf("name=%s, age=%s\n", name, age)
type Person struct {
Name string `query:"name"` // 参数值自动 UrlDecode
Age int `query:"age"`
}
p := &Person{}
err := ctx.ShouldBindQuery(p)
if err != nil {
panic(err)
}
fmt.Printf("p=%+v\n", p)
ctx.String(http.StatusOK, "succeed")
})
err := router.Run(":8080")
if err != nil {
panic(err)
}
}
访问:http://localhost:8080/get?name=tom&age=18
5. 处理表单提交
获取 Form 表单字段的相关方法:
ctx.PostForm(key string) (value string)
:获取表单中指定字段的值。ctx.GetPostForm(key string) (string, bool)
:获取表单中指定字段的值,返回(字段值, 是否存在)
。ctx.DefaultPostForm(key, defaultValue string) string
:获取表单中指定字段的值,如果不存在则返回defaultValue
。ctx.PostFormArray(key string) (values []string)
:以数组的方式获取表单值。ctx.GetPostFormArray(key string) (values []string, ok bool)
:以数组的方式获取表单值。
上面获取表单字段值的方法,同时支持 application/x-www-form-urlencoded
和 multipart/form-data
格式的表单。
获取 multipart/form-data
复合表单中的文件字段:
ctx.FormFile(name string) (*multipart.FileHeader, error)
:获取复合表单中的文件字段。ctx.SaveUploadedFile(file *multipart.FileHeader, dst string) error
:保存表单文件字段的文件内容到指定路径。ctx.MultipartForm() (*multipart.Form, error)
:获取解析后的复合表单,包括文件字段。
处理表单提交示例:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
router := gin.Default()
// 为 multipart forms 设置较低的内存限制 (默认是 32 MB)
router.MaxMultipartMemory = 10 << 20 // 10 MB
router.POST("/post", func(ctx *gin.Context) {
// 获取表单字段
name := ctx.PostForm("name")
age := ctx.DefaultPostForm("age", "18")
fmt.Printf("name=%s, age=%s\n", name, age)
// 获取上传的文件
file, err := ctx.FormFile("file")
if err != nil {
panic(err)
}
if file != nil {
fmt.Printf("file: name=%s, size=%d, MIME=%s\n", file.Filename, file.Size, file.Header["Content-Type"])
// file.Open() // 打开文件流
// 保存文件到指定路径
err = ctx.SaveUploadedFile(file, "./"+file.Filename)
if err != nil {
panic(err)
}
}
ctx.String(http.StatusOK, "succeed")
})
err := router.Run(":8080")
if err != nil {
panic(err)
}
}
测试:
curl -F "name=tom" -F "age=18" -F "file=@/path/demo.png" "http://localhost:8080/post"
6. 绑定 Request Body
如果 Request Body 是 JSON、XML、YAML、TOML 格式的内容,Gin 支持直接将其绑定到对象,相关方法:
ctx.ShouldBindBodyWithJSON(obj any) error
ctx.ShouldBindBodyWithXML(obj any) error
ctx.ShouldBindBodyWithYAML(obj any) error
ctx.ShouldBindBodyWithTOML(obj any) error
ctx.ShouldBindBodyWith(obj any, bb binding.BindingBody) (err error)
:上面的方法内部调用的是此方法。ctx.ShouldBind(obj any) error
:根据请求头中的Content-Type
字段值自动选择绑定类型,例如:"application/json"
--> JSON binding"application/xml"
--> XML binding
如果要获取 Request Body 原始数据,可以调用
ctx.GetRawData() ([]byte, error)
。
绑定 Request Body 示例:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
router := gin.Default()
router.POST("/post", func(ctx *gin.Context) {
type Person struct {
Name string `json:"name" yaml:"name" toml:"name" binding:"required"`
Age int `json:"age" yaml:"age" toml:"age" binding:"required"`
}
p := &Person{}
err := ctx.ShouldBind(p) // 自动识别 Request Body 内容类型, 并绑定到对象
if err != nil { // 如果不满足 类型 和 required 约束, 则返回 error
panic(err)
}
fmt.Printf("p: %+v\n", p)
ctx.String(http.StatusOK, "succeed")
})
err := router.Run(":8080")
if err != nil {
panic(err)
}
}
测试:
curl -H "Content-Type: application/json" -d '{"name": "tom", "age": 18}' http://localhost:8080/post
7. 设置响应头、设置/获取Cookie
设置响应头方法:
ctx.Header(key, value string)
:设置(覆盖)响应头,如果value=""
则移除响应头。
设置/获取 Cookie 方法:
ctx.SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool)
:设置 Cookie 到响应头。ctx.Cookie(name string) (string, error)
:从请求头中获取 Cookie。
package main
import (
"errors"
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
router := gin.Default()
router.POST("/login", func(ctx *gin.Context) {
ctx.Header("X-Server", "My Server")
username := ctx.PostForm("username")
password := ctx.PostForm("password")
if username == "tom" && password == "123" {
ctx.SetCookie("token", "uuid*****", 3600, "/", "localhost", false, true)
ctx.String(http.StatusOK, "succeed")
} else {
ctx.String(http.StatusOK, "failed")
}
})
router.GET("/home", func(ctx *gin.Context) {
token, err := ctx.Cookie("token")
if err != nil {
if errors.Is(err, http.ErrNoCookie) {
fmt.Println("no token cookie")
} else {
panic(err)
}
} else {
fmt.Println("Token:", token)
}
ctx.Header("X-Server", "My Server")
ctx.String(http.StatusOK, "home")
})
err := router.Run(":8080")
if err != nil {
panic(err)
}
}
测试:
curl -v -d "username=tom&password=123" http://localhost:8080/login
curl -v -H "Cookie: token=uuid%2A%2A%2A%2A%2A" http://localhost:8080/home
8. 中间件
中间件注册方法:ctx.Use(middleware ...HandlerFunc) IRoutes
。
HandlerFunc
是一个函数类型:type HandlerFunc func(*gin.Context)
。
8.1 自定义中间件
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
// Middleware1 返回一个中间件函数
func Middleware1() gin.HandlerFunc {
return func(ctx *gin.Context) {
fmt.Println("Middleware1 Start")
ctx.Set("name", "tom") // 在 Context 中保存变量
ctx.Next() // 调用下一个中间件,如果没有更多中间件,则调用 handler
fmt.Println("Middleware1 End")
}
}
// Middleware2 一个中间件函数
func Middleware2(ctx *gin.Context) {
name, _ := ctx.Get("name") // 获取 Context 中保存的变量
fmt.Println("Middleware2 Start, name:", name)
ctx.Next() // 调用下一个中间件,如果没有更多中间件,则调用 handler
fmt.Println("Middleware2 End")
}
func main() {
router := gin.Default()
// 注册中间件,多个中间件将依次调用
router.Use(Middleware1())
router.Use(Middleware2)
router.GET("/ping", func(ctx *gin.Context) {
name, _ := ctx.Get("name")
fmt.Println("GET Handler: name:", name)
ctx.String(http.StatusOK, "pong")
})
err := router.Run(":8080")
if err != nil {
panic(err)
}
}
访问 http://localhost:8080/ping
,输出日志:
Middleware1 Start
Middleware2 Start, name: tom
GET Handler: name: tom
Middleware2 End
Middleware1 End
8.2 日志中间件
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"time"
)
func LogFormatter(param gin.LogFormatterParams) string {
// 自定义日志格式
return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
param.ClientIP,
param.TimeStamp.Format(time.RFC1123),
param.Method,
param.Path,
param.Request.Proto,
param.StatusCode,
param.Latency,
param.Request.UserAgent(),
param.ErrorMessage,
)
}
func main() {
router := gin.Default()
// gin.LoggerWithFormatter 中间件会写入日志到 gin.DefaultWriter
// 默认 gin.DefaultWriter = os.Stdout
router.Use(gin.LoggerWithFormatter(LogFormatter))
router.GET("/ping", func(ctx *gin.Context) {
ctx.String(http.StatusOK, "pong")
})
err := router.Run(":8080")
if err != nil {
panic(err)
}
}
8.3 BasicAuth 中间件
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
router := gin.Default()
// 全局注册 BasicAuth 中间件 (也可以在某个 path 注册, 或某个路由组注册)
router.Use(gin.BasicAuth(gin.Accounts{
"user1": "pass1", // 账号 {用户名: 密码}
"user2": "pass2",
}))
router.GET("/user", func(ctx *gin.Context) {
// 获取通过 BasicAuth 中间件认证的用户名,在 gin.BasicAuth 中通过 ctx.Set(gin.AuthUserKey, user) 设置
user := ctx.MustGet(gin.AuthUserKey).(string)
ctx.String(http.StatusOK, "hello %s", user)
})
err := router.Run(":8080")
if err != nil {
panic(err)
}
}
测试访问:
curl -v "http://user1:pass1@localhost:8080/user"
8.4 Gzip 中间件
Gin 官方的 GitHub 中提供了许多中间件,例如:gin-contrib/gzip、gin-contrib/cors、gin-contrib/sessions、gin-contrib/i18n 等,具体参考 gin-contrib — GitHub。
安装 GZIP 中间件:
go get github.com/gin-contrib/gzip
给支持 gzip
压缩的客户端进行 gzip 压缩:
package main
import (
"github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin"
"net/http"
"time"
)
func main() {
router := gin.Default()
// 注册 gzip 插件,使用默认的压缩等级,排除 ".pdf" 和 ".mp4" 后缀的资源
router.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedExtensions([]string{".pdf", ".mp4"})))
// gzip.WithExcludedPathsRegexs() // 排除指定的路径规则 (正则匹配)
// gzip.WithExcludedPaths() // 排除指定的路径
router.GET("/ping", func(ctx *gin.Context) {
text := "pong\n"
for i := 0; i < 100; i++ {
text += time.Now().String() + "\n"
}
ctx.String(http.StatusOK, text)
})
err := router.Run(":8080")
if err != nil {
panic(err)
}
}
9. 路由组
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
v1 := router.Group("/v1")
{
// v1.Use(...) // 路由组内中间件
v1.GET("/home", v1HomeHander) // 访问: GET "/v1/home"
v1.POST("/submit", v1SubmitHander) // 访问: POST "/v1/submit"
}
v2 := router.Group("/v1")
{
v2.GET("/home", v2HomeHander) // 访问: GET "/v2/home"
v2.POST("/submit", v2SubmitHander) // 访问: POST "/v2/submit"
}
err := router.Run(":8080")
if err != nil {
panic(err)
}
}
10. 重定向与转发
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
router := gin.Default()
router.GET("/hi", func(ctx *gin.Context) {
ctx.String(http.StatusOK, "hi")
})
router.GET("/hello1", func(ctx *gin.Context) {
// 外部重定向(由客户端重定向)
ctx.Redirect(http.StatusMovedPermanently, "/hi")
})
router.GET("/hello2", func(ctx *gin.Context) {
// 内部重定向(服务端转发)
ctx.Request.URL.Path = "/hi"
router.HandleContext(ctx)
})
err := router.Run(":8080")
if err != nil {
panic(err)
}
}
11. 静态文件服务
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
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")
err := router.Run(":8080")
if err != nil {
panic(err)
}
}
静态文件服务,支持以下功能:
- 响应头中会返回
Last-Modified
,再次请求时如果携带If-Modified-Since
,如果内容没改变则会返回304 Not Modified
。 - 同时支持
GET
和HEAD
请求。 - 支持
Range: bytes=start-end
请求头。
12. 自定义 HTTP 配置
gin.Default()
或 gin.New()
返回的是一个 *gin.Engine
实例,它实现了 gin.IRoutes
路由接口,也是实现了 http.Handler
接口。因此可以直接使用 Go 内置的 net/http
模块把 *gin.Engine
实例作为 http.Handler
启动 HTTP/HTTPS 服务器。
import "net/http"
func main() {
router := gin.Default()
http.ListenAndServe(":8080", router)
}
或
import "net/http"
func main() {
router := gin.Default()
server := &http.Server{
Addr: ":8080",
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
server.ListenAndServe()
}
实际上 gin.Default().Run()
内部也是通过调用 http.ListenAndServe()
启动服务。
13. HTTP2 Server Push
package main
import (
"github.com/gin-gonic/gin"
"html/template"
"log"
"net/http"
)
func main() {
// 内存中创建一个 HTML 模板,名称为 "index"
indexHtml, err := template.New("index").Parse(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello</title>
<script src="/assets/app.js"></script>
</head>
<body>
Hello World
</body>
</html>
`)
if err != nil {
panic(err)
}
router := gin.Default()
router.Static("/assets", "./assets")
router.SetHTMLTemplate(indexHtml)
router.GET("/", func(ctx *gin.Context) {
// 主动推送资源
if pusher := ctx.Writer.Pusher(); pusher != nil {
if err = pusher.Push("/assets/app.js", nil); err != nil {
log.Printf("Push Failed: %+v\n", err)
}
}
ctx.HTML(http.StatusOK, "index", gin.H{})
})
// 必须是 TLS 服务才支持 HTTP2 Server Push
err = router.RunTLS(":8080", "cert.pem", "key.pem")
if err != nil {
panic(err)
}
}
14. HTML 渲染器
使用 HTML 渲染器,要先加载 HTML 模板文件,相关方法:
engine.LoadHTMLGlob(pattern string)
:加载由 glob pattern 标识的 HTML 文件,并将结果与 HTML 渲染器相关联。默认文件名为 HTML 文件模板的名称。engine.LoadHTMLFiles(files ...string)
:加载一段 HTML 文件,并将结果与 HTML 渲染器相关联。默认文件名为 HTML 文件模板的名称。
HTML 渲染器方法:
ctx.HTML(code int, name string, obj any)
:渲染 HTML 模板,name 表示加载的模板名称,obj 是模板中可以使用对象。自动设置Content-Type: text/html; charset=utf-8
响应头。
14.1 HTML 渲染器简单示例
下面是一个 HTML 模板文件:./templates/hello.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello</title>
</head>
<body>
My name is {{ .name }}, age is {{ .age }}.
</body>
</html>
{{ .name }}
和 {{ .age }}
表示引用对象的属性插值。模板中引用对象属性,默认使用 {{ }}
分隔符,可以通过 router.Delims(left, right string)
自定义设置分隔符,例如:router.Delims("{[{", "}]}")
。
加载模板,启动 HTTP 服务器:./main.go
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
router := gin.Default()
router.LoadHTMLGlob("./templates/*.html") // 把 "./templates/" 目录下的所有 html 文件加载为模板,对应的模板名称为文件名
// router.LoadHTMLFiles("./templates/hello.html") // 加载单个 HTML 文件模板
// GET /hello
router.GET("/hello", func(ctx *gin.Context) {
// 渲染名称为 "hello.html" 的已加载模板,并向模板中传入一个对象
ctx.HTML(http.StatusOK, "hello.html", gin.H{
"name": "tom",
"age": 18,
})
})
err := router.Run(":8080")
if err != nil {
panic(err)
}
}
测试访问:
$ curl http://localhost:8080/hello
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello</title>
</head>
<body>
My name is tom, age is 18.
</body>
</html>
14.2 自定义模板名称
LoadHTMLGlob()
和 LoadHTMLFiles()
加载的 HTML 模板,默认使用文件名作为模板名称。
在 HTML 模板文件中通过 {{ define "name" }}
和 {{ end }}
包裹 HTML 自定义 HTML 对应的模板名称。HTML 模板文件,可以命名为 *.tmpl
,例如下面一个模板文件:./templates/hello.tmpl
{{ define "hello-tmpl" }}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello</title>
</head>
<body>
hello-tmpl: My name is {{ .name }}, age is {{ .age }}.
</body>
</html>
{{ end }}
上面模板文件中,模板名称为 hello-tmpl
,当加载这个模板文件后,自动注册该模板,由 {{ define "name" }}
和 {{ end }}
中间包裹的内容就是该模板名称对应的 HTML 渲染内容:
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
router := gin.Default()
router.LoadHTMLGlob("./templates/*.tmpl")
// router.LoadHTMLFiles("./templates/hello.tmpl")
router.GET("/hello", func(ctx *gin.Context) {
// 渲染名称为 "hello-tmpl" 的已加载模板,并向模板中传入一个对象
ctx.HTML(http.StatusOK, "hello-tmpl", gin.H{
"name": "tom",
"age": 18,
})
})
err := router.Run(":8080")
if err != nil {
panic(err)
}
}
测试访问:
$ curl http://localhost:8080/hello
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello</title>
</head>
<body>
hello-tmpl: My name is tom, age is 18.
</body>
</html>
一个 HTML 模板文件中,可以有多组 {{ define "name" }} ... {{ end }}
,分别对应多个模板。模板文件本身也会以文件名为名称注册一个模板,其模板内容为 {{ define "name" }} ... {{ end }}
之外的内容。
14.3 自定义模板功能
模板中通过 {{ .attr }}
可以直接引用对象属性,还可以在其中调用一个函数:
package main
import (
"github.com/gin-gonic/gin"
"html/template"
"net/http"
"time"
)
func FormatAsDate(t time.Time) string {
return t.Format("2006-01-02 15:04:05")
}
func main() {
router := gin.Default()
// 设置函数映射
router.SetFuncMap(template.FuncMap{
"formatAsDate": FormatAsDate,
})
// 加载 HTML 模板
router.LoadHTMLGlob("./*.tmpl")
router.GET("/get-date", func(ctx *gin.Context) {
ctx.HTML(http.StatusOK, "get-date.tmpl", gin.H{
"now": time.Now(),
})
})
err := router.Run(":8080")
if err != nil {
panic(err)
}
}
在模板中调用函数:./get-date.tmpl
Date: {{ .now | formatAsDate }}
结果:
Date: 2025-10-01 15:00:00
15. 其他
15.1 gin.Context 副本
如果要在中间件中使用 Goroutine 执行异步任务,则使用的上下文必须是 gin.Context 的副本:
package main
import (
"github.com/gin-gonic/gin"
"log"
"net/http"
"time"
)
func main() {
router := gin.Default()
router.GET("/run-async-task", func(ctx *gin.Context) {
cpCtx := ctx.Copy()
go func() {
time.Sleep(5 * time.Second)
log.Println("Done! in path ", cpCtx.Request.URL.Path)
}()
ctx.String(http.StatusOK, "task start")
})
err := router.Run(":8080")
if err != nil {
panic(err)
}
}
15.2 优雅地重启或停止
优雅地重启或停止服务,主要是通过接收操作系统发出的 中断信号(os.Interrupt
) 或 强制停止信号(os.Kill
),然后做收尾工作,再主动关闭服务。
package main
import (
"context"
"errors"
"github.com/gin-gonic/gin"
"log"
"net/http"
"os"
"os/signal"
"time"
)
func main() {
router := gin.Default()
router.GET("/ping", func(ctx *gin.Context) {
ctx.String(http.StatusOK, "pong")
})
server := &http.Server{
Addr: ":8080",
Handler: router,
}
// 异步启动服务
go func() {
err := server.ListenAndServe()
if err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("listen error: %+v\n", err)
}
}()
// 监听中断信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
<-quit
log.Println("Interrupt signal received.")
// 收到中断信号后,主动关闭服务(设置5秒超时时间,超过5秒没有正常关闭服务,则取消上下文)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() // 如果顺利关闭服务,则提前取消掉上下文
err := server.Shutdown(ctx) // 主动关闭服务,关闭服务前有5秒钟时间做收尾工作(例如关闭其他关联的服务,把缓存的日志刷到磁盘)
if err != nil {
log.Fatalf("Server Shutdown Error: %+v\n", err)
}
log.Println("Server Shutdown.")
}
启动服务后,按 Ctrl + C 向进程发出中断信号。
15.3 运行多个服务
一个进程可以运行多个服务,包括 HTTP服务、gRPC服务、Kafka服务 等。
管理多个服务,可以使用 golang.org/x/sync/errgroup 或 sync.WaitGroup。
errgroup
包为执行共同任务的子任务 goroutine 组提供了同步、error传播 和 上下文取消功能。
errgroup.Group
内部使用 sync.WaitGroup
实现,但增加了对返回错误的任务的处理。
安装 errgroup
:
go get -u golang.org/x/sync/errgroup
下面代码中启动两个 HTTP 服务 和 一个等待接收中断信号的服务,并在接收到中断信号时优雅地关闭服务:
package main
import (
"context"
"errors"
"github.com/gin-gonic/gin"
"golang.org/x/sync/errgroup"
"log"
"net/http"
"os"
"os/signal"
"time"
)
func main() {
ctx := context.Background()
// 返回一个新的 errgroup.Group 和一个从 ctx 派生的关联 Context。
// 当传递给 group.Go() 的函数第一次返回非 nil error 或 第一次 group.Wait() 返回时(以先发生者为准),返回的 ctx 会被取消
group, ctx := errgroup.WithContext(ctx)
// HTTP 服务
router1 := gin.Default()
router1.GET("/ping", func(ctx *gin.Context) {
ctx.String(http.StatusOK, "pong")
})
server1 := &http.Server{
Addr: ":8080",
Handler: router1,
}
// 在新的 goroutine 中启动服务
group.Go(func() error {
log.Println("Server1 Start.")
return server1.ListenAndServe()
})
// HTTP 服务
router2 := gin.Default()
router2.GET("/ping", func(ctx *gin.Context) {
ctx.String(http.StatusOK, "pong")
})
server2 := &http.Server{
Addr: ":8081",
Handler: router1,
}
group.Go(func() error {
log.Println("Server2 Start.")
return server2.ListenAndServe()
})
// 异步接收中断信号的服务
group.Go(func() error {
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
<-quit
log.Println("Interrupt Signal Received.")
// 收到中断信号后,主动关闭服务(超时5秒则强制取消上下文)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 主动关闭 HTTP 服务
if err := server1.Shutdown(ctx); err != nil {
log.Printf("Shutdown Server1 Error: %+v\n", err)
} else {
log.Println("Server1 Shutdown.")
}
// 主动关闭 HTTP 服务
if err := server2.Shutdown(ctx); err != nil {
log.Printf("Shutdown Server2 Error: %+v\n", err)
} else {
log.Println("Server2 Shutdown.")
}
log.Println("All Server Safely Exited.")
return nil
})
// 等待组内所有 goroutine 结束(如果其中一个 goroutine 返回了 err,则会取消 ctx),
// 返回的 err 是这一组 goroutine 中第一个返回的非 nil error
if err := group.Wait(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalln(err)
}
// group.Wait() 内部也是通过调用 sync.WaitGroup 的 Wait() 方法实现的等待
log.Println("APP EXISTS.")
}