作为一款企业级生产力的web框架,gin的优势是显而易见的,高性能,轻量级,易用的api,以及众多的使用者,都为这个框架注入了可靠的因素。截止目前为止,github上面已经有了 35,994 star. 一个开源框架,关注数越多,就会越可靠,因为大家会在使用当中不断地对它进行改进。

下面放几个链接方便进行查看:

几个流行的go框架进行比较

go几大web框架比较 这个主页对几大web框架进行了一些比较,主要是统计了github star last commit time 等等信息,可以作为一个参考。

几大优势

  • 速度快: 高性能,无反射代码,低内存消耗
  • 中间件(拦截器): 可以更优雅的实现请求链路上下文的控制,比如日志,身份验证等等
  • Crash保活: 当一个请求挂掉之后,并不影响服务器的稳定运行
  • 数据验证
  • 分组的API管理: 当需要给特定请求加验证,一些请求又不需要的时候,可以很方便的实现
  • 错误管理
  • 简单易用而丰富的类型支持: Json, Xml, Html 等等

简单的使用

引入项目

现在有方便的go mod支持,引入变得非常简单,直接在需要使用的代码文件处 import "github.com/gin-gonic/gin" 即可

gin的HelloWorld

package main

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

func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080
}

使用如上代码,便可轻松启动一个监听所有请求,端口为8080(默认) 的服务了。可以尝试用 curl 进行测试:

$ curl localhost:8080/ping
output: {"message":"pong"}

如果想监听在其它端口,可以进行修改 r.Run("0.0.0.0:9000")

Get 请求以及参数获取

现在要发起一个请求: curl 'localhost:8080/send?a=1&b=2',现在来看看我们如何通过 *gin.Context 拿到传参呢,这里我们省去一些代码

g.GET("/send", func(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"a": ctx.Query("a")
"b": ctx.Query("b"),
})
}) // output: {"a":"1","b":"2"}

我们把拿到的参数又返回给了客户端

假如前端此时需要传一个数组到服务器,通过GET方式,这时候该怎么办呢,此时有三个办法

  • 客户端 curl 'localhost:8080/send?a=1&a=2' 传递同样的 key, web 框架会当做数组处理
g.GET("/send", func(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"a": ctx.QueryArray("a"),
})
}) // output: {"a":["1","2"]}
  • 客户端 curl 'localhost:8080/send?a=["1", "2"]' 这里是使用json字符串的形式传递数组,注意这里面包含了 url 不允许直接传输的字符,比如 [ ]" 等,需要进行url编码, 可以在 UrlEncode编码/UrlDecode解码 - 站长工具 这里进行转换一下,转换后的结果如下:

    curl 'localhost:8080/send?a=%5b%221%22%2c+%222%22%5d', gin 相关代码如下:
g.GET("/send", func(ctx *gin.Context) {
out := []string{} err := json.Unmarshal([]byte(ctx.Query("a")), &out)
if err != nil {
ctx.JSON(200, gin.H{
"error": err.Error(),
})
return
} ctx.JSON(200, gin.H{
"a": out,
})
}) // output: {"a":["1","2"]}
  • 第三种就是传递参数的时候,传递一个字符串,每个元素之间用,号等分割一下,在服务端取到该字符串之后,再利用strings.Split()函数分割成数组即可,这里就不例举代码了。

NOTE: 如果 query 取的key不存在,会得到什么呢?答案是空字符串,或者你也可以使用

func (c *Context) GetQuery(key string) (string, bool) 这个方法,可以返回一个 bool 用来判断是否存在

路径参数Path该如何获取

curl 'localhost:8080/send/1?b=2'

g.GET("/send/:id", func(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"id": ctx.Param("id"),
"b": ctx.Query("b"),
})
}) // output: {"b":"2","id":"1"}

同样,如果获取不到则为空字符串,如果路径参数忘了传,则url匹配不上,就会报404

Post 请求及其参数获取

众所周知,post请求,传输的数据是会在body里面的,在gin里面是怎么获取的呢

curl -XPOST 'localhost:8080/send?a=1' -d "b=2&c=3", 这里也带上了 query parameter

g.POST("/send", func(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"a": ctx.Query("a"),
"b": ctx.PostForm("b"),
"c": ctx.PostForm("c"),
})
}) // output: {"a":"1","b":"2","c":"3"}

