Gin框架从入门到上手学习指南

推荐一个Golang的学习站:Go中文学习文档 (halfiisland.com)

Gin

官方文档:Gin Web Framework (gin-gonic.com)

仓库地址:gin-gonic/gin: Gin is a HTTP web framework written in Go (Golang)

官方示例:gin-gonic/examples: A repository to host examples and tutorials for Gin. (github.com)

介绍

Gin 是一个用 Go (Golang) 编写的 Web 框架。 它具有类似 martini 的 API,性能要好得多,多亏了 httprouter,速度提高了 40 倍。 如果您需要性能和良好的生产力,您一定会喜欢 Gin。Gin相比于Iris和Beego而言,更倾向于轻量化的框架,只负责Web部分,追求极致的路由性能,功能或许没那么全,胜在轻量易拓展,这也是它的优点。因此,在所有的Web框架中,Gin是最容易上手和学习的。

Gin是一个Web框架,并非MVC框架,MVC的功能需要开发者自行实现。这里推荐一个很优秀的GinServer端项目: gin-vue-admin | GVA 文档站,里面的项目结构,代码,路由等都很值得学习。

特性

  • 快速:基于 Radix 树的路由,小内存占用。没有反射。可预测的 API 性能。

  • 支持中间件:传入的 HTTP 请求可以由一系列中间件和最终操作来处理。 例如:Logger,Authorization,GZIP,最终操作 DB。

  • Crash 处理:Gin 可以 catch 一个发生在 HTTP 请求中的 panic 并 recover 它。这样,你的服务器将始终可用。

  • JSON 验证:Gin 可以解析并验证请求的 JSON,例如检查所需值的存在。

  • 路由组:更好地组织路由。是否需要授权,不同的 API 版本…… 此外,这些组可以无限制地嵌套而不会降低性能。

  • 错误管理:Gin 提供了一种方便的方法来收集 HTTP 请求期间发生的所有错误。最终,中间件可以将它们写入日志文件,数据库并通过网络发送。

  • 内置渲染:Gin 为 JSON,XML 和 HTML 渲染提供了易于使用的 API。

  • 可扩展性:新建一个中间件非常简单

安装

截止目前2022/11/22,gin支持的go最低版本为1.16,建议使用go mod来管理项目依赖。

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

导入

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

快速开始

package main

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

func main() {
   engine := gin.Default() //创建gin引擎
   engine.GET("/ping", func(context *gin.Context) {
      context.JSON(http.StatusOK, gin.H{
         "message": "pong",
      })
   })
   engine.Run() //开启服务器,默认监听localhost:8080
}

请求URL

GET localhost:8080/ping

返回

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 22 Nov 2022 08:47:11 GMT
Content-Length: 18

{
  "message": "pong"
}
Response file saved.
> 2022-11-22T164711.200.json

教程

其实Gin官方文档里面并没有多少教程,大多数只是一些介绍和基本使用和一些例子,但是gin-gonic/ 组织下,有一个gin-gonic/examples仓库,这是一个由社区共同维护的gin示例仓库。都是全英文,更新时间并不是特别频繁,笔者也是从这里慢慢学习的gin框架。

示例仓库地址:gin-gonic/examples: A repository to host examples and tutorials for Gin. (github.com)

开始之前建议可以阅读一下HttpRouter的简单教程: HttpRouter | Go中文学习文档 (halfiisland.com)

参数解析

gin中的参数解析总共支持三种方式:路由参数,URL参数,表单参数,下面逐一讲解并结合代码示例,比较简单易懂。

路由参数

路由参数其实是封装了HttpRouter的参数解析功能,使用方法基本上与HttpRouter一致。

package main

import (
   "github.com/gin-gonic/gin"
   "log"
   "net/http"
)

func main() {
   e := gin.Default()
   e.GET("/findUser/:username/:userid", FindUser)
   e.GET("/downloadFile/*filepath", UserPage)

   log.Fatalln(e.Run(":8080"))
}

// 命名参数示例
func FindUser(c *gin.Context) {
   username := c.Param("username")
   userid := c.Param("userid")
   c.String(http.StatusOK, "username is %s\n userid is %s", username, userid)
}

// 路径参数示例
func UserPage(c *gin.Context) {
   filepath := c.Param("filepath")
   c.String(http.StatusOK, "filepath is  %s", filepath)
}

示例一

curl --location --request GET '127.0.0.1:8080/findUser/jack/001'
username is jack
 userid is 001

示例二

curl --location --request GET '127.0.0.1:8080/downloadFile/img/fruit.png'
filepath is  /img/fruit.png

URL参数

传统的URL参数,格式就是/url?key=val&key1=val1&key2=val2。

package main

import (
   "github.com/gin-gonic/gin"
   "log"
   "net/http"
)

func main() {
   e := gin.Default()
   e.GET("/findUser", FindUser)
   log.Fatalln(e.Run(":8080"))
}

func FindUser(c *gin.Context) {
   username := c.DefaultQuery("username", "defaultUser")
   userid := c.Query("userid")
   c.String(http.StatusOK, "username is %s\nuserid is %s", username, userid)
}

