1.初始gin

想学习gin框架第一步就是先下载依赖,在控制台输入下行命令安装gin依赖

go get github.com/gin-gonic/gin

安装好后先写一个简单的web项目试一下吧

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

func Index(context *gin.Context) {
  context.String(200, "Hello World")
}

func main() {
  // 创建一个默认的路由
  router := gin.Default()

  // 绑定路由规则和路由函数,访问/index的路由,将由对应的函数去处理
  router.GET("/index", Index)

  // 启动监听,gin会把web服务运行在本机的0.0.0.0:8080端口上
  router.Run("0.0.0.0:8080")
  // 用原生http服务的方式, router.Run本质就是http.ListenAndServe的进一步封装
  http.ListenAndServe(":8080", router)
}
  1. router:=gin.Default():这是默认的服务器。使用gin的Default方法创建一个路由Handler
  2. 然后通过Http方法绑定路由规则和路由函数。不同于net/http库的路由函数,gin进行了封装,把requestresponse都封装到了gin.Context的上下文环境中。
  3. 最后启动路由的Run方法监听端口。还可以用http.ListenAndServe(":8080", router),或者自定义Http服务器配置。

2.请求和响应

1.响应

  • 返回json

    router.GET("/json", func(c *gin.Context) {
      c.JSON(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
    })
    // 结构体转json
    router.GET("/moreJSON", func(c *gin.Context) {
      // 也可以使用结构体
      type Msg struct {
        Name    string `json:"user"`
        Message string
        Number  int
      }
      msg := Msg{"zhangsan", "hey", 21}
      // 注意 msg.Name 变成了 "user" 字段
      // 以下方式都会输出 :   {"user": "zhangsan", "Message": "hey", "Number": 21}
      c.JSON(http.StatusOK, msg)
    })
    
  • 返回html

    先要使用 LoadHTMLGlob()或者LoadHTMLFiles()方法来加载模板文件

    //加载模板 加载 gin框架/templates目录下所有html文件
    router.LoadHTMLGlob("gin框架/templates/*")
    //router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")
    //定义路由
    router.GET("/tem", func(c *gin.Context) {
      //根据完整文件名渲染模板,并传递参数
      c.HTML(http.StatusOK, "index.html", gin.H{
        "title": "Main website",
      })
    })
    

    在模板中使用这个title,需要使用{{ .title }}

    不同文件夹下模板名字可以相同,此时需要 LoadHTMLGlob() 加载两层模板路径。

    router.LoadHTMLGlob("templates/**/*")
    router.GET("/posts/index", func(c *gin.Context) {
        c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{
            "title": "Posts",
        })
        c.HTML(http.StatusOK, "users/index.tmpl", gin.H{
            "title": "Users",
        })
    
    })
    
  • 响应文件

    // 在golang总,没有相对文件的路径,它只有相对项目的路径
    // 网页请求这个静态目录的前缀, 第二个参数是一个目录,注意,前缀不要重复
    router.StaticFS("/static", http.Dir("static/static"))
    // 配置单个文件, 网页请求的路由,文件的路径
    router.StaticFile("/titian.png", "static/titian.png")
    
  • 重定向

    router.GET("/redirect", func(c *gin.Context) {
        //支持内部和外部的重定向
        c.Redirect(http.StatusMovedPermanently, "http://www.baidu.com/")
    })
    

    301 Moved Permanently

    被请求的资源已永久移动到新位置,并且将来任何对此资源的引用都应该使用本响应返回的若干个 URI 之一。如果可能,拥有链接编辑功能的客户端应当自动把请求的地址修改为从服务器反馈回来的地址。除非额外指定,否则这个响应也是可缓存的。

    302 Found

    请求的资源现在临时从不同的 URI 响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。只有在Cache-Control或Expires中进行了指定的情况下,这个响应才是可缓存的。

