Gin

2021-08-21T14:21:02+08:00 | 11分钟阅读 | 更新于 2021-08-21T14:21:02+08:00

@

学习目标

学完本节,你应该能够:

  1. 跑起第一个 Gin 服务,并讲清 gin.New()gin.Default() 的本质区别——后者只是前者加上了 LoggerRecovery 两个默认中间件。
  2. 用路由分组组织 API:能写嵌套分组、给分组挂中间件(如鉴权),理解它对版本管理和代码组织的价值。
  3. 从各种来源取参数并校验:URL 路径参数、GET/POST 表单、JSON 请求体,并用 binding 标签做参数校验,还能把英文报错翻译成中文。
  4. 写自定义中间件并讲清执行顺序:理解 c.Next() / c.Abort() 背后的"洋葱模型",说清全局、分组、单路由三层中间件的先后关系。
  5. 实现优雅退出:讲清 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 错误

对比

方法LoggerRecovery适用场景
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固定长度
email邮箱格式
urlURL格式
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()
}

中间件执行顺序

  1. 全局中间件(Use添加)
  2. 路由组中间件(Group.Use添加)
  3. 路由中间件(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()

优雅退出流程

  1. 接收到退出信号(SIGINT/SIGTERM)
  2. 停止接收新请求
  3. 等待当前请求处理完成
  4. 关闭数据库连接等资源
  5. 退出进程

下面这张图把"优雅退出"的时序画清楚——重点是收到信号后拒绝新请求、给在途请求一个宽限期

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() 的写法,才能拿到关闭入口。

自测题与动手练习

自测题(合上书能答出来,才算懂)

  1. gin.Default() 相比 gin.New() 多挂了哪两个中间件?如果生产环境不想用默认 Logger,该用 New 还是 Default
  2. 路由分组 r.Group("/api").Group("/v1") 最终拼出的路径前缀是什么?给分组 Use(AuthMiddleware()) 后,分组内所有路由都会经过它吗?
  3. c.Param("id") 取的是哪类参数?它和 c.Query("id")c.PostForm("id") 分别来自请求的哪个部分?
  4. c.Abort() 之后,后面的中间件和业务 Handler 还会执行吗?如果某个中间件在 c.Abort() 之前已经 c.Next() 了,它的"后置"代码(Next 之后的逻辑)还会跑吗?
  5. 优雅退出时,为什么要用 srv.Shutdown(ctx) 而不是直接 os.Exit()context 超时在这里控制的是什么?

动手练习(建议真做一遍)

  1. AuthMiddleware(requiredRole string),只对 /admin 路由生效;用 curl -H "X-Role: admin" http://localhost:8080/admin 验证通过、缺头返回 403。
  2. 定义一个带 binding:"required,email" 的结构体,用 ShouldBindJSON 接收;传一个非法邮箱,再把英文报错通过 universal-translator 翻译成中文返回。
  3. 把"优雅退出"完整示例跑起来,发起一个会 Sleep 5s 的请求,在请求中途 kill -TERM <pid>,观察日志:请求是否处理完才退出?再把宽限期改成 1s 看请求是否被截断。

本章小结

  • 初始化二选一gin.New() 是裸路由器,gin.Default() = New() + Logger + Recovery;要自定义中间件链就用 New()
  • 分组 = 前缀 + 共享中间件:路由分组用来做版本管理(/api/v1)和按模块挂鉴权,避免每个路由重复写中间件。
  • 取参四件套:路径 Param、GET Query/DefaultQuery、POST PostForm/DefaultPostForm、JSON ShouldBindJSON;校验靠 binding 标签,翻译靠 universal-translator
  • 中间件是洋葱模型c.Next() 前是"前置"、后是"后置",c.Abort() 直接截断后续;执行顺序是 全局 → 分组 → 单路由。
  • 优雅退出靠 srv.Shutdown:信号 + context 宽限期,做到"拒新接旧、在途处理完再走"。
  • 下一篇可以顺着路由和中间件,去读 Gin 的源码(Engine.Handle / Context.Next),或进阶到 gRPC / Kitex 这类 RPC 框架,对比"HTTP 路由"和"RPC 方法"两种服务定义方式。
About Me

没什么想介绍的,一个很大众的码农…

喜欢代码,车,马,真的是 🐎

讨厌别人让我给自己的代码写注释 最厌烦别人的程序没有写注释

目标

学AI,加油!加油!