跳到主要内容

Gin Web 框架

Gin 是一个用 Go 语言编写的高性能 HTTP Web 框架。它基于 HttpRouter 实现,提供了类似 Martini 的 API,但性能比 Martini 快 40 倍。Gin 是目前 Go 社区最受欢迎的 Web 框架之一。

安装

go get -u github.com/gin-gonic/gin

快速开始

第一个 Gin 应用

package main

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

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

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

r.Run(":8080")
}

运行后访问 http://localhost:8080/ping,将返回 {"message":"pong"}

代码解释

  • gin.Default():创建一个默认的路由引擎,包含 Logger 和 Recovery 中间件
  • r.GET():注册 GET 路由
  • c *gin.Context:请求上下文,包含请求和响应信息
  • c.JSON():返回 JSON 格式响应
  • gin.Hmap[string]interface{} 的简写
  • r.Run():启动 HTTP 服务器,默认端口 8080

路由

基本路由

package main

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

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

r.GET("/users", getUsers)
r.POST("/users", createUser)
r.PUT("/users/:id", updateUser)
r.DELETE("/users/:id", deleteUser)
r.PATCH("/users/:id", patchUser)
r.HEAD("/users", headUsers)
r.OPTIONS("/users", optionsUsers)

r.Run(":8080")
}

func getUsers(c *gin.Context) {
c.JSON(200, gin.H{"method": "GET"})
}

func createUser(c *gin.Context) {
c.JSON(201, gin.H{"method": "POST"})
}

func updateUser(c *gin.Context) {
c.JSON(200, gin.H{"method": "PUT"})
}

func deleteUser(c *gin.Context) {
c.JSON(200, gin.H{"method": "DELETE"})
}

func patchUser(c *gin.Context) {
c.JSON(200, gin.H{"method": "PATCH"})
}

func headUsers(c *gin.Context) {
c.Status(200)
}

func optionsUsers(c *gin.Context) {
c.JSON(200, gin.H{"method": "OPTIONS"})
}

路径参数

package main

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

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

r.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(200, "Hello %s", name)
})

r.GET("/user/:name/:action", func(c *gin.Context) {
name := c.Param("name")
action := c.Param("action")
c.String(200, "%s is %s", name, action)
})

r.GET("/files/*filepath", func(c *gin.Context) {
filepath := c.Param("filepath")
c.String(200, "File path: %s", filepath)
})

r.Run(":8080")
}

参数说明

  • :name:匹配单个路径段,如 /user/john
  • *filepath:匹配剩余所有路径,如 /files/a/b/c.txt

查询参数

package main

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

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

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,
})
})

r.GET("/filter", func(c *gin.Context) {
tags := c.QueryArray("tag")
c.JSON(200, gin.H{
"tags": tags,
})
})

r.GET("/map", func(c *gin.Context) {
params := c.QueryMap("filter")
c.JSON(200, gin.H{
"filter": params,
})
})

r.Run(":8080")
}

访问示例

  • /search?keyword=go&page=2
  • /filter?tag=go&tag=web&tag=api
  • /map?filter[name]=张三&filter[age]=25

表单参数

package main

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

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

r.POST("/form", func(c *gin.Context) {
name := c.PostForm("name")
email := c.DefaultPostForm("email", "[email protected]")

c.JSON(200, gin.H{
"name": name,
"email": email,
})
})

r.POST("/upload", func(c *gin.Context) {
tags := c.PostFormArray("tag")
c.JSON(200, gin.H{
"tags": tags,
})
})

r.Run(":8080")
}

路由分组

package main

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

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

api := r.Group("/api")
{
api.GET("/status", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})

users := api.Group("/users")
{
users.GET("", listUsers)
users.POST("", createUser)
users.GET("/:id", getUser)
users.PUT("/:id", updateUser)
users.DELETE("/:id", deleteUser)
}

posts := api.Group("/posts")
{
posts.GET("", listPosts)
posts.POST("", createPost)
posts.GET("/:id", getPost)
}
}