2.请求

  • 查询参数 Query

    func _query(c *gin.Context) {
      fmt.Println(c.Query("user"))
      fmt.Println(c.GetQuery("user"))
      fmt.Println(c.QueryArray("user")) // 拿到多个相同的查询参数
      fmt.Println(c.DefaultQuery("addr", "四川省"))
    }
    
  • 动态参数Param

    func _param(c *gin.Context) {
      fmt.Println(c.Param("user_id"))
      fmt.Println(c.Param("book_id"))
    }
    
    
    router.GET("/param/:user_id/", _param)	// ?param/12
    router.GET("/param/:user_id/:book_id", _param)	// ?param/12/123
    
  • 表单PostForm

    可以接收 multipart/form-data;application/x-www-form-urlencoded

    func _form(c *gin.Context) {
      fmt.Println(c.PostForm("name"))
      fmt.Println(c.PostFormArray("name"))
      fmt.Println(c.DefaultPostForm("addr", "四川省")) // 如果用户没传,就使用默认值
      forms, err := c.MultipartForm()               // 接收所有的form参数,包括文件
      fmt.Println(forms, err)
    }
    
  • 文件上传

    router.POST("users", func(c *gin.Context) {
        fileHeader, err := c.FormFile("file")
        if err != nil {
            fmt.Println(err)
            return
        }
        fmt.Println(fileHeader.Filename) //文件名
        fmt.Println(fileHeader.Size)     //文件大小,单位是字节
    
        file, _ := fileHeader.Open()
        byteData, _ := io.ReadAll(file)
    
        err = os.WriteFile("xxx.jpg", byteData, 0666)
        fmt.Println(err)
    })
    
    • 简便的上传方法 单文件

      router.POST("users", func(c *gin.Context) {
          fileHeader, err := c.FormFile("file")
          if err != nil {
              fmt.Println(err)
              return
          }
          fmt.Println(fileHeader.Filename) //文件名
          fmt.Println(fileHeader.Size)     //文件大小,单位是字节
      
          err = c.SaveUploadedFile(fileHeader, "uploads/xxx/yyy/"+fileHeader.Filename)
          fmt.Println(err)
      })
      
    • 多文件上传

      router.POST("users", func(c *gin.Context) {
          form, err := c.MultipartForm()
          if err != nil {
              fmt.Println(err)
              return
          }
          for _, headers := range form.File {
              for _, header := range headers {
                  c.SaveUploadedFile(header, "uploads/"+header.Filename)
              }
          }
      })
      
  • 原始内容读取

    router.POST("users", func(c *gin.Context) {
        byteData, _ := io.ReadAll(c.Request.Body)
        fmt.Println(string(byteData))
        //读了之后 body就没了,阅后即焚
        c.Request.Body = io.NopCloser(bytes.NewReader(byteData)) //解决方法
    })
    

3.bind参数绑定

gin中的bind可以很方便的将 前端传递 来的数据与 结构体 进行 参数绑定 ,以及参数校验。在使用这个功能的时候,需要给结构体加上Tag json form uri xml yaml。bind分为Must Bind(不常用,校验失败会改状态码,在此不过多介绍)和Should Bind可以绑定json,query,param,yaml,xml,如果校验不通过会返回错误。

首先我们先创建一个结构体用于将数据绑定上去

type UserInfo struct {
  Name string `json:"name" form:"name" uri:"name"`
  Age  int    `json:"age" form:"age" uri:"age"`
  Sex  string `json:"sex" form:"sex" uri:"sex"`
}

后续绑定数据均以此结构体为例