同样,如果参数不存在,也是获取到空字符串

模型绑定

gin提供了模型绑定,方便参数的规范化,简单来说,模型绑定就是把参数解析出来,放在你定义好的结构体里面。模型绑定的好处如下

  • 规范化数据
  • 能够将string数据解析为你希望的类型,比如 uint32
  • 能够使用参数验证器

最常使用的模型绑定方法

gin 对模型绑定出错的处理分了两个大类

  • Bind*方法,以及MustBindWith方法 出错会将返回code置为400
  • ShouldBind* 方法,出错不会设置返回code,可以自己控制返回的code,一般来说,直接调 ShouldBind方法就行了,它会自动判断 Content-Type 选择相应的绑定

Query Param 绑定

请求为 curl 'localhost:8080/send?a=haha&b=123', go代码如下

g.GET("/send", func(ctx *gin.Context) {
type Param struct {
A string `form:"a" binding:"required"`
B int `form:"b" binding:"required"`
} param := new(Param)
if err := ctx.ShouldBind(param); err != nil {
ctx.JSON(400, gin.H{
"err": err.Error(),
})
return
} ctx.JSON(200, gin.H{
"Content-Type": ctx.ContentType(),
"a": param.A,
"b": param.B,
})
}) // output: {"Content-Type":"","a":"haha","b":123}

如果什么都不传,因为设置了 binding:"required" 这个tag,于是在绑定最后验证时候,会报错

Query 与 Form Param 同时绑定

请求为 curl -XPOST 'localhost:8080/send?a=haha' -d "b=2&c=3", go代码如下

g.POST("/send", func(ctx *gin.Context) {
type Param struct {
A string `form:"a" binding:"required"`
B int `form:"b" binding:"required"`
C int `form:"c" binding:"required"`
} param := new(Param)
if err := ctx.ShouldBind(param); err != nil {
ctx.JSON(400, gin.H{
"err": err.Error(),
})
return
} ctx.JSON(200, gin.H{
"a": param.A,
"b": param.B,
"c": param.C,
})
}) // output: {"a":"haha","b":2,"c":3}

可以看到,Query 和 Form 参数都是用的 form 这个tag

Path 路径参数绑定

上面看到了,Query 和 Form 是可以绑定到一个结构体当中,但是路径参数就只能单独进行绑定了(如果不需要使用参数验证,则直接用 ctx.Param(key)方法即可),需要单独绑定到一个结构体当中, 使用ctx.ShouldBindUri() 这个方法进行绑定。

请求为 curl 'localhost:8080/send/haha', go代码如下

g.GET("/send/:name", func(ctx *gin.Context) {
type Param struct {
A string `uri:"name" binding:"required"`
} param := new(Param)
if err := ctx.ShouldBindUri(param); err != nil {
ctx.JSON(200, gin.H{
"err": err.Error(),
})
return
} ctx.JSON(200, gin.H{
"a": param.A,
})
})

如果觉得绑定到2个结构体很麻烦,可以自己实现 Binding 接口,然后使用自己实现的Bind方法即可

模型绑定方法总结

强制绑定

  • func (c *Context) MustBindWith(obj interface{}, b binding.Binding) error 通用的强制绑定方法,出错则置返回code为400,一般不直接用此方法
  • func (c *Context) Bind(obj interface{}) error 调用 MustBindWith 自动根据请求类型来判断绑定
  • func (c *Context) BindHeader(obj interface{}) error 调用 MustBindWith 绑定请求头,tag使用header
  • func (c *Context) BindJSON(obj interface{}) error 调用 MustBindWith 绑定json,tag使用json
  • func (c *Context) BindQuery(obj interface{}) error 调用 MustBindWith 绑定 Query Param,tag使用form
  • func (c *Context) BindUri(obj interface{}) error 调用 MustBindWith 绑定Path路径参数,tag使用uri
  • func (c *Context) BindXML(obj interface{}) error 调用 MustBindWith 绑定xml,tag使用xml
  • func (c *Context) BindYAML(obj interface{}) error 调用 MustBindWith 绑定yaml,tag使用yaml

非强制绑定

  • func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error 通用的绑定方法
  • func (c *Context) ShouldBind(obj interface{}) error 调用 ShouldBindWith 自动根据请求类型判断绑定
  • 其余方法这里不列出,都是在上述方法基础上加 Should 并且都是调用 ShouldBindWith, 下面说两个不一样的