admin := r.Group("/admin")
admin.Use(AuthMiddleware())
{
admin.GET("/dashboard", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "admin dashboard"})
})
}

r.Run(":8080")
}

func listUsers(c *gin.Context) {
c.JSON(200, []gin.H{{"id": 1, "name": "张三"}})
}

func createUser(c *gin.Context) {
c.JSON(201, gin.H{"id": 2, "name": "李四"})
}

func getUser(c *gin.Context) {
c.JSON(200, gin.H{"id": c.Param("id"), "name": "张三"})
}

func updateUser(c *gin.Context) {
c.JSON(200, gin.H{"id": c.Param("id"), "name": "张三更新"})
}

func deleteUser(c *gin.Context) {
c.Status(204)
}

func listPosts(c *gin.Context) {
c.JSON(200, []gin.H{{"id": 1, "title": "文章1"}})
}

func createPost(c *gin.Context) {
c.JSON(201, gin.H{"id": 2, "title": "文章2"})
}

func getPost(c *gin.Context) {
c.JSON(200, gin.H{"id": c.Param("id"), "title": "文章"})
}

func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(401, gin.H{"error": "未授权"})
c.Abort()
return
}
c.Next()
}
}

请求处理

获取请求信息

package main

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

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

r.GET("/info", func(c *gin.Context) {
c.JSON(200, gin.H{
"method": c.Request.Method,
"path": c.Request.URL.Path,
"host": c.Request.Host,
"remoteAddr": c.ClientIP(),
"headers": c.Request.Header,
"userAgent": c.GetHeader("User-Agent"),
"contentType": c.ContentType(),
})
})

r.Run(":8080")
}

绑定请求参数

Gin 支持多种绑定方式,可以自动将请求参数绑定到结构体:

package main

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

type User struct {
Name string `form:"name" json:"name" binding:"required"`
Email string `form:"email" json:"email" binding:"required,email"`
Age int `form:"age" json:"age" binding:"gte=0,lte=130"`
}

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

r.GET("/query", func(c *gin.Context) {
var user User
if err := c.ShouldBindQuery(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, user)
})

r.POST("/form", func(c *gin.Context) {
var user User
if err := c.ShouldBind(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, user)
})

r.POST("/json", func(c *gin.Context) {
var user User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, user)
})

r.POST("/bind", func(c *gin.Context) {
var user User
if err := c.ShouldBind(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, user)
})

r.Run(":8080")
}

绑定 URI 参数

package main

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

type UserURI struct {
ID string `uri:"id" binding:"required"`
Name string `uri:"name" binding:"required"`
}

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

r.GET("/user/:id/:name", func(c *gin.Context) {
var user UserURI
if err := c.ShouldBindUri(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{
"id": user.ID,
"name": user.Name,
})
})

r.Run(":8080")
}

绑定 Header

package main

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

type HeaderParams struct {
Token string `header:"Authorization" binding:"required"`
Rate int `header:"Rate"`
}

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

r.GET("/header", func(c *gin.Context) {
var h HeaderParams
if err := c.ShouldBindHeader(&h); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{
"token": h.Token,
"rate": h.Rate,
})
})

r.Run(":8080")
}

响应处理

JSON 响应

package main

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

type User struct {
ID int `json:"id"`
Name string `json:"name"`
}

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

r.GET("/json", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "hello"})
})

r.GET("/json/struct", func(c *gin.Context) {
c.JSON(200, User{ID: 1, Name: "张三"})
})

r.GET("/json/array", func(c *gin.Context) {
users := []User{
{ID: 1, Name: "张三"},
{ID: 2, Name: "李四"},
}
c.JSON(200, users)
})

r.GET("/json/indent", func(c *gin.Context) {
c.IndentedJSON(200, User{ID: 1, Name: "张三"})
})

r.GET("/json/secure", func(c *gin.Context) {
c.SecureJSON(200, []string{"张三", "李四"})
})

r.Run(":8080")
}

其他响应类型

package main

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

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

r.GET("/string", func(c *gin.Context) {
c.String(200, "Hello, %s", "World")
})

