Gin框架学习记录
Gin框架
原生net/http库开发痛点
1、参数解析与验证繁琐,需要手动解析URL参数、表单数据、JSON数据等
2、路由不直观,没有内置路径参数支持,需要手动解析URL路径
3、响应处理比较原始,需要手动序列化JSON、设置响应头等
4、缺少中间件机制,日志、鉴权、限流等功能需要自行实现
5、错误处理分散,没有统一的错误管理机制
Gin框架简介
Gin 是 Go 语言生态中最受欢迎的轻量级高性能 Web 框架,基于 httprouter 实现,路由采用基数树(Radix Tree)数据结构,提供 Martini-like API 但性能提升 40 倍以上。适合构建 RESTful API 和微服务。
核心特性:
- 零分配路由:极其高效的路由匹配,无堆内存分配
- 中间件支持:可扩展的中间件系统(洋葱模型)
- Crash-free:内置 Recovery 中间件防止 panic 导致服务崩溃
- JSON 验证:自动请求/响应 JSON 绑定与验证
- 路由分组:组织相关路由并应用公共中间件
- 内置渲染:支持 JSON、XML、YAML、HTML 模板等多种响应格式
一、入门示例
1、设置环境及初始化
D:\go\opsAdminPermission> go env -w GO111MODULE=on # 启用 Go Modules 功能
D:\go\opsAdminPermission> go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct #加速
D:\go\opsAdminPermission> go mod init opsAdminPermission #初始化项目,创建go.mod文件
D:\go\opsAdminPermission> go mod tidy #增加丢失的module,去掉未用的module
2、gin框架基本请求示例
1、使用gin的Default方法创建一个路由Handler
2、通过http方法绑定路由规则和路由函数。不同于net/http库的路由函数,gin进行了封装,把request、response都封装到了gin.Context的上下文环境中
3、最后启动路由的Run方法监听端口,或者自定义http服务器配置启动都可以
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
// gin框架入门
func main() {
//默认初始化,返回gin的引擎
r := gin.Default()
//绑定路由规则及函数
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
r.POST("/upload", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "upload success",
})
})
r.PUT("/put", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "put success",
})
})
r.DELETE("/delete", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "deleted success",
})
})
//绑定端口,启动监听
r.Run(":8080")
}
3、路由分组
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
//路由分组示例
rg := r.Group("/api/user")
{
rg.POST("/add", useradd)
rg.DELETE("/delete", userdel)
}
r.Run(":8080")
}
func userdel(c *gin.Context) {}
func useradd(c *gin.Context) {}
4、获取路径请求参数
4.1、单个url参数
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
// gin框架入门
func main() {
r := gin.Default()
rg := r.Group("/api/users")
rg.GET("/:id", func(ctx *gin.Context) {
id := ctx.Param("id")
ctx.JSON(http.StatusOK, gin.H{
"id": id,
})
})
r.Run(":8080")
}
4.2、多个url参数
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type Param struct {
Name string `uri:"name"`
Age int `uri:"age"`
}
// gin框架入门
func main() {
r := gin.Default()
rg := r.Group("/api/users")
rg.GET("/:name/:age", func(ctx *gin.Context) {
var param Param
err := ctx.ShouldBindUri(¶m)
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{
"msg": "参数非法",
})
return
}
ctx.JSON(http.StatusOK, gin.H{
"name": param.Name,
"age": param.Age,
})
})
r.Run(":8080")
}

5、获取get和post参数
5.1、获取get请求参数(Query参数)

package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
// 获取用户从前端传过来的参数 URL中携带?问号拼接的这种
func main() {
r := gin.Default()
rg := r.Group("/api/users")
// http://localhost:8080/api/users/detail?name=csdn&id=29
rg.GET("/search", func(ctx *gin.Context) {
nameQuery := ctx.Query("name")
idQuery := ctx.Query("id")
ctx.JSON(http.StatusOK, gin.H{
"name": nameQuery, //给前端展示回去
"id": idQuery,
})
})
r.Run(":8080")
}
使用 ShouldBindQuery 绑定结构体:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
// 获取用户从前端传过来的参数 ?问号拼接的这种
type Query struct {
Id int `form:"id"`
Name string `form:"name"`
}
func main() {
r := gin.Default()
rg := r.Group("/api/users")
// http://localhost:8080/api/users/detail?name=csdn&id=29
rg.GET("/search", func(ctx *gin.Context) {
var query Query
err := ctx.ShouldBindQuery(&query)
if err != nil {
ctx.JSON(400, gin.H{
"msg": "参数非法", //给前端返回错误
})
return
}
ctx.JSON(http.StatusOK, gin.H{
"name": query.Name, //给前端展示回去
"id": query.Id,
})
})
r.Run(":8080")
}
5.2、获取post请求参数
post请求form-data参数示例

package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
rg := r.Group("/api/users")
rg.POST("/list", func(ctx *gin.Context) {
id := ctx.PostForm("id")
ctx.JSON(200, gin.H{
"id": id,
})
})
r.Run(":8081")
}