示例一

curl --location --request GET '127.0.0.1:8080/findUser?username=jack&userid=001'
username is jack
userid is 001

示例二

curl --location --request GET '127.0.0.1:8080/findUser'
username is defaultUser
userid is 

表单参数

表单的内容类型一般有application/json,application/x-www-form-urlencoded,application/xml,multipart/form-data。

package main

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

func main() {
	e := gin.Default()
	e.POST("/register", RegisterUser)
	e.POST("/update", UpdateUser)
	e.Run(":8080")
}

func RegisterUser(c *gin.Context) {
	username := c.PostForm("username")
	password := c.PostForm("password")
	c.String(http.StatusOK, "successfully registered,your username is [%s],password is [%s]", username, password)
}

func UpdateUser(c *gin.Context) {
	var form map[string]string
	c.ShouldBind(&form)
	c.String(http.StatusOK, "successfully update,your username is [%s],password is [%s]", form["username"], form["password"])
}

示例一:使用form-data

curl --location --request POST '127.0.0.1:8080/register' \
--form 'username="jack"' \
--form 'password="123456"'
successfully registered,your username is [jack],password is [123456]

PostForm方法默认解析application/x-www-form-urlencoded和multipart/form-data类型的表单。

示例二:使用json

curl --location --request POST '127.0.0.1:8080/update' \
--header 'Content-Type: application/json' \
--data-raw '{
    "username":"username",
    "password":"123456"
}'
successfully update,your username is [username],password is [123456]

数据解析

在大多数情况下,我们都会使用结构体来承载数据,而不是直接解析参数。在gin中,用于数据绑定的方法主要是Bind()和ShouldBind(),两者的区别在于前者内部也是直接调用的ShouldBind(),当然返回err时,会直接进行400响应,后者则不会。如果想要更加灵活的进行错误处理,建议选择后者。这两个函数会自动根据请求的content-type来进行推断用什么方式解析。

func (c *Context) MustBindWith(obj any, b binding.Binding) error {
    // 调用了ShouldBindWith()
	if err := c.ShouldBindWith(obj, b); err != nil {
		c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // 直接响应400 badrequest
		return err
	}
	return nil
}

如果想要自行选择可以使用BindWith()和ShouldBindWith(),例如

c.MustBindWith(obj, binding.JSON) //json
c.MustBindWith(obj, binding.XML) //xml

gin支持的绑定类型有如下几种实现:

var (
   JSON          = jsonBinding{}
   XML           = xmlBinding{}
   Form          = formBinding{}
   Query         = queryBinding{}
   FormPost      = formPostBinding{}
   FormMultipart = formMultipartBinding{}
   ProtoBuf      = protobufBinding{}
   MsgPack       = msgpackBinding{}
   YAML          = yamlBinding{}
   Uri           = uriBinding{}
   Header        = headerBinding{}
   TOML          = tomlBinding{}
)

示例

package main

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

type LoginUser struct {
	Username string `bind:"required" json:"username" form:"username" uri:"username"`
	Password string `bind:"required" json:"password" form:"password" uri:"password"`
}

func main() {
    e := gin.Default()
	e.POST("/loginWithJSON", Login)
	e.POST("/loginWithForm", Login)
	e.GET("/loginWithQuery/:username/:password", Login)
	e.Run(":8080")
}

func Login(c *gin.Context) {
	var login LoginUser
    // 使用ShouldBind来让gin自动推断
	if c.ShouldBind(&login) == nil && login.Password != "" && login.Username != "" {
		c.String(http.StatusOK, "login successfully !")
	} else {
		c.String(http.StatusBadRequest, "login failed !")
	}
	fmt.Println(login)
}

Json数据绑定

curl --location --request POST '127.0.0.1:8080/loginWithJSON' \
--header 'Content-Type: application/json' \
--data-raw '{
    "username":"root",
    "password":"root"
}'
login successfully !

表单数据绑定

curl --location --request POST '127.0.0.1:8080/loginWithForm' \
--form 'username="root"' \
--form 'password="root"'
login successfully !

URL数据绑定

curl --location --request GET '127.0.0.1:8080/loginWithQuery/root/root'
login failed !

到了这里就会发生错误了,因为这里输出的content-type是空字符串,无法推断到底是要如何进行数据解析。所以当使用URL参数时,我们应该手动指定解析方式,例如:

if err := c.ShouldBindUri(&login); err == nil && login.Password != "" && login.Username != "" {
   c.String(http.StatusOK, "login successfully !")
} else {
   fmt.Println(err)
   c.String(http.StatusBadRequest, "login failed !")
}

多次绑定

一般方法都是通过调用 c.Request.Body 方法绑定数据,但不能多次调用这个方法,例如c.ShouldBind,不可重用,如果想要多次绑定的话,可以使用

c.ShouldBindBodyWith。