r.GET("/xml", func(c *gin.Context) {
c.XML(200, gin.H{"message": "hello"})
})

r.GET("/yaml", func(c *gin.Context) {
c.YAML(200, gin.H{"message": "hello"})
})

r.GET("/html", func(c *gin.Context) {
c.HTML(200, "index.html", gin.H{
"title": "主页",
})
})

r.GET("/file", func(c *gin.Context) {
c.File("./static/file.txt")
})

r.GET("/attachment", func(c *gin.Context) {
c.FileAttachment("./static/file.txt", "download.txt")
})

r.GET("/data", func(c *gin.Context) {
c.Data(200, "text/plain; charset=utf-8", []byte("Hello"))
})

r.GET("/redirect", func(c *gin.Context) {
c.Redirect(302, "/string")
})

r.Run(":8080")
}

中间件

内置中间件

package main

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

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

r.Use(gin.Logger())
r.Use(gin.Recovery())

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

r.Run(":8080")
}

自定义中间件

package main

import (
"log"
"time"

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

func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path

c.Next()

latency := time.Since(start)
status := c.Writer.Status()

log.Printf("[%s] %s %d %v", c.Request.Method, path, status, latency)
}
}

func Auth() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(401, gin.H{"error": "未授权"})
c.Abort()
return
}

c.Set("userID", "123")
c.Next()
}
}

func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")

if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}

c.Next()
}
}

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

r.Use(Logger())
r.Use(gin.Recovery())
r.Use(CORSMiddleware())

public := r.Group("/api")
{
public.POST("/login", login)
}

protected := r.Group("/api")
protected.Use(Auth())
{
protected.GET("/profile", profile)
}

r.Run(":8080")
}

func login(c *gin.Context) {
c.JSON(200, gin.H{"token": "fake-token"})
}

func profile(c *gin.Context) {
userID := c.GetString("userID")
c.JSON(200, gin.H{"userID": userID})
}

中间件控制

package main

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

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

r.GET("/normal", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "normal"})
})

r.GET("/abort", func(c *gin.Context) {
c.JSON(401, gin.H{"error": "unauthorized"})
c.Abort()
})

r.GET("/next", func(c *gin.Context) {
c.Next()
c.JSON(200, gin.H{"message": "after next"})
})

r.Run(":8080")
}

文件上传

单文件上传

package main

import (
"fmt"
"path/filepath"

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

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

r.MaxMultipartMemory = 8 << 20

r.POST("/upload", func(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}

filename := filepath.Base(file.Filename)
dst := filepath.Join("./uploads", filename)

if err := c.SaveUploadedFile(file, dst); err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}

c.JSON(200, gin.H{
"filename": filename,
"size": file.Size,
"header": file.Header,
})
})

r.Run(":8080")
}

多文件上传

package main

import (
"net/http"
"path/filepath"

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

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

r.POST("/uploads", func(c *gin.Context) {
form, _ := c.MultipartForm()
files := form.File["files"]

var results []gin.H

for _, file := range files {
filename := filepath.Base(file.Filename)
dst := filepath.Join("./uploads", filename)

if err := c.SaveUploadedFile(file, dst); err != nil {
results = append(results, gin.H{
"filename": filename,
"error": err.Error(),
})
continue
}

results = append(results, gin.H{
"filename": filename,
"size": file.Size,
})
}

c.JSON(http.StatusOK, gin.H{"files": results})
})

r.Run(":8080")
}

模板渲染

HTML 模板

package main

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

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

r.LoadHTMLGlob("templates/**/*")

r.GET("/index", func(c *gin.Context) {
c.HTML(200, "index.html", gin.H{
"title": "主页",
"message": "欢迎来到 Gin",
})
})

r.GET("/posts/index", func(c *gin.Context) {
c.HTML(200, "posts/index.html", gin.H{
"title": "文章列表",
"posts": []gin.H{
{"id": 1, "title": "文章1"},
{"id": 2, "title": "文章2"},
},
})
})

r.Run(":8080")
}

静态文件

package main

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

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