这里说一个需要注意的问题,如果是数据存储于 Body 里面的,gin是封装的标准库的http,而 Body 是io.ReadCloser 类型的,只能读取一次,之后就关闭,内容只允许读一次,也就是说,上述的 Bind 凡是读 Body 的,都不能再读第二次,这个可以用其他办法解决,这里暂且只说一个,那就是

func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (err error) 方法,这个方法允许调用多次,因为它将内容暂时存在了 gin.Context 当中,比如绑定json如下代码所示:

ctx.ShouldBindBodyWith(&objA, binding.JSON)

还有个注意点就是,绑定的结构体,如果包含有子结构体,对于 form 传参来说,是不会有什么影响的,比如 a=1&b=2&c=3 , a b c 可以分别在不同的结构体中,可以是结构体指针也可以是结构体,具体可以参考 这里

服务器返回

这里总结下服务器返回的方法,不过调用完成之后,记得return

func (c *Context) String(code int, format string, values …interface{}) 返回类型为 string

func (c *Context) JSON(code *int*, obj interface{}) 这个用得最多的,返回 json

还有许多方法,这里不一一列举,可以参考 gin 源码学习

中间件(或叫拦截器)

中间件是在请求前后做一些事情,比如验证登录,打印日志等等工作,可以将接口逻辑划分开来,与业务代码分离,下面看看中间件是怎么使用的

中间件函数的定义其实和普通请求接口的定义是一样的,都是 type HandlerFunc func(*Context),中间件分为以下三类作用域

  • 全局中间件
  • group中间件
  • 单个接口级别的中间件

中间件的作用顺序是,定义在前面的先生效,也就是定义在前面的会先调用,而且可以定义多个中间件

全局中间件: 对所有请求接口都有效

group中间件: 对该组的接口有效

单个接口级别中间件: 只对该接口有效

现在介绍几个gin自带的全局中间件,还记得初始化gin的时候,调用的哪个方法吗,就是 gin.Default(),下面看看它的源码

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

可以看到,也是调用 New() 这个函数构造了 Engine 对象,并且初始化了 2 个中间件,一个用于日志打印,另一个用于崩溃恢复,这2个都是全局中间件

gin主页的README有段代码,清晰的解释了这三种中间件的定义

func main() {
// 使用New()初始化
r := gin.New() // 全局中间件: 日志打印
r.Use(gin.Logger()) // 全局中间件:
r.Use(gin.Recovery()) // 单个接口的中间件
r.GET("/benchmark", MyBenchLogger(), benchEndpoint) authorized := r.Group("/")
// 分组中间件: 只对该组接口有效
authorized.Use(AuthRequired())
{
authorized.POST("/login", loginEndpoint)
authorized.POST("/submit", submitEndpoint)
authorized.POST("/read", readEndpoint) testing := authorized.Group("testing")
testing.GET("/analytics", analyticsEndpoint)
} r.Run(":8080")
}

中间件彼此形成一条链条,对于每个请求来说,它的调用关系如下图:

  • 在中间件内部调用 ctx.Next() 即是调用链条的下一级方法,比如,在全局中间件里调用 Next,则表示调用 group中间件 函数,这就可以使用切面编程思想,把链条下一级函数看做一个切面,然后在前后做一些事情,比如计算接口的调用时间等。
  • 如果不显示调用Next,则该中间件函数执行完之后,会执行链条的下一级函数
  • 如果想要中断链条,则调用ctx.Abort() 函数,调用之后,会正常执行完当前中间件函数,但是不会再执行链条下一级了,而是准备返回接口。

举个例子

一般来说,定义一个中间件,都遵循下面这种风格,YourFunc() HandlerFunc 返回这个处理函数的方式,当然你也可以直接定义一个 HandlerFunc 也是可以的。

现在要实现一个功能,能够计算某个请求的耗时,使用中间件来完成,代码如下

