Gin 的使用

邹业盛 2022-04-04 04:36 更新
  1. 简介
  2. Hello World
  3. Context
  4. 中间件与路由配置
  5. 模型绑定
  6. Chunked 响应

1. 简介

Gin 是 go 的一个 web 框架,https://github.com/gin-gonic/gin ,它像 Node 下的 Express ,风格上追求简洁。

与 Python 中的框架比的话,像早期的 webpy 。只看 HTTP 核心部分,其实与 Tornado 也差不多,只是 Handler 的实现上,因为 go 中只有“函数”,没有“类”,所以 Tornado 中的“实例”方法,在 gin 中就只能使用传入 Handler 函数的一个固定的 Context 对象来封装。同时,“函数”不像“类”可以通过“继承”以内化的方式完成抽象逻辑的组织,对应的,只能单独设计“中间件”的机制来额外地组装这些抽象逻辑。

因为 go 是静态语言, gin 中专门设计了一组 Bind 行为的 API ,用于将请求时的参数(或者头),以特定的格式,如 json, xml 等,与即定的结构,接口做绑定,好方便针对请求参数的逻辑处理过程。

2. Hello World

package main

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

func main() {
    router := gin.Default()
    router.GET("/", func(c *gin.Context){
        c.String(200, "Hello World")
    })
    router.Run("0.0.0.0:8888")
}

Hello World 的例子很简单,可以看成两个部分:

把 HTTP Status 作为各种 Response Writer 的第一个参数,是我最不喜欢的一个 API 设计点。

3. Context

3.1. 请求参数和路径匹配

package main

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

func main() {
    router := gin.Default()
    router.GET("/:resource/*action", func(c *gin.Context){
        resource := c.Param("resource")
        action := c.Param("action")
        name := c.Query("name")
        number := c.DefaultQuery("number", "100")
        data := c.DefaultPostForm("data", "DefaultData")
        c.String(200,
        `
        resouce: %s;
        action: %s;
        name: %s;
        number: %s;
        data: %s;
        `,
        resource, action, name, number, data)
    })
    router.Run("0.0.0.0:8888")
}

这里有一个小注意点: DefaultQuery()DefaultPostForm() 的函数签名都是 (string, string) ,这意味着,你无法使用它们完成“判断参数是否存在”。要判断参数的存在,需要使用:

package main

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

func main() {
    router := gin.Default()
    router.GET("/:resource/*action", func(c *gin.Context){
        if _, ok := c.GetQuery("name"); ok {
            c.String(200, "YES")
        } else {
            c.String(200, "NO")
        }
    })
    router.Run("0.0.0.0:8888")
}

3.2. Multipart 及上传的文件

使用 c.FormFile() 或者 c.MultipartForm()

TODO

3.3. 获取原始 Body

使用 GetRawData()

浏览器相关的场景,这个应该几乎不会被用到了。

3.4. 获取头

package main

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

func main() {
    router := gin.Default()
    router.GET("/:resource/*action", func(c *gin.Context){
        c.String(200, c.GetHeader("User-Agent"))
    })
    router.Run("0.0.0.0:8888")
}

使用 c.GetHeader()

头的名字,大小写无所谓,但是 - 不能少,不能错。

3.5. 设置头

设置头:

package main

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

func main() {
    router := gin.Default()
    router.Get("/", func(c *gin.Context){
        c.Header("X-Name", "xx")
        c.String(200, "Hello World")
    })
    router.Run("0.0.0.0:8888")
}

使用 Header() 方法,同样,大小写无所为, - 不能错。

3.6. 获取请求方法

package main

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

func main() {
    router := gin.Default()
    router.Any("/:resource/*action", func(c *gin.Context){
        c.String(200, c.Request.Method)
    })
    router.Run("0.0.0.0:8888")
}

这个能力在 Context 中没有现成的方法,可以使用 c.Request.Method 获取。

3.7. 设置 Cookie

使用

SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool)

因为 go 没有现成的“默认参数”语法,所以,一共需要传递 7 个参数。

少了一个 SameSite ,是另有一个单独方法:

SetSameSite(samesite http.SameSite) 

从源码上看,必须先调用 SetSameSite ,再调用 SetCookie 这样 SameSite 才有效。

SetCookie 的参数:

package main

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

func main() {
    router := gin.Default()
    router.Any("/:resource/*action", func(c *gin.Context){
        c.SetCookie("test", "123", 60, "/", "localhost", false, true)
        c.String(200, c.Request.Method)
    })
    router.Run("0.0.0.0:8888")
}

3.8. 获取 Cookie

package main

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

func main() {
    router := gin.Default()
    router.Any("/:resource/*action", func(c *gin.Context){
        c.SetCookie("test", "123", 0, "", "", false, true)
        cookie, err := c.Cookie("test")
        if (err != nil) {
            c.String(200, "ERR")
        } else {
            c.String(200, cookie)
        }
    })
    router.Run("0.0.0.0:8888")
}

使用 Cookie 方法,注意,它的返回是: value, err ,指定的 Cookie 有可能不存在。

3.9. 重定向

重定向有一个现成方法, Redirect(code int, location string)

package main

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

func main() {
    router := gin.Default()
    router.GET("/", func(c *gin.Context){
        c.Redirect(301, "https://www.zouyesheng.com")
    })
    router.Run("0.0.0.0:8888")
}

3.10. 响应内容

响应内容的具体格式大概可能分成这几类:

字符串前面都用过了。模板无视它,反正我现在也用不着。

JSON 的返回, gin 根据是否转义 <> 之类的符号,是否转义非 ASCII 字符,搞了好几个方法,个人建议直接用 AsciiJSON 就好了。

字节等下举个例子。

其它的,有 XMLYAMLProtoBuf 等格式的。也有像 FileFileFromFS 这种快捷方法。还有 Stream

JSON 返回:

package main

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

func main() {
    router := gin.Default()
    router.GET("/", func(c *gin.Context){
        c.AsciiJSON(200, gin.H{"name": "<h1>中文</h1>"})
    })
    router.Run("0.0.0.0:8888")
}

字节返回:

package main

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

func main() {
    router := gin.Default()
    router.GET("/", func(c *gin.Context){
        c.Data(200, "text/plain", []byte{'a', 'a', 'a'})
    })
    router.Run("0.0.0.0:8888")
}

4. 中间件与路由配置

4.1. 路由配置

中间件定义好之后要应用,就依赖于路由的一套配置方法,所以先说一下路由相关的东西。

router := gin.Default()

这里的 Default() ,其实已经在 router 中加入了 logger 之类的中间件功能了。

如果要一个裸的配置,可以使用:

router := gin.New()

这时你再访问,就没有相应的访问日志输出了。

路由和中间件关系的配置,简单来说,就是“分组”,因为分组之后,对于不同的路由组就可以有不同的统一逻辑应用。

package main

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

func main() {
    router := gin.Default()
    v1 := router.Group("")
    v1.GET("/", func(c *gin.Context){
        c.Data(200, "text/plain", []byte{'a', 'a', 'a'})
    })

    v2 := router.Group("/sub")

        v2.GET("/hello", func(c *gin.Context){
            c.JSON(200, gin.H{
                "code": 0,
                "msg": "ok",
            })
        })

        v3 := v2.Group("/next")
            v3.GET("/world", func(c *gin.Context){
                c.JSON(200, gin.H{"code": 0})
            })

    router.Run("0.0.0.0:8888")
}

使用 .Group() 之后,会有一个路径的传递的效果。

4.2. 中间件定义

我看官方文档并没有明确说明中间件定义的接口规范,不过可以从源码中的 logger.go 参考一下。

其实中间件和普通的 Handler 没有区别,只是在合适的地方,可以选择性地使用 ContextNext() 方法,或者使用其它的一些 API ,以实现在单个中间件的个体内,完成像 pre , post 这类的勾子行为。

package main

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

func log(c *gin.Context) {
    print("before\n")
    c.Next()
    print("after\n")
}

func main() {
    router := gin.Default()
    v1 := router.Group("")
    v1.Use(log)
    v1.GET("/", func(c *gin.Context){
        print("in GET\n")
        c.Data(200, "text/plain", []byte{'a', 'a', 'a'})
    })

    v2 := router.Group("/sub")
    v2.GET("", func(c *gin.Context){
        c.String(200, "ok")
    })
    router.Run("0.0.0.0:8888")
}

上面的示例代码很好理解,如果想实现 post 行为,有 Next() 方法可用。要应用到不同的路由匹配上,可以通过 Group() 之后,再 Use()

如果你不需要 post 勾子行为的话,那么不调用 Next() 也可以。

需要取消处理,立即返回,可以使用 Abort() 方法:

package main

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


func log(c *gin.Context) {
    c.String(200, "over")
    c.Abort()
}

func main() {
    router := gin.Default()
    router.Use(log)
    v1 := router.Group("")
    v1.GET("/", func(c *gin.Context){
        c.String(200, "here")
    })

    router.Run("0.0.0.0:8888")
}

4.3. Context 的 Get 和 Set

中间件不光需要勾子能力,还需要状态的保存能力。典型的,以用户认证的场景来看。当检查了输入,确定了用户身份,肯定是需要把用户信息保存下来,以便后面的业务 Handler 直接使用。所以,尝试直接找对应功能的 API 。没找错的话,就是 ContextGetSet 方法。( Get 还有对应的 GetString GetInt 种种 )

当第一次使用 Set 时,当前 Context 会创建一个 Keysmap 来保存数据。

package main

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

type Person struct {
    name string
}

func log(c *gin.Context) {
    c.Set("first", &Person{name: "person name"})
}

func main() {
    router := gin.Default()
    router.Use(log)
    v1 := router.Group("")
    v1.GET("/", func(c *gin.Context){
        if val, exists := c.Get("first"); exists {
            c.String(200, val.(*Person).name)
            return
        }
        c.String(200, "no")
    })

    router.Run("0.0.0.0:8888")
}

Get() 的获取,会返回两个值,第二个是表示是否存在。

5. 模型绑定

模型绑定的机制,可以让你先定义一个结构,然后通过一个方法的调用,自动地把请求参数,或者请求头中的数据用于填充结构。(也许和 ORM 中的 Model 绝配?)

package main

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

type Person struct {
    UserId string `form:"user_id"`
    Name string `form:"name"`
}

func (p Person) getName() string {
    return p.UserId + " - " + p.Name
}


func main() {
    router := gin.Default()
    router.GET("/", func(c *gin.Context){
        var p Person
        if err := c.ShouldBind(&p); err != nil {
            c.String(200, "error")
        } else {
            c.String(200, p.getName())
        }
    })

    router.Run("0.0.0.0:8888")
}

6. Chunked 响应

package main

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

const OK int = 200

func main() {
    router := gin.Default()
    router.GET("/", func(c *gin.Context){
        c.String(OK, "OK")
        c.Writer.Flush()
        time.Sleep(3 * time.Second)
        c.String(OK, "TIME")
    })

    router.Run("0.0.0.0:8888")
}

API 设计上,响应的内容是可以多次写入的,所以自然会想到 Chunked 的响应方式。

直接使用 c.Writer.Flush() 就可以把当前缓冲区的内容写回连接了。API 后续写回时的状态码会被忽略,只有第一次的状态码会被使用。

zys@shopee:/home/zys >>> telnet localhost 8888
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET / HTTP/1.1
Host: localhost

HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Wed, 30 Mar 2022 19:36:03 GMT
Transfer-Encoding: chunked

2
OK
4
TIME
0
评论
©2010-2022 zouyesheng.com All rights reserved. Powered by GitHub , txt2tags , MathJax