1.Should Bind

  • ShouldBindJSON

    router.POST("/", func(c *gin.Context) {
        var userInfo UserInfo
        err := c.ShouldBindJSON(&userInfo)
        if err != nil {
            c.JSON(200, gin.H{"msg": "你错了"})
            return
        }
        c.JSON(200, userInfo)
    })
    
  • ShouldBindQuery

    绑定查询参数, tag对应为form

    router.POST("/query", func(c *gin.Context) {
        var userInfo UserInfo
        err := c.ShouldBindQuery(&userInfo)
        if err != nil {
            c.JSON(200, gin.H{"msg": "你错了"})
            return
        }
        c.JSON(200, userInfo)
    })
    
  • ShouldBindUri

    绑定动态参数, tag对应为uri

    router.POST("/uri/:name/:age/:sex", func(c *gin.Context) {
        var userInfo UserInfo
        err := c.ShouldBindUri(&userInfo)
        if err != nil {
            fmt.Println(err)
            c.JSON(200, gin.H{"msg": "你错了"})
            return
        }
        c.JSON(200, userInfo)
    })
    
  • ShouldBind

    会根据请求头中的content-type去自动绑定,form-data的参数也用这个,tag用form,默认的tag就是form,不能解析x-www-form-urlencode

    router.POST("/form", func(c *gin.Context) {
        var userInfo UserInfo
        err := c.ShouldBind(&userInfo)
        if err != nil {
            fmt.Println(err)
            c.JSON(200, gin.H{"msg": "你错了"})
            return
        }
        c.JSON(200, userInfo)
    })
    
  • ShouldBindHeader

    router.POST("/form", func(c *gin.Context) {
        type User struct {
            Name string `header:"Name"`
            Age  int    `header:"Age"`
            UserAgent  string `header:"User-Agent"`
            ContentType  string `header:"Content-Type"`
        }
        var user User
        err := c.ShouldBindHeader(&user)
        if err != nil {
            fmt.Println(err)
            c.JSON(200, gin.H{"msg": "你错了"})
            return
        }
        c.JSON(200, user)
    })
    

2.bind绑定器

需要使用参数验证功能,需要加binding tag

  • 常用验证器

    // 不能为空,并且不能没有这个字段
    required: 必填字段,如:binding:"required"  
    
    // 针对字符串的长度
    min 最小长度,如:binding:"min=5"
    max 最大长度,如:binding:"max=10"
    len 长度,如:binding:"len=6"
    
    // 针对数字的大小
    eq 等于,如:binding:"eq=3"
    ne 不等于,如:binding:"ne=12"
    gt 大于,如:binding:"gt=10"
    gte 大于等于,如:binding:"gte=10"
    lt 小于,如:binding:"lt=10"
    lte 小于等于,如:binding:"lte=10"
    
    // 针对同级字段的
    eqfield 等于其他字段的值,如:PassWord string `binding:"eqfield=Password"`
    nefield 不等于其他字段的值
    
    
    - 忽略字段,如:binding:"-"
    
  • gin内置验证器

    // 枚举  只能是red 或green
    oneof=red green 
    
    // 字符串  
    contains=zhangsan  // 包含zhangsan的字符串
    excludes // 不包含
    startswith  // 字符串前缀
    endswith  // 字符串后缀
    
    // 数组
    dive  // dive后面的验证就是针对数组中的每一个元素
    
    // 网络验证
    ip
    ipv4
    ipv6
    uri
    url
    // uri 在于I(Identifier)是统一资源标示符,可以唯一标识一个资源。
    // url 在于Locater,是统一资源定位符,提供找到该资源的确切路径
    
    // 日期验证  1月2号下午3点4分5秒在2006年
    datetime=2006-01-02
    
  • 自定义验证的错误信息

    当验证不通过时,会给出错误的信息,但是原始的错误信息不太友好,不利于用户查看,只需要给结构体加一个msg 的tag

    type UserInfo struct {
      Username string `json:"username" binding:"required" msg:"用户名不能为空"`
      Password string `json:"password" binding:"min=3,max=6" msg:"密码长度不能小于3大于6"`
      Email    string `json:"email" binding:"email" msg:"邮箱地址格式不正确"`
    }
    

    当出现错误时,就可以来获取出错字段上的msg。

    • err:这个参数为ShouldBindJSON返回的错误信息
    • obj:这个参数为绑定的结构体
    • 还有一点要注意的是,validator这个包要引用v10这个版本的,否则会出错
    // GetValidMsg 返回结构体中的msg参数
    func GetValidMsg(err error, obj any) string {
      // 使用的时候,需要传obj的指针
      getObj := reflect.TypeOf(obj)
      // 将err接口断言为具体类型
      if errs, ok := err.(validator.ValidationErrors); ok {
        // 断言成功
        for _, e := range errs {
          // 循环每一个错误信息
          // 根据报错字段名,获取结构体的具体字段
          if f, exits := getObj.Elem().FieldByName(e.Field()); exits {
            msg := f.Tag.Get("msg")
            return msg
          }
        }
      }
    
      return err.Error()
    }
    

