Skip to content

介绍

Gin 是一个简单易用,高性能的web框架, 由go语言编写, 以 httprouter 为核心开发而来

注: gin 需要 go1.25 及以上版本

安装&启动

sh
# 创建项目目录
mkdir gin-app-demo && cd gin-app-demo

# 初始化模块
go mod init gin_app_demo

# 添加依赖
go get -u github.com/gin-gonic/gin

# 创建 main.go 程序入口
touch main.go

# 修改 main.go, 输入后续代码后编译运行
go run main.go
go
package main

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

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

	router.Run() // listens on 0.0.0.0:8080 by default
}

测试服务状态

打开浏览器访问 http://localhost:8080/ping 如果能够正常看到响应信息说明程序启动成功

关闭调试模式

go
package main

import (
	"net/http"

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

func main() {
	// 设置 release 模式(不会输出调试信息)
	gin.SetMode(gin.ReleaseMode)
	router := gin.Default()

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

	router.Run()
}

自动重启

现在修改代码后, 必须手动停止服务, 然后再次执行 go run main.go 要想改动代码后, 立即自动重启, 就需要借助这两个工具, 达到类似 nodemon 的效果

  • air
  • just 根据配置文件执行命令
  • watchexec 监听文件变化, 自动重新执行某个命令
just
alias d := dev
alias s := start

# just d 或 just dev   会执行这个 dev
# just s 或 just start 会执行 start
# 监听所有文件扩展名为go的文件内容变化后自动重新执行 `go run main.go`
dev:
  watchexec -e go -r go run main.go

start:
  go run main.go

路由 Routes

http方法 HTTP-method

Gin 提供了直接映射到 HTTP 动词的方法, 使构建 RESTful API 变得简单直接

go
package main

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

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

	// 仅允许 Get 方式访问
	router.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})

	// 仅允许 post 方式访问
	router.POST("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "from post method",
		})
	})

	router.Run()
}

路由分组

go
package main

import (
	"net/http"

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

func loginV1(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{"msg": "login api v1"})
}

func loginV2(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{"msg": "login api v2"})
}

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

	{
		// simple group: /api/v1
		v1 := router.Group("/api/v1")
		v1.POST("/login", loginV1)
	}

	{
		// simple group: /api/v2
		v2 := router.Group("/api/v2")
		v2.POST("/login", loginV2)
	}

	router.Run() // http://localhost:8080
}

获取请求信息

go
package main

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

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

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

	///// 0.获取请求路径
	router.GET("/test/example", func(c *gin.Context) {
		requestPath := c.Request.URL.Path
		c.JSON(http.StatusOK, gin.H{
			"request-path": requestPath,
		})
	})

	///// 1.获取路径参数
	router.GET("/students/:id", func(c *gin.Context) {
		studentId := c.Param("id")
		c.JSON(http.StatusOK, gin.H{
			"message": "Hello " + studentId,
		})
	})

	///// 2.定义多个路径参数
	router.GET("/teachers/:id/:name", func(c *gin.Context) {
		teacherId := c.Param("id")
		teacherName := c.Param("name")

		message := fmt.Sprintf("Hello %s(%s)", teacherName, teacherId)
		c.JSON(http.StatusOK, gin.H{
			"message": message,
		})
	})

	///// 3.获取查询参数
	router.GET("/search", func(c *gin.Context) {
		// 获取查询参数
		keyworld := c.Query("q")

		// 获取查询参数如果没有则使用默认值(注意默认值必须是字符串)
		page := c.DefaultQuery("page", "1")

		c.JSON(http.StatusOK, gin.H{
			"msg": fmt.Sprintf("查询参数(%s) 页码(%s)", keyworld, page),
		})
	})

	///// 4.获取表单数据
	router.POST("/form", func(c *gin.Context) {
		email := c.PostForm("email")
		password := c.PostForm("password")
		nickname := c.DefaultPostForm("nickname", "default-nickname")

		c.JSON(http.StatusOK, gin.H{
			"email":    email,
			"password": password,
			"nickname": nickname,
		})
	})

	// 5.获取上传文件数据
	router.POST("/upload", func(c *gin.Context) {
		file, err := c.FormFile("avatar")
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		// 保存文件
		dst := filepath.Join("./files/", filepath.Base(file.Filename))
		c.SaveUploadedFile(file, dst)
		c.JSON(http.StatusOK, gin.H{
			"msg":  "upload success",
			"path": dst,
		})
	})

	// 6.获取 json 数据: 需要绑定数据到 Go struct
	//
	// router.POST("/json", func(c *gin.Context) {
	// })

	// 7.获取原生的 http.Request 的实例
	router.GET("/request", func(c *gin.Context) {
		fmt.Println(c.Request)

		c.JSON(http.StatusOK, gin.H{
			"msg": "success",
		})
	})

	router.Run()
}