func SomeHandler(c *gin.Context) {
  objA := formA{}
  objB := formB{}
  // 读取 c.Request.Body 并将结果存入上下文。
  if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {
    c.String(http.StatusOK, `the body should be formA`)
  // 这时, 复用存储在上下文中的 body。
  }
  if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
    c.String(http.StatusOK, `the body should be formB JSON`)
  // 可以接受其他格式
  }
  if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil {
    c.String(http.StatusOK, `the body should be formB XML`)
  } 
}
c.ShouldBindBodyWith 会在绑定之前将 body 存储到上下文中。 这会对性能造成轻微影响,如果调用一次就能完成绑定的话,那就不要用这个方法。只有某些格式需要此功能,如 JSON, XML, MsgPack, ProtoBuf。 对于其他格式, 如 Query, Form, FormPost, FormMultipart 可以多次调用c.ShouldBind() 而不会造成任何性能损失 。

数据校验

gin内置的校验工具其实是github.com/go-playground/validator/v10,使用方法也几乎没有什么差别,前往Validator教程

简单示例

type LoginUser struct {
   Username string `binding:"required"  json:"username" form:"username" uri:"username"`
   Password string `binding:"required" json:"password" form:"password" uri:"password"`
}

func main() {
   e := gin.Default()
   e.POST("/register", Register)
   log.Fatalln(e.Run(":8080"))
}

func Register(ctx *gin.Context) {
   newUser := &LoginUser{}
   if err := ctx.ShouldBind(newUser); err == nil {
      ctx.String(http.StatusOK, "user%+v", *newUser)
   } else {
      ctx.String(http.StatusBadRequest, "invalid user,%v", err)
   }
}

测试

curl --location --request POST 'http://localhost:8080/register' \
--header 'Content-Type: application/json' \
--data-raw '{
    "username":"jack1"

}'

输出

invalid user,Key: 'LoginUser.Password' Error:Field validation for 'Password' failed on the 'required' tag

::: tip

需要注意的一点是,gin中validator的校验tag是binding,而单独使用validator的的校验tag是validator

:::

数据响应

数据响应是接口处理中最后一步要做的事情,后端将所有数据处理完成后,通过HTTP协议返回给调用者,gin对于数据响应提供了丰富的内置支持,用法简洁明了,上手十分容易。

简单示例

func Hello(c *gin.Context) {
    // 返回纯字符串格式的数据,http.StatusOK代表着200状态码,数据为"Hello world !"
	c.String(http.StatusOK, "Hello world !")
}

HTML渲染

文件加载的时候,默认根路径是项目路径,也就是go.mod文件所在的路径,下面例子中的index.html即位于根路径下的index.html,不过一般情况下这些模板文件都不会放在根路径,而是会存放在静态资源文件夹中
func main() {
   e := gin.Default()
    // 加载HTML文件,也可以使用Engine.LoadHTMLGlob()
   e.LoadHTMLFiles("index.html")
   e.GET("/", Index)
   log.Fatalln(e.Run(":8080"))
}

func Index(c *gin.Context) {
   c.HTML(http.StatusOK, "index.html", gin.H{})
}

测试

curl --location --request GET 'http://localhost:8080/'

返回





	
	GinLearn



	

Hello World!

This is a HTML Template Render Example

快速响应

前面经常用到context.String()方法来进行数据响应,这是最原始的响应方法,直接返回一个字符串,gin中其实还内置了许多了快速响应的方法例如:

// 使用Render写入响应头,并进行数据渲染
func (c *Context) Render(code int, r render.Render)

// 渲染一个HTML模板,name是html路径,obj是内容
func (c *Context) HTML(code int, name string, obj any)

// 以美化了的缩进JSON字符串进行数据渲染,通常不建议使用这个方法,因为会造成更多的传输消耗。
func (c *Context) IndentedJSON(code int, obj any) 

// 安全的JSON,可以防止JSON劫持,详情了解:https://www.cnblogs.com/xusion/articles/3107788.html
func (c *Context) SecureJSON(code int, obj any) 

// JSONP方式进行渲染
func (c *Context) JSONP(code int, obj any) 

// JSON方式进行渲染
func (c *Context) JSON(code int, obj any) 

// JSON方式进行渲染,会将unicode码转换为ASCII码
func (c *Context) AsciiJSON(code int, obj any) 

// JSON方式进行渲染,不会对HTML特殊字符串进行转义
func (c *Context) PureJSON(code int, obj any) 

// XML方式进行渲染
func (c *Context) XML(code int, obj any) 

// YML方式进行渲染
func (c *Context) YAML(code int, obj any) 

// TOML方式进行渲染
func (c *Context) TOML(code int, obj interface{}) 

// ProtoBuf方式进行渲染
func (c *Context) ProtoBuf(code int, obj any) 

// String方式进行渲染
func (c *Context) String(code int, format string, values ...any) 

// 重定向到特定的位置
func (c *Context) Redirect(code int, location string) 

// 将data写入响应流中
func (c *Context) Data(code int, contentType string, data []byte) 

// 通过reader读取流并写入响应流中
func (c *Context) DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string) 

// 高效的将文件写入响应流
func (c *Context) File(filepath string) 

