学习目标
学完本节,你应该能够:
- 跑起第一个 Gin 服务,并讲清
gin.New()与gin.Default()的本质区别——后者只是前者加上了Logger和Recovery两个默认中间件。 - 用路由分组组织 API:能写嵌套分组、给分组挂中间件(如鉴权),理解它对版本管理和代码组织的价值。
- 从各种来源取参数并校验:URL 路径参数、GET/POST 表单、JSON 请求体,并用
binding标签做参数校验,还能把英文报错翻译成中文。 - 写自定义中间件并讲清执行顺序:理解
c.Next()/c.Abort()背后的"洋葱模型",说清全局、分组、单路由三层中间件的先后关系。 - 实现优雅退出:讲清 SIGINT/SIGTERM 信号 +
context超时如何做到"停止接新请求、把在途请求处理完再退出"。
前置知识:Go 基础(函数、结构体、interface)、net/http 的基本概念(Handler、Request、ResponseWriter)。
本章你会动手做的事:
- 跑通 HelloWorld,用
curl访问/看返回。 - 写一个
AuthMiddleware,只对/admin路由生效,缺少X-Role头返回 403。 - 用
c.ShouldBindJSON接收登录请求,故意传非法 JSON 看校验报错;再把报错翻译成中文返回。 - 加优雅退出,
kill -TERM进程时观察"在途请求处理完才退出"。
类比:Gin 的"中间件"就像快递中转站的层层安检。一个请求从进来到出去,要依次经过安检1、安检2、安检3,到达真正的"分发中心"(你的 Handler),然后再原路返回穿过安检3、安检2、安检1。任何一层说"不合格"(Abort),后面的安检和分发中心就都不走了,直接回头。这种"进去一趟、出来一趟"的结构就是后面要讲的"洋葱模型"。
Gin的HelloWorld体验
安装Gin
go get -u github.com/gin-gonic/gin
基本示例
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Hello, Gin!",
})
})
r.Run(":8080")
}
运行方式
go run main.go
访问 http://localhost:8080 即可看到响应。
项目结构
gin-app/
├── go.mod
├── go.sum
└── main.go
使用New和Default初始化路由器的区别
gin.New()
r := gin.New()
创建一个不带任何默认中间件的路由器。
gin.Default()
r := gin.Default()
创建一个带有默认中间件的路由器,包括:
- Logger:日志中间件,记录请求信息
- Recovery:恢复中间件,捕获 panic 并返回 500 错误
对比
| 方法 | Logger | Recovery | 适用场景 |
|---|---|---|---|
| gin.New() | 否 | 否 | 需要自定义中间件 |
| gin.Default() | 是 | 是 | 快速开发、调试 |
下面这张图把"Default 其实是 New + 两个中间件"这件事画清楚:
flowchart LR
A[gin.New] -->|无中间件| B[空路由器]
C[gin.Default] --> D[gin.New 的空路由器]
D --> E[+ Logger 中间件]
E --> F[+ Recovery 中间件]
F --> G[带默认中间件的路由器]手动添加中间件
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())
Gin的路由分组
基本路由分组
r := gin.Default()
api := r.Group("/api")
{
api.GET("/users", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "users"})
})
api.GET("/posts", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "posts"})
})
}
嵌套路由分组
v1 := api.Group("/v1")
{
v1.GET("/users", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "v1 users"})
})
}
分组中间件
auth := r.Group("/auth")
auth.Use(AuthMiddleware())
{
auth.POST("/login", LoginHandler)
auth.POST("/register", RegisterHandler)
}
路由组的优势
- 代码组织:按功能模块组织路由
- 复用中间件:为整个分组添加中间件
- 版本管理:便于 API 版本控制
白话类比:路由分组就像公司大楼的分部门。一楼是
/api,二楼是/api/v1,每个部门共享楼里的电梯和门禁(分组中间件),但不同部门内部的房间(具体路由)各管各的。你要找"v1 的用户接口",地址就是/api/v1/users——层级清晰,不会撞名。
flowchart TD
R["根路由器 /"] --> API["/api"]
API --> Users["/api/users"]
API --> Posts["/api/posts"]
API --> V1["/api/v1"]
V1 --> V1Users["/api/v1/users"]
Auth["/auth 带AuthMiddleware"] --> Login["/auth/login"]
Auth --> Register["/auth/register"]获取URL中的变量
基本参数
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{"user_id": id})
})
多个参数
r.GET("/users/:id/posts/:post_id", func(c *gin.Context) {
id := c.Param("id")
postID := c.Param("post_id")
c.JSON(200, gin.H{
"user_id": id,
"post_id": postID,
})
})
可选参数
r.GET("/users/:id/*action", func(c *gin.Context) {
id := c.Param("id")
action := c.Param("action")
c.JSON(200, gin.H{
"user_id": id,
"action": action,
})
})
参数验证
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
userID, err := strconv.Atoi(id)
if err != nil {
c.JSON(400, gin.H{"error": "invalid user id"})
return
}
c.JSON(200, gin.H{"user_id": userID})
})
获取GET和POST表单信息
GET参数
r.GET("/search", func(c *gin.Context) {
keyword := c.Query("keyword")
page := c.DefaultQuery("page", "1")
c.JSON(200, gin.H{
"keyword": keyword,
"page": page,
})
})
POST表单
r.POST("/login", func(c *gin.Context) {
username := c.PostForm("username")
password := c.PostForm("password")
c.JSON(200, gin.H{
"username": username,
"password": password,
})
})
默认值
username := c.DefaultPostForm("username", "guest")
获取所有参数
r.POST("/submit", func(c *gin.Context) {
form := make(map[string]string)
c.Bind(&form)
c.JSON(200, form)
})
JSON请求体
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
r.POST("/login", func(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{
"username": req.Username,
"password": req.Password,
})
})
Gin返回Protobuf
安装依赖
go get github.com/golang/protobuf/proto
go get google.golang.org/protobuf
定义Proto文件
syntax = "proto3";
package hello;
option go_package = "./pb";
message HelloResponse {
string message = 1;
int32 code = 2;
}
生成Go代码
protoc --go_out=. hello.proto
返回Protobuf
import (
"github.com/gin-gonic/gin"
"github.com/golang/protobuf/proto"
"yourproject/pb"
)
r.GET("/hello", func(c *gin.Context) {
resp := &pb.HelloResponse{
Message: "Hello, Protobuf!",
Code: 200,
}
c.ProtoBuf(200, resp)
})
设置Content-Type
c.Header("Content-Type", "application/x-protobuf")
c.ProtoBuf(200, resp)
客户端调用
resp, err := http.Get("http://localhost:8080/hello")
if err != nil {
panic(err)
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
var hello pb.HelloResponse
proto.Unmarshal(data, &hello)
fmt.Println(hello.Message)
登录的表单验证
使用Binding
type LoginRequest struct {
Username string `form:"username" binding:"required"`
Password string `form:"password" binding:"required,min=6"`
}
r.POST("/login", func(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBind(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{"message": "login success"})
})
验证规则
| 规则 | 说明 |
|---|---|
| required | 必填 |
| min | 最小长度 |
| max | 最大长度 |
| len | 固定长度 |
| 邮箱格式 | |
| url | URL格式 |
| regexp | 正则表达式 |
自定义验证
func ValidatePassword(fl validator.FieldLevel) bool {
password := fl.Field().String()
return len(password) >= 6 && len(password) <= 20
}
r := gin.Default()
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("password", ValidatePassword)
}
type LoginRequest struct {
Password string `form:"password" binding:"required,password"`
}
注册表单的验证
复杂验证示例
type RegisterRequest struct {
Username string `form:"username" binding:"required,min=3,max=20"`
Email string `form:"email" binding:"required,email"`
Password string `form:"password" binding:"required,min=6,max=20"`
Age int `form:"age" binding:"gte=18,lte=100"`
}
r.POST("/register", func(c *gin.Context) {
var req RegisterRequest
if err := c.ShouldBind(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{"message": "register success"})
})
跨字段验证
func ValidatePasswordConfirm(fl validator.FieldLevel) bool {
password := fl.Field().String()
confirmPassword := fl.Parent().FieldByName("PasswordConfirm").String()
return password == confirmPassword
}
type RegisterRequest struct {
Password string `form:"password" binding:"required,min=6"`
PasswordConfirm string `form:"password_confirm" binding:"required,eqfield=Password"`
}
验证错误处理
if err := c.ShouldBind(&req); err != nil {
var errs validator.ValidationErrors
if errors.As(err, &errs) {
result := make(map[string]string)
for _, e := range errs {
field := e.Field()
tag := e.Tag()
result[field] = fmt.Sprintf("%s validation failed: %s", field, tag)
}
c.JSON(400, gin.H{"errors": result})
}
return
}
表单验证错误翻译成中文
安装翻译包
go get github.com/go-playground/locales
go get github.com/go-playground/universal-translator
配置翻译
import (
"github.com/go-playground/locales/zh"
"github.com/go-playground/universal-translator"
ut "github.com/go-playground/universal-translator"
)
func main() {
r := gin.Default()
zh := zh.New()
ut := ut.New(zh, zh)
translator, _ := ut.GetTranslator("zh")
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
_ = zh.RegisterDefaultTranslations(v, translator)
// 自定义翻译
v.RegisterTranslation("required", translator, func(ut ut.Translator) error {
return ut.Add("required", "{0} 必填", true)
}, func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("required", fe.Field())
return t
})
}
}
使用翻译
func TranslateErrors(err error, translator ut.Translator) map[string]string {
var errs validator.ValidationErrors
if errors.As(err, &errs) {
result := make(map[string]string)
for _, e := range errs {
result[e.Field()] = e.Translate(translator)
}
return result
}
return nil
}
r.POST("/login", func(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBind(&req); err != nil {
errors := TranslateErrors(err, translator)
c.JSON(400, gin.H{"errors": errors})
return
}
})
表单中文翻译的JSON格式化细节
自定义格式化函数
func FormatValidationErrors(errs validator.ValidationErrors, translator ut.Translator) []map[string]interface{} {
result := make([]map[string]interface{}, 0)
for _, e := range errs {
result = append(result, map[string]interface{}{
"field": e.Field(),
"message": e.Translate(translator),
"tag": e.Tag(),
"value": e.Value(),
})
}
return result
}
返回格式化JSON
r.POST("/register", func(c *gin.Context) {
var req RegisterRequest
if err := c.ShouldBind(&req); err != nil {
var errs validator.ValidationErrors
if errors.As(err, &errs) {
formatted := FormatValidationErrors(errs, translator)
c.JSON(400, gin.H{
"code": 400,
"message": "validation failed",
"errors": formatted,
})
return
}
}
c.JSON(200, gin.H{"message": "success"})
})
响应示例
{
"code": 400,
"message": "validation failed",
"errors": [
{
"field": "Username",
"message": "Username 必填",
"tag": "required",
"value": ""
},
{
"field": "Password",
"message": "Password 长度必须至少为 6 个字符",
"tag": "min",
"value": "123"
}
]
}
自定义Gin中间件
基本中间件
func LoggerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
startTime := time.Now()
c.Next()
duration := time.Since(startTime)
log.Printf("[%s] %s %s %v",
c.Request.Method,
c.Request.URL.Path,
c.Request.Proto,
duration,
)
}
}
r.Use(LoggerMiddleware())
带参数的中间件
func AuthMiddleware(requiredRole string) gin.HandlerFunc {
return func(c *gin.Context) {
role := c.GetHeader("X-Role")
if role != requiredRole {
c.JSON(403, gin.H{"error": "forbidden"})
c.Abort()
return
}
c.Next()
}
}
r.GET("/admin", AuthMiddleware("admin"), func(c *gin.Context) {
c.JSON(200, gin.H{"message": "admin page"})
})
全局中间件
r := gin.Default()
r.Use(LoggerMiddleware())
r.Use(RecoveryMiddleware())
路由组中间件
api := r.Group("/api")
api.Use(AuthMiddleware())
{
api.GET("/users", GetUsers)
}
单个路由中间件
r.GET("/profile", AuthMiddleware(), func(c *gin.Context) {
c.JSON(200, gin.H{"message": "profile"})
})
通过Abort终止中间件后续逻辑的执行
基本用法
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(401, gin.H{"error": "unauthorized"})
c.Abort()
return
}
c.Next()
}
}
AbortWithStatus
c.AbortWithStatus(401)
AbortWithStatusJSON
c.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"})
AbortWithError
c.AbortWithError(401, errors.New("unauthorized"))
中间件执行流程
中间件1 → 中间件2 → 中间件3 → 处理器 → 中间件3 → 中间件2 → 中间件1
↑
Abort()
终止执行,直接返回
检查是否已终止
func LoggerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if c.IsAborted() {
return
}
// 日志逻辑
c.Next()
}
}
白话类比(洋葱模型):每个请求像一根串着多层的竹签,从外层中间件穿进去,到达最里面的 Handler(业务),再原路穿出来。在任意一层调用
c.Abort(),相当于在那一层的入口把竹签"折了"——里面的 Handler 和后续中间件都不会执行,直接回到最外层返回响应。下面这张图把"进去 → 出来"和"Abort 截断"画清楚:
flowchart TD
Req[请求进入] --> MW1[中间件1 前置]
MW1 --> MW2[中间件2 前置]
MW2 --> MW3[中间件3 前置]
MW3 --> H[业务 Handler]
H --> MW3b[中间件3 后置]
MW3b --> MW2b[中间件2 后置]
MW2b --> MW1b[中间件1 后置]
MW1b --> Resp[响应返回]
Req2[请求进入] --> A1[中间件1 前置]
A1 --> A2[中间件2 前置]
A2 -->|Abort| Stop[直接返回响应
后续不执行]Gin的中间件原理源码分析
HandlerChain
type HandlerChain []HandlerFunc
Context结构
type Context struct {
handlers HandlersChain
index int8
// ...
}
Next方法
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
Abort方法
func (c *Context) Abort() {
c.index = abortIndex
}
Use方法
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
return engine
}
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
路由匹配
func (engine *Engine) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath)
handlers = group.combineHandlers(handlers)
engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
中间件执行顺序
- 全局中间件(Use添加)
- 路由组中间件(Group.Use添加)
- 路由中间件(Handle时传入)
Gin返回HTML
基本用法
r.LoadHTMLGlob("templates/*")
r.GET("/", func(c *gin.Context) {
c.HTML(200, "index.html", gin.H{
"title": "Hello, Gin!",
"name": "Alice",
})
})
模板文件
<!-- templates/index.html -->
<!DOCTYPE html>
<html>
<head>
<title>{{ .title }}</title>
</head>
<body>
<h1>Hello, {{ .name }}!</h1>
</body>
</html>
模板函数
r.SetFuncMap(template.FuncMap{
"upper": strings.ToUpper,
})
r.LoadHTMLGlob("templates/*")
使用模板函数
<h1>{{ upper .name }}</h1>
自定义分隔符
r.Delims("{{{", "}}}")
加载多个HTML文件
加载多个目录
r.LoadHTMLGlob("templates/**/*")
模板结构
templates/
├── index.html
├── about.html
└── admin/
├── dashboard.html
└── users.html
引用模板
r.GET("/admin/dashboard", func(c *gin.Context) {
c.HTML(200, "admin/dashboard.html", gin.H{})
})
模板继承
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
<title>{{ .title }}</title>
</head>
<body>
{{ block "content" . }}{{ end }}
</body>
</html>
<!-- templates/index.html -->
{{ template "base.html" . }} {{ define "content" }}
<h1>Hello, {{ .name }}!</h1>
{{ end }}
包含模板
{{ template "header.html" }}
<div>Content</div>
{{ template "footer.html" }}
Static静态文件的处理
基本用法
r.Static("/static", "./static")
访问静态文件
<link rel="stylesheet" href="/static/css/style.css" />
<img src="/static/images/logo.png" />
静态文件结构
static/
├── css/
│ └── style.css
├── js/
│ └── app.js
└── images/
└── logo.png
单个文件
r.StaticFile("/favicon.ico", "./static/favicon.ico")
静态文件索引
r.StaticFS("/static", http.Dir("./static"))
生产环境优化
- 使用 CDN 托管静态文件
- 启用 Gzip 压缩
- 设置缓存头
Gin的优雅退出
基本实现
func main() {
r := gin.Default()
r.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "hello"})
})
srv := &http.Server{
Addr: ":8080",
Handler: r,
}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
<-quit
log.Println("Shutdown Server ...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server Shutdown:", err)
}
log.Println("Server exiting")
}
处理信号
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
超时控制
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
优雅退出流程
- 接收到退出信号(SIGINT/SIGTERM)
- 停止接收新请求
- 等待当前请求处理完成
- 关闭数据库连接等资源
- 退出进程
下面这张图把"优雅退出"的时序画清楚——重点是收到信号后拒绝新请求、给在途请求一个宽限期:
flowchart TD
A[进程运行 接收请求] --> B[收到 SIGINT/SIGTERM]
B --> C[通知 Server 停止 Accept 新连接]
C --> D{在途请求
5~10s 宽限期内完成?}
D -->|完成| E[关闭 DB 等资源]
D -->|超时| F[强制结束在途请求]
E --> G[进程退出]
F --> G完整示例
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/", func(c *gin.Context) {
time.Sleep(5 * time.Second)
c.JSON(200, gin.H{"message": "hello"})
})
srv := &http.Server{
Addr: ":8080",
Handler: r,
}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}
log.Println("Server exiting")
}
⚠️ 新手必踩的坑:优雅退出时
context超时设太短。上面示例给在途请求 10 秒宽限期,但你的 Handler 如果可能跑 30 秒(比如大查询),宽限期一到请求就被掐断,客户端拿到 500。原则是:宽限期要大于你最慢的正常请求耗时,或者 Handler 内部也监听c.Request.Context().Done()主动提前停止。
⚠️ 新手必踩的坑:
r.Run()是阻塞的,没法直接优雅退出。r.Run()内部直接http.ListenAndServe,返回不了*http.Server,你就拿不到Shutdown方法。生产要用srv := &http.Server{Handler: r}+go srv.ListenAndServe()的写法,才能拿到关闭入口。
自测题与动手练习
自测题(合上书能答出来,才算懂):
gin.Default()相比gin.New()多挂了哪两个中间件?如果生产环境不想用默认 Logger,该用New还是Default?- 路由分组
r.Group("/api").Group("/v1")最终拼出的路径前缀是什么?给分组Use(AuthMiddleware())后,分组内所有路由都会经过它吗? c.Param("id")取的是哪类参数?它和c.Query("id")、c.PostForm("id")分别来自请求的哪个部分?c.Abort()之后,后面的中间件和业务 Handler 还会执行吗?如果某个中间件在c.Abort()之前已经c.Next()了,它的"后置"代码(Next 之后的逻辑)还会跑吗?- 优雅退出时,为什么要用
srv.Shutdown(ctx)而不是直接os.Exit()?context超时在这里控制的是什么?
动手练习(建议真做一遍):
- 写
AuthMiddleware(requiredRole string),只对/admin路由生效;用curl -H "X-Role: admin" http://localhost:8080/admin验证通过、缺头返回 403。 - 定义一个带
binding:"required,email"的结构体,用ShouldBindJSON接收;传一个非法邮箱,再把英文报错通过universal-translator翻译成中文返回。 - 把"优雅退出"完整示例跑起来,发起一个会
Sleep 5s的请求,在请求中途kill -TERM <pid>,观察日志:请求是否处理完才退出?再把宽限期改成1s看请求是否被截断。
本章小结
- 初始化二选一:
gin.New()是裸路由器,gin.Default()=New()+ Logger + Recovery;要自定义中间件链就用New()。 - 分组 = 前缀 + 共享中间件:路由分组用来做版本管理(/api/v1)和按模块挂鉴权,避免每个路由重复写中间件。
- 取参四件套:路径
Param、GETQuery/DefaultQuery、POSTPostForm/DefaultPostForm、JSONShouldBindJSON;校验靠binding标签,翻译靠universal-translator。 - 中间件是洋葱模型:
c.Next()前是"前置"、后是"后置",c.Abort()直接截断后续;执行顺序是 全局 → 分组 → 单路由。 - 优雅退出靠
srv.Shutdown:信号 +context宽限期,做到"拒新接旧、在途处理完再走"。 - 下一篇可以顺着路由和中间件,去读 Gin 的源码(
Engine.Handle/Context.Next),或进阶到 gRPC / Kitex 这类 RPC 框架,对比"HTTP 路由"和"RPC 方法"两种服务定义方式。