设置响应信息 Response

go
package main

import (
	"net/http"

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

type Student struct {
	Id   int    `json:"id"`
	Name string `json:"name"`
}

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

	///// 1. 重定向响应
	router.GET("/baidu", func(c *gin.Context) {
		// 1.1 永久重定向
		c.Redirect(http.StatusMovedPermanently, "https://www.baidu.com")
	})
	router.GET("/qq", func(c *gin.Context) {
		// 1.2 临时重定向
		c.Redirect(http.StatusFound, "https://www.qq.com")
	})

	///// 2.响应 json
	router.GET("/json", func(c *gin.Context) {
		// 2.1 c.JSON 方法 + gin.H 响应一个 json 对象
		// c.JSON 方法会自动设置响应头, Content-Type: "application/json"
		// gin.H 就是自定义类型: type gin.H map[string]any
		c.JSON(http.StatusOK, gin.H{
			"message": "Hello World!",
		})
	})

	router.GET("/json2", func(c *gin.Context) {
		// 2.2 c.JSON 方法 + 自定义 map 响应一个 json 对象
		student := map[string]any{
			"id":    1,
			"name":  "zhangsan",
			"email": "zhangsan@example.com",
		}

		c.JSON(http.StatusOK, student)
	})

	router.GET("/json3", func(c *gin.Context) {
		// 2.3 c.JSON 方法 + 自定义 struct 响应一个 json 对象
		type Teacher struct {
			Id   int    `json:"id"`
			Name string `json:"name"`
		}
		teeacher := Teacher{
			Id:   1001,
			Name: "wangwu",
		}
		c.JSON(http.StatusOK, teeacher)
	})

	///// 3.响应 html/xml
	// 3.1 需要先加载静态文件
	router.LoadHTMLFiles("./files/index.html")
	router.GET("/html", func(c *gin.Context) {
		// 3.2 响应一个 html 文件内容
		c.HTML(http.StatusOK, "index.html", nil)
	})

	///// 4.响应纯文本
	router.GET("/text", func(c *gin.Context) {
		// 4.1 响应一个纯文本内容
		c.String(http.StatusOK, "Hello World!")
	})
	router.GET("/text2", func(c *gin.Context) {
		// 4.2 设置响应头,并响应纯文本内容
		c.Writer.Header().Set("Content-Type", "text/css")
		c.String(http.StatusOK, "div{background:red;}")
	})

	///// 5.流式响应(文件下载)
	router.GET("/download", func(c *gin.Context) {
		// 5.1 响应一个文件下载: 自动设置响应头
		// Content-Disposition: "attachment; filename='xxx'"
		c.FileAttachment("./files/avatar_ai.png", "avatar_ai.png")
	})

	router.Run()
}

静态文件服务

go
package main

import (
	"net/http"

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

type Student struct {
	Id   int    `json:"id"`
	Name string `json:"name"`
}

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

	// 1.提供整个目录
	router.Static("/public", "./files")

	// 2.提供自定义列表
	router.StaticFS("/files", http.Dir("./files"))

	// 3.提供一个文件的映射
	router.StaticFile("/avatar_ai.png", "./files/avatar_ai.png")

	router.Run()
}

