Go语言gin框架入门到精通,涵盖文件服务器、中间件、安全认证、数据库

发布时间 2023-11-03 15:09:19作者: 天使angl
Go语言gin框架入门到精通,涵盖文件服务器、中间件、安全认证、数据库

 

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"
 

快速开始

  1.  
    package main
  2.  
     
  3.  
    import (
  4.  
    "github.com/gin-gonic/gin"
  5.  
    "net/http"
  6.  
    )
  7.  
     
  8.  
    func main() {
  9.  
    engine := gin.Default() //创建gin引擎
  10.  
    engine.GET("/ping", func(context *gin.Context) {
  11.  
    context.JSON(http.StatusOK, gin.H{
  12.  
    "message": "pong",
  13.  
    })
  14.  
    })
  15.  
    engine.Run() //开启服务器,默认监听localhost:8080
  16.  
    }

请求URL

GET localhost:8080/ping
 

返回

  1.  
    HTTP/1.1 200 OK
  2.  
    Content-Type: application/json; charset=utf-8
  3.  
    Date: Tue, 22 Nov 2022 08:47:11 GMT
  4.  
    Content-Length: 18
  5.  
     
  6.  
    {
  7.  
    "message": "pong"
  8.  
    }
  9.  
    Response file saved.
  10.  
    > 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一致。

  1.  
    package main
  2.  
     
  3.  
    import (
  4.  
    "github.com/gin-gonic/gin"
  5.  
    "log"
  6.  
    "net/http"
  7.  
    )
  8.  
     
  9.  
    func main() {
  10.  
    e := gin.Default()
  11.  
    e.GET("/findUser/:username/:userid", FindUser)
  12.  
    e.GET("/downloadFile/*filepath", UserPage)
  13.  
     
  14.  
    log.Fatalln(e.Run(":8080"))
  15.  
    }
  16.  
     
  17.  
    // 命名参数示例
  18.  
    func FindUser(c *gin.Context) {
  19.  
    username := c.Param("username")
  20.  
    userid := c.Param("userid")
  21.  
    c.String(http.StatusOK, "username is %s\n userid is %s", username, userid)
  22.  
    }
  23.  
     
  24.  
    // 路径参数示例
  25.  
    func UserPage(c *gin.Context) {
  26.  
    filepath := c.Param("filepath")
  27.  
    c.String(http.StatusOK, "filepath is %s", filepath)
  28.  
    }

示例一

curl --location --request GET '127.0.0.1:8080/findUser/jack/001'
 
  1.  
    username is jack
  2.  
    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。

  1.  
    package main
  2.  
     
  3.  
    import (
  4.  
    "github.com/gin-gonic/gin"
  5.  
    "log"
  6.  
    "net/http"
  7.  
    )
  8.  
     
  9.  
    func main() {
  10.  
    e := gin.Default()
  11.  
    e.GET("/findUser", FindUser)
  12.  
    log.Fatalln(e.Run(":8080"))
  13.  
    }
  14.  
     
  15.  
    func FindUser(c *gin.Context) {
  16.  
    username := c.DefaultQuery("username", "defaultUser")
  17.  
    userid := c.Query("userid")
  18.  
    c.String(http.StatusOK, "username is %s\nuserid is %s", username, userid)
  19.  
    }

示例一

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

示例二

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

表单参数

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

  1.  
    package main
  2.  
     
  3.  
    import (
  4.  
    "github.com/gin-gonic/gin"
  5.  
    "net/http"
  6.  
    )
  7.  
     
  8.  
    func main() {
  9.  
    e := gin.Default()
  10.  
    e.POST("/register", RegisterUser)
  11.  
    e.POST("/update", UpdateUser)
  12.  
    e.Run(":8080")
  13.  
    }
  14.  
     
  15.  
    func RegisterUser(c *gin.Context) {
  16.  
    username := c.PostForm("username")
  17.  
    password := c.PostForm("password")
  18.  
    c.String(http.StatusOK, "successfully registered,your username is [%s],password is [%s]", username, password)
  19.  
    }
  20.  
     
  21.  
    func UpdateUser(c *gin.Context) {
  22.  
    var form map[string]string
  23.  
    c.ShouldBind(&form)
  24.  
    c.String(http.StatusOK, "successfully update,your username is [%s],password is [%s]", form["username"], form["password"])
  25.  
    }

