Skip to content

第一个 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 对每个常用方法都提供了对应的注册函数。

方法注册函数语义
GETr.GET获取资源
POSTr.POST创建资源
PUTr.PUT更新资源(整体替换)
DELETEr.DELETE删除资源
PATCHr.PATCH更新资源(部分修改)
HEADr.HEAD只获取响应头
OPTIONSr.OPTIONS探测服务器支持的通信选项(CORS 预检常用)
ANYr.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-urlencodedmultipart/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% 的路由需求:

  1. Hello World:跑通了第一个 Gin 程序,理解了 gin.Defaultr.GETc.JSONr.Run 几个核心 API。
  2. gin.New vs gin.Default:明白了默认中间件(Logger + Recovery)的作用,以及何时自己组合中间件。
  3. HTTP 方法路由:掌握 GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS/ANY 的注册与语义。
  4. 路由参数:会用 :id*filepath,并理解两者区别。
  5. 参数获取c.Param(路径)、c.Query/c.DefaultQuery(查询)、c.PostForm/c.DefaultPostForm(表单)。
  6. 混合参数:能在同一个 handler 里组合三类参数。
  7. 重定向:外部 302 跳转与内部 HandleContext 转发。
  8. 别名:通过复用 handler 函数实现路由别名。
  9. NoRoute / NoMethod:自定义 404/405 响应,统一错误格式。

下一篇我们将学习「参数绑定与验证」,把零散的 c.Queryc.PostForm 升级为更优雅、更工程化的结构体绑定方式,并引入参数校验规则。


上一篇01-Gin简介与环境搭建下一篇03-请求处理:参数绑定与验证