楔子

这次我们来学习一下Go语言中的一个Web框架,虽然我们之前介绍了net/http,但对于开发一个完整的项目来说,标准库net/http还是不太方便的。尽管它的基本功能都有了,但对于快速、高效的开发一个项目还是不太适合的,所以我们需要一个功能更加强大的Web框架。

而Go语言中的第三方Web框架有很多,我们即将学习的是gin框架。而选择这个框架的原因有几下几点:

  • gin是一个老牌框架, 社区庞大;
  • 性能非常优秀;
  • 学习简单;

如果有喜欢名侦探柯南的小伙伴的话,那么看到这个框架的名字一定很熟悉,这不是最惨反派琴酒吗?琴酒你特么好惨啊。

那么下面就来学习gin这个框架吧,并感受它的魅力。

gin 初识

在使用之前我们首先要进行安装,安装很简单,直接:go get -u github.com/gin-gonic/gin 即可。但是因为一些众所周知的原因,你在下载时候会面临一些困难,于是我们可以考虑使用代理。比如:七牛云、阿里云等等。

# 开启 go module
go env -w GO111MODULE=on
# 设置代理
go env -w GOPROXY=https://goproxy.cn,direct
# 下载 gin,由于设置了代理,你会发现下载速度非常非常快
go get -u github.com/gin-gonic/gin

下面我们就来写一个服务,返回一个 "hello world" 吧。

package main

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

func main() {
    router := gin.Default()  // 生成默认的路由器

    // 注册一个路由, 使用GET方法, 里面一个 路径字符串 和 一个函数
    router.GET("/hello", func(context *gin.Context) {
        // 返回一个字符串, 可以调用 context.String()方法
        context.String(200, "hello world")
    })
    // 启动服务, 指定 ip 和 端口
    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

点击右键执行即可,注意:如果你发现运行的时候提示如下信息:cannot find module providing package github.com/gin-gonic/gin: working directory is not part of a module,那么你需要在当前目录执行 go mod init gin;如果执行完之后再启动的时候又出现了:$GOPATH/go.mod exists but should not,那么将当前目录从 GOPATH 中移除即可。

我们访问:localhost:22333/hello,会看到返回的内容,这里就不截图了,可以自己打开页面访问一下。当然除了浏览器,你也可以使用postman进行测试。

那么如果我们想返回json格式的字符串呢?

package main

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

func jsonHandler(context *gin.Context) {
    // 返回json格式通过 gin.H, 里面的写类似于map的键值对, 类型可以任意
    context.JSON(200, gin.H{
        "name": "夏色祭",
        "age": 16,
        "gender": "female",
        "hobby": []string{"斯哈斯哈", "呼吸"},
    })
}

func main() {
    router := gin.Default()  // 生成默认的路由器

    router.GET("/hello", func(context *gin.Context) {
        context.String(200, "hello world")
    })
    router.GET("/json", jsonHandler)

    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

访问 localhost:22333/json 即可看到返回的json数据,怎么样是不是很简单呢。如果我们访问了一个不存在的路由,那么会返回一个 404 page not found

关于路由,记得在 net/http 中我们说过,如果定义的是:/hello,那么页面中只能通过 /hello 访问,/hello/ 是访问不到的;如果定义的是:/hello/,那么在页面中 /hello 和 /hello/ 都是可以访问。

而在gin里面,无论定义的路由是 /hello 还是 /hello/,在页面中都可以使用 /hello 和 /hello/ 进行访问。

我们在启动服务的时候,是通过 router.Run() 的方式启动的,但底层其实还是依赖了 net/http。

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

    router.GET("/hello", func(context *gin.Context) {
        context.String(200, "hello world")
    })
    router.GET("/json/", jsonHandler)
	
    // 这种方式启动也是可以的, 事实上 router.Run 底层也是这么做的, 只是多了一些输出
    if err := http.ListenAndServe("localhost:22333", router); err != nil {
        log.Fatal(err)
    }
}

所以我们看到这个 router 是不是有点像net/http 里面的多路复用器呢?因此它们本质是一样的。

当然我们也可以自己定义服务器的配置:

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

    router.GET("/hello", func(context *gin.Context) {
        context.String(200, "hello world")
    })
    router.GET("/json/", jsonHandler)

    s := http.Server{
        Addr: "localhost:22333",
        Handler: router,
        ReadTimeout: 10 * time.Second,
        WriteTimeout: 10 * time.Second,
        MaxHeaderBytes: 1 << 20,
    }

    if err := s.ListenAndServe(); err != nil {
        log.Fatal(err)
    }
}

gin router

gin 框架中采用的路由库是 httprouter,我们后面会分析它的源码。目前为止我们只见到了 GET  请求,而其它的请求也是类似的。

router.GET("/someGet", getting)
router.POST("/somePost", posting)
router.PUT("/somePut", putting)
router.DELETE("/someDelete", deleting)
router.PATCH("/somePatch", patching)
router.HEAD("/someHead", head)
router.OPTIONS("/someOptions", options)

Gin 的路由支持 GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS 请求,同时还有一个 Any 函数,可以同时支持以上的所有请求。如果需要一个路由支持多种请求,那么就可以使用 Any,演示一下:

package main

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

func anyHandler(context *gin.Context) {
    // context.Request 就是net/http 中的 *http.Request
    if context.Request.Method == "GET" {
        // http.StatusOK 是一个常量, 值为200
        context.String(http.StatusOK, "你访问了GET请求")
    } else if context.Request.Method == "POST" {
        context.String(http.StatusOK, "你访问了POST请求")
    } else {
        context.String(http.StatusOK, "你访问了GET、POST请求之外的请求")
    }
}

func main() {
    router := gin.Default()  
    router.GET("/any/", anyHandler)
    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

此外,gin 还提供了一个 NoRoute,意思是没有匹配到符合条件的路由是会走这个路由。

package main

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


func main() {
    router := gin.Default()
    router.NoRoute(func(context *gin.Context) {
        context.String(http.StatusNotFound, "你要找到页面去火星了······")
    })
    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

在页面上输入一个url,会先匹配我们定义的路由,如果都无法匹配,那么就会走 NoRoute。

路由参数

gin 的路由来自 httprouter 库,因此 httprouter 库具有的功能 gin 也具有,只不过 gin 不支持路由的正则表达式。

路径参数

路径参数通过 Context 的 Param 方法获取。

package main

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

func main() {
    router := gin.Default()
    router.GET("/girl/:name", func(context *gin.Context) {
        name := context.Param("name")
        context.String(200, "你输入了%q", name)
    })
    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

使用一个冒号加上一个参数名即可组成路径参数,可以使用 context.Param 方法读取相应的值,结果是一个字符串。而访问的时候可以通过:/girl/xxx 来访问,代码中的 name 就是对应的 xxx。

请求: localhost:22333/girl/夏色祭
返回: 你输入了"夏色祭"

除了调用 context.Param 返回指定的路径参数,还可以使用 context.Params 返回所有的路径参数。

package main

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

func main() {
    router := gin.Default()
    router.GET("/girl/:name1/:name2/:name3", func(context *gin.Context) {
        names := context.Params
        context.String(200, "你输入了%v", names)
    })
    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

测试一下:

请求: http://localhost:22333/girl/夏色祭/神乐七奈/碧居结衣
返回: 你输入了[{name1 夏色祭} {name2 神乐七奈} {name3 碧居结衣}]

所以:如果获取单个路径参数的值,那么可以通过 context.Param("key"),获取所有的路径参数,可以使用 context.Params,那么这两者之间的关系是怎么样的呢?

首先里面的 context.Params 是一个 Param 类型的切片。

type Context struct {
	writermem responseWriter
	Request   *http.Request
	Writer    ResponseWriter
	Params   Params  // 成员名 和 类型名 是一样的, 容易混淆
    // ......
}
type Params []Param  // 一个 Param类型的切片

type Param struct {
    Key   string  // 路径参数的名, 比如: name
    Value string  // 给路径参数传递的值, 比如之前传递的: 夏色祭
}

但是我们调用的 context.Param 可不是上面这个结构体,而是一个方法,再说结构体也没法调用啊。

// 我们看到这个方法名也叫Param, 所以确实有点容易混淆
func (c *Context) Param(key string) string {
    // 调用了c.Params.ByName(key), 至于逻辑, 不用想肯定是不断地循环每一个路径参数, 看看名字是否一样
	return c.Params.ByName(key)
}

func (ps Params) ByName(name string) (va string) {
	va, _ = ps.Get(name)
	return
}

func (ps Params) Get(name string) (string, bool) {
    // 循环, 如果 Key 和 指定的name相同, 那么返回对应的 value 和 true
	for _, entry := range ps {
		if entry.Key == name {
			return entry.Value, true
		}
	}
    // 否则返回空字符串 和 false
    // 所以从这里也能看出, 如果指定的路径参数不存在, 那么得到就是一个空字符
	return "", false
}

查询参数

Web提供的服务通常是client和server的交互。其中客户端向服务器发送请求,除了路径参数,其他的参数无非两种,查询字符串query string和报文体body参数。所谓query string,即路由中在路径之后用?连接的 key1=value2&key2=value2 的形式的参数。当然这个key-value会经过urlencode编码。

而查询字符串(查询参数)可以通过 DefaultQuery 或 Query 方法获取。而对于参数的处理,经常会出现参数不存在的情况,对于是否提供默认值,gin也考虑了,并且给出了一个优雅的方案。使用c.DefaultQuery方法读取参数,其中当参数不存在的时候,提供一个默认值;使用 Query 方法读取正常参数,当参数不存在的时候,返回空字串。

package main

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

func main() {
    router := gin.Default()
    router.GET("/girl", func(context *gin.Context) {
        // 不存在的话指定一个默认参数
        name := context.DefaultQuery("name", "夏色祭")
        // context.Query 等价于 context.Request.URL.Query().Get
        age := context.Request.URL.Query().Get("age")
        context.String(200, "姓名: %s; 年龄: %s", name, age)
    })
    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

我们来测试一下:

请求: http://localhost:22333/girl?age=16
返回: 姓名: 夏色祭; 年龄: 16

请求: http://localhost:22333/girl
返回: 姓名: 夏色祭; 年龄: 

请求: http://localhost:22333/girl?name=神乐七奈
返回: 姓名: 神乐七奈; 年龄: 

不过问题来了,查询参数是可以重复的,比如:

请求: http://localhost:22333/girl?name=神乐七奈&name=夏色祭
返回: 姓名: 神乐七奈; 年龄:

我们看到只返回了一个,如果希望都返回的话该怎么做呢?

package main

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

func main() {
    router := gin.Default()
    router.GET("/girl", func(context *gin.Context) {
        name := context.QueryArray("name")  // 多个值组成一个列表
        age := context.QueryArray("age")  // 多个值组成一个列表
        context.String(200, "姓名: %v; age: %v", name, age)
    })
    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

我们测试一下,看看返回结果。

请求: http://localhost:22333/girl?name=神乐七奈&name=夏色祭
返回: 姓名: [神乐七奈 夏色祭]; age: []

我们看到返回了都是切片,如果不存在相应的查询参数,则返回一个空切片。

表单参数

http的报文体传输数据就比query string稍微复杂一点,常见的格式就有四种。例如 application/json、application/x-www-form-urlencoded、application/xml 和 multipart/form-data,表单参数通过 PostForm 方法获取。

我们先定义一个模板文件吧。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登陆</title>
</head>
<body>
    <form action="/login" method="post" enctype="application/x-www-form-urlencoded">
        <table>
            <tr>
                <td>用户名: </td>
                <td><input type="text" name="username"></td>
            </tr>
            <tr>
                <td>密码: </td>
                <td><input type="password" name="password"></td>
            </tr>
            <tr>
                <td>夏色祭可爱吗: </td>
                <td>
                    <input type="checkbox" name="hobby" value="girl">可爱
                    <input type="checkbox" name="hobby" value="girl">非常可爱
                    <input type="checkbox" name="hobby" value="girl">超级可爱
                </td>
            </tr>
        </table>
    </form>
</body>
</html>

然后我们使用 gin 来渲染这个模板,所以这个 Handler 要同时支持 GET 和 POST 请求。

package main

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

func loginHandler (context *gin.Context) {
    if context.Request.Method == "GET" {
        // 调用context.HTML 渲染模板
        // 状态码、模板名、参数( 用于渲染模板中的 {{}}, 这里我们没有使用模板语法, 所以传个 gin.H{} 即可 )
        context.HTML(200, `login.html`, nil)
    } else {
        // 如果不存在的话, 得到的是空字符串, 但是我们也可以设置默认值, 和Query是类似的
        username := context.DefaultPostForm("username", "夏色祭")
        password := context.PostForm("password")
        // 如果提交多个值, 我们可以使用PostFormArray获取
        可愛い := context.PostFormArray("可愛い?")  // 变量命名请用英文, 我这里只是单纯为了好玩
        context.String(200, "姓名: %v; 密码: %v; 可愛い: %v", username, password, 可愛い)
    }
}

func main() {
    router := gin.Default()
    // 这里很关键, 我们的 login.html 是写在当前目录的 templates 目录中的, 所以必须指定模板所在的目录
    // templates/* 表示从templates目录中加载模板文件
    router.LoadHTMLGlob("templates/*")
    router.Any("/login", loginHandler)
    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

下面访问一下:localhost:22333/login 看看结果。

gin 框架的使用

还是很方便的,顺便提一句,gin 底层也是大量使用了 net/http 标准库的,只是使用起来更加的方便,下面总结一下:

  • 路径参数: 形如/girl/:name
    • context.Param("name")获取指定名称的路径参数的值;
    • context.Params获取所以路径参数的值;
  • 查询参数: 形如/girl?name=夏色祭&age=16
    • context.DefaultQuery("name", "夏色祭"), 获取指定查询参数的值, 不存在的话使用默认值;
    • context.Query("name"), 获取指定查询参数的值, 不存在的话返回空字符串;
    • context.QueryArray("name"), 获取指定查询参数的值, 以列表形式返回, 因为可以指定多个同名的查询参数;
  • 表单参数: 和查询参数类似
    • context.DefaultPostForm("username", "夏色祭"), 获取指定表单参数的值, 不存在的话使用默认值;
    • context.PostForm("username"), 获取指定表单参数的值, 不存在的话使用空字符串;
    • context.PostFormArray("name"), 获取指定表单参数的值, 以列表形式返回, 因为可以指定多个同名的表单参数;

文件上传

前面介绍了基本的数据发送,其中 multipart/form-data 专用于文件上传。gin文件上传也很方便,和原生的net/http方法类似,不同在于gin把原生的Request封装到context.Request中了。

老规矩,先定义一个html文件:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登陆</title>
</head>
<body>
    <form action="/upload" method="post" enctype="multipart/form-data">
        <table>
            <tr>
                <td>用户名: </td>
                <td><input type="text" name="username"></td>
            </tr>
            <tr>
                <td>密码: </td>
                <td><input type="password" name="password"></td>
            </tr>
            <tr>
                <td>文件: </td>
                <td>
                    <input type="file" name="file_upload">
                </td>
            </tr>
            <tr>
                <td>提交: <input type="submit" value="提交"></td>
            </tr>
        </table>
    </form>
</body>
</html>

然后看看如何使用 gin 接收文件:

package main

import (
    "github.com/gin-gonic/gin"
    "io/ioutil"
    "log"
)

func loginHandler (context *gin.Context) {
    if context.Request.Method == "GET" {
        context.HTML(200, "upload.html", nil)
    } else {
        username := context.DefaultPostForm("username", "夏色祭")
        password := context.PostForm("password")
        // 直接使用 FromFile 即可, 返回一个 FileHeader 和一个 error, 这个 FileHeader 我们在介绍 net/http 的时候说过
        file, _ := context.FormFile("file_upload")
        // _ = context.SaveUploadedFile(file, "新上传的: " + file.Filename) // 可以直接保存起来, 传递一个 *FileHeader 和 一个路径(string)
        // 但是在介绍net/http的时候说过, 也可以调用 open 方法, 直接返回 io.Reader
        f, _ := file.Open()
        data, _ := ioutil.ReadAll(f)
        context.String(200, "姓名: %v; \n密码: %v; \n文件内容: \n%v", username, password, string(data))
    }
}

func main() {
    router := gin.Default()
    router.LoadHTMLGlob("templates/*")
    router.Any("/upload", loginHandler)
    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

gin 框架的使用

所以我们看到还是很方便的,主要是 gin 把所有的参数都封装到 *gin.Context 中了,我们直接通过 context 就可以进行调用了。

但是注意,如果用户没有传递文件,那么得到的 file 就是个 nil。所以 FormFile 返回两个参数,第二个参数是一个 error,只不过这里我们省略了,但是生产环境中是需要进行处理的。

多文件上传

多文件上传也很简单,只是多写一个循环罢了。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登陆</title>
</head>
<body>
    <form action="/upload" method="post" enctype="multipart/form-data">
        <table>
            <tr>
                <td>用户名: </td>
                <td><input type="text" name="username"></td>
            </tr>
            <tr>
                <td>密码: </td>
                <td><input type="password" name="password"></td>
            </tr>
            <tr>
                <td>文件: </td>
                <td>
                    <input type="file" name="file_upload" multiple>
                </td>
            </tr>
            <tr>
                <td>提交: <input type="submit" value="提交"></td>
            </tr>
        </table>
    </form>
</body>
</html>

我们看看如何使用 gin 来接收多个文件:

package main

import (
    "fmt"
    "github.com/gin-gonic/gin"
    "io/ioutil"
    "log"
    "strings"
)

func loginHandler (context *gin.Context) {
    if context.Request.Method == "GET" {
        context.HTML(200, `upload.html`,nil)
    } else {
        buf := strings.Builder{}
        username := context.DefaultPostForm("username", "夏色祭")
        password := context.PostForm("password")
        buf.WriteString(fmt.Sprintf("姓名: %v;\n\n密码: %v;\n\n", username, password))
        // 这里和net/http是类似的, 返回的 form 是一个 *multipart.Form
        // 这是一个结构体里面有两个成员, 分别是 Value map[string][]string 和 File map[string][]*FileHeader
        // Value用于获取表单内容, File用于获取文件内容; 但是我们只获取文件, 表单内容依旧可以通过 context 直接获取
        form, err := context.MultipartForm()
        if err != nil {
            context.String(404, err.Error())
            return
        }

        // 获取多个文件, 会返回一个 []*FileHeader
        files := form.File["file_upload"]
        // 直接循环遍历即可
        for _, file := range files {
            // 可以直接保存, 这里我们返回给客户端
            if f, err := file.Open(); err != nil {
                context.String(404, err.Error())
            } else {
                data, _ := ioutil.ReadAll(f)
                buf.WriteString(fmt.Sprintf("文件名: %v, 文件内容:\n%v\n\n", file.Filename, string(data)))
            }
        }
        context.String(200, buf.String())
    }
}

func main() {
    router := gin.Default()
    router.LoadHTMLGlob("templates/*")
    router.MaxMultipartMemory = 8 << 20  // 限制上传的文件大小
    router.Any("/upload", loginHandler)
    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

下面就来上传文件,我们将之前的文件再拷贝两份之后上传吧。

gin 框架的使用

路由组

router group是为了方便管理一部分相同的URL,举个栗子:

/shop/index
/shop/cart
/shop/order
/shop/rank

/book/index
/book/cart
/book/order
/book/rank

上面这种路由明显是可以进行分类的,shop分为一组,book分为一组。而 gin 也支持我们这么做:

package main

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

func main() {
    router := gin.Default()
    // 定义一个组, 然后使用这个组注册路由
    v1 := router.Group("/v1")
    // 这个大括号有没有均可, 只是为了美观
    {	
        // 以后在访问的时候要加上对应的前缀, 比如: v1/login
        v1.GET("/login", func(context *gin.Context) {
            context.String(200, "v1: login")
        })
        v1.GET("/access", func(context *gin.Context) {
            context.String(200, "v1: access")
        })
    }

    v2 := router.Group("/v2")
    {
        v2.GET("/login", func(context *gin.Context) {
            context.String(200, "v2: login")
        })
        v2.GET("/access", func(context *gin.Context) {
            context.String(200, "v2: access")
        })
    }
    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

比较简单,相信不需要解释,我们演示一下:

请求: http://localhost:22333/v1/login
返回: v1: login

请求: http://localhost:22333/v1/access
返回: v1: access

请求: http://localhost:22333/v2/login
返回: v2: login

请求: http://localhost:22333/v2/access
返回: v2: access

当然组之间也是可以层层嵌套的,举个栗子:

package main

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

func main() {
    router := gin.Default()
    // 定义一个组, 然后使用这个组注册路由
    v1 := router.Group("/v1")
    {
        v1.GET("/login", func(context *gin.Context) {
            context.String(200, "v1: login")
        })
        v1.GET("/access", func(context *gin.Context) {
            context.String(200, "v1: access")
        })
		
        // 嵌套路由组
        v2 := v1.Group("/v2")
        v2.GET("/login", func(context *gin.Context) {
            context.String(200, "v1/v2: login")
        })
        v2.GET("/access", func(context *gin.Context) {
            context.String(200, "v1/v2: access")
        })
    }

    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

相信你已经知道结果了。

请求: http://localhost:22333/v1/login
返回: v1: login

请求: http://localhost:22333/v1/access
返回: v1: access

请求: http://localhost:22333/v1/v2/login
返回: v1/v2: login

请求: http://localhost:22333/v1/v2/access
返回: v1/v2: access

因此路由组最明显的好处就是分类,在划分业务逻辑和API版本的时候推荐使用路由组。

映射请求数据

映射请求数据指的是什么呢?首先我们在解析数据的时候是不是一个字段一个字段解析的呢,比如:username解析一下、password解析一下,如果字段非常多的话是不是很麻烦呢?而 gin 已经帮我们想好了,那么下面就来看看它是怎么做的吧。

数据解析绑定

顾名思义,就是将数据绑定给一个类型,然后根据用户传递的数据解析到指定的结构体变量中(类似于序列化和反序列化)。

package main

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

type Girl struct {
    // 这里字段一定要大写, 至于``里面的内容一会单独说
    Username string `form:"username1" json:"username2"`
    Password string `form:"password1" json:"password2"`
}

func loginHandler(context *gin.Context) {
    var g Girl
    // 将数据和结构体对象g绑定在一起, 这样传递数据时就会自动赋值给g里面对应的成员
    err := context.ShouldBind(&g) // 一定要传递指针, 因为Go是值传递
    if err != nil {
        context.String(404, "解析错误")
        return
    }
    context.String(200, "username: %v, password: %v", g.Username, g.Password)
}

func main() {
    router := gin.Default()
    router.POST("/login", loginHandler)

    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

这里我们解释一下结构体中的:form:"username1" json:"username2",因为绑定的数据可以来自于表单(application/x-www-form-urlencoded),也可以来自于json(application/json)。如果是通过表单,那么表单中name="username1"的值会给结构体中Username成员,name="password1"的值会给结构体中Password成员;如果是通过json提交,那么json中key为"username2"对应的value会给Username成员,key为"password2"对应的成员会给Password成员。

所以``当中的form和json应该都是同一个名字,只不过我们这里为了演示用法可以用的不同的名字。

下面我们使用Python来测试一下,当然你也可以使用Postman。

gin 框架的使用

但是问题来了,gin 是怎么做到的呢?因为我们提交的时候会有一个 Content-Type,gin 会根据请求类型判断解析的时候究竟该用哪种方式解析。

另外如果解析失败的话,就会报错,举个栗子:

type Girl struct {
    // 一般form和json起的都是相同的名字, 因为无论使用表单提交、还是使用json提交, 参数名应该都是一样的
    // 不存在表单提交使用 username1、password1, json提交使用 username2、password2
    Username string `form:"username" json:"username"`
    Password int `form:"password" json:"password"`  // 这里我们将 string 改成 int
}

func loginHandler(context *gin.Context) {
    var g Girl
    err := context.ShouldBind(&g) // 一定要传递指针, 因为Go是值传递
    if err != nil {
        context.String(404, "解析错误, 错误原因: %v", err.Error())
        return
    }
    context.String(200, "username: %v, password: %v", g.Username, g.Password)
}

我们来测试一下:

gin 框架的使用

除了ShouldBind之外,还有BindJSON,默认按照json格式解析;以及Bind,默认按照form表单格式进行解析,它们的用法都是一样的。

注意:使用绑定方法时,gin 会根据请求头中的 Content-Type 来自动推断需要解析的类型。另外,如果没有传递相应的值,那么绑定字段会得到零值,如果你希望这种情况下不给零值、而是报错的话,那么可以在结构体成员标签中加上 binding:"required",此时如果值为空,那么会请求失败并返回错误。

package main

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

type Girl struct {
    // 要求字段必须传递
    Username string `form:"username" json:"username" binding:"required"`
    Password string `form:"password" json:"password" binding:"required"`
}

func loginHandler(context *gin.Context) {
    var g Girl
    err := context.ShouldBind(&g)
    if err != nil {
        context.String(404, "解析错误, 错误原因: %v", err.Error())
        return
    }
    context.String(200, "username: %v, password: %v", g.Username, g.Password)
}

func main() {
    router := gin.Default()
    router.POST("/login", loginHandler)

    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

gin 框架的使用

所以如果希望只支持 form 或者 json 的话,那么就将标签中对应的 form 或者 json去掉,然后加上 binding:"required" 即可,这样的话就必须传递而且通过指定的方式进行传递。

URL绑定

下面来看看如何在GET请求中,如何和URL中的路径参数进行绑定:

package main

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

type Girl struct {
    // 路径参数的话, 标签使用uri, 当然这里的 form 和 json可以删掉了, 但是写上也不碍事
    Username string `form:"username" json:"username" uri:"username"`
    Password string `form:"password" json:"password" uri:"password"`
}

func loginHandler(context *gin.Context) {
    var g Girl
    // 将路径参数的值绑定到结构体上面, 这里使用ShouldBindUri
    err := context.ShouldBindUri(&g)
    if err != nil {
        context.String(404, "解析错误, 错误原因: %v", err.Error())
        return
    }
    // 通过context.Param进行获取
    username := context.Param("username")
    password := context.Param("password")
    // 然后进行显示, 结果肯定是一样的
    context.String(200, "username: %v, password: %v\n", username, password)
    context.String(200, "username: %v, password: %v", g.Username, g.Password)
}

func main() {
    router := gin.Default()
    // 指定路径参数
    router.GET("/login/:username/:password", loginHandler)

    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

然后请求一下试试:

gin 框架的使用

这里的结构体中的字段可以不需要 binding:"required" 了,因为这是GET请求、而且是路径参数,如果不传递的话压根就访问不到这个路由。

当然啦,form表单有了、json也有了,路径参数也有啦,那么怎么能少了查询参数呢?

package main

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

type Girl struct {
    // 令人费解的是, 查询参数对应的tag也是使用 query
    Username string `form:"username"`
    Password string `form:"password"`
}

func loginHandler(context *gin.Context) {
    var g Girl
    // 将查询参数的值绑定到结构体上面, 这里使用ShouldBindQuery
    err := context.ShouldBindQuery(&g)
    if err != nil {
        context.String(404, "解析错误, 错误原因: %v", err.Error())
        return
    }
    username := context.Query("username")
    password := context.Query("password")
    context.String(200, "username: %v, password: %v\n", username, password)
    context.String(200, "username: %v, password: %v", g.Username, g.Password)
}

func main() {
    router := gin.Default()
    router.GET("/login", loginHandler)

    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

gin 框架的使用

还没完,除了上述之外,gin 还支持请求头的数据绑定,我们来看一下:

package main

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

type Girl struct {
    Username string `header:"username"`
    Password string `header:"password"`
}

func loginHandler(context *gin.Context) {
    var g Girl
    // 绑定请求头的话使用ShouldBindHeader
    err := context.ShouldBindHeader(&g)
    if err != nil {
        context.String(404, "解析错误, 错误原因: %v", err.Error())
        return
    }
    // 等价于 context.Request.Header.Get()
    username := context.GetHeader("username")
    password := context.GetHeader("password")
    context.String(200, "username: %v, password: %v\n", username, password)
    context.String(200, "username: %v, password: %v", g.Username, g.Password)
}

func main() {
    router := gin.Default()
    router.GET("/login", loginHandler)

    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

gin 框架的使用

常见的数据绑定就是以上几种,我们总结一下:

  • ShouldBind: 用于绑定json和form, 结构体字段的标签也是用json和form;
  • ShouldBindJson: 用于绑定json;
  • ShouldBindUri: 用于绑定uri, 结构体字段的标签使用uri;
  • ShouldBindQuery: 用于绑定查询字段, 结构体字段的标签使用form;
  • ShouldBindHeader: 用于绑定请求头, 结构体字段的标签使用header;

以上这些统统可以使用ShouldBindWith:

context.ShouldBindWith(&g, binding.JSON)
context.ShouldBindWith(&g, binding.Header)
//.......

但是一般我们很少使用这种方式。

gin 响应数据

响应数据我们已经见识过了,像 context.String、context.JSON、context.HTML等等;调用的方法不同,那么响应头也是不一样的,比如:

  • 如果是 context.String: 那么响应头中的 Content-Type 是 text/plain; charset=utf-8;
  • 如果是 context.JSON: 那么响应体头的 Content-Type 是 application/json; charset=utf-8;
  • 如果是 context.HTML: 那么响应体头的 Content-Type 是 text/html; charset=utf-8;

当然还有 context.XML、context.YAML、context.ProtoBuf 等等,这里我们主要介绍如何设置返回的响应头,正如请求一样,我们在返回数据的时候也可以进行设置一些头部信息,并且 gin 也提供了很优雅的方法。

package main

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

func main() {
    router := gin.Default()
    router.GET("/test", func(context *gin.Context) {
        // 等价于 context.Writer.Header().Set, 要是获取头部信息的话直接使用 context.GetHeader 即可
        context.Header("hello", "matsuri")
        context.Header("ping", "pong")
    })
    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

gin 框架的使用

这里自动将key大写了,当然这不奇怪,因为浏览器也是这样的,我们看一下:

package main

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

func main() {
    router := gin.Default()
    router.GET("/test", func(context *gin.Context) {
        // 设置 Content-Type
        context.Header("Content-Type", "xxx")
        context.Header("ping", "pong")
    })
    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

gin 框架的使用

当然啦我们还可以设置 cookie,context.Cookie 表示获取cookie,context.SetCookie表示设置cookie,可以自己试一下。

另外之前对了忘记说了,还有一个重定向,gin 里面如何实现重定向呢?我们来看一下:

package main

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

func main() {
    router := gin.Default()
    router.GET("/test", func(context *gin.Context) {
        context.Redirect(302, "http://www.baidu.com")
    })
    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

当我们访问 /test 的时候,就会跳转到百度页面。当然,gin 还支持我们神不知鬼不觉地跳转。

package main

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

func main() {
    router := gin.Default()
    router.GET("/test", func(context *gin.Context) {
        // 将路由换成 /test2
        context.Request.URL.Path = "/test2"
        // 直接执行 /test2 对应的处理函数
        router.HandleContext(context)
    })

    router.GET("/test2", func(context *gin.Context) {
        context.String(200, "这是 /test2")
    })
    
    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

gin 框架的使用

最后,我们还可以异步返回。比如:有一个处理函数,需要连接数据库执行复杂的任务,但是我们又不希望等到任务结束之后才返回。那么这个时候,就可以将任务异步执行。

package main

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

func main() {
    router := gin.Default()
    router.GET("/test", func(context *gin.Context) {
        go func() {
            // 由于 context 是贯穿整个请求的, 所以最好不让多个goroutine操作同一个context
            // 因此我们需要拷贝一份, 当然获取查询参数其实直接在外面获取就行
            c := context.Copy()
            query := c.Query("query")
            time.Sleep(time.Second * 2)  // 模拟任务执行

            // 注册一个路由
            router.GET("/test2", func(context *gin.Context) {
                context.String(200, "%s 已经执行完毕", query)
            })
        }()
        context.String(200, "任务已提交, 请稍后通过 /test2 查看")
    })

    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

gin 框架的使用

所以我们发现 go 里面的协程真的是非常方便,goroutine 确实是 go 的一个大杀器。

文件响应

这里我们再看看如何在 gin 中返回一个文件,我们之前都是将文件的内容读取出来之后返回的。另外,gin 里面还有关于模板方面的知识,这里我们就不说了,因为前后端分离现在是主流,后端只需要返回相应的json数据即可,前端拿到数据直接渲染。

如果还使用传统的mvc,那么一个模板文件里面必定会混有前端和后端逻辑,这就要求前端人员懂后端模板渲染语法,或者要求后端人员懂的前端语法。所以前后端分离之后就解决了这一问题,避免了前端和后端的直接交互。

当然我这里并不是说这一个项目的时候就一定要使用前后端分离,具体看自身的业务,只是我这里偷懒想找一个借口,因此 gin  渲染模板方面的知识这里就不说了。

func main() {
    router := gin.Default()
    // 显示指定目录下的文件
    router.StaticFS("/show_dir", http.Dir(`C:\Users\satori\go\src\github.com\gin-gonic\gin\render`))
    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

gin 框架的使用

点击的话,还可以查看具体文件的内容。当然啦,还可以返回一个具体的文件。

func main() {
    router := gin.Default()
    router.StaticFile("/image", "./kww7z6.jpg")
    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

但是这两种方式都不是很完美,因为StaticFS将整个目录的内容都返回了,而StaticFile一次只能返回一个文件。我们希望的是通过路径参数,来返回指定的文件,比如:/image/1.png,那么就返回 1.png。但是很遗憾,StaticFile不支持路径参数,因此只能回到传统的方式。

func main() {
    router := gin.Default()
    router.GET("/image/:file", func(context *gin.Context) {
        // 获取文件名
        filename := context.Param("file")
        // 设置请求头中的 Content-Disposition
        context.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
        // 读取文件内容
        data, _ := ioutil.ReadFile(filename)
        // 返回, 调用 context.Data 方法
        context.Data(200, "application/x-gzip", data)
    })
    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

此时即可指定访问的资源,会自动下载到本地。

gin 中间件

Go 的标准库 net/http 设计的一大特点就是特别容易构建中间件,而 gin 也提供为了类似的中间件。需要注意的是,中间件只对注册过的路由函数起作用,对于分组路由,嵌套使用中间件,可以限定中间件的作用范围。中间件分为 全局中间件、单个路由中间件、群组中间件。

我们之前说过,context 是 gin 的核心,它的构造如下:

// Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example.
type Context struct {
	writermem responseWriter
	Request   *http.Request
	Writer    ResponseWriter

	Params   Params
	handlers HandlersChain
	index    int8
	fullPath string

	engine *Engine
	params *Params

	// This mutex protect Keys map
	mu sync.RWMutex

	// Keys is a key/value pair exclusively for the context of each request.
	Keys map[string]interface{}

	// Errors is a list of errors attached to all the handlers/middlewares who used this context.
	Errors errorMsgs

	// Accepted defines a list of manually accepted formats for content negotiation.
	Accepted []string

	// queryCache use url.ParseQuery cached the param query result from c.Request.URL.Query()
	queryCache url.Values

	// formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH,
	// or PUT body parameters.
	formCache url.Values

	// SameSite allows a server to define a cookie attribute making it impossible for
	// the browser to send this cookie along with cross-site requests.
	sameSite http.SameSite
}

看一下里面的成员 handlers HandlersChain,通过源码我们知道 HandlersChain 不过是 []HandlerFunc 的别名。

type HandlersChain []HandlerFunc

// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)  // 而HandlerFunc是接收一个 *gin.Context 、没有返回值的函数

所以中间件和普通的处理函数、或者说路由函数之间没有任何区别,我们怎么写 HandlerFunc 就可以怎么写中间件。

说了这么多,那么什么是中间件呢?

中间件,英文名是 middleware,如果你了解 Python 的话,你可以简单地认为 中间件 就类似于 Python 里面的装饰器。如果每一个请求过来在处理的时候,都要经过相同的一步或多步,那么我们便可以将这一步或多步以中间件的形式抽离出来。

比如:如果你用过 flask,那么 @login_required 估计你再熟悉不过了,有些请求必须是登陆之后才可以进行的。那么便可以针对需要登陆的请求设置一个中间件,让那些所有需要登陆的请求都经过这个中间件,而不用每一个处理函数都写一个登陆验证逻辑。

所以我们看到中间件的一个好处就是可以减少代码量,使逻辑更加清晰。那么下面来看看,如何在 gin 里面使用中间件。

首先是全局中间件,注册中间件的方式使用 router.Use,我们在创建router的时候,是使用 gin.Default(),这里面便使用了中间件,我们看一下。

// gin 里面叫做 engine, 不过更习惯将其叫成 router
func Default() *Engine {
	debugPrintWARNINGDefault()
	engine := New()  // 创建一个engine
    // 调用Use方法, 我们看到这里注册了两个中间件, 一个是 Logger() 用于打印日志, 还有一个是 Recovery()
    // 因为我们在启动服务的时候, 请求到来的时候, 控制台会出现大量的日志信息
    // 以及某个处理函数出错的时候, 并没有挂掉, 这些都是中间件在背后"捣的鬼"(做的幕后工作)
	engine.Use(Logger(), Recovery())
	return engine
}

// 而Use方法也很简单, 接受多个HandlerFunc, 这个HandlerFunc我们之前见过, 就是一个 func(*Context)
// 它和 GET、POST等请求里面的处理函数是一样的
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
	engine.RouterGroup.Use(middleware...)
	engine.rebuild404Handlers()
	engine.rebuild405Handlers()
	return engine
}

下面我们来进行编码:

package main

import (
    "github.com/gin-gonic/gin"
    "log"
)
// 定义两个中间件
func middleware1(context *gin.Context) {
    context.String(200, "middleware1\n")
}
func middleware2(context *gin.Context) {
    context.String(200, "middleware2\n")
}

func main() {
    router := gin.Default()
    // 注册两个中间件, 此时注册的是全局中间件
    router.Use(middleware1, middleware2)
    router.GET("/test", func(context *gin.Context) {
        context.String(200, "test")
    })
    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

gin 框架的使用

此时在访问的时候,会先走两个中间件,然后再执行注册的处理函数。并且所有的内容都会返回给客户端,注意:我们之前一直没有说,如果在两个 context.String之间加上一个 time.Sleep 会怎么样。由于 HTTP 协议是无状态的,一次请求、一次返回,所以即便存在 time.Sleep,也会将整个函数逻辑执行完之后将内容一次性返回给客户端。另外我们看到,指定一个不存在的路由本来应该是返回 404 page not found,但是由于定义了全局中间件,所以即使访问不存在的路由,也会执行中间件。

package main

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

func middleware1(context *gin.Context) {
    context.String(200, "middleware1\n")
}
func middleware2(context *gin.Context) {
    context.String(200, "middleware2\n")
}

func main() {
    router := gin.Default()
    router.Use(middleware1, middleware2)
    router.NoRoute(func(context *gin.Context) {
        context.String(404, "页面去火星了。。。。")
    })
    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

gin 框架的使用

全局中间件说完了,那么如何注册局部中间件呢?很简单,我们看一下GET、POST这些方法。

// POST is a shortcut for router.Handle("POST", path, handle).
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodPost, relativePath, handlers)
}

// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodGet, relativePath, handlers)
}

这些方法都是可以接收多个 HandlerFunc,所以很明显,就在这里面注册即可。

package main

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

func middleware1(context *gin.Context) {
    context.String(200, "middleware1\n")
}
func middleware2(context *gin.Context) {
    context.String(200, "middleware2\n")
}

func main() {
    router := gin.Default()
    // 这里注册的是全局中间件, 任何请求都会执行这两个中间件
    router.Use(middleware1, middleware2)
    // 局部中间件
    router.GET("/test", middleware1, func(context *gin.Context) {
        context.String(200, "xxxxxx\n")
    }, middleware2)
    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

gin 框架的使用

因此正如之前说的,中间件和处理函数本质上没有任何区别,一个路由可以对应多个处理函数。如果有一个处理函数是所有请求都必须经过的,那么就可以将其注册成全局中间件的形式。

此外,中间件之间可以传递变量。通过 context.Set 设置变量,context.Get 获取变量。

func main() {
    router := gin.Default()
    router.GET("/test", func(context *gin.Context) {
        // 设置, Set方法接收 一个 string 和 一个 interface{}
        context.Set("ping", "pong")
    }, func(context *gin.Context) {
        // 获取, 然后返回 interface{} 和 error, 可以使用断言判断返回值类型, 不过这里我们就直接返回了
        s, _ := context.Get("ping")
        context.String(200, "%v", s)
    })

    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

gin 框架的使用

路由组也是可以进行中间件注册的,我们来看一下:

package main

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

func middleware1(context *gin.Context) {
    context.String(200, "middleware1\n")
}
func middleware2(context *gin.Context) {
    context.String(200, "middleware2\n")
}

func main() {
    router := gin.Default()
    v1_group := router.Group("/v1")
    v1_group.Use(middleware1, middleware2)
    /*
    或者直接使用
    v1_group := router.Group("/v1", middleware1, middleware2)
    也是可以的, 效果一样
     */
    {
        v1_group.GET("/test", func(context *gin.Context) {
            context.String(200, "xxxxxx\n")
        })
    }
    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

gin 框架的使用

此时只有 /v1 开头的路由过来才会走这两个中间件。

但是很多时候,我们都会讲中间件写成闭包的形式,比如:

package main

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

func login_required (ok bool) func (context *gin.Context) {
    return func(context *gin.Context) {
        if ok {
            // 需要登录验证, 必须通过查询参数传递 username 和 password
            _, ok1 := context.GetQuery("username")
            _, ok2 := context.GetQuery("password")
            // context.Query("username") 底层调用了 context.GetQuery("username")
            // context.GetQuery("username") 会返回一个 string 和 bool, 我们可以根据这个bool来判断用户是否传递了指定的查询参数
            if !ok1 || !ok2 {
                context.String(404, "请传递查询参数 username 和 password")
                // 我们知道一个路由可以对应多个处理函数(中间件), 当然还有全局中间件
                // 它们会像链子一样串起来, 一个执行完了执行下一个
                // 但是程序走到这里我们显然不希望再执行后面的处理函数, 而是让用户指定查询参数重新发起请求
                // 于是调用 context.Abort(), 表示就此结束, 不再执行后面的 HandlerFunc
                context.Abort()
            }
        }
    }
}

func main() {
    router := gin.Default()
    router.GET("/access1", login_required(true), func(context *gin.Context) {
        context.String(200, "/access1")
    })
    router.GET("/access2", login_required(false), func(context *gin.Context) {
        context.String(200, "/access2")
    })
    
    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

gin 框架的使用

所以我们便实现了相关的登录控制,/access1 要求传递相关查询参数,所以是 login_required(true);但是 /access2 不需要,所以是 login_required(false),但是事实上,这里也不需要这个中间件,当然如果还有其它逻辑的话就另当别论了。

context.Next

首先我们知道 context.Abort() 是中止后续 HandlerFunc 的执行,而除了 context.Abort之外还有一个 context.Next。从名字上看,context.Next 是直接执行下一个 HandlerFunc。

举个栗子:

package main

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

func middleware1(context *gin.Context) {
    context.String(200, "middleware1\n")
}

func middleware2(context *gin.Context) {
    // 如果是 /test1, 那么执行 Next()
    if context.Request.URL.Path == "/test1" {
        context.Next()
    }
    context.String(200, "middleware2\n")
}

func middleware3(context *gin.Context) {
    // 如果是 /test2, 那么执行 Next()
    if context.Request.URL.Path == "/test2" {
        context.Next()
    }
    context.String(200, "middleware3\n")
}

func middleware4(context *gin.Context) {
    context.String(200, "middleware4\n")
}

func main() {
    router := gin.Default()
    router.GET("/test1", middleware1, middleware2, middleware3, middleware4, func(context *gin.Context) {
        context.String(200, "/test1\n")
    })
    router.GET("/test2", middleware1, middleware2, middleware3, middleware4, func(context *gin.Context) {
        context.String(200, "/test2\n")
    })

    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

gin 框架的使用

我们来逐步分析,首先是 /test1,在经过中间件 middleware1 的时候,会返回 middleware1,很简单;但是在经过中间件 middleware2 的时候,调用了 context.Next(),这里会直接去执行 middleware3,然后是 middleware4,最后是处理函数本身。当最后一个 HandlerFunc 执行完毕之后,再将 middleware2 剩余的代码执行完毕即可。当然,对于 /test2 也是同理。

那么问题来了,这背后的机制是怎样的呢?首先多个 HandlerFunc 会被放在一个切片里,每个 HandlerFunc 都会有一个索引,当执行 context.Next 的时候,会直接将 index++,然后取出对应的 HandlerFunc,进行执行。我们通过源码来更好地理解这一过程:

func (c *Context) Next() {
    // 直接将 index++
    c.index++
    // 这里转成了int8, 说明每个路由对应的处理函数的数量是有限的
    for c.index < int8(len(c.handlers)) {
        // 选择下一个HandlerFunc, 传入*Context进行执行
	    c.handlers[c.index](c)
        // index++, 继续循环, 直到将最后一个 HandlerFunc 执行完毕
	    c.index++
    }
}

理解完 context.Next() 之后,那么 context.Abort() 就很好解释了,显然是直接将索引设置为最大值即可。

func (c *Context) Abort() {
    // 将整个 HandlerFunc 链条中断掉
    c.index = abortIndex
}
// 一个路由对应的 HandlerFunc 的最大数量是 63 个
const abortIndex int8 = math.MaxInt8 / 2

所以如果需要执行多个Next,那么执行顺序很容易观察。

package main

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

func middleware1(context *gin.Context) {
    context.String(200, "middleware1\n")
}

func middleware2(context *gin.Context) {
    context.Next()
    context.String(200, "middleware2\n")
}

func middleware3(context *gin.Context) {
    context.Next()
    context.String(200, "middleware3\n")
}

func middleware4(context *gin.Context) {
    context.String(200, "middleware4\n")
}

func main() {
    router := gin.Default()
    router.GET("/test1", middleware1, middleware2, middleware3, middleware4, func(context *gin.Context) {
        context.String(200, "/test1\n")
    })

    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

gin 框架的使用

执行到 middleware2,跳转到 middleware3,然后跳转到 middleware4;然后转过头执行 middleware3 剩余的代码,然后再执行 middleware2 剩余的代码。

如果是 Next 和 Abort 混合,那么也依旧不难。

package main

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

func middleware1(context *gin.Context) {
    context.String(200, "middleware1\n")
}

func middleware2(context *gin.Context) {
    context.Next()
    context.String(200, "middleware2\n")
}

func middleware3(context *gin.Context) {
    context.Abort()
    context.String(200, "middleware3\n")
}

func middleware4(context *gin.Context) {
    context.String(200, "middleware4\n")
}

func main() {
    router := gin.Default()
    router.GET("/test1", middleware1, middleware2, middleware3, middleware4, func(context *gin.Context) {
        context.String(200, "/test1\n")
    })

    if err := router.Run("localhost:22333"); err != nil {
        log.Fatal(err)
    }
}

gin 框架的使用

执行到 middleware3 的时候出现了Abort,改变index;当 middleware3 执行完毕的时候,由于index的改变,导致 middleware4 和处理函数不会被执行;然后回到 middleware2,执行完剩余部分代码,整个请求结束。

所以 *gin.Context 是gin里面最重要的部分,它贯穿了整个请求。

所以 *gin.Context 是gin里面最重要的部分,它贯穿了整个请求。

BasicAuth简单认证

关于认证,gin 也提供了相应的策略,而且使用起来非常简单。

package main

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

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

    // 定义一个路由组, 同时绑定一个中间件, 里面是用户名和密码
    authorized := router.Group("/admin", gin.BasicAuth(gin.Accounts{
        "夏色祭": "123456",
        "雫_lulu": "123456",
        "绯赤艾利欧": "123456",
        "神乐七奈": "123456",
    }))

    authorized.GET("/secrets", func(c *gin.Context) {
        // 访问的时候会弹出输入框, 提示输入用户名和密码
        // 如果用户名和密码输入正确, 那么 user 就是返回的用户名, 否则的话为 nil
        user, _ := c.Get(gin.AuthUserKey)

        if user != nil  {
            c.JSON(http.StatusOK, gin.H{"user": user})
        } else {
            c.JSON(http.StatusOK, gin.H{"user": user})
        }
    })

    if err := router.Run(":22333"); err != nil {
        log.Fatal(err)
    }
}

gin 框架的使用

认证失败的话,会让你重新填写;认证成功的话,会返回内容。

gin 路由原理

gin 的路由底层使用的是 httprouter,但是做了一些简单的修改,掌握了 httprouter,那么了解 gin 的路由实现也就不难了。

首先路由其实就相当于字符串匹配,路由器依赖于一种叫做前缀树的结构,这个有兴趣可以自己了解一下。

总结

总的来说,gin 是一个非常优秀的框架,在 GitHub 上所有 Go 的 Web 框架中,它的 Star 数是最高的。而且使用起来也比较简单,当然性能也是非常优秀的。琴酒,你好特么快啊。

相关文章:

  • 2021-05-24
  • 2021-11-22
  • 2021-05-21
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
猜你喜欢
  • 2022-12-23
  • 2022-02-21
  • 2021-03-25
  • 2022-01-04
  • 2021-09-21
  • 2022-12-23
  • 2022-12-23
相关资源
相似解决方案