package main
import (
"github.com/gin-gonic/gin"
)
type Query struct {
Id int `form:"id" json:"id"`
}
func main() {
r := gin.Default()
rg := r.Group("/api/users")
rg.POST("/list", func(ctx *gin.Context) {
var a Query
err := ctx.ShouldBind(&a) //自动判断传过来的参数类型
if err != nil {
return
}
ctx.JSON(200, gin.H{
"id": a.Id,
})
})
r.Run(":8081")
}
6、返回值

package main
import (
"github.com/gin-gonic/gin"
)
type Query struct {
Id int `form:"id" json:"id"`
}
func main() {
r := gin.Default()
rg := r.Group("/api/users")
rg.GET("/list", func(ctx *gin.Context) {
//string方式返回
ctx.String(200, "请求成功")
//json方式返回
ctx.JSON(200, gin.H{
"msg": "11",
})
//结构体返回
ctx.JSON(200, Query{
Id: 1,
})
})
r.Run(":8081")
}
7、validator校验包
package main
import (
"fmt"
"github.com/go-playground/validator/v10"
)
// 参数必填、密码长度及复杂度校验
type Query struct {
Name string `json:"name" validate:"required"`
Pwd string `json:"pwd" validate:"required,min=6,max=15"`
}
func main() {
//实例化结构体
req := &Query{
Name: "admin",
Pwd: "12345",
}
//创建一个新的 validator 实例,用于验证结构体字段。
validate := validator.New()
//调用 validate.Struct(req) 来对 req 进行验证
err := validate.Struct(req)
if err != nil {
//err断言
for _, e := range err.(validator.ValidationErrors) {
fmt.Println("===========", e)
}
return
}
fmt.Println("------------")
}
//如下所示 当pwd为5位时,运行代码提示不符合pwd的最短密码位数规则

7.1、自定义验证规则

package main
import (
"fmt"
"github.com/go-playground/validator/v10"
)
// 参数必填、密码长度及复杂度校验
type Query struct {
Name string `json:"name" validate:"required,CustomValidationErrors"`
Pwd string `json:"pwd" validate:"required,min=6,max=15"`
}
func main() {
req := &Query{
Name: "admi",
Pwd: "12345",
}
validate := validator.New()
//validator.RegisterValidation 接收三个参数:tag、fn 和 callValidationEvenIfNull
/*
tag:自定义验证规则的名称,你用来标识验证规则
fn:验证函数,类型是 validator.Func,该函数接受一个 validator.FieldLevel 类型的参数并返回一个布尔值,表示验证是否通过
callValidationEvenIfNull:一个可选参数,布尔类型。默认情况下,如果字段值为 nil 或空值,验证器将跳过该字段的验证。如果设置为 true,即使字段为空,验证器仍然会调用你的自定义验证函数
*/
_ = validate.RegisterValidation("CustomValidationErrors", CustomValidationErrors)
err := validate.Struct(req)
if err != nil {
for _, e := range err.(validator.ValidationErrors) {
fmt.Println("===========", e)
}
return
}
fmt.Println("------------")
}
func CustomValidationErrors(fl validator.FieldLevel) bool {
//如果字段的值不是 "admin",则返回 false,表示验证失败
return fl.Field().String() != "admin"
}
7.2、将错误翻译成中文

package main
import (
"fmt"
"reflect"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
zh_translation "github.com/go-playground/validator/v10/translations/zh"
)
// 参数必填、密码长度及复杂度校验
type Query struct {
Name string `json:"name" validate:"required,CustomValidationErrors"`
Pwd string `json:"pwd" validate:"required,min=6,max=15" label:"密码"`
}
func main() {
req := &Query{
Name: "admin",
Pwd: "12345",
}
validate := validator.New()
uni := ut.New(zh.New())
//拿到翻译器
trans, _ := uni.GetTranslator("zh")
err := zh_translation.RegisterDefaultTranslations(validate, trans)
if err != nil {
fmt.Println(err)
return
}
//获取结构体中字段的标签名称,前提是字段中配置了label标签,这样就能通过Translate获取到字段名称翻译
validate.RegisterTagNameFunc(func(field reflect.StructField) string {
return field.Tag.Get("label")
})
//validator.RegisterValidation 接收三个参数:tag、fn 和 callValidationEvenIfNull
/*
tag:自定义验证规则的名称,你用来标识验证规则
fn:验证函数,类型是 validator.Func,该函数接受一个 validator.FieldLevel 类型的参数并返回一个布尔值,表示验证是否通过
callValidationEvenIfNull:一个可选参数,布尔类型。默认情况下,如果字段值为 nil 或空值,验证器将跳过该字段的验证。如果设置为 true,即使字段为空,验证器仍然会调用你的自定义验证函数
*/
_ = validate.RegisterValidation("CustomValidationErrors", CustomValidationErrors)
err = validate.Struct(req)
if err != nil {
for _, e := range err.(validator.ValidationErrors) {
fmt.Println("===========", e.Translate(trans)) //将错误翻译为中文
}
return
}
fmt.Println("------------")
}
func CustomValidationErrors(fl validator.FieldLevel) bool {
//如果字段的值不是 "admin",则返回 false,表示验证失败,反之为成功
return fl.Field().String() == "admin"
}
7.3、gin框架中把错误翻译成中文