示例一:使用form-data

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

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

示例二:使用json

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

数据解析

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

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

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

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

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

  1.  
    var (
  2.  
    JSON = jsonBinding{}
  3.  
    XML = xmlBinding{}
  4.  
    Form = formBinding{}
  5.  
    Query = queryBinding{}
  6.  
    FormPost = formPostBinding{}
  7.  
    FormMultipart = formMultipartBinding{}
  8.  
    ProtoBuf = protobufBinding{}
  9.  
    MsgPack = msgpackBinding{}
  10.  
    YAML = yamlBinding{}
  11.  
    Uri = uriBinding{}
  12.  
    Header = headerBinding{}
  13.  
    TOML = tomlBinding{}
  14.  
    )

示例

  1.  
    package main
  2.  
     
  3.  
    import (
  4.  
    "fmt"
  5.  
    "github.com/gin-gonic/gin"
  6.  
    "net/http"
  7.  
    )
  8.  
     
  9.  
    type LoginUser struct {
  10.  
    Username string `bind:"required" json:"username" form:"username" uri:"username"`
  11.  
    Password string `bind:"required" json:"password" form:"password" uri:"password"`
  12.  
    }
  13.  
     
  14.  
    func main() {
  15.  
    e := gin.Default()
  16.  
    e.POST("/loginWithJSON", Login)
  17.  
    e.POST("/loginWithForm", Login)
  18.  
    e.GET("/loginWithQuery/:username/:password", Login)
  19.  
    e.Run(":8080")
  20.  
    }
  21.  
     
  22.  
    func Login(c *gin.Context) {
  23.  
    var login LoginUser
  24.  
    // 使用ShouldBind来让gin自动推断
  25.  
    if c.ShouldBind(&login) == nil && login.Password != "" && login.Username != "" {
  26.  
    c.String(http.StatusOK, "login successfully !")
  27.  
    } else {
  28.  
    c.String(http.StatusBadRequest, "login failed !")
  29.  
    }
  30.  
    fmt.Println(login)
  31.  
    }

Json数据绑定

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

表单数据绑定

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

URL数据绑定

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

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

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

多次绑定

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

c.ShouldBindBodyWith。

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

数据校验

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

简单示例

  1.  
    type LoginUser struct {
  2.  
    Username string `binding:"required" json:"username" form:"username" uri:"username"`
  3.  
    Password string `binding:"required" json:"password" form:"password" uri:"password"`
  4.  
    }
  5.  
     
  6.  
    func main() {
  7.  
    e := gin.Default()
  8.  
    e.POST("/register", Register)
  9.  
    log.Fatalln(e.Run(":8080"))
  10.  
    }
  11.  
     
  12.  
    func Register(ctx *gin.Context) {
  13.  
    newUser := &LoginUser{}
  14.  
    if err := ctx.ShouldBind(newUser); err == nil {
  15.  
    ctx.String(http.StatusOK, "user%+v", *newUser)
  16.  
    } else {
  17.  
    ctx.String(http.StatusBadRequest, "invalid user,%v", err)
  18.  
    }
  19.  
    }

测试

  1.  
    curl --location --request POST 'http://localhost:8080/register' \
  2.  
    --header 'Content-Type: application/json' \
  3.  
    --data-raw '{
  4.  
    "username":"jack1"
  5.  
     
  6.  
    }'

输出

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对于数据响应提供了丰富的内置支持,用法简洁明了,上手十分容易。

简单示例

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

HTML渲染

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

测试

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

返回

  1.  
    <!DOCTYPE html>
  2.  
    <html lang="en">
  3.  
     
  4.  
    <head>
  5.  
    <meta charset="UTF-8">
  6.  
    <title>GinLearn</title>
  7.  
    </head>
  8.  
     
  9.  
    <body>
  10.  
    <h1>Hello World!</h1>
  11.  
    <h1>This is a HTML Template Render Example</h1>
  12.  
    </body>
  13.  
     
  14.  
    </html>