r.Static("/assets", "./assets")
r.StaticFile("/favicon.ico", "./favicon.ico")
r.StaticFS("/static", http.Dir("./static"))

r.Run(":8080")
}

数据验证

Gin 使用 validator 包进行数据验证:

package main

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

type RegisterRequest struct {
Username string `json:"username" binding:"required,min=3,max=20"`
Password string `json:"password" binding:"required,min=6,max=20"`
Email string `json:"email" binding:"required,email"`
Age int `json:"age" binding:"gte=1,lte=120"`
Phone string `json:"phone" binding:"omitempty,len=11"`
Website string `json:"website" binding:"omitempty,url"`
}

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

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{
"username": req.Username,
"email": req.Email,
"age": req.Age,
})
})

r.Run(":8080")
}

常用验证标签

标签说明
required必填
min=3最小长度/值
max=20最大长度/值
gte=1大于等于
lte=120小于等于
email邮箱格式
urlURL 格式
len=11精确长度
oneof=男 女枚举值
dive深入切片/数组验证

错误处理

统一错误处理

package main

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

type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}

func Success(c *gin.Context, data interface{}) {
c.JSON(200, Response{
Code: 0,
Message: "success",
Data: data,
})
}

func Error(c *gin.Context, code int, message string) {
c.JSON(code, Response{
Code: code,
Message: message,
})
}

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

r.GET("/success", func(c *gin.Context) {
Success(c, gin.H{"name": "张三"})
})

r.GET("/error", func(c *gin.Context) {
Error(c, 400, "参数错误")
})

r.NoRoute(func(c *gin.Context) {
Error(c, 404, "路由不存在")
})

r.NoMethod(func(c *gin.Context) {
Error(c, 405, "方法不允许")
})

r.Run(":8080")
}

Recovery 中间件

package main

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

func RecoveryMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
c.JSON(500, gin.H{
"code": 500,
"message": "服务器内部错误",
"error": err,
})
c.Abort()
}
}()
c.Next()
}
}

func main() {
r := gin.New()
r.Use(RecoveryMiddleware())

r.GET("/panic", func(c *gin.Context) {
panic("故意触发的错误")
})

r.Run(":8080")
}

项目结构示例

project/
├── main.go
├── config/
│ └── config.go
├── controllers/
│ ├── user.go
│ └── post.go
├── models/
│ ├── user.go
│ └── post.go
├── services/
│ ├── user.go
│ └── post.go
├── middlewares/
│ ├── auth.go
│ └── logger.go
├── routes/
│ └── router.go
├── utils/
│ └── response.go
├── templates/
│ └── index.html
└── static/
├── css/
└── js/

路由分离

package routes

import (
"github.com/gin-gonic/gin"
"myapp/controllers"
"myapp/middlewares"
)

func SetupRouter() *gin.Engine {
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())

api := r.Group("/api")
{
api.POST("/login", controllers.Login)
api.POST("/register", controllers.Register)
}

userGroup := api.Group("/users")
userGroup.Use(middlewares.Auth())
{
userGroup.GET("", controllers.ListUsers)
userGroup.GET("/:id", controllers.GetUser)
userGroup.PUT("/:id", controllers.UpdateUser)
userGroup.DELETE("/:id", controllers.DeleteUser)
}

return r
}
package main

import (
"myapp/routes"
)

func main() {
r := routes.SetupRouter()
r.Run(":8080")
}

小结

本章学习了 Gin 框架的核心内容:

  1. 路由:基本路由、路径参数、查询参数、路由分组
  2. 请求处理:获取请求信息、参数绑定
  3. 响应处理:JSON、XML、HTML、文件响应
  4. 中间件:内置中间件、自定义中间件
  5. 文件上传:单文件、多文件上传
  6. 模板渲染:HTML 模板、静态文件
  7. 数据验证:常用验证标签
  8. 错误处理:统一错误处理、Recovery

练习

  1. 实现一个完整的 RESTful API,包含 CRUD 操作
  2. 编写一个 JWT 认证中间件
  3. 实现文件上传和下载功能
  4. 使用 Gin 构建一个简单的博客系统