数据绑定

go
package main

import (
	"net/http"

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

// 注: 绑定 GET 请求的 query 参数 struct tag 也是 form
// binding: "required" 表示请求的json参数必须包含这个字段
// default: 1          表示请求参数的默认值
type SearchQueryDto struct {
	Keyword string `form:"keyword" binding:"required"`
	Page    int    `form:"page,default=1"`
	Limit   int    `form:"limit,default=10"`
}

type LoginJsonDto struct {
	Email    string `json:"email" binding:"required"`
	Password string `json:"password" binding:"required"`
}

// 绑定 POST 请求表单数据 struct tag 也是 form
type LoginFormDto struct {
	Email    string `form:"email" binding:"required"`
	Password string `form:"password" binding:"required"`
}

// 注意绑定到请求头中的字段 tag 不用区分大小写
type AuthHeaderDto struct {
	AccessToken  string `header:"x-access-token" binding:"required"`
	RefreshToken string `header:"X-REFRESH-TOKEN" binding:"required"`
}

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

	// 0.将GET的url参数(如/search?keyword=go)绑定到 struct
	router.GET("/query", func(c *gin.Context) {
		var searchQuery SearchQueryDto
		if err := c.ShouldBindQuery(&searchQuery); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		c.JSON(http.StatusOK, gin.H{
			"keyword": searchQuery.Keyword,
			"page":    searchQuery.Page,
			"limit":   searchQuery.Limit,
		})
	})

	// 1.将请求体中的 json 数据绑定到 struct
	router.POST("/json", func(c *gin.Context) {
		// 将 struct 实例绑定到 json 数据
		var loginJson LoginJsonDto
		if err := c.ShouldBindJSON(&loginJson); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		if loginJson.Email == "admin@example.com" && loginJson.Password == "123456" {
			c.JSON(http.StatusOK, gin.H{
				"message":     "success",
				"accessToken": "mock-access-token-string",
			})
			return
		}

		c.JSON(http.StatusUnauthorized, gin.H{"message": "invalid credentials"})
	})

	// 2.将请求体中的 form 数据绑定到 struct
	router.POST("/form", func(c *gin.Context) {
		// 将 struct 实例绑定到 form 数据
		var loginForm LoginFormDto
		if err := c.ShouldBind(&loginForm); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		if loginForm.Email == "admin@example.com" && loginForm.Password == "123456" {
			c.JSON(http.StatusOK, gin.H{
				"message":     "success",
				"accessToken": "mock-access-token-string",
			})
			return
		}

		c.JSON(http.StatusUnauthorized, gin.H{"message": "invalid email or password"})
	})

	// 3.将请求头中的 header 字段绑定到 struct
	router.GET("/header", func(c *gin.Context) {
		var header AuthHeaderDto
		if err := c.ShouldBindHeader(&header); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		}

		c.JSON(http.StatusOK, gin.H{
			"your-accessToken":  header.AccessToken,
			"your-refreshToken": header.RefreshToken,
		})
	})

	router.Run()
}

验证数据

由于 Gin 框架自带的数据验证用的是: validator, 所以请直接查看文档即可

go
package main

import (
	"net/http"

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

// json: 表示数据从POST请求体的json中获取这个字段
// binding: 表示这个字段数据的验证规则(验证规则不够可以自定义)
//
//	自定义验证器: https://gin-gonic.com/zh-cn/docs/binding/custom-validators/
//	内置验证器规则: https://github.com/go-playground/validator
type LoginFormDto struct {
	Email    string `json:"email" binding:"required,email"`
	Password string `json:"password" binding:"required,min=6,max=20"`
}

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

	router.POST("/login", func(c *gin.Context) {
		// 绑定数据
		var loginData LoginFormDto
		err := c.ShouldBindJSON(&loginData)
		// 如果有错误, 说明数据没有通过验证(无法绑定)
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		// 通过
		c.JSON(http.StatusOK, gin.H{"message": "login success"})
	})

	router.Run() // http://localhost:8080
}

