介绍
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.gogo
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 的效果
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
}