package main
import (
"errors"
"fmt"
"reflect"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
en_translation "github.com/go-playground/validator/v10/translations/en"
zh_translation "github.com/go-playground/validator/v10/translations/zh"
)
//定义一个全局变量 trans,类型为 ut.Translator,它用于存储翻译器实例
var trans ut.Translator
// 初始化翻译转换器
func InitTrans(local string) error {
//通过类型断言 (*validator.Validate),将验证引擎强制转换为 *validator.Validate 类型
var err error
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
//处理自定义的tag label:名称
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
return fld.Tag.Get("label")
})
//注册翻译器
uni := ut.New(en.New(), zh.New(), en.New())
//返回的是一个 ut.Translator 类型的实例,它将用于将验证错误信息翻译成目标语言
var ok bool
trans, ok = uni.GetTranslator(local)
if !ok {
return fmt.Errorf("uni.getTrans(%s) failed", local)
}
switch local {
case "en":
err = en_translation.RegisterDefaultTranslations(v, trans)
case "zh":
err = zh_translation.RegisterDefaultTranslations(v, trans)
default:
err = en_translation.RegisterDefaultTranslations(v, trans)
}
return err
}
return errors.New("binding.validator failed")
}
func FormatValidatorErr(fields map[string]string) string {
var str string
for _, err := range fields {
fmt.Println(fields) //map[Req.年龄:年龄为必填字段] key Req.年龄 value 年龄为必填字段
str = err
break
}
return str
}
func main() {
if err := InitTrans("zh"); err != nil {
panic(err)
}
r := gin.Default()
rg := r.Group("/api/users")
rg.POST("/list", func(ctx *gin.Context) {
type Req struct {
Name string `json:"name" binding:"required" label:"姓名"`
Age int `json:"age" binding:"required" label:"年龄"`
}
var req Req
err := ctx.ShouldBind(&req)
if err != nil {
errs, ok := err.(validator.ValidationErrors)
if !ok {
ctx.JSON(200, gin.H{
"msg": "参数错误",
})
return
}
fmt.Println(errs.Translate(trans))
ctx.JSON(200, gin.H{
//errs.Translate(trans)实际返回ValidationErrorsTranslations类型,但它的类型为map[string]string 刚好切合FormatValidatorErr()函数的参数
"msg": FormatValidatorErr(errs.Translate(trans)), //返回一个翻译后的错误信息,以便返回给客户端
})
return
}
ctx.JSON(200, gin.H{"msg": "success"})
})
r.Run(":8081")
}
7.4、常用Validator验证规则速查表
| Tag | 说明 | 示例 |
|---|---|---|
required |
必填字段 | validate:"required" |
min / max |
数值/字符串长度范围 | validate:"min=6,max=15" |
len |
字符串长度等于指定值 | validate:"len=11" |
eq / ne |
等于/不等于 | validate:"eq=admin" |
gt / gte |
大于/大于等于 | validate:"gt=0" |
lt / lte |
小于/小于等于 | validate:"lt=100" |
email |
邮箱格式 | validate:"email" |
url |
URL格式 | validate:"url" |
oneof |
枚举值 | validate:"oneof=male female" |
numeric |
数字字符串 | validate:"numeric" |
alpha |
纯字母 | validate:"alpha" |
alphanum |
字母+数字 | validate:"alphanum" |
ip |
IP地址 | validate:"ip" |
8、post几种绑定参数的方式
1、直接使用golang封装好的状态码,在请求后返回对应的状态
2、请求统一返回200状态码,然后代码中自行添加错误码code,根据code码排错
c.JSON(200,gin.H{
"msg": "success",
"code": 20000,
})
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
rg := r.Group("/api/users")
rg.POST("/list", func(ctx *gin.Context) {
type Req struct {
Name string `json:"name" binding:"required"`
Age int `json:"age" binding:"required"`
}
var req Req
// MustBindWith 如果绑定失败,会直接返回400状态码
// err := ctx.MustBindWith(&req, binding.JSON)
err := ctx.ShouldBind(&req)
if err != nil {
ctx.JSON(200, gin.H{
"error": err.Error(),
"code": 40000,
})
return
}
ctx.JSON(200, gin.H{
"msg": "success",
"code": 20000,
})
})
r.Run(":8081")
}
9、响应
返回字符串
func main() {
r := gin.Default()
r.GET("/ping", func(ctx *gin.Context) {
ctx.String(200, "pong")
})
r.Run(":8080")
}
返回JSON
func main() {
//创建默认路由
r := gin.Default()
//绑定路由规则及函数
r.POST("/upload", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "upload success",
})
})
}
二、gin中间件
1、中间件引入

