Appearance
第一个 Gin 程序与路由基础
本篇是 Gin 系列教程的第二篇。我们将从「Hello World」起步,逐步讲解 Gin 的核心对象、HTTP 方法路由、路由参数、查询参数、表单参数以及路由的高级用法(重定向、NoRoute 处理等)。学完本篇,你能够独立编写一个能处理各种请求参数的 Gin 服务。
一、第一个 Hello World 程序
我们从最经典的 Hello World 开始。如果你已经按上一篇搭建好环境,直接新建 main.go 即可。
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
// 1. 创建 Gin 引擎
r := gin.Default()
// 2. 注册一个 GET 路由
r.GET("/hello", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "Hello, Gin!",
})
})
// 3. 启动服务,监听 8080 端口
r.Run(":8080")
}启动与访问
bash
go run main.go服务启动后,浏览器访问 http://localhost:8080/hello,会看到:
json
{
"message": "Hello, Gin!"
}代码逐行解析
gin.Default():创建一个默认的 Gin 引擎(*gin.Engine),并自动注册Logger(日志)和Recovery(崩溃恢复)两个中间件。r.GET(path, handler):注册一个 GET 请求路由,path是 URL 路径,handler是处理函数。func(c *gin.Context):处理函数签名固定,参数c是上下文对象,贯穿整个请求生命周期,用于读取请求、写响应。c.JSON(code, obj):以 JSON 格式返回响应,第一个参数是 HTTP 状态码,第二个是要序列化的对象。gin.H:是map[string]interface{}的别名,构造 JSON 时很方便。r.Run(":8080"):启动 HTTP 服务,监听 8080 端口,会阻塞当前 goroutine。
二、理解 gin.Default() 和 gin.New() 的区别
Gin 提供两种方式创建引擎,理解它们的差异很重要。
gin.New()
gin.New() 返回一个空白的 Gin 引擎,不带任何中间件。你需要自己添加中间件。
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
// 创建空白引擎
r := gin.New()
// 手动添加 Logger 和 Recovery 中间件
r.Use(gin.Logger())
r.Use(gin.Recovery())
r.GET("/hello", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Hello, New!"})
})
r.Run(":8080")
}gin.Default()
gin.Default() 等价于:
go
r := gin.New()
r.Use(gin.Logger(), gin.Recovery())它只是把上面两步合并成一个常用快捷方式。
什么时候用哪个
| 场景 | 推荐 |
|---|---|
| 快速开发 / 学习 / 一般项目 | gin.Default() |
| 需要完全控制中间件链(如自定义日志、不用 Recovery) | gin.New() + 自定义 Use |
| 生产环境想替换默认中间件 | gin.New() |
经验法则:如果你不确定,就用
gin.Default()。当你需要对中间件做精细控制时,再切到gin.New()。
三、路由基础:HTTP 方法
HTTP 协议定义了一系列请求方法,每种方法语义不同。Gin 对每个常用方法都提供了对应的注册函数。
| 方法 | 注册函数 | 语义 |
|---|---|---|
| GET | r.GET | 获取资源 |
| POST | r.POST | 创建资源 |
| PUT | r.PUT | 更新资源(整体替换) |
| DELETE | r.DELETE | 删除资源 |
| PATCH | r.PATCH | 更新资源(部分修改) |
| HEAD | r.HEAD | 只获取响应头 |
| OPTIONS | r.OPTIONS | 探测服务器支持的通信选项(CORS 预检常用) |
| ANY | r.ANY | 处理所有方法 |
下面是一个综合示例:
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// GET:获取资源
r.GET("/users", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"action": "获取用户列表"})
})
// POST:创建资源
r.POST("/users", func(c *gin.Context) {
c.JSON(http.StatusCreated, gin.H{"action": "创建用户"})
})
// PUT:更新整个资源
r.PUT("/users/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(http.StatusOK, gin.H{"action": "更新用户", "id": id})
})
// PATCH:部分更新资源
r.PATCH("/users/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(http.StatusOK, gin.H{"action": "部分更新用户", "id": id})
})
// DELETE:删除资源
r.DELETE("/users/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(http.StatusOK, gin.H{"action": "删除用户", "id": id})
})
// HEAD:只返回响应头
r.HEAD("/ping", func(c *gin.Context) {
c.Header("X-Custom", "hello")
c.Status(http.StatusOK)
})
// OPTIONS:常用于 CORS 预检
r.OPTIONS("/users", func(c *gin.Context) {
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Status(http.StatusNoContent)
})
// ANY:处理所有方法
r.ANY("/any", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"method": c.Request.Method})
})
r.Run(":8080")
}用 curl 测试
bash
# GET
curl http://localhost:8080/users
# POST
curl -X POST http://localhost:8080/users
# PUT 带参数
curl -X PUT http://localhost:8080/users/123
# DELETE
curl -X DELETE http://localhost:8080/users/123
# ANY,看返回的方法名
curl -X PATCH http://localhost:8080/any在 RESTful API 设计中,方法语义要与操作对齐:GET 不应有副作用(不修改数据),POST 用于创建,PUT/PATCH 用于更新,DELETE 用于删除。良好的方法使用能让 API 自解释。
四、路由参数:/users/:id
路径中的动态片段叫「路由参数」,用 :name 形式声明,通过 c.Param("name") 获取。
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// 单个路由参数
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(http.StatusOK, gin.H{"user_id": id})
})
// 多个路由参数
r.GET("/users/:id/posts/:postId", func(c *gin.Context) {
id := c.Param("id")
postId := c.Param("postId")
c.JSON(http.StatusOK, gin.H{
"user_id": id,
"post_id": postId,
})
})
r.Run(":8080")
}访问:
bash
curl http://localhost:8080/users/42
# {"user_id":"42"}
curl http://localhost:8080/users/42/posts/7
# {"post_id":"7","user_id":"42"}注意事项
- 路由参数不能跨斜杠:
/users/:id不会匹配/users/42/posts。 - 同一层级只能有一个参数,且不同参数名不能冲突:
/users/:id和/users/:name会注册失败(冲突)。 - 静态路径优先级高于参数路径:
/users/me会优先于/users/:id匹配。
五、通配符路由:/files/*filepath
当需要匹配任意层级路径时(如静态文件服务),使用通配符 *name,它会捕获剩余的全部路径(包括斜杠)。
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// 通配符路由,*filepath 会匹配 /files/ 后面的所有内容(含多级目录)
r.GET("/files/*filepath", func(c *gin.Context) {
filepath := c.Param("filepath")
c.JSON(http.StatusOK, gin.H{
"filepath": filepath,
})
})
r.Run(":8080")
}访问测试:
bash
curl http://localhost:8080/files/
# {"filepath":"/"}
curl http://localhost:8080/files/images/logo.png
# {"filepath":"/images/logo.png"}
curl http://localhost:8080/files/a/b/c/d.txt
# {"filepath":"/a/b/c/d.txt"}注意 filepath 的值包含前导斜杠 /,因为它捕获的是从 * 位置开始的完整路径段。
:id 与 *filepath 的区别
| 写法 | 匹配 | 示例 |
|---|---|---|
:id | 单个路径段(不含 /) | /users/42 ✓,/users/42/33 ✗ |
*filepath | 剩余所有路径(含 /) | /files/a/b/c ✓ |
通配符路由每个层级只能有一个,且必须位于路径末尾。
六、路由参数获取:c.Param、c.Query、c.DefaultQuery
Gin 提供了几种获取请求参数的方法,对应不同来源。
1. c.Param:获取路由参数
如上文示例,用于获取路径中 :name 或 *name 声明的参数。
go
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id") // 字符串类型
})2. c.Query:获取查询字符串参数
URL 中 ? 后面的键值对,例如 /search?q=gin&page=1。
go
r.GET("/search", func(c *gin.Context) {
q := c.Query("q") // 不存在返回 ""
page := c.Query("page")
c.JSON(200, gin.H{"q": q, "page": page})
})3. c.DefaultQuery:带默认值的查询参数
当查询参数不存在时返回指定的默认值,避免出现空字符串。
go
r.GET("/list", func(c *gin.Context) {
// page 不传时默认为 1
page := c.DefaultQuery("page", "1")
size := c.DefaultQuery("size", "10")
c.JSON(200, gin.H{"page": page, "size": size})
})访问 /list 返回 {"page":"1","size":"10"};访问 /list?page=3&size=20 返回 {"page":"3","size":"20"}。
完整示例
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// 综合演示 Param / Query / DefaultQuery
r.GET("/users/:id/orders", func(c *gin.Context) {
id := c.Param("id") // 路径参数
status := c.Query("status") // 查询参数,可空
page := c.DefaultQuery("page", "1") // 查询参数带默认值
size := c.DefaultQuery("size", "20")
c.JSON(http.StatusOK, gin.H{
"user_id": id,
"status": status,
"page": page,
"size": size,
})
})
r.Run(":8080")
}测试:
bash
curl "http://localhost:8080/users/42/orders?status=paid&page=3"
# {"page":"3","size":"20","status":"paid","user_id":"42"}七、表单参数:c.PostForm、c.DefaultPostForm
POST/PUT 请求通常通过表单(application/x-www-form-urlencoded 或 multipart/form-data)提交数据,用 c.PostForm 获取。
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// POST 表单
r.POST("/login", func(c *gin.Context) {
username := c.PostForm("username")
password := c.PostForm("password")
// 带默认值
remember := c.DefaultPostForm("remember", "0")
c.JSON(http.StatusOK, gin.H{
"username": username,
"password": password,
"remember": remember,
})
})
r.Run(":8080")
}测试:
bash
curl -X POST http://localhost:8080/login \
-d "username=admin&password=123456&remember=1"
# {"password":"123456","remember":"1","username":"admin"}c.PostForm 与 c.Query 的关系
c.PostForm读取的是请求体中的表单字段(也兼容 URL query 中同名字段,Gin 会合并)。c.Query只读取 URL query string。- 实际开发中建议按数据来源选用对应方法,语义更清晰。
八、多种参数混合使用示例
真实业务里一个请求往往同时包含路径参数、查询参数和表单参数。下面是一个混合示例:
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// 混合参数:路径 + 查询 + 表单
r.POST("/users/:id/posts", func(c *gin.Context) {
// 1. 路径参数
userID := c.Param("id")
// 2. 查询参数(URL ?...)
draft := c.DefaultQuery("draft", "false")
// 3. 表单参数(请求体)
title := c.PostForm("title")
content := c.PostForm("content")
// 带默认值的表单参数
tag := c.DefaultPostForm("tag", "untagged")
c.JSON(http.StatusCreated, gin.H{
"user_id": userID,
"draft": draft,
"title": title,
"content": content,
"tag": tag,
})
})
r.Run(":8080")
}测试:
bash
curl -X POST "http://localhost:8080/users/42/posts?draft=true" \
-d "title=Hello&content=Gin is awesome"
# {"content":"Gin is awesome","draft":"true","tag":"untagged","title":"Hello","user_id":"42"}可以看到,路径、查询、表单三类参数都被正确读取。
九、Query + PostForm 综合示例
下面再做一个分页查询 + 表单筛选的完整示例,模拟实际列表筛选接口。
go
package main
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// 模拟商品列表筛选:GET 分页 + POST 表单筛选条件
r.POST("/products/search", func(c *gin.Context) {
// 分页参数从 query 取
page := c.DefaultQuery("page", "1")
size := c.DefaultQuery("size", "10")
// 筛选条件从表单取
keyword := c.PostForm("keyword")
category := c.DefaultPostForm("category", "all")
minPrice := c.DefaultPostForm("min_price", "0")
maxPrice := c.DefaultPostForm("max_price", "999999")
// 简单的业务处理:返回查询条件
c.JSON(http.StatusOK, gin.H{
"page": page,
"size": size,
"keyword": strings.TrimSpace(keyword),
"category": category,
"min_price": minPrice,
"max_price": maxPrice,
})
})
r.Run(":8080")
}测试:
bash
curl -X POST "http://localhost:8080/products/search?page=2&size=20" \
-d "keyword=phone&category=digital&min_price=1000&max_price=5000"返回:
json
{
"category": "digital",
"keyword": "phone",
"max_price": "5000",
"min_price": "1000",
"page": "2",
"size": "20"
}十、路由重定向:c.Redirect
Gin 支持内部和外部重定向。重定向分两种:
- 外部重定向:跳转到另一个 URL(HTTP 302),浏览器地址栏会变化。
- 内部重定向:在服务端把请求转发到另一个路由(
c.Request.URL.Path改写 +r.HandleContext)。
外部重定向
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/old-page", func(c *gin.Context) {
// 302 跳转到 /new-page
c.Redirect(http.StatusFound, "/new-page")
})
r.GET("/new-page", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "这是新页面"})
})
// 跳转到外站
r.GET("/google", func(c *gin.Context) {
c.Redirect(http.StatusFound, "https://www.google.com")
})
r.Run(":8080")
}测试:
bash
curl -i http://localhost:8080/old-page
# HTTP/1.1 302 Found
# Location: /new-page内部重定向
内部重定向不会发起二次 HTTP 请求,而是直接在服务端重新走一次路由匹配:
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/v1/user", func(c *gin.Context) {
// 内部转发到 /v2/user
c.Request.URL.Path = "/v2/user"
r.HandleContext(c)
})
r.GET("/v2/user", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"version": "v2", "user": "tom"})
})
r.Run(":8080")
}访问 /v1/user 会返回 /v2/user 的内容。注意内部重定向要小心避免循环。
常用状态码:301 永久重定向、302 临时重定向、307 临时重定向(保留方法)、308 永久重定向(保留方法)。
十一、路由别名
Go 语言层面没有「路由别名」这一概念,但我们可以通过让多个路径注册同一个 handler 函数来达到别名效果。
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
// 抽取成独立函数,便于复用
func healthHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"uptime": "99.9%",
})
}
func main() {
r := gin.Default()
// 主路由
r.GET("/health", healthHandler)
// 别名路由,指向同一个 handler
r.GET("/healthcheck", healthHandler)
r.GET("/ping", healthHandler)
r.Run(":8080")
}访问 /health、/healthcheck、/ping 都会返回相同结果。把 handler 抽成函数而非内联闭包,是项目变大后保持代码整洁的关键。
十二、NoRoute 和 NoMethod 处理
默认情况下,访问不存在的路由返回 404,请求方法不支持返回 405。Gin 允许你自定义这两个响应。
NoRoute:自定义 404
go
r.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{
"code": 404,
"message": "页面不存在",
})
})NoMethod:自定义 405
需要先开启 HandleMethodNotAllowed,否则不支持的 method 会直接返回 404。
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// 开启 405 处理
r.HandleMethodNotAllowed = true
r.GET("/items", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"items": []string{"a", "b"}})
})
// 自定义 404
r.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{
"code": 404,
"message": "资源不存在",
})
})
// 自定义 405
r.NoMethod(func(c *gin.Context) {
c.JSON(http.StatusMethodNotAllowed, gin.H{
"code": 405,
"message": "不支持的方法",
})
})
r.Run(":8080")
}测试:
bash
# 404
curl http://localhost:8080/not-exist
# {"code":404,"message":"资源不存在"}
# 405:/items 只允许 GET,这里用 POST
curl -X POST http://localhost:8080/items
# {"code":405,"message":"不支持的方法"}实际项目里统一 404/405 响应格式,能让前端错误处理更一致。
十三、综合实战:一个小型路由演示
把本篇学到的内容综合到一个文件里,作为复习参考:
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.HandleMethodNotAllowed = true
// ===== 基础路由 =====
r.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"app": "gin-demo", "version": "v1.0"})
})
// ===== 路由参数 =====
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(http.StatusOK, gin.H{"user_id": id})
})
// ===== 通配符 =====
r.GET("/files/*filepath", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"filepath": c.Param("filepath")})
})
// ===== Query / DefaultQuery =====
r.GET("/search", func(c *gin.Context) {
q := c.Query("q")
page := c.DefaultQuery("page", "1")
c.JSON(http.StatusOK, gin.H{"q": q, "page": page})
})
// ===== PostForm =====
r.POST("/login", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"username": c.PostForm("username"),
"remember": c.DefaultPostForm("remember", "0"),
})
})
// ===== 重定向 =====
r.GET("/old", func(c *gin.Context) {
c.Redirect(http.StatusFound, "/")
})
// ===== 别名 =====
r.GET("/health", healthHandler)
r.GET("/ping", healthHandler)
// ===== 404 / 405 =====
r.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "Not Found"})
})
r.NoMethod(func(c *gin.Context) {
c.JSON(http.StatusMethodNotAllowed, gin.H{"code": 405, "message": "Method Not Allowed"})
})
r.Run(":8080")
}
func healthHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
}启动后,你可以逐一测试每个路由,对照返回结果加深理解。
十四、小结
本篇是 Gin 路由学习的核心一课,覆盖了日常开发 90% 的路由需求:
- Hello World:跑通了第一个 Gin 程序,理解了
gin.Default、r.GET、c.JSON、r.Run几个核心 API。 - gin.New vs gin.Default:明白了默认中间件(Logger + Recovery)的作用,以及何时自己组合中间件。
- HTTP 方法路由:掌握 GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS/ANY 的注册与语义。
- 路由参数:会用
:id和*filepath,并理解两者区别。 - 参数获取:
c.Param(路径)、c.Query/c.DefaultQuery(查询)、c.PostForm/c.DefaultPostForm(表单)。 - 混合参数:能在同一个 handler 里组合三类参数。
- 重定向:外部 302 跳转与内部
HandleContext转发。 - 别名:通过复用 handler 函数实现路由别名。
- NoRoute / NoMethod:自定义 404/405 响应,统一错误格式。
下一篇我们将学习「参数绑定与验证」,把零散的 c.Query、c.PostForm 升级为更优雅、更工程化的结构体绑定方式,并引入参数校验规则。
上一篇:01-Gin简介与环境搭建下一篇:03-请求处理:参数绑定与验证