照着做完这一篇,你能从零跑起一个带分组路由、自定义中间件、参数绑定与字段校验的 Gin 服务,并知道每一步容易在哪里翻车。
第一步:初始化项目,先跑通一个 hello
新建目录并初始化模块:
mkdir gin-demo && cd gin-demo
go mod init gin-demo
go get github.com/gin-gonic/gin
最小可运行代码 main.go:
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default() // 自带 Logger 与 Recovery 中间件
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})
r.Run(":8080") // 默认监听 0.0.0.0:8080
}
go run . 后访问 http://localhost:8080/ping 看到 {"message":"pong"} 就算通了。
注意:上线前把 gin.Default() 换成 gin.New() 加自己挑的中间件,并设置 gin.SetMode(gin.ReleaseMode),否则控制台会一直打调试日志。
第二步:路由——分组、路径参数、查询参数
r := gin.Default()
// 路径参数
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{"id": id})
})
// 通配符(放在最后一段)
r.GET("/static/*filepath", func(c *gin.Context) {
c.JSON(200, gin.H{"path": c.Param("filepath")})
})
// 查询参数
r.GET("/search", func(c *gin.Context) {
kw := c.Query("kw") // 取不到返回 ""
page := c.DefaultQuery("page", "1")
tags := c.QueryArray("tag") // ?tag=a&tag=b
c.JSON(200, gin.H{"kw": kw, "page": page, "tags": tags})
})
// 路由分组:把版本号、公共中间件收在一起
v1 := r.Group("/api/v1")
{
v1.GET("/posts", listPosts)
v1.POST("/posts", createPost)
}
注意:Gin 的路由树不允许同一层出现「静态段与参数段冲突」,同时注册 /users/:id 和 /users/new 会在启动时直接 panic,只能改成 /users/:id + /users/detail 这类结构。
第三步:中间件——全局、分组、单路由三层
func AuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token != "Bearer secret" {
c.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"})
return
}
c.Set("uid", int64(1001)) // 往下游传值
c.Next() // 放行
}
}
三种挂载方式:
r.Use(gin.Recovery()) // 全局
api := r.Group("/api", AuthRequired()) // 分组
r.GET("/me", AuthRequired(), meHandler) // 单路由
下游取值:uid := c.MustGet("uid").(int64),或 c.Get("uid") 返回 (any, bool)。
注意:c.Next() 之前的代码是「请求前」,之后的是「响应后」。如果你想在响应后记录耗时、改写状态码,写在 c.Next() 后面是可行的;但一旦在 c.Next() 之前已经写了 c.JSON,后面再写就会报 headers already written。拦截请求请用 c.Abort(),别用 return 了事——不 Abort 后面的处理函数照样执行。
第四步:参数绑定——结构体 tag 是关键
Gin 用 tag 决定从哪读数据:json 对 Body、form 对查询/表单、uri 对路径参数。
type CreateUserReq struct {
Name string `json:"name" binding:"required,min=2,max=20"`
Email string `json:"email" binding:"required,email"`
Age int `json:"age" binding:"gte=0,lte=120"`
Password string `json:"password" binding:"required,min=8"`
Role string `json:"role" binding:"omitempty,oneof=user admin"`
Tags []string `json:"tags" binding:"omitempty,max=5,dive,required"`
}
func createUser(c *gin.Context) {
var req CreateUserReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{"data": req})
}
常用绑定方法:
ShouldBindJSON / ShouldBindQuery / ShouldBindUri / ShouldBind(按 Content-Type 自动选)
- 路径参数统一走
c.ShouldBindUri(&req),tag 写 uri:"id"
注意:ShouldBind 系列只返回错误,由你自己决定状态码;Bind 系列会直接帮你写 400 并 Abort,返回的 error 主要用于打日志,别重复写响应。
注意:请求 Body 只能被读一次。同一个 handler 里先 ShouldBindJSON 再 ShouldBind 会失败,需要多次读就改用 c.ShouldBindBodyWith(&req, binding.JSON)。
注意:required 对数字类型意味着「不等于零值」,age 想允许 0 就得用 gte=0,或者把字段声明成 *int。
第五步:校验进阶——错误信息与自定义规则
打开 validator 引擎注册自定义规则,顺便把错误里的字段名换成 JSON 名:
import (
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
"reflect"
)
func notAdmin(fl validator.FieldLevel) bool {
return fl.Field().String() != "admin"
}
func initValidator() {
v, ok := binding.Validator.Engine().(*validator.Validate)
if !ok {
return
}
v.RegisterTagNameFunc(func(f reflect.StructField) string {
name := strings.SplitN(f.Tag.Get("json"), ",", 2)[0]
if name == "" || name == "-" {
return f.Name
}
return name
})
_ = v.RegisterValidation("notadmin", notAdmin)
}
友好错误输出:
func badRequest(err error) gin.H {
var ve validator.ValidationErrors
if errors.As(err, &ve) {
out := make(map[string]string, len(ve))
for _, e := range ve {
switch e.Tag() {
case "required":
out[e.Field()] = e.Field() + " 不能为空"
case "email":
out[e.Field()] = "邮箱格式不正确"
default:
out[e.Field()] = "不符合规则 " + e.Tag()
}
}
return gin.H{"errors": out}
}
return gin.H{"error": err.Error()}
}
这样前端拿到的是 {"errors":{"email":"邮箱格式不正确"}},而不是一长串英文说明。
注意:min/max 作用在字符串上是长度,作用在数字上是数值。字符串长度限制、数值区间限制分开用,能少一半误报。
小结
- 环境:
go mod init + go get gin,最小服务用 gin.Default().Run(":8080"),生产切 ReleaseMode。
- 路由:
:id 取路径参数、*filepath 取通配、Query/DefaultQuery/QueryArray 取查询参数,Group 做版本分组,静态段与参数段不能同级冲突。
- 中间件:全局
r.Use、分组 r.Group(..., mw)、单路由挂参;拦截用 c.Abort(),传值用 c.Set/c.Get。
- 绑定:
json/form/uri 三类 tag 对应三类数据源,Body 只能读一次,多次读用 ShouldBindBodyWith。
- 校验:
binding tag 覆盖必填、长度、区间、枚举、嵌套切片;自定义规则注册到 binding.Validator.Engine(),并重写字段名映射来输出中文错误。