Skip to content

19|Gin 路由与参数

go-gateway 的代理转发层用标准库 net/http 实现,代码轻量,能看清 HTTP 转发的底层机制。但管理后台需要处理多个 API 端点、路径参数、查询参数、请求体解析和统一响应,用标准库写会比较冗长。Gin 是国内 Go 后端开发最常用的 Web 框架,路由性能高、API 简洁、中间件生态丰富。本章用 Gin 搭建管理后台,运行在独立的 :8081 端口。

一、安装 Gin

bash
go get -u github.com/gin-gonic/gin
go mod tidy

二、最小 Gin 服务

go
package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	r.GET("/ping", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"message": "pong",
		})
	})

	r.Run(":8081")
}

gin.Default() 创建一个带默认中间件(日志和恢复)的引擎。r.GET 注册 GET 路由,c.JSON 返回 JSON 响应。r.Run 等价于 http.ListenAndServe

验证:

bash
go run main.go
curl http://127.0.0.1:8081/ping

三、路由组

管理 API 通常以 /admin 为前缀,用路由组组织:

go
admin := r.Group("/admin")
{
	admin.GET("/routes", listRoutes)
	admin.POST("/routes", addRoute)
	admin.DELETE("/routes/:id", deleteRoute)
}

路由组可以批量设置中间件和路径前缀。上面的代码等价于:

go
r.GET("/admin/routes", listRoutes)
r.POST("/admin/routes", addRoute)
r.DELETE("/admin/routes/:id", deleteRoute)

但路由组让代码结构更清晰,也方便后续给所有管理 API 统一加认证中间件。

四、路径参数

Gin 用 : 定义路径参数:

go
func getRoute(c *gin.Context) {
	id := c.Param("id")
	c.JSON(http.StatusOK, gin.H{"id": id})
}

admin.GET("/routes/:id", getRoute)

请求 /admin/routes/abc123 时,id 的值是 abc123

路径参数可以出现在任意位置:

go
r.GET("/api/:version/users", func(c *gin.Context) {
	version := c.Param("version")
	c.JSON(http.StatusOK, gin.H{"version": version})
})

五、查询参数

查询参数用 c.Query 读取:

go
func listRoutes(c *gin.Context) {
	page := c.DefaultQuery("page", "1")
	limit := c.DefaultQuery("limit", "10")

	c.JSON(http.StatusOK, gin.H{
		"page":  page,
		"limit": limit,
	})
}

c.Query("page") 读取查询参数,不存在时返回空字符串。c.DefaultQuery 在参数不存在时返回默认值。

查询参数类型都是字符串,转成数字需要显式转换:

go
pageInt, _ := strconv.Atoi(c.DefaultQuery("page", "1"))

不要忽略转换错误——如果用户传了 page=abcAtoi 会返回错误,此时应该返回 400 而不是默默用 0。

六、请求体解析

POST 和 PUT 请求通常带 JSON 请求体。Gin 用 c.ShouldBindJSON 解析到结构体:

go
type AddRouteRequest struct {
	Prefix  string `json:"prefix" binding:"required"`
	Backend string `json:"backend" binding:"required,url"`
}

func addRoute(c *gin.Context) {
	var req AddRouteRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// 添加路由逻辑...
	c.JSON(http.StatusCreated, req)
}

binding:"required" 表示字段必填,缺失时返回 400。binding:"url" 验证字段值是合法的 URL 格式。Gin 的验证基于 go-playground/validator,支持丰富的验证标签。

七、统一响应格式

管理 API 需要统一的响应结构,方便前端处理:

go
type Response struct {
	Code int         `json:"code"`
	Msg  string      `json:"msg"`
	Data interface{} `json:"data,omitempty"`
}

func OK(c *gin.Context, data interface{}) {
	c.JSON(http.StatusOK, Response{Code: 0, Msg: "ok", Data: data})
}

func Fail(c *gin.Context, code int, msg string) {
	c.JSON(code, Response{Code: code, Msg: msg})
}

使用:

go
func listRoutes(c *gin.Context) {
	routes := router.ListRoutes()
	OK(c, routes)
}

func addRoute(c *gin.Context) {
	var req AddRouteRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		Fail(c, http.StatusBadRequest, err.Error())
		return
	}
	// ...
	OK(c, req)
}

八、当前管理 API 代码

go
package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

func main() {
	// 代理层跑在 :8080
	go startProxy()

	// 管理后台跑在 :8081
	r := gin.Default()

	admin := r.Group("/admin")
	{
		admin.GET("/routes", listRoutes)
		admin.POST("/routes", addRoute)
		admin.GET("/routes/:id", getRoute)
		admin.DELETE("/routes/:id", deleteRoute)
	}

	r.Run(":8081")
}

代理层和管理后台跑在同一个进程的不同端口上。go startProxy() 在新 goroutine 里启动代理,主 goroutine 启动 Gin 服务。

九、常见错误

绑定 JSON 时忽略错误

go
var req AddRouteRequest
c.ShouldBindJSON(&req) // 错误:忽略了返回值

请求体格式错误时,ShouldBindJSON 返回错误,但上面的代码继续用 req 的零值处理,可能导致脏数据写入。

路径参数和查询参数混用

go
r.GET("/users/:id", func(c *gin.Context) {
	id := c.Query("id") // 错误:应该用 c.Param("id")
})

路径参数用 c.Param,查询参数用 c.Query。两者来源不同,不要混淆。

并发修改路由表不加锁

管理 API 修改路由表时,代理层可能正在读取路由表。如果不用锁保护,会出现数据竞争。下一讲会介绍如何用中间件统一处理这类问题。