4.中间件

Gin中的中间件必须是一个gin.HandlerFunc类型

1.单个中间件

func indexHandler(c *gin.Context) {
  fmt.Println("index.....")
  c.JSON(http.StatusOK, gin.H{
    "msg": "index",
  })
}

//定义一个中间件
func m1(c *gin.Context) {
  fmt.Println("m1 in.........")
}
func main() {
  r := gin.Default()
  //m1处于indexHandler函数的前面,请求来之后,先走m1,再走index
  r.GET("/index", m1, indexHandler)

  _ = r.Run()
}

2.多个中间件

router.GET,后面可以跟很多HandlerFunc方法,这些方法其实都可以叫中间件

func m1(c *gin.Context) {
  fmt.Println("m1 ...in")
}
func m2(c *gin.Context) {
  fmt.Println("m2 ...in")
}

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

  router.GET("/", m1, func(c *gin.Context) {
    fmt.Println("index ...")
    c.JSON(200, gin.H{"msg": "响应数据"})
  }, m2)

  router.Run(":8080")
}

/*
m1  ...in
index ...
m2  ...in
*/

3.中间件拦截响应

c.Abort()拦截,后续的HandlerFunc就不会执行了

func m1(c *gin.Context) {
  fmt.Println("m1 ...in")
  c.JSON(200, gin.H{"msg": "第一个中间件拦截了"})
  c.Abort()
}
func m2(c *gin.Context) {
  fmt.Println("m2 ...in")
}

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

  router.GET("/", m1, func(c *gin.Context) {
    fmt.Println("index ...")
    c.JSON(200, gin.H{"msg": "响应数据"})
  }, m2)

  router.Run(":8080")
}

4.中间件放行

c.Next(),Next前后形成了其他语言中的请求中间件和响应中间件

func m1(c *gin.Context) {
  fmt.Println("m1 ...in")
  c.Next()
  fmt.Println("m1 ...out")
}
func m2(c *gin.Context) {
  fmt.Println("m2 ...in")
  c.Next()
  fmt.Println("m2 ...out")
}

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

  router.GET("/", m1, func(c *gin.Context) {
    fmt.Println("index ...in")
    c.JSON(200, gin.H{"msg": "响应数据"})
    c.Next()
    fmt.Println("index ...out")
  }, m2)

  router.Run(":8080")
}

/*
m1 ...in
index ...in
m2 ...in   
m2 ...out  
index ...out
m1 ...out
*/

gin.png

如果其中一个中间件响应了c.Abort(),后续中间件将不再执行,直接按照顺序走完所有的响应中间件

5.全局注册中间件

package main

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

func m10(c *gin.Context) {
  fmt.Println("m1 ...in")
  c.Next()
  fmt.Println("m1 ...out")
}

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

  router.Use(m10)
  router.GET("/", func(c *gin.Context) {
    fmt.Println("index ...in")
    c.JSON(200, gin.H{"msg": "index"})
    c.Next()
    fmt.Println("index ...out")
  })

  router.Run(":8080")

}

使用Use去注册全局中间件,Use接收的参数也是多个HandlerFunc

6.中间件传递数据

使用Set设置一个key-value,

在后续中间件中使用Get接收数据

func m10(c *gin.Context) {
  fmt.Println("m1 ...in")
  c.Set("name", "张三")
}

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

  router.Use(m10)
  router.GET("/", func(c *gin.Context) {
    fmt.Println("index ...in")
    name, _ := c.Get("name")
    fmt.Println(name)
    
    c.JSON(200, gin.H{"msg": "index"})
  })

  router.Run(":8080")

}

value的类型是any类型,所有我们可以用它传任意类型,在接收的时候做好断言即可