快速响应

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

  1.  
    // 使用Render写入响应头,并进行数据渲染
  2.  
    func (c *Context) Render(code int, r render.Render)
  3.  
     
  4.  
    // 渲染一个HTML模板,name是html路径,obj是内容
  5.  
    func (c *Context) HTML(code int, name string, obj any)
  6.  
     
  7.  
    // 以美化了的缩进JSON字符串进行数据渲染,通常不建议使用这个方法,因为会造成更多的传输消耗。
  8.  
    func (c *Context) IndentedJSON(code int, obj any)
  9.  
     
  10.  
    // 安全的JSON,可以防止JSON劫持,详情了解:https://www.cnblogs.com/xusion/articles/3107788.html
  11.  
    func (c *Context) SecureJSON(code int, obj any)
  12.  
     
  13.  
    // JSONP方式进行渲染
  14.  
    func (c *Context) JSONP(code int, obj any)
  15.  
     
  16.  
    // JSON方式进行渲染
  17.  
    func (c *Context) JSON(code int, obj any)
  18.  
     
  19.  
    // JSON方式进行渲染,会将unicode码转换为ASCII码
  20.  
    func (c *Context) AsciiJSON(code int, obj any)
  21.  
     
  22.  
    // JSON方式进行渲染,不会对HTML特殊字符串进行转义
  23.  
    func (c *Context) PureJSON(code int, obj any)
  24.  
     
  25.  
    // XML方式进行渲染
  26.  
    func (c *Context) XML(code int, obj any)
  27.  
     
  28.  
    // YML方式进行渲染
  29.  
    func (c *Context) YAML(code int, obj any)
  30.  
     
  31.  
    // TOML方式进行渲染
  32.  
    func (c *Context) TOML(code int, obj interface{})
  33.  
     
  34.  
    // ProtoBuf方式进行渲染
  35.  
    func (c *Context) ProtoBuf(code int, obj any)
  36.  
     
  37.  
    // String方式进行渲染
  38.  
    func (c *Context) String(code int, format string, values ...any)
  39.  
     
  40.  
    // 重定向到特定的位置
  41.  
    func (c *Context) Redirect(code int, location string)
  42.  
     
  43.  
    // 将data写入响应流中
  44.  
    func (c *Context) Data(code int, contentType string, data []byte)
  45.  
     
  46.  
    // 通过reader读取流并写入响应流中
  47.  
    func (c *Context) DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string)
  48.  
     
  49.  
    // 高效的将文件写入响应流
  50.  
    func (c *Context) File(filepath string)
  51.  
     
  52.  
    // 以一种高效的方式将fs中的文件流写入响应流
  53.  
    func (c *Context) FileFromFS(filepath string, fs http.FileSystem)
  54.  
     
  55.  
    // 以一种高效的方式将fs中的文件流写入响应流,并且在客户端会以指定的文件名进行下载
  56.  
    func (c *Context) FileAttachment(filepath, filename string)
  57.  
     
  58.  
    // 将服务端推送流写入响应流中
  59.  
    func (c *Context) SSEvent(name string, message any)
  60.  
     
  61.  
    // 发送一个流响应并返回一个布尔值,以此来判断客户端是否在流中间断开
  62.  
    func (c *Context) Stream(step func(w io.Writer) bool) bool

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

异步处理

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

  1.  
    // copy返回一个当前Context的副本以便在当前Context作用范围外安全的使用,可以用于传递给一个goroutine
  2.  
    func (c *Context) Copy() *Context
  1.  
    func main() {
  2.  
    e := gin.Default()
  3.  
    e.GET("/hello", Hello)
  4.  
    log.Fatalln(e.Run(":8080"))
  5.  
    }
  6.  
     
  7.  
    func Hello(c *gin.Context) {
  8.  
    ctx := c.Copy()
  9.  
    go func() {
  10.  
    // 子协程应该使用Context的副本,不应该使用原始Context
  11.  
    log.Println("异步处理函数: ", ctx.HandlerNames())
  12.  
    }()
  13.  
    log.Println("接口处理函数: ", c.HandlerNames())
  14.  
    c.String(http.StatusOK, "hello")
  15.  
    }

测试

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

输出

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

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

文件传输

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