package main
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.New()
//全局中间件
r.Use(gin.Logger())
//recovery中间件会recover任何panic,如果有panic的话会写入500
r.Use(gin.Recovery())
r.GET("/user", MyLogger(), func(ctx *gin.Context) {
fmt.Println("当代码运行到下方的ctx.Next()时,会先执行此处的func(ctx *gin.Context),之后才会执行ctx.Next()下方剩余的代码")
})
r.Run(":8081")
}
func MyLogger() gin.HandlerFunc {
return func(ctx *gin.Context) {
t := time.Now().UnixMilli()
ctx.Set("token", "123")
ctx.Next()
latency := time.Now().UnixMilli() - t
fmt.Printf("耗时: %d\n", latency)
//ctx.Next() 如果在此处,则最后才会执行上方GET请求中的func(ctx *gin.Context)回调函数
}
}
2、Next和Abort
- ctx.Next():执行后续中间件和处理函数,执行完后再返回当前中间件继续执行
ctx.Next()之后的代码(洋葱模型) - ctx.Abort():阻止后续中间件和处理函数执行,但不会停止当前函数,只会跳过后续链上的函数


package main
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.New()
//全局中间件
r.Use(gin.Logger())
//recovery中间件会recover任何panic,如果有panic的话会写入500
r.Use(gin.Recovery())
r.GET("/user", MyLogger(), func(ctx *gin.Context) {
//不会执行到此回调函数,因为ctx.Abort()阻止了后续执行
})
r.Run(":8081")
}
func MyLogger() gin.HandlerFunc {
return func(ctx *gin.Context) {
//阻止挂起的函数,不会停止当前的函数,会停止下一次要请求的函数
//在此处,不会执行上述GET请求中的func(ctx *gin.Context)回调函数
ctx.Abort()
t := time.Now().UnixMilli()
ctx.Set("token", "123")
//执行请求(执行下一次的请求)
// ctx.Next()
latency := time.Now().UnixMilli() - t
fmt.Printf("耗时: %d\n", latency)
}
}
3、中间件数据传递
通过 ctx.Set(key, value) 和 ctx.Get(key) 在中间件和处理函数之间共享数据:
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("userID", 1001)
c.Next()
}
}
func main() {
r := gin.Default()
r.GET("/profile", AuthMiddleware(), func(c *gin.Context) {
userID, _ := c.Get("userID")
c.JSON(200, gin.H{"userID": userID})
})
r.Run(":8081")
}
4、AbortWithStatusJSON
中间件中可以直接终止请求并返回JSON响应:
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatusJSON(401, gin.H{"error": "未登录"})
return
}
c.Next()
}
}
5、常用第三方中间件
| 中间件 | 功能 | 导入路径 |
|---|---|---|
| CORS | 跨域处理 | github.com/gin-contrib/cors |
| Sessions | 会话管理 | github.com/gin-contrib/sessions |
| Gzip | 响应压缩 | github.com/gin-contrib/gzip |
| Cache | 缓存 | github.com/gin-contrib/cache |
| PProf | 性能分析 | github.com/gin-contrib/pprof |
| RequestID | 请求ID | github.com/gin-contrib/requestid |
| Zap | 结构化日志 | github.com/gin-contrib/zap |
三、优雅的重启和退出