中间件 Middleware

局部中间件

需要手动应用到某一个路由

go
package main

import (
	"net/http"

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

func AuthMiddleware(c *gin.Context) {
	// 判断是否登录
	if c.GetHeader("Authorization") == "mock-access-token" {
		c.Next() // 通过: 会执行后续的 handler 回调
		return
	}

	c.Abort() // 中断: 不会再执行后续的 handler 回调
	c.JSON(http.StatusUnauthorized, gin.H{
		"error":   "unauthorized",
		"message": "please login first",
	})
}

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

	router.POST("/login", func(c *gin.Context) {
		// 没有应用 AuthMiddleware, 所以不会进入 AuthMiddleware 判断流程
		c.JSON(http.StatusOK, gin.H{
			"message": "from login handler",
		})
	})

  // 应用中间件
	router.GET("/profile", AuthMiddleware, func(c *gin.Context) {
		// 局部中间件: 必须通过 AuthMiddleware 才能访问到这个 handler 回调函数
		c.JSON(http.StatusOK, gin.H{
			"message": "from profile handler",
		})
	})

	router.Run() // http://localhost:8080
}

全局中间件 & 分组中间件

go
package main

import (
	"fmt"
	"net/http"

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

// 记录访问日志中间件
func AccessLogger(c *gin.Context) {
	fmt.Println("[AccessLogger]", c.Request.Method, c.Request.URL.Path)
	c.Next()
}

// 登录验证中间件
func Auth(c *gin.Context) {
	if c.GetHeader("Authorization") == "mock-access-token" {
		c.Set("uid", "1001") // 给后续 hander 传参
		c.Next()
		return
	}

	c.Abort()
	c.String(http.StatusUnauthorized, "Unauthorized")
}

func main() {
	gin.SetMode(gin.ReleaseMode)
	router := gin.Default()

	///// 1.应用全局中间件: 所有 router 下的路由/路由分组都会执行 AccessLogger
	router.Use(AccessLogger)

	// /ping
	router.GET("/ping", func(c *gin.Context) {
		c.String(http.StatusOK, "server is running")
	})

	apiGroup := router.Group("/api")            // /api
	authGroup := apiGroup.Group("/auth")        // /api/auth
	articleGroup := apiGroup.Group("/articles") // /api/articles

	///// 2.应用分组路由中间件: 所有 articleGroup 下的路由都会执行 Auth
	articleGroup.Use(Auth)

	// POST /api/auth/login
	authGroup.POST("/login", func(c *gin.Context) {
		c.String(http.StatusOK, "login api")
	})

	// POST /api/auth/register
	authGroup.POST("/register", func(c *gin.Context) {
		c.String(http.StatusOK, "register api")
	})

	// GET /api/auth/refresh-access-token
	authGroup.GET("/refresh-access-token", func(c *gin.Context) {
		c.String(http.StatusOK, "refresh-access-token api")
	})

	// GET /api/articles
	articleGroup.GET("/", func(c *gin.Context) {
		uid := c.GetString("uid")
		c.String(http.StatusOK, "articles list, author id is "+uid)
	})

	// GET /api/articles/:id
	articleGroup.GET("/:id", func(c *gin.Context) {
		c.String(http.StatusOK, "articles detail")
	})

	// POST /api/articles
	articleGroup.POST("/", func(c *gin.Context) {
		c.String(http.StatusOK, "create articles")
	})

	// PATCH /api/articles/:id
	articleGroup.PATCH("/:id", func(c *gin.Context) {
		c.String(http.StatusOK, "create articles")
	})

	router.Run() // http://localhost:8080
}

全局异常处理 ErrorHandler

跨域中间

日志

配置

Released under the MIT License.