单文件上传

  1.  
    func main() {
  2.  
    e := gin.Default()
  3.  
    e.POST("/upload", uploadFile)
  4.  
    log.Fatalln(e.Run(":8080"))
  5.  
    }
  6.  
     
  7.  
    func uploadFile(ctx *gin.Context) {
  8.  
    // 获取文件
  9.  
    file, err := ctx.FormFile("file")
  10.  
    if err != nil {
  11.  
    ctx.String(http.StatusBadRequest, "%+v", err)
  12.  
    return
  13.  
    }
  14.  
    // 保存在本地
  15.  
    err = ctx.SaveUploadedFile(file, "./"+file.Filename)
  16.  
    if err != nil {
  17.  
    ctx.String(http.StatusBadRequest, "%+v", err)
  18.  
    return
  19.  
    }
  20.  
    // 返回结果
  21.  
    ctx.String(http.StatusOK, "upload %s size:%d byte successfully!", file.Filename, file.Size)
  22.  
    }

测试

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

结果

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

多文件上传

  1.  
    func main() {
  2.  
    e := gin.Default()
  3.  
    e.POST("/upload", uploadFile)
  4.  
    e.POST("/uploadFiles", uploadFiles)
  5.  
    log.Fatalln(e.Run(":8080"))
  6.  
    }
  7.  
     
  8.  
    func uploadFiles(ctx *gin.Context) {
  9.  
    // 获取gin解析好的multipart表单
  10.  
    form, _ := ctx.MultipartForm()
  11.  
    // 根据键值取得对应的文件列表
  12.  
    files := form.File["files"]
  13.  
    // 遍历文件列表,保存到本地
  14.  
    for _, file := range files {
  15.  
    err := ctx.SaveUploadedFile(file, "./"+file.Filename)
  16.  
    if err != nil {
  17.  
    ctx.String(http.StatusBadRequest, "upload failed")
  18.  
    return
  19.  
    }
  20.  
    }
  21.  
    // 返回结果
  22.  
    ctx.String(http.StatusOK, "upload %d files successfully!", len(files))
  23.  
    }

测试

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

输出

upload 3 files successfully!
 

文件下载

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

  1.  
    func main() {
  2.  
    e := gin.Default()
  3.  
    e.POST("/upload", uploadFile)
  4.  
    e.POST("/uploadFiles", uploadFiles)
  5.  
    e.GET("/download/:filename", download)
  6.  
    log.Fatalln(e.Run(":8080"))
  7.  
    }
  8.  
     
  9.  
    func download(ctx *gin.Context) {
  10.  
    // 获取文件名
  11.  
    filename := ctx.Param("filename")
  12.  
    // 返回对应文件
  13.  
    ctx.FileAttachment(filename, filename)
  14.  
    }

测试

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

结果

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

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

  1.  
    func download(ctx *gin.Context) {
  2.  
    // 获取参数
  3.  
    filename := ctx.Param("filename")
  4.  
     
  5.  
    // 请求响应对象和请求对象
  6.  
    response, request := ctx.Writer, ctx.Request
  7.  
    // 写入响应头
  8.  
    // response.Header().Set("Content-Type", "application/octet-stream") 以二进制流传输文件
  9.  
    response.Header().Set("Content-Disposition", `attachment; filename*=UTF-8''`+url.QueryEscape(filename)) // 对文件名进行安全转义
  10.  
    response.Header().Set("Content-Transfer-Encoding", "binary") // 传输编码
  11.  
    http.ServeFile(response, request, filename)
  12.  
    }

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

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

路由管理

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

路由组

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

  1.  
    func Hello(c *gin.Context) {
  2.  
     
  3.  
    }
  4.  
     
  5.  
    func Login(c *gin.Context) {
  6.  
     
  7.  
    }
  8.  
     
  9.  
    func Update(c *gin.Context) {
  10.  
     
  11.  
    }
  12.  
     
  13.  
    func Delete(c *gin.Context) {
  14.  
     
  15.  
    }

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

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

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

  1.  
    func main() {
  2.  
    e := gin.Default()
  3.  
    v1 := e.Group("v1")
  4.  
    {
  5.  
    v1.GET("/hello", Hello)
  6.  
    v1.GET("/login", Login)
  7.  
    }
  8.  
    v2 := e.Group("v2")
  9.  
    {
  10.  
    v2.POST("/update", Update)
  11.  
    v2.DELETE("/delete", Delete)
  12.  
    }
  13.  
    }

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

404路由

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

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

我们拿上个例子举例

  1.  
    func main() {
  2.  
    e := gin.Default()
  3.  
    v1 := e.Group("v1")
  4.  
    {
  5.  
    v1.GET("/hello", Hello)
  6.  
    v1.GET("/login", Login)
  7.  
    }
  8.  
    v2 := e.Group("v2")
  9.  
    {
  10.  
    v2.POST("/update", Update)
  11.  
    v2.DELETE("/delete", Delete)
  12.  
    }
  13.  
    // 注册处理器
  14.  
    e.NoRoute(func(context *gin.Context) { // 这里只是演示,不要在生产环境中直接返回HTML代码
  15.  
    context.String(http.StatusNotFound, "<h1>404 Page Not Found</h1>")
  16.  
    })
  17.  
    log.Fatalln(e.Run(":8080"))
  18.  
    }

随便发一个请求

curl --location --request GET 'http://localhost:8080/'
 
<h1>404 Page Not Found</h1>
 

405路由

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

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

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

  1.  
    func main() {
  2.  
    e := gin.Default()
  3.  
    // 需要将其设置为true
  4.  
    e.HandleMethodNotAllowed = true
  5.  
    v1 := e.Group("/v1")
  6.  
    {
  7.  
    v1.GET("/hello", Hello)
  8.  
    v1.GET("/login", Login)
  9.  
    }
  10.  
    v2 := e.Group("/v2")
  11.  
    {
  12.  
    v2.POST("/update", Update)
  13.  
    v2.DELETE("/delete", Delete)
  14.  
    }
  15.  
    e.NoRoute(func(context *gin.Context) {
  16.  
    context.String(http.StatusNotFound, "<h1>404 Page Not Found</h1>")
  17.  
    })
  18.  
    // 注册处理器
  19.  
    e.NoMethod(func(context *gin.Context) {
  20.  
    context.String(http.StatusMethodNotAllowed, "method not allowed")
  21.  
    })
  22.  
    log.Fatalln(e.Run(":8080"))
  23.  
    }

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

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

至此配置成功

重定向

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

  1.  
    func main() {
  2.  
    e := gin.Default()
  3.  
    e.GET("/", Index)
  4.  
    e.GET("/hello", Hello)
  5.  
    log.Fatalln(e.Run(":8080"))
  6.  
    }
  7.  
     
  8.  
    func Index(c *gin.Context) {
  9.  
    c.Redirect(http.StatusMovedPermanently, "/hello")
  10.  
    }
  11.  
     
  12.  
    func Hello(c *gin.Context) {
  13.  
    c.String(http.StatusOK, "hello")
  14.  
    }

测试

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

输出

hello
 

中间件

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

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

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

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

  1.  
    func Default() *Engine {
  2.  
    debugPrintWARNINGDefault()
  3.  
    engine := New()
  4.  
    engine.Use(Logger(), Recovery())
  5.  
    return engine
  6.  
    }

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

全局中间件

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

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

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

  1.  
    func main() {
  2.  
    e := gin.Default()
  3.  
    // 注册全局中间件
  4.  
    e.Use(GlobalMiddleware())
  5.  
    v1 := e.Group("/v1")
  6.  
    {
  7.  
    v1.GET("/hello", Hello)
  8.  
    v1.GET("/login", Login)
  9.  
    }
  10.  
    v2 := e.Group("/v2")
  11.  
    {
  12.  
    v2.POST("/update", Update)
  13.  
    v2.DELETE("/delete", Delete)
  14.  
    }
  15.  
    log.Fatalln(e.Run(":8080"))
  16.  
    }

测试

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

输出

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

局部中间件

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

  1.  
    func main() {
  2.  
    e := gin.Default()
  3.  
    // 注册全局中间件
  4.  
    e.Use(GlobalMiddleware())
  5.  
    // 注册路由组局部中间件
  6.  
    v1 := e.Group("/v1", LocalMiddleware())
  7.  
    {
  8.  
    v1.GET("/hello", Hello)
  9.  
    v1.GET("/login", Login)
  10.  
    }
  11.  
    v2 := e.Group("/v2")
  12.  
    {
  13.  
    // 注册单个路由局部中间件
  14.  
    v2.POST("/update", LocalMiddleware(), Update)
  15.  
    v2.DELETE("/delete", Delete)
  16.  
    }
  17.  
    log.Fatalln(e.Run(":8080"))
  18.  
    }

测试

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

输出

  1.  
    全局中间件被执行...
  2.  
    局部中间件被执行
  3.  
    [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()方法。

  1.  
    if value.handlers != nil {
  2.  
    // 将调用链赋值给Context
  3.  
    c.handlers = value.handlers
  4.  
    c.fullPath = value.fullPath
  5.  
    // 调用中间件
  6.  
    c.Next()
  7.  
    c.writermem.WriteHeaderNow()
  8.  
    return
  9.  
    }

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

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

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

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

输出结果为

[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

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

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

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

:::

计时器中间件

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

  1.  
    func TimeMiddleware() gin.HandlerFunc {
  2.  
    return func(context *gin.Context) {
  3.  
    // 记录开始时间
  4.  
    start := time.Now()
  5.  
    // 执行后续调用链
  6.  
    context.Next()
  7.  
    // 计算时间间隔
  8.  
    duration := time.Since(start)
  9.  
    // 输出纳秒,以便观测结果
  10.  
    fmt.Println("请求用时: ", duration.Nanoseconds())
  11.  
    }
  12.  
    }
  13.  
     
  14.  
    func main() {
  15.  
    e := gin.Default()
  16.  
    // 注册全局中间件,计时中间件
  17.  
    e.Use(GlobalMiddleware(), TimeMiddleware())
  18.  
    // 注册路由组局部中间件
  19.  
    v1 := e.Group("/v1", LocalMiddleware())
  20.  
    {
  21.  
    v1.GET("/hello", Hello)
  22.  
    v1.GET("/login", Login)
  23.  
    }
  24.  
    v2 := e.Group("/v2")
  25.  
    {
  26.  
    // 注册单个路由局部中间件
  27.  
    v2.POST("/update", LocalMiddleware(), Update)
  28.  
    v2.DELETE("/delete", Delete)
  29.  
    }
  30.  
    log.Fatalln(e.Run(":8080"))
  31.  
    }

测试

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

输出

请求用时:  517600
 

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

服务配置

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

Http配置

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

  1.  
    func main() {
  2.  
    router := gin.Default()
  3.  
    server := &http.Server{
  4.  
    Addr: ":8080",
  5.  
    Handler: router,
  6.  
    ReadTimeout: 10 * time.Second,
  7.  
    WriteTimeout: 10 * time.Second,
  8.  
    MaxHeaderBytes: 1 << 20,
  9.  
    }
  10.  
    log.Fatal(server.ListenAndServe())
  11.  
    }

静态资源配置

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

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

  1.  
    // 加载某一静态文件夹
  2.  
    func (group *RouterGroup) Static(relativePath, root string) IRoutes
  3.  
     
  4.  
    // 加载某一个fs
  5.  
    func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes
  6.  
     
  7.  
    // 加载某一个静态文件
  8.  
    func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes
relativePath是映射到网页URL上的相对路径,root是文件在项目中的实际路径

假设项目的目录如下

  1.  
    root
  2.  
    |
  3.  
    |-- static
  4.  
    | |
  5.  
    | |-- a.jpg
  6.  
    | |
  7.  
    | |-- favicon.ico
  8.  
    |
  9.  
    |-- view
  10.  
    |
  11.  
    |-- html
  1.  
    func main() {
  2.  
    router := gin.Default()
  3.  
    // 加载静态文件目录
  4.  
    router.Static("/static", "./static")
  5.  
    // 加载静态文件目录
  6.  
    router.StaticFS("/view", http.Dir("view"))
  7.  
    // 加载静态文件
  8.  
    router.StaticFile("/favicon", "./static/favicon.ico")
  9.  
     
  10.  
    router.Run(":8080")
  11.  
    }

跨域配置

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

  1.  
    func CorsMiddle() gin.HandlerFunc {
  2.  
    return func(c *gin.Context) {
  3.  
    method := c.Request.Method
  4.  
    origin := c.Request.Header.Get("Origin")
  5.  
    if origin != "" {
  6.  
    // 生产环境中的服务端通常都不会填 *,应当填写指定域名
  7.  
    c.Header("Access-Control-Allow-Origin", origin)
  8.  
    // 允许使用的HTTP METHOD
  9.  
    c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
  10.  
    // 允许使用的请求头
  11.  
    c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
  12.  
    // 允许客户端访问的响应头
  13.  
    c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
  14.  
    // 是否需要携带认证信息 Credentials 可以是 cookies、authorization headers 或 TLS client certificates
  15.  
    // 设置为true时,Access-Control-Allow-Origin不能为 *
  16.  
    c.Header("Access-Control-Allow-Credentials", "true")
  17.  
    }
  18.  
    // 放行OPTION请求,但不执行后续方法
  19.  
    if method == "OPTIONS" {
  20.  
    c.AbortWithStatus(http.StatusNoContent)
  21.  
    }
  22.  
    // 放行
  23.  
    c.Next()
  24.  
    }
  25.  
    }

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

会话控制

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

Cookie

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

优点:

  • 结构简单

  • 数据持久

缺点:

  • 大小受限

  • 明文存储

  • 容易受到CSRF攻击

  1.  
    import (
  2.  
    "fmt"
  3.  
     
  4.  
    "github.com/gin-gonic/gin"
  5.  
    )
  6.  
     
  7.  
    func main() {
  8.  
     
  9.  
    router := gin.Default()
  10.  
     
  11.  
    router.GET("/cookie", func(c *gin.Context) {
  12.  
     
  13.  
    // 获取对应的cookie
  14.  
    cookie, err := c.Cookie("gin_cookie")
  15.  
     
  16.  
    if err != nil {
  17.  
    cookie = "NotSet"
  18.  
    // 设置cookie 参数:key,val,存在时间,目录,域名,是否允许他人通过js访问cookie,仅http
  19.  
    c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
  20.  
    }
  21.  
     
  22.  
    fmt.Printf("Cookie value: %s \n", cookie)
  23.  
    })
  24.  
     
  25.  
    router.Run()
  26.  
    }

单纯的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

  1.  
    func main() {
  2.  
    r := gin.Default()
  3.  
    // 创建基于Cookie的存储引擎
  4.  
    store := cookie.NewStore([]byte("secret"))
  5.  
    // 设置Session中间件,mysession即session名称,也是cookie的名称
  6.  
    r.Use(sessions.Sessions("mysession", store))
  7.  
    r.GET("/incr", func(c *gin.Context) {
  8.  
    // 初始化session
  9.  
    session := sessions.Default(c)
  10.  
    var count int
  11.  
    // 获取值
  12.  
    v := session.Get("count")
  13.  
    if v == nil {
  14.  
    count = 0
  15.  
    } else {
  16.  
    count = v.(int)
  17.  
    count++
  18.  
    }
  19.  
    // 设置
  20.  
    session.Set("count", count)
  21.  
    // 保存
  22.  
    session.Save()
  23.  
    c.JSON(200, gin.H{"count": count})
  24.  
    })
  25.  
    r.Run(":8000")
  26.  
    }

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

JWT

优点:

  • 基于JSON,多语言通用

  • 可以存储非敏感信息

  • 占用很小,便于传输

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

缺点:

  • Token刷新问题

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

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

日志管理

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

控制台颜色

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

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

日志写入文件

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

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

  1.  
    func main() {
  2.  
    router := gin.New()
  3.  
    // LoggerWithFormatter 中间件会写入日志到 gin.DefaultWriter
  4.  
    // 默认 gin.DefaultWriter = os.Stdout
  5.  
    router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
  6.  
    //TODO 写入对应文件的逻辑
  7.  
    ......
  8.  
    // 输出自定义格式
  9.  
    return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
  10.  
    param.ClientIP,
  11.  
    param.TimeStamp.Format(time.RFC1123),
  12.  
    param.Method,
  13.  
    param.Path,
  14.  
    param.Request.Proto,
  15.  
    param.StatusCode,
  16.  
    param.Latency,
  17.  
    param.Request.UserAgent(),
  18.  
    param.ErrorMessage,
  19.  
    )
  20.  
    }))
  21.  
    router.Use(gin.Recovery())
  22.  
    router.GET("/ping", func(c *gin.Context) {
  23.  
    c.String(200, "pong")
  24.  
    })
  25.  
    router.Run(":8080")
  26.  
    }

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

路由调试日志格式

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

  1.  
    func main() {
  2.  
    e := gin.Default()
  3.  
    gin.SetMode(gin.DebugMode)
  4.  
    gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
  5.  
    if gin.IsDebugging() {
  6.  
    log.Printf("路由 %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)
  7.  
    }
  8.  
    }
  9.  
    e.GET("/hello", Hello)
  10.  
    log.Fatalln(e.Run(":8080"))
  11.  
    }

输出

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

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