package main
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"time"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/", func(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"msg": "success",
})
})
// 创建 HTTP Server
srv := &http.Server{
Addr: ":8081",
Handler: r,
}
//开启协程
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Printf("listen: %s\n", err)
}
}()
// 创建无缓冲通道,发送和接收操作都是阻塞的
// 等待中断信号
// kill 默认会发送 syscall.SIGTERM 信号
// kill -2 发送 syscall.SIGINT 信号,我们常用的Ctrl+C就是触发系统SIGINT信号
// kill -9 发送 syscall.SIGKILL 信号,但是不能被捕获,所以不需要添加它
// signal.Notify把收到的 syscall.SIGINT或syscall.SIGTERM 信号转发给quit
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
// 从quit通道中读取信号
// 阻塞在此,当接收到上述两种信号时才会往下执行
<-quit
fmt.Println("shutdown server....")
// 创建一个 5 秒的超时上下文
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 关闭 HTTP Server
// 5秒内优雅关闭服务(将未处理完的请求处理完再关闭服务),超过5秒就超时退出
if err := srv.Shutdown(ctx); err != nil {
fmt.Println("Server Shutdown:", err)
}
fmt.Println("Server exiting")
}
四、跨域问题解决(CORS)
1、什么是跨域
浏览器同源策略限制:协议、域名、端口任一不同即为跨域。前端 http://localhost:3000 请求后端 http://localhost:8081 时,浏览器会拦截响应。
2、手动设置CORS中间件
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
// Cors 自定义跨域中间件
func Cors() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
c.Header("Access-Control-Allow-Origin", "*") // 允许所有域名,生产环境应指定具体域名
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization, Token")
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Max-Age", "86400") // 预检请求缓存时间(秒)
// OPTIONS 预检请求直接返回 204
if method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
func main() {
r := gin.Default()
r.Use(Cors()) // 全局启用跨域
r.GET("/api/users", func(c *gin.Context) {
c.JSON(200, gin.H{"msg": "success"})
})
r.Run(":8081")
}
3、使用gin-contrib/cors中间件(推荐)
package main
import (
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// 配置CORS
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"http://localhost:3000", "https://example.com"}, // 允许的域名
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization", "Token"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true, // 是否允许携带Cookie
AllowOriginFunc: func(origin string) bool {
return origin == "http://localhost:3000"
},
MaxAge: 12 * time.Hour, // 预检请求缓存时间
}))
r.GET("/api/users", func(c *gin.Context) {
c.JSON(200, gin.H{"msg": "success"})
})
r.Run(":8081")
}
五、文件上传与下载
1、单文件上传
package main
import (
"fmt"
"net/http"
"path/filepath"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// 设置上传文件大小限制(8MB)
r.MaxMultipartMemory = 8 << 20
r.POST("/upload", func(c *gin.Context) {
// 从表单中获取文件,参数名 "file" 对应前端的 name="file"
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "文件上传失败: " + err.Error()})
return
}
// 保存文件到指定路径
dst := filepath.Join("./uploads", file.Filename)
if err := c.SaveUploadedFile(file, dst); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "文件保存失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"msg": "上传成功",
"filename": file.Filename,
"size": file.Size,
})
})
r.Run(":8081")
}
2、多文件上传
r.POST("/upload-multiple", func(c *gin.Context) {
form, err := c.MultipartForm()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
files := form.File["files"] // 前端 input 的 name="files" 且设置 multiple
var uploadedFiles []string
for _, file := range files {
dst := filepath.Join("./uploads", file.Filename)
if err := c.SaveUploadedFile(file, dst); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("文件 %s 保存失败", file.Filename)})
return
}
uploadedFiles = append(uploadedFiles, file.Filename)
}
c.JSON(http.StatusOK, gin.H{
"msg": "批量上传成功",
"files": uploadedFiles,
})
})
3、文件下载
r.GET("/download/:filename", func(c *gin.Context) {
filename := c.Param("filename")
filePath := filepath.Join("./uploads", filename)
// 检查文件是否存在
if _, err := os.Stat(filePath); os.IsNotExist(err) {
c.JSON(http.StatusNotFound, gin.H{"error": "文件不存在"})
return
}
// c.File 直接返回文件(浏览器预览)
// c.File(filePath)
// c.FileAttachment 以附件形式下载
c.FileAttachment(filePath, filename)
})
六、静态文件服务与HTML模板渲染
1、静态文件服务
func main() {
r := gin.Default()
// 将 /static 路径映射到 ./public 目录
// 如访问 http://localhost:8080/static/js/app.js 对应 ./public/js/app.js
r.Static("/static", "./public")
// 将单个文件映射到指定路由
r.StaticFile("/favicon.ico", "./public/favicon.ico")
r.Run(":8080")
}
2、HTML模板渲染
目录结构:
project/
├── main.go
└── templates/
├── index.html
└── user/
└── profile.html
加载模板:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// 加载 templates 目录下所有模板文件
r.LoadHTMLGlob("templates/**/*")
// 或者只加载指定模式的文件
// r.LoadHTMLGlob("templates/*.html")
r.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{
"title": "首页",
"name": "Gin框架",
})
})
r.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.HTML(http.StatusOK, "user/profile.html", gin.H{
"name": name,
"age": 25,
})
})
r.Run(":8080")
}
templates/index.html:
<!DOCTYPE html>
<html>
<head>
<title>{{ .title }}</title>
</head>
<body>
<h1>欢迎来到 {{ .name }}</h1>
</body>
</html>
3、模板自定义函数
func main() {
r := gin.Default()
// 注册自定义模板函数
r.SetFuncMap(template.FuncMap{
"formatTime": func(t time.Time) string {
return t.Format("2006-01-02 15:04:05")
},
})
r.LoadHTMLGlob("templates/**/*")
// ...
}
七、路由进阶
1、支持全部HTTP方法 — Any
// Any 匹配 GET、POST、PUT、PATCH、HEAD、OPTIONS、DELETE、CONNECT、TRACE 所有请求方法
r.Any("/api/any", func(c *gin.Context) {
c.JSON(200, gin.H{
"method": c.Request.Method,
})
})
2、处理不存在的路由 — NoRoute
r.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{
"code": 404,
"msg": "页面不存在",
})
})
3、处理不支持的方法 — NoMethod
r.NoMethod(func(c *gin.Context) {
c.JSON(http.StatusMethodNotAllowed, gin.H{
"code": 405,
"msg": "请求方法不允许",
})
})
4、路由重定向
// HTTP重定向
r.GET("/old", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "/new")
})
// 路由内跳转(HandleContext)
r.GET("/jump", func(c *gin.Context) {
c.Request.URL.Path = "/target"
r.HandleContext(c)
})
r.GET("/target", func(c *gin.Context) {
c.JSON(200, gin.H{"msg": "target route"})
})
5、路由组嵌套与中间件结合
func main() {
r := gin.Default()
// 公开路由组
public := r.Group("/api/public")
{
public.GET("/ping", func(c *gin.Context) { c.String(200, "pong") })
}
// 需要认证的路由组
private := r.Group("/api/admin")
private.Use(AuthMiddleware())
{
private.GET("/dashboard", adminDashboard)
// 嵌套子路由组也自动继承父组中间件
users := private.Group("/users")
{
users.GET("/", listUsers)
users.POST("/", createUser)
}
}
r.Run(":8080")
}
八、数据绑定详解
1、各种绑定方法对比
| 方法 | 说明 | 失败时行为 |
|---|---|---|
c.ShouldBind(&obj) |
根据Content-Type自动选择绑定方式 | 返回错误 |
c.ShouldBindJSON(&obj) |
绑定JSON | 返回错误 |
c.ShouldBindXML(&obj) |
绑定XML | 返回错误 |
c.ShouldBindQuery(&obj) |
绑定查询参数(Query) | 返回错误 |
c.ShouldBindUri(&obj) |
绑定路径参数 | 返回错误 |
c.ShouldBindYAML(&obj) |
绑定YAML | 返回错误 |
c.ShouldBindHeader(&obj) |
绑定请求头 | 返回错误 |
c.MustBind(&obj) |
同ShouldBind,失败直接返回400 | 直接返回400 |
c.MustBindWith(&obj, bindingType) |
指定绑定类型,失败直接返回400 | 直接返回400 |
2、JSON绑定示例
type User struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
Age int `json:"age" binding:"gte=0,lte=130"`
}
r.POST("/user", 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, gin.H{"user": user})
})
3、Form表单绑定
type LoginForm struct {
Username string `form:"username" binding:"required"`
Password string `form:"password" binding:"required,min=6"`
}
r.POST("/login", func(c *gin.Context) {
var form LoginForm
if err := c.ShouldBind(&form); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// 业务逻辑...
c.JSON(200, gin.H{"msg": "登录成功"})
})
4、请求头绑定
type Headers struct {
Token string `header:"Authorization" binding:"required"`
ContentType string `header:"Content-Type"`
}
r.GET("/header", func(c *gin.Context) {
var h Headers
if err := c.ShouldBindHeader(&h); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{"token": h.Token})
})
九、日志系统
1、默认日志中间件
func main() {
r := gin.Default() // 默认已经包含了 Logger() 和 Recovery() 中间件
// 等效于:
// r := gin.New()
// r.Use(gin.Logger())
// r.Use(gin.Recovery())
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
r.Run(":8080")
}
2、将日志写入文件
func main() {
// 禁用控制台颜色
gin.DisableConsoleColor()
// 创建日志文件
f, _ := os.Create("gin.log")
// 将日志同时写入文件和控制台
gin.DefaultWriter = io.MultiWriter(f, os.Stdout)
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
r.Run(":8080")
}
3、自定义日志格式
func main() {
// 自定义日志格式
r := gin.New()
r.Use(gin.LoggerWithFormatter(func(params gin.LogFormatterParams) string {
return fmt.Sprintf("[GIN] %s | %d | %s | %s | %s | %s\n",
params.TimeStamp.Format("2006-01-02 15:04:05"),
params.StatusCode,
params.Latency,
params.ClientIP,
params.Method,
params.Path,
)
}))
r.Use(gin.Recovery())
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
r.Run(":8080")
}
4、LogColor 彩色日志
// 启用彩色日志
gin.ForceConsoleColor()
十、Cookie与Session
1、Cookie操作
func main() {
r := gin.Default()
// 设置Cookie
r.GET("/set-cookie", func(c *gin.Context) {
c.SetCookie("username", "zhangsan", 3600, "/", "localhost", false, true)
// SetCookie(name, value, maxAge(秒), path, domain, secure, httpOnly)
c.String(200, "Cookie设置成功")
})
// 读取Cookie
r.GET("/get-cookie", func(c *gin.Context) {
username, err := c.Cookie("username")
if err != nil {
c.String(400, "Cookie未找到")
return
}
c.String(200, "username: %s", username)
})
// 删除Cookie
r.GET("/del-cookie", func(c *gin.Context) {
c.SetCookie("username", "", -1, "/", "localhost", false, true)
c.String(200, "Cookie已删除")
})
r.Run(":8080")
}
2、Session使用(gin-contrib/sessions)
import (
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// 创建基于Cookie的session存储(生产环境建议用Redis存储)
store := cookie.NewStore([]byte("secret-key"))
r.Use(sessions.Sessions("mysession", store))
// 设置Session
r.POST("/login", func(c *gin.Context) {
session := sessions.Default(c)
session.Set("userID", 1001)
session.Save()
c.JSON(200, gin.H{"msg": "登录成功"})
})
// 读取Session
r.GET("/profile", func(c *gin.Context) {
session := sessions.Default(c)
userID := session.Get("userID")
if userID == nil {
c.JSON(401, gin.H{"error": "未登录"})
return
}
c.JSON(200, gin.H{"userID": userID})
})
// 清除Session
r.POST("/logout", func(c *gin.Context) {
session := sessions.Default(c)
session.Clear()
session.Save()
c.JSON(200, gin.H{"msg": "已退出"})
})
r.Run(":8080")
}
十一、Gin项目结构最佳实践
以下是推荐的标准项目目录结构:
project/
├── main.go # 入口文件
├── config/ # 配置文件
│ └── config.go
├── router/ # 路由定义
│ └── router.go
├── middleware/ # 中间件
│ ├── auth.go
│ ├── cors.go
│ └── logger.go
├── controller/ # 控制器层(处理请求)
│ ├── user_controller.go
│ └── order_controller.go
├── service/ # 业务逻辑层
│ ├── user_service.go
│ └── order_service.go
├── model/ # 数据模型
│ ├── user.go
│ └── order.go
├── dao/ # 数据访问层
│ ├── user_dao.go
│ └── order_dao.go
├── dto/ # 数据传输对象(请求/响应结构体)
│ ├── user_dto.go
│ └── order_dto.go
├── common/ # 公共工具
│ ├── response.go # 统一响应格式
│ ├── error_code.go # 错误码定义
│ └── validator.go # 验证器初始化
└── utils/ # 工具函数
└── helper.go
统一响应格式示例
// common/response.go
package common
import (
"net/http"
"github.com/gin-gonic/gin"
)
type Response struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data,omitempty"`
}
func Success(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, Response{
Code: 20000,
Msg: "success",
Data: data,
})
}
func Error(c *gin.Context, code int, msg string) {
c.JSON(http.StatusOK, Response{
Code: code,
Msg: msg,
})
}
十二、GORM数据库集成
GORM 是 Go 语言中最流行的 ORM 库,与 Gin 配合使用非常常见。
1、安装
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql # MySQL驱动
go get -u gorm.io/driver/postgres # PostgreSQL驱动
2、数据库初始化和连接
package main
import (
"fmt"
"log"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var DB *gorm.DB
func InitDB() {
dsn := "root:password@tcp(127.0.0.1:3306)/testdb?charset=utf8mb4&parseTime=True&loc=Local"
var err error
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info), // 打印SQL日志
})
if err != nil {
log.Fatal("数据库连接失败:", err)
}
// 设置连接池
sqlDB, _ := DB.DB()
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
fmt.Println("数据库连接成功")
}
3、模型定义与迁移
// model/user.go
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:50;not null" json:"name"`
Email string `gorm:"size:100;uniqueIndex" json:"email"`
Age int `gorm:"default:0" json:"age"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// 自动迁移
func InitDB() {
// ...连接数据库
DB.AutoMigrate(&User{}) // 自动创建/更新表结构
}
4、Gin + GORM 完整CRUD示例
package main
import (
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var DB *gorm.DB
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"size:50;not null"`
Email string `json:"email" gorm:"size:100;uniqueIndex"`
}
func main() {
// 连接数据库
dsn := "root:password@tcp(127.0.0.1:3306)/testdb?charset=utf8mb4&parseTime=True"
DB, _ = gorm.Open(mysql.Open(dsn), &gorm.Config{})
DB.AutoMigrate(&User{})
r := gin.Default()
rg := r.Group("/api/users")
{
rg.POST("/", createUser)
rg.GET("/", listUsers)
rg.GET("/:id", getUser)
rg.PUT("/:id", updateUser)
rg.DELETE("/:id", deleteUser)
}
r.Run(":8080")
}
func createUser(c *gin.Context) {
var user User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
DB.Create(&user)
c.JSON(201, gin.H{"data": user, "msg": "创建成功"})
}
func listUsers(c *gin.Context) {
var users []User
DB.Find(&users)
c.JSON(200, gin.H{"data": users})
}
func getUser(c *gin.Context) {
id := c.Param("id")
var user User
if err := DB.First(&user, id).Error; err != nil {
c.JSON(404, gin.H{"error": "用户不存在"})
return
}
c.JSON(200, gin.H{"data": user})
}
func updateUser(c *gin.Context) {
id := c.Param("id")
var user User
if err := DB.First(&user, id).Error; err != nil {
c.JSON(404, gin.H{"error": "用户不存在"})
return
}
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
DB.Save(&user)
c.JSON(200, gin.H{"data": user, "msg": "更新成功"})
}
func deleteUser(c *gin.Context) {
id := c.Param("id")
if err := DB.Delete(&User{}, id).Error; err != nil {
c.JSON(500, gin.H{"error": "删除失败"})
return
}
c.JSON(200, gin.H{"msg": "删除成功"})
}
5、GORM常用查询
// 条件查询
DB.Where("name = ?", "zhangsan").First(&user)
DB.Where("age > ?", 18).Find(&users)
DB.Where("name LIKE ?", "%zhang%").Find(&users)
DB.Where("age BETWEEN ? AND ?", 18, 30).Find(&users)
// 分页查询
var page, pageSize = 1, 10
DB.Offset((page - 1) * pageSize).Limit(pageSize).Find(&users)
// 排序
DB.Order("age desc").Find(&users)
// 链式组合
DB.Where("age > ?", 18).Order("id desc").Limit(10).Find(&users)
// 统计
var count int64
DB.Model(&User{}).Where("age > ?", 18).Count(&count)
十三、Swagger API文档生成
1、安装
go get -u github.com/swaggo/swag/cmd/swag
go get -u github.com/swaggo/gin-swagger
go get -u github.com/swaggo/files
2、添加API注释
package main
import (
"net/http"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
_ "your-project/docs" // swag init 生成的docs包
)
// @title Gin框架API文档
// @version 1.0
// @description 这是一个Gin框架的示例API文档
// @host localhost:8080
// @BasePath /api/v1
// @securityDefinitions.apikey Bearer
// @in header
// @name Authorization
func main() {
r := gin.Default()
// Swagger文档路由
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
v1 := r.Group("/api/v1")
{
v1.GET("/users", GetUsers)
v1.POST("/users", CreateUser)
}
r.Run(":8080")
}
// GetUsers 获取用户列表
// @Summary 获取用户列表
// @Description 返回所有用户信息
// @Tags 用户管理
// @Accept json
// @Produce json
// @Param page query int false "页码" default(1)
// @Param size query int false "每页数量" default(10)
// @Success 200 {object} common.Response{data=[]User}
// @Router /users [get]
func GetUsers(c *gin.Context) {
// ...
var users []User
c.JSON(http.StatusOK, gin.H{"data": users})
}
// CreateUser 创建用户
// @Summary 创建用户
// @Description 创建一个新用户
// @Tags 用户管理
// @Accept json
// @Produce json
// @Param user body User true "用户信息"
// @Success 201 {object} common.Response
// @Failure 400 {object} common.Response
// @Security Bearer
// @Router /users [post]
func CreateUser(c *gin.Context) {
// ...
}
3、生成文档
# 在项目根目录执行(main.go 所在目录)
swag init
# 如果main.go在cmd目录下
swag init -g cmd/main.go
生成后访问 http://localhost:8080/swagger/index.html 即可查看API文档。
十四、单元测试
1、创建测试文件
// main_test.go
package main
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
// 初始化测试路由
func setupRouter() *gin.Engine {
gin.SetMode(gin.TestMode) // 使用测试模式
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})
r.GET("/user/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{"id": id})
})
r.POST("/login", func(c *gin.Context) {
type Req struct {
Name string `json:"name" binding:"required"`
}
var req Req
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{"name": req.Name})
})
return r
}
// 测试GET请求
func TestPingRoute(t *testing.T) {
router := setupRouter()
req, _ := http.NewRequest("GET", "/ping", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "pong")
}
// 测试路径参数
func TestUserRoute(t *testing.T) {
router := setupRouter()
req, _ := http.NewRequest("GET", "/user/123", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "123")
}
// 测试POST请求
func TestLoginRoute(t *testing.T) {
router := setupRouter()
body := strings.NewReader(`{"name":"zhangsan"}`)
req, _ := http.NewRequest("POST", "/login", body)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "zhangsan")
}
// 运行测试: go test -v
2、运行测试命令
# 运行所有测试
go test ./... -v
# 运行指定测试函数
go test -run TestPingRoute -v
# 查看测试覆盖率
go test -cover
# 生成覆盖率报告
go test -coverprofile=coverage.out
go tool cover -html=coverage.out
十五、常见问题与性能优化
1、gin.Default() vs gin.New()
| 方法 | 说明 |
|---|---|
gin.Default() |
创建带 Logger 和 Recovery 中间件的引擎 |
gin.New() |
创建空白引擎,不包含任何中间件,适用于自定义中间件场景 |
2、ShouldBind vs MustBind
ShouldBind:绑定失败返回 error,由开发者自行处理MustBind:绑定失败直接返回 400 错误并终止请求链
3、绑定JSON时常见问题
// 错误:前端传JSON,后端用了form标签
type Req struct {
Name string `form:"name"` // 错误!JSON请求应该用json标签
}
// 正确:JSON绑定使用json标签
type Req struct {
Name string `json:"name" binding:"required"`
}
4、性能优化建议
- 生产环境设置Gin模式
gin.SetMode(gin.ReleaseMode)
- 设置请求体大小限制
r.MaxMultipartMemory = 8 << 20 // 8MB
- 连接池配置(使用GORM时)
sqlDB, _ := DB.DB()
sqlDB.SetMaxIdleConns(10) // 空闲连接数
sqlDB.SetMaxOpenConns(100) // 最大连接数
sqlDB.SetConnMaxLifetime(time.Hour)
-
合理使用中间件:全局中间件会影响所有路由,只在全局层面添加必要的中间件(如日志、恢复),特定路由的中间件使用组级别注册
-
避免在循环中进行数据库查询:使用批量查询替代N+1查询
-
使用context超时控制
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
defer cancel()
DB.WithContext(ctx).Find(&users)
5、常见错误处理模式
// 统一错误处理包裹函数
func wrapHandler(fn func(*gin.Context) error) gin.HandlerFunc {
return func(c *gin.Context) {
if err := fn(c); err != nil {
c.JSON(500, gin.H{"error": err.Error()})
}
}
}
// 使用示例
r.GET("/api", wrapHandler(func(c *gin.Context) error {
result, err := someBusinessLogic()
if err != nil {
return err
}
c.JSON(200, result)
return nil
}))
本文档持续更新中,如有遗漏或错误欢迎补充修正。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)