type User struct {
  Name string
  Age  int
}

func m10(c *gin.Context) {
  fmt.Println("m1 ...in")
  c.Set("name", User{"张三", 21})
  c.Next()
  fmt.Println("m1 ...out")
}

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

  router.Use(m10)
  router.GET("/", func(c *gin.Context) {
    fmt.Println("index ...in")
    name, _ := c.Get("name")
    user := name.(User)
    fmt.Println(user.Name, user.Age)
    c.JSON(200, gin.H{"msg": "index"})
  })
  router.Run(":8080")
}

中间件案例

权限验证

以前后端最流行的jwt为例,如果用户登录了,前端发来的每一次请求都会在请求头上携带上token

后台拿到这个token进行校验,验证是否过期,是否非法

如果通过就说明这个用户是登录过的

不通过就说明用户没有登录

package main

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

func JwtTokenMiddleware(c *gin.Context) {
  // 获取请求头的token
  token := c.GetHeader("token")
  // 调用jwt的验证函数
  if token == "1234" {
    // 验证通过
    c.Next()
    return
  }
  // 验证不通过
  c.JSON(200, gin.H{"msg": "权限验证失败"})
  c.Abort()
}

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

  api := router.Group("/api")

  apiUser := api.Group("")
  {
    apiUser.POST("login", func(c *gin.Context) {
      c.JSON(200, gin.H{"msg": "登录成功"})
    })
  }
  apiHome := api.Group("system").Use(JwtTokenMiddleware)
  {
    apiHome.GET("/index", func(c *gin.Context) {
      c.String(200, "index")
    })
    apiHome.GET("/home", func(c *gin.Context) {
      c.String(200, "home")
    })
  }

  router.Run(":8080")
}

耗时统计

统计每一个视图函数的执行时间

func TimeMiddleware(c *gin.Context) {
  startTime := time.Now()
  c.Next()
  since := time.Since(startTime)
  // 获取当前请求所对应的函数
  f := c.HandlerName()
  fmt.Printf("函数 %s 耗时 %d\n", f, since)
}

5.路由分组

将一系列的路由放到一个组下,统一管理

例如,以下的路由前面统一加上api的前缀

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

  r := router.Group("/api")
  r.GET("/index", func(c *gin.Context) {
    c.String(200, "index")
  })
  r.GET("/home", func(c *gin.Context) {
    c.String(200, "home")
  })

  router.Run(":8080")
}

路由分组注册中间件

func middle(c *gin.Context) {
  fmt.Println("middle ...in")
}

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

  r := router.Group("/api").Use(middle)  // 可以链式,也可以直接r.Use(middle)
  r.GET("/index", func(c *gin.Context) {
    c.String(200, "index")
  })
  r.GET("/home", func(c *gin.Context) {
    c.String(200, "home")
  })

  router.Run(":8080")
}

这样写我们就可以指定哪一些分组下可以使用中间件了

当然,中间件还有一种写法,就是使用函数加括号的形式

func middle(c *gin.Context) {
  fmt.Println("middle ...in")
}
func middle1() gin.HandlerFunc {
  // 这里的代码是程序一开始就会执行
  return func(c *gin.Context) {
    // 这里是请求来了才会执行
    fmt.Println("middle1 ...inin")
  }
}

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

  r := router.Group("/api").Use(middle, middle1())
  r.GET("/index", func(c *gin.Context) {
    c.String(200, "index")
  })
  r.GET("/home", func(c *gin.Context) {
    c.String(200, "home")
  })

  router.Run(":8080")
}

gin.Default

func Default() *Engine {
  debugPrintWARNINGDefault()
  engine := New()
  engine.Use(Logger(), Recovery())
  return engine
}

gin.Default()默认使用了Logger和Recovery中间件,其中:

Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release。 Recovery中间件会recover任何panic。如果有panic的话,会写入500响应码。 如果不想使用上面两个默认的中间件,可以使用gin.New()新建一个没有任何默认中间件的路由。

使用gin.New,如果不指定日志,那么在控制台中就不会有日志显示