// 以一种高效的方式将fs中的文件流写入响应流
func (c *Context) FileFromFS(filepath string, fs http.FileSystem) 

// 以一种高效的方式将fs中的文件流写入响应流,并且在客户端会以指定的文件名进行下载
func (c *Context) FileAttachment(filepath, filename string) 

// 将服务端推送流写入响应流中
func (c *Context) SSEvent(name string, message any) 

// 发送一个流响应并返回一个布尔值,以此来判断客户端是否在流中间断开
func (c *Context) Stream(step func(w io.Writer) bool) bool

对于大多数应用而言,用的最多的还是context.JSON,其他的相对而言要少一些,这里就不举例子演示了,因为都比较简单易懂,差不多都是直接调用的事情。

异步处理

在gin中,异步处理需要结合goroutine使用,使用起来十分简单。

// copy返回一个当前Context的副本以便在当前Context作用范围外安全的使用,可以用于传递给一个goroutine
func (c *Context) Copy() *Context
func main() {
	e := gin.Default()
	e.GET("/hello", Hello)
	log.Fatalln(e.Run(":8080"))
}

func Hello(c *gin.Context) {
	ctx := c.Copy()
	go func() {
		// 子协程应该使用Context的副本,不应该使用原始Context
		log.Println("异步处理函数: ", ctx.HandlerNames())
	}()
	log.Println("接口处理函数: ", c.HandlerNames())
	c.String(http.StatusOK, "hello")
}

测试

curl --location --request GET 'http://localhost:8080/hello'

输出

2022/12/21 13:33:47 异步处理函数:  []
2022/12/21 13:33:47 接口处理函数:  [github.com/gin-gonic/gin.LoggerWithConfig.func1 github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1 main.Hello]
[GIN] 2022/12/21 - 13:33:47 | 200 |     11.1927ms |             ::1 | GET      "/hello"

可以看到两者输出不同,副本在复制时,为了安全考虑,删掉了许多元素的值。

文件传输

文件传输是Web应用的一个不可或缺的功能,gin对于此的支持也是封装的十分简单,但其实本质上和用原生的net/http的流程都差不多。流程都是从请求体中读取文件流,然后再保存到本地。

单文件上传

func main() {
	e := gin.Default()
	e.POST("/upload", uploadFile)
	log.Fatalln(e.Run(":8080"))
}

func uploadFile(ctx *gin.Context) {
	// 获取文件
	file, err := ctx.FormFile("file")
	if err != nil {
		ctx.String(http.StatusBadRequest, "%+v", err)
		return
	}
	// 保存在本地
	err = ctx.SaveUploadedFile(file, "./"+file.Filename)
	if err != nil {
		ctx.String(http.StatusBadRequest, "%+v", err)
		return
	}
	// 返回结果
	ctx.String(http.StatusOK, "upload %s size:%d byte successfully!", file.Filename, file.Size)
}

测试

curl --location --request POST 'http://localhost:8080/upload' \
--form 'file=@"/C:/Users/user/Pictures/Camera Roll/a.jpg"'

结果

upload a.jpg size:1424 byte successfully!
一般情况下,上传文件的Method都会指定用POST,一些公司可能会倾向于使用PUT,前者是简单HTTP请求,后者是复杂HTTP请求,具体区别不作赘述,如果使用后者的话,尤其是前后端分离的项目时,需要进行相应的跨域处理,而Gin默认的配置是不支持跨域的 跨域配置。

多文件上传

func main() {
   e := gin.Default()
   e.POST("/upload", uploadFile)
   e.POST("/uploadFiles", uploadFiles)
   log.Fatalln(e.Run(":8080"))
}

func uploadFiles(ctx *gin.Context) {
	// 获取gin解析好的multipart表单
	form, _ := ctx.MultipartForm()
	// 根据键值取得对应的文件列表
	files := form.File["files"]
	// 遍历文件列表,保存到本地
	for _, file := range files {
		err := ctx.SaveUploadedFile(file, "./"+file.Filename)
		if err != nil {
			ctx.String(http.StatusBadRequest, "upload failed")
			return
		}
	}
	// 返回结果
	ctx.String(http.StatusOK, "upload %d files successfully!", len(files))
}

测试

curl --location --request POST 'http://localhost:8080/uploadFiles' \
--form 'files=@"/C:/Users/Stranger/Pictures/Camera Roll/a.jpg"' \
--form 'files=@"/C:/Users/Stranger/Pictures/Camera Roll/123.jpg"' \
--form 'files=@"/C:/Users/Stranger/Pictures/Camera Roll/girl.jpg"'

输出

upload 3 files successfully!

文件下载

关于文件下载的部分Gin对于原有标准库的API再一次封装,使得文件下载异常简单。

func main() {
	e := gin.Default()
	e.POST("/upload", uploadFile)
	e.POST("/uploadFiles", uploadFiles)
	e.GET("/download/:filename", download)
	log.Fatalln(e.Run(":8080"))
}

func download(ctx *gin.Context) {
    // 获取文件名
	filename := ctx.Param("filename")
    // 返回对应文件
	ctx.FileAttachment(filename, filename)
}

测试

curl --location --request GET 'http://localhost:8080/download/a.jpg'

结果

Content-Disposition: attachment; filename="a.jpg"
Date: Wed, 21 Dec 2022 08:04:17 GMT
Last-Modified: Wed, 21 Dec 2022 07:50:44 GMT

是不是觉得简单过头了,不妨不用框架的方法,自行编写一遍过程

func download(ctx *gin.Context) {
   // 获取参数
   filename := ctx.Param("filename")

   // 请求响应对象和请求对象
   response, request := ctx.Writer, ctx.Request
   // 写入响应头
   // response.Header().Set("Content-Type", "application/octet-stream") 以二进制流传输文件
   response.Header().Set("Content-Disposition", `attachment; filename*=UTF-8''`+url.QueryEscape(filename)) // 对文件名进行安全转义
   response.Header().Set("Content-Transfer-Encoding", "binary")                                            // 传输编码
   http.ServeFile(response, request, filename)
}

其实net/http也已经封装的足够好了

一般情况下,上传文件的Method都会指定用POST,一些公司可能会倾向于使用PUT,前者是简单HTTP请求,后者是复杂HTTP请求,具体区别不作赘述,如果使用后者的话,尤其是前后端分离的项目时,需要进行相应的跨域处理,而Gin默认的配置是不支持跨域的 跨域配置。

路由管理

路由管理是一个系统中非常重要的部分,需要确保每一个请求都能被正确的映射到对应的函数上。

路由组

创建一个路由组是将接口分类,不同类别的接口对应不同的功能,也更易于管理。

func Hello(c *gin.Context) {

}

func Login(c *gin.Context) {

}

func Update(c *gin.Context) {

}

func Delete(c *gin.Context) {

}

假设我们有以上四个接口,暂时不管其内部实现,Hello,Login是一组,Update,Delete是一组。

func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup

在创建分组的时候,我们也可以给分组的根路由注册处理器,不过大多数时候并不会这么做。

func main() {
	e := gin.Default()
	v1 := e.Group("v1")
	{
		v1.GET("/hello", Hello)
		v1.GET("/login", Login)
	}
	v2 := e.Group("v2")
	{
		v2.POST("/update", Update)
		v2.DELETE("/delete", Delete)
	}
}

我们将其分成了v1,v2两个分组,其中的花括号{}仅仅只是为了规范,表名花括号内注册的处理器是属于同一个路由分组,在功能上没有任何作用。同样的,gin也支持嵌套分组,方法与上例一致,这里就不再演示。

404路由

gin 中的Engine结构体提供了一个方法NoRoute,来设置当访问的URL不存在时如何处理,开发者可以将逻辑写入此方法中,以便路由未找到时自动调用,默认会返回404状态码

func (engine *Engine) NoRoute(handlers ...HandlerFunc)

我们拿上个例子举例

func main() {
   e := gin.Default()
   v1 := e.Group("v1")
   {
      v1.GET("/hello", Hello)
      v1.GET("/login", Login)
   }
   v2 := e.Group("v2")
   {
      v2.POST("/update", Update)
      v2.DELETE("/delete", Delete)
   }
   // 注册处理器
   e.NoRoute(func(context *gin.Context) { // 这里只是演示,不要在生产环境中直接返回HTML代码
      context.String(http.StatusNotFound, "

404 Page Not Found

") }) log.Fatalln(e.Run(":8080")) }

随便发一个请求

curl --location --request GET 'http://localhost:8080/'

404 Page Not Found

405路由

Http状态码中,405代表着当前请求的方法类型是不允许的,gin中提供了如下方法

func (engine *Engine) NoMethod(handlers ...HandlerFunc)

来注册一个处理器,以便在发生时自动调用,前提是设置Engine.HandleMethodNotAllowed = true。

func main() {
   e := gin.Default()
   // 需要将其设置为true
   e.HandleMethodNotAllowed = true
   v1 := e.Group("/v1")
   {
      v1.GET("/hello", Hello)
      v1.GET("/login", Login)
   }
   v2 := e.Group("/v2")
   {
      v2.POST("/update", Update)
      v2.DELETE("/delete", Delete)
   }
   e.NoRoute(func(context *gin.Context) {
      context.String(http.StatusNotFound, "

404 Page Not Found

") }) // 注册处理器 e.NoMethod(func(context *gin.Context) { context.String(http.StatusMethodNotAllowed, "method not allowed") }) log.Fatalln(e.Run(":8080")) }

配置好后,gin默认的header是不支持OPTION请求的,测试一下

curl --location --request OPTIONS 'http://localhost:8080/v2/delete'
method not allowed

至此配置成功

重定向

gin中的重定向十分简单,调用gin.Context.Redirect()方法即可。

func main() {
	e := gin.Default()
	e.GET("/", Index)
	e.GET("/hello", Hello)
	log.Fatalln(e.Run(":8080"))
}

func Index(c *gin.Context) {
	c.Redirect(http.StatusMovedPermanently, "/hello")
}

func Hello(c *gin.Context) {
	c.String(http.StatusOK, "hello")
}

测试

curl --location --request GET 'http://localhost:8080/'

输出

hello

中间件

gin十分轻便灵活,拓展性非常高,对于中间件的支持也非常友好。在Gin中,所有的接口请求都要经过中间件,通过中间件,开发者可以自定义实现很多功能和逻辑,gin虽然本身自带的功能很少,但是由第三方社区开发的gin拓展中间件十分丰富。

中间件本质上其实还是一个接口处理器

// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)

从某种意义上来说,每一个请求对应的处理器也是中间件,只不过是作用范围非常小的局部中间件。

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

查看gin的源代码,Default函数中,返回的默认Engine就使用两个默认中间件Logger(),Recovery(),如果不想使用默认的中间件也可以使用gin.New()来代替。

全局中间件

全局中间件即作用范围为全局,整个系统所有的请求都会经过此中间件。

func GlobalMiddleware() gin.HandlerFunc {
   return func(ctx *gin.Context) {
      fmt.Println("全局中间件被执行...")
   }
}

先创建一个闭包函数来创建中间件,再通过Engine.Use()来注册全局中间件。

func main() {
   e := gin.Default()
   // 注册全局中间件
   e.Use(GlobalMiddleware())
   v1 := e.Group("/v1")
   {
      v1.GET("/hello", Hello)
      v1.GET("/login", Login)
   }
   v2 := e.Group("/v2")
   {
      v2.POST("/update", Update)
      v2.DELETE("/delete", Delete)
   }
   log.Fatalln(e.Run(":8080"))
}

测试

curl --location --request GET 'http://localhost:8080/v1/hello'

输出

[GIN-debug] Listening and serving HTTP on :8080
全局中间件被执行...
[GIN] 2022/12/21 - 11:57:52 | 200 |       538.9µs |             ::1 | GET      "/v1/hello"

局部中间件

局部中间件即作用范围为局部,系统中局部的请求会经过此中间件。局部中间件可以注册到单个路由上,不过更多时候是注册到路由组上。

func main() {
   e := gin.Default()
   // 注册全局中间件
   e.Use(GlobalMiddleware())
   // 注册路由组局部中间件
   v1 := e.Group("/v1", LocalMiddleware())
   {
      v1.GET("/hello", Hello)
      v1.GET("/login", Login)
   }
   v2 := e.Group("/v2")
   {
      // 注册单个路由局部中间件
      v2.POST("/update", LocalMiddleware(), Update)
      v2.DELETE("/delete", Delete)
   }
   log.Fatalln(e.Run(":8080"))
}

测试

curl --location --request POST 'http://localhost:8080/v2/update'

输出

全局中间件被执行...
局部中间件被执行
[GIN] 2022/12/21 - 12:05:03 | 200 |       999.9µs |             ::1 | POST     "/v2/update"

中间件原理

Gin中间的使用和自定义非常容易,其内部的原理也比较简单,为了后续的学习,需要简单的了解下内部原理。Gin中的中间件其实用到了责任链模式,Context中维护着一个HandlersChain,本质上是一个[]HandlerFunc,和一个index,其数据类型为int8。在Engine.handlerHTTPRequest(c *Context)方法中,有一段代码表明了调用过程:gin在路由树中找到了对应的路由后,便调用了Next()方法。

if value.handlers != nil {
   // 将调用链赋值给Context
   c.handlers = value.handlers
   c.fullPath = value.fullPath
   // 调用中间件
   c.Next()
   c.writermem.WriteHeaderNow()
   return
}

Next()的调用才是关键,Next()会遍历路由的handlers中的HandlerFunc 并执行,此时可以看到index的作用就是记录中间件的调用位置。其中,给对应路由注册的接口函数也在handlers内,这也就是为什么前面会说接口也是一个中间件。

func (c *Context) Next() {
   // 一进来就+1是为了避免陷入递归死循环,默认值是-1
   c.index++
   for c.index < int8(len(c.handlers)) {
      // 执行HandlerFunc
      c.handlers[c.index](c)
      // 执行完毕,index+1
      c.index++
   }
}

修改一下Hello()的逻辑,来验证是否果真如此

func Hello(c *gin.Context) {
   fmt.Println(c.HandlerNames())
}

输出结果为

[github.com/gin-gonic/gin.LoggerWithConfig.func1 github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1 main.GlobalMiddleware.func1 main.LocalMiddleware.func1 main.Hello]

可以看到中间件调用链的顺序为:Logger -> Recovery -> GlobalMiddleware -> LocalMiddleWare -> Hello,调用链的最后一个元素才是真正要执行的接口函数,前面的都是中间件。

::: tip

在注册局部路由时,有如下一个断言

finalSize := len(group.Handlers) + len(handlers) //中间件总数
assert1(finalSize < int(abortIndex), "too many handlers")

其中abortIndex int8 = math.MaxInt8 >> 1值为63,即使用系统时路由注册数量不要超过63个。

:::

计时器中间件

在知晓了上述的中间件原理后,就可以编写一个简单的请求时间统计中间件。

func TimeMiddleware() gin.HandlerFunc {
   return func(context *gin.Context) {
      // 记录开始时间
      start := time.Now()
      // 执行后续调用链
      context.Next()
      // 计算时间间隔
      duration := time.Since(start)
      // 输出纳秒,以便观测结果
      fmt.Println("请求用时: ", duration.Nanoseconds())
   }
}

func main() {
	e := gin.Default()
	// 注册全局中间件,计时中间件
	e.Use(GlobalMiddleware(), TimeMiddleware())
	// 注册路由组局部中间件
	v1 := e.Group("/v1", LocalMiddleware())
	{
		v1.GET("/hello", Hello)
		v1.GET("/login", Login)
	}
	v2 := e.Group("/v2")
	{
		// 注册单个路由局部中间件
		v2.POST("/update", LocalMiddleware(), Update)
		v2.DELETE("/delete", Delete)
	}
	log.Fatalln(e.Run(":8080"))
}

测试

curl --location --request GET 'http://localhost:8080/v1/hello'

输出

请求用时:  517600

一个简单的计时器中间件就已经编写完毕了,后续可以凭借自己的摸索编写一些功能更实用的中间件。

服务配置

光是使用默认的配置是远远不够的,大多数情况下都需求修改很多的服务配置才能达到需求。

Http配置

可以通过net/http创建Server来配置,Gin本身也支持像原生API一样使用Gin。

func main() {
   router := gin.Default()
   server := &http.Server{
      Addr:           ":8080",
      Handler:        router,
      ReadTimeout:    10 * time.Second,
      WriteTimeout:   10 * time.Second,
      MaxHeaderBytes: 1 << 20,
   }
   log.Fatal(server.ListenAndServe())
}

静态资源配置

静态资源在以往基本上是服务端不可或缺的一部分,尽管在现在使用占比正在逐渐减少,但仍旧有大量的系统还是使用单体架构的情况。

Gin提供了三个方法来加载静态资源

// 加载某一静态文件夹 
func (group *RouterGroup) Static(relativePath, root string) IRoutes

// 加载某一个fs
func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes

// 加载某一个静态文件
func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes
relativePath是映射到网页URL上的相对路径,root是文件在项目中的实际路径

假设项目的目录如下

root
|
|-- static
|	|
|	|-- a.jpg
|	|
|	|-- favicon.ico
|
|-- view
	|
	|-- html
func main() {
   router := gin.Default()
   // 加载静态文件目录
   router.Static("/static", "./static")
   // 加载静态文件目录
   router.StaticFS("/view", http.Dir("view"))
   // 加载静态文件
   router.StaticFile("/favicon", "./static/favicon.ico")

   router.Run(":8080")
}

跨域配置

Gin本身是没有对于跨域配置做出任何处理,需要自行编写中间件来进行实现相应的需求,其实难度也不大,稍微熟悉HTTP协议的人一般都能写出来,逻辑基本上都是那一套。

func CorsMiddle() gin.HandlerFunc {
   return func(c *gin.Context) {
      method := c.Request.Method
      origin := c.Request.Header.Get("Origin")
      if origin != "" {
         // 生产环境中的服务端通常都不会填 *,应当填写指定域名
         c.Header("Access-Control-Allow-Origin", origin)
         // 允许使用的HTTP METHOD
         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")
         // 允许客户端访问的响应头
         c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
         // 是否需要携带认证信息 Credentials 可以是 cookies、authorization headers 或 TLS client certificates 
         // 设置为true时,Access-Control-Allow-Origin不能为 *
         c.Header("Access-Control-Allow-Credentials", "true")
      }
      // 放行OPTION请求,但不执行后续方法
      if method == "OPTIONS" {
         c.AbortWithStatus(http.StatusNoContent)
      }
      // 放行
      c.Next()
   }
}

将中间件注册为全局中间件即可

会话控制

在目前的时代中,流行的三种Web会话控制总共有三种,cookie,session,JWT。

Cookie

ookie中的信息是以键值对的形式储存在浏览器中,而且在浏览器中可以直接看到数据

优点:

  • 结构简单

  • 数据持久

缺点:

  • 大小受限

  • 明文存储

  • 容易受到CSRF攻击

import (
    "fmt"

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

func main() {

    router := gin.Default()

    router.GET("/cookie", func(c *gin.Context) {

       	// 获取对应的cookie
        cookie, err := c.Cookie("gin_cookie")

        if err != nil {
            cookie = "NotSet"
            // 设置cookie 参数:key,val,存在时间,目录,域名,是否允许他人通过js访问cookie,仅http
            c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
        }

        fmt.Printf("Cookie value: %s \n", cookie)
    })

    router.Run()
}

单纯的cookie在五六年前用的比较多,不过作者一般很少使用单纯的cookie来做会话控制,这样做确实不太安全。

Session

session存储在服务器中,然后发送一个cookie存储在浏览器中,cookie中存储的是session_id,之后每次请求服务器通过session_id可以获取对应的session信息

优点:

  • 存储在服务端,增加安全性,便于管理

缺点:

  • 存储在服务端,增大服务器开销,降低性能

  • 基于cookie识别,不安全

  • 认证信息在分布式情况下不同步

Session与Cookie是不分家的,每次要用到Session,默认就是要用到Cookie了。Gin默认是不支持Session的,因为Cookie是Http协议里面的内容,但Session不是,不过有第三方中间件支持,安装依赖即可,仓库地址:gin-contrib/sessions: Gin middleware for session management (github.com)

go get github.com/gin-contrib/sessions

支持cookie,Redis,MongoDB,GORM,PostgreSQL

func main() {
   r := gin.Default()
   // 创建基于Cookie的存储引擎
   store := cookie.NewStore([]byte("secret"))
   // 设置Session中间件,mysession即session名称,也是cookie的名称
   r.Use(sessions.Sessions("mysession", store))
   r.GET("/incr", func(c *gin.Context) {
      // 初始化session
      session := sessions.Default(c)
      var count int
      // 获取值
      v := session.Get("count")
      if v == nil {
         count = 0
      } else {
         count = v.(int)
         count++
      }
      // 设置
      session.Set("count", count)
      // 保存
      session.Save()
      c.JSON(200, gin.H{"count": count})
   })
   r.Run(":8000")
}

一般不推荐通过Cookie存储Sesison,推荐使用Redis,其他例子还请自行去官方仓库了解。

JWT

优点:

  • 基于JSON,多语言通用

  • 可以存储非敏感信息

  • 占用很小,便于传输

  • 服务端无需存储,利于分布式拓展

缺点:

  • Token刷新问题

  • 一旦签发则无法主动控制

自从前端革命以来,前端程序员不再只是一个“写页面的”,前后端分离的趋势愈演愈烈,JWT是最适合前后端分离和分布式系统来做会话控制的,具有很大的天然优势。考虑到JWT已经完全脱离Gin的内容,且没有任何中间件支持,因为JWT本身就是不局限于任何框架任何语言,在这里就不作细致的讲解,可以前往另一篇教程:[JWT使用教程](JWT | Go中文学习文档 (halfiisland.com))

日志管理

Gin默认使用的日志中间件采用的是os.Stdout,只有最基本的功能,毕竟Gin只专注于Web服务,大多数情况下应该使用更加成熟的日志框架,不过这并不在本章的讨论范围内,而且Gin的拓展性很高,可以很轻易的整合其他框架,这里只讨论其自带的日志服务。

控制台颜色

gin.DisableConsoleColor() // 关闭控制台日志颜色

除了在开发的时候,大多数时候都不建议开启此项

日志写入文件

func main() {
	e := gin.Default()
    // 关掉控制台颜色
	gin.DisableConsoleColor()
    // 创建两个日志文件
	log1, _ := os.Create("info1.log")
	log2, _ := os.Create("info2.log")
    // 同时记录进两个日志文件
	gin.DefaultWriter = io.MultiWriter(log1, log2)
	e.GET("/hello", Hello)
	log.Fatalln(e.Run(":8080"))
}

gin自带的日志支持写入多个文件,但内容是相同的,使用起来不太方便,并且不会将请求日志写入文件中。

func main() {
	router := gin.New()
	// LoggerWithFormatter 中间件会写入日志到 gin.DefaultWriter
	// 默认 gin.DefaultWriter = os.Stdout
	router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
        //TODO 写入对应文件的逻辑
        ......
		// 输出自定义格式
		return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
				param.ClientIP,
				param.TimeStamp.Format(time.RFC1123),
				param.Method,
				param.Path,
				param.Request.Proto,
				param.StatusCode,
				param.Latency,
				param.Request.UserAgent(),
				param.ErrorMessage,
		)
	}))
	router.Use(gin.Recovery())
	router.GET("/ping", func(c *gin.Context) {
		c.String(200, "pong")
	})
	router.Run(":8080")
}

通过自定义中间件,可以实现日志写入文件中

路由调试日志格式

这里修改的只是启动时输出路由信息的的日志

func main() {
   e := gin.Default()
   gin.SetMode(gin.DebugMode)
   gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
      if gin.IsDebugging() {
         log.Printf("路由 %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)
      }
   }
   e.GET("/hello", Hello)
   log.Fatalln(e.Run(":8080"))
}

输出

2022/12/21 17:19:13 路由 GET /hello main.Hello 3

结语:Gin算是Go语言Web框架中最易学习的一种,因为Gin真正做到了职责最小化,只是单纯的负责Web服务,其他的认证逻辑,数据缓存等等功能都交给开发者自行完成,相比于那些大而全的框架,轻量简洁的Gin对于初学者而言更适合也更应该去学习,因为Gin并没有强制使用某一种规范,项目该如何构建,采用什么结构都需要自行斟酌,对于初学者而言更能锻炼能力。

你可能感兴趣的:(gin,golang,后端)