timeCalc := func() gin.HandlerFunc {
return func(ctx *gin.Context) {
if ctx.Query("a") == "" {
ctx.Abort() // 终止调用链条
ctx.JSON(http.StatusBadRequest, gin.H{
"message": "a参数有问题,请检查参数",
})
return
} start := time.Now() // Next 在这里相当于 接口函数,在Next之前则在接口函数之前执行
fmt.Println("Next之前") ctx.Next() fmt.Println("Next之后")
cost := time.Since(start) // Next 之后,则相当于在接口函数之后执行,形成了一个切面 fmt.Printf("用时 %d 微秒\n", cost.Microseconds())
}
} g.GET("/send", timeCalc(), func(ctx *gin.Context) {
fmt.Println("进入接口函数")
ctx.JSON(http.StatusOK, gin.H{
"a": ctx.Query("a"),
})
}) // 服务端输出:
// Next之前
// 进入接口函数
// Next之后
// 用时 231 微秒

NOTE: 如果需要在接口链条的某一处,开辟一个gorutine进行处理,如果需要用到 gin.Context 的话,需要调用 ctx.Copy() 函数进行一份拷贝,然后在开辟的gorutine当中使用该拷贝

Gin一些开源中间件 这里可以找到一些比较实用的中间件,可以自己探索下

MODE

目前Gin有三种模式: debug release test 三种,可以通过设置 GIN_MODE 这个环境变量来控制

比如现在需要将这个web应用发布到正式环境,那么需要将生产机器上的gin的环境变量设置为 release: export GIN_MODE= release

在debug模式下,会在开头多一些打印

单元测试

下面这个例子可以参考一下

package main

import (
"encoding/json"
"io/ioutil"
"net/http"
"strings"
"testing" "github.com/stretchr/testify/assert"
) func do(t *testing.T, req *http.Request) ([]byte, error) {
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
} body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
} assert.Equal(t, 200, resp.StatusCode) return body, nil
} // curl 'localhost:8080/send?a=1&b=2'
func TestGet(t *testing.T) {
req, _ := http.NewRequest("GET", "http://localhost:8080/send?a=1&b=2", nil) body, err := do(t, req)
assert.NoError(t, err)
assert.Equal(t, "{\"a\":\"1\",\"b\":\"2\"}\n", string(body))
} // curl -XPOST 'localhost:8080/send' -H 'Content-Type: application/json' -d '{"a":1,"b":2,"c":3}'
func TestPost(t *testing.T) {
req, _ := http.NewRequest("POST", "http://localhost:8080/send", strings.NewReader(`{"a":1,"b":2,"c":3}`))
req.Header.Set("Content-Type", "application/json") // 传json记得修改 body, err := do(t, req)
assert.NoError(t, err) type Resp struct {
A int `json:"a"`
B int `json:"b"`
C int `json:"c"`
} resp := new(Resp)
assert.NoError(t, json.Unmarshal(body, resp)) assert.Equal(t, &Resp{
A: 1,
B: 2,
C: 3,
}, resp)
}

当你的代码嵌套比较多,并且不易于在单元测试当中去启动这个服务的时候,可以使用这个方法,单元测试就相当于开了一个http client,去请求已启动的服务,这时候需要先启动项目的服务,才能调用单元测试哦。

下面介绍一个独立的,也是gin源码经常使用的这种测试方法,可以独立运行,不依赖于已启动的服务

func TestIndependent0(t *testing.T) {
w := httptest.NewRecorder() // 用于返回的数据
ctx, _ := gin.CreateTestContext(w) // 模拟返回数据
ctx.JSON(http.StatusOK, gin.H{
"a": 1,
}) assert.Equal(t, "{\"a\":1}\n", string(w.Body.Bytes()))
} func TestIndependent1(t *testing.T) {
w := httptest.NewRecorder()
_, router := gin.CreateTestContext(w) router.GET("/send", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"a": 1,
})
})
router.ServeHTTP(w, httptest.NewRequest("GET", "http://localhost:8080/send", nil)) t.Log(string(w.Body.Bytes())) // output: {"a":1}
}

最后

自定义日志以及一些别的应用,留到下一篇文章

GO语言web框架Gin之完全指南(一)的更多相关文章

  1. GO语言web框架Gin之完全指南

    GO语言web框架Gin之完全指南 作为一款企业级生产力的web框架,gin的优势是显而易见的,高性能,轻量级,易用的api,以及众多的使用者,都为这个框架注入了可靠的因素.截止目前为止,github ...

  2. GO语言web框架Gin之完全指南(二)

    这篇主要讲解自定义日志与数据验证 参数验证 我们知道,一个请求完全依赖前端的参数验证是不够的,需要前后端一起配合,才能万无一失,下面介绍一下,在Gin框架里面,怎么做接口参数验证的呢 gin 目前是使 ...

  3. Go语言web框架 gin

    Go语言web框架 GIN gin是go语言环境下的一个web框架, 它类似于Martini, 官方声称它比Martini有更好的性能, 比Martini快40倍, Ohhhh….看着不错的样子, 所 ...

  4. 最好的6个Go语言Web框架

    原文:Top 6 web frameworks for Go as of 2017 作者:Edward Marinescu 译者:roy 译者注:本文介绍截至目前(2017年)最好的6个Go语言Web ...

  5. Go语言Web框架gwk介绍4

    Go语言Web框架gwk介绍 (四)   事件 gwk支持事件系统,但并没有硬编码有哪些事件,而是采用了比较松散的定义方式. 订阅事件有两种方式: 调用On函数或者OnFunc函数 func On(m ...

  6. Go语言Web框架gwk介绍 3

    Go语言Web框架gwk介绍 (三)   上一篇忘了ChanResult ChanResult 可以用来模拟BigPipe,定义如下 type ChanResult struct { Wait syn ...

  7. Go语言Web框架gwk介绍2

    Go语言Web框架gwk介绍 (二) HttpResult 凡是实现了HttpResult接口的对象,都可以作为gwk返回Web客户端的内容.HttpResult接口定义非常简单,只有一个方法: ty ...

  8. Go语言Web框架gwk介绍 1

    Go语言Web框架gwk介绍 (一)   今天看到Golang排名到前30名了,看来关注的人越来越多了,接下来几天详细介绍Golang一个web开发框架GWK. 现在博客园支持markdown格式发布 ...

  9. Go组件学习——Web框架Gin

    以前学Java的时候,和Spring全家桶打好关系就行了,从Spring.Spring MVC到SpringBoot,一脉相承. 对于一个Web项目,使用Spring MVC,就可以基于MVC的思想开 ...

随机推荐

  1. 安装 Kali Linux 2018.1 及之后的事

    本文为原创文章,转载请标明出处 目录 制作U盘启动盘 安装 Kali Linux 之后的事 更新源 配置 Zsh 配置 Vim 修改 Firefox 语言为中文 安装 Gnome 扩展 美化 安装 G ...

  2. 吴裕雄--天生自然 R语言开发学习:基本图形(续一)

    #---------------------------------------------------------------# # R in Action (2nd ed): Chapter 6 ...

  3. babel-loader的原理

    本文转载自默语的博客. Babel包的构成 核心包 babel-core:是babel转译器本身,提供转译的API,例如babel.transform等,webpack的babel-loader就是调 ...

  4. python入门机器学习,3行代码搞定线性回归

    本文着重是重新梳理一下线性回归的概念,至于几行代码实现,那个不重要,概念明确了,代码自然水到渠成. “机器学习”对于普通大众来说可能会比较陌生,但是“人工智能”这个词简直是太火了,即便是风云变化的股市 ...

  5. drbd配置

    DRBD就是网络RAID1,可以通过网络同步两个主机上的硬盘,drbd每次只允许对一个节点进行读写访问. 一.安装DRBD CentOS 6.x rpm -ivh http://www.elrepo. ...

  6. flume install

    flume install flume 安装 123456 [root@10 app][root@10 app]# mv apache-flume-1.7.0-bin /mnt/app/flume[r ...

  7. LeetCode 225题用队列实现栈(Implement Stack using Queues) Java语言求解

    链接 https://leetcode-cn.com/problems/implement-stack-using-queues/ 思路 首先演示push()操作:将元素依次进入队1,进入时用top元 ...

  8. GIT 使用(二):创建仓库并提交代码

    基本操作 所用命令使用 windows 下安装 git-bash 运行 Table of Contents 先决条件 已经安装了 GIT 客户端 已经设置用户信息 如果没做可以看安装和配置 获取 Gi ...

  9. C++扬帆远航——12(抓小偷)

    /* * Copyright (c) 2016,烟台大学计算机与控制工程学院 * All rights reserved. * 文件名:zhaoxiaotou.cpp * 作者:常轩 * 微信公众号: ...

  10. C++走向远洋——21(项目一,三角形,类)

    */ * Copyright (c) 2016,烟台大学计算机与控制工程学院 * All rights reserved. * 文件名:sanjiaoxing.cpp * 作者:常轩 * 微信公众号: ...