作为一款企业级生产力的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. java集合 list与Set、Map区别

      1.List,Set都是继承自Collection接口,Map则不是. 2.List特点:元素有放入顺序,元素可重复 ,Set特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉 ,(注意:元素虽 ...

  2. Python实现线程交替打印字符串

    import threading con = threading.Condition() word = u"12345上山打老虎" def work(): global word ...

  3. 一站式自动化测试平台 http://www.Autotestplat.com

    Autotestplat 一站式自动化测试平台及解决方案 自动化平台开发 3.1 自动化平台开发方案 3.1.1 功能需求 支持 API.AppUI.WebUI 性能等自动化测试,集成实现测试用例管理 ...

  4. Leetcode 946. Validate Stack Sequences 验证栈序列

    946. Validate Stack Sequences 题目描述 Given two sequences pushed and popped with distinct values, retur ...

  5. Choway Blog

    choway 2018-12-11 09:23:46 JavaJVM Java 虚拟机(JVM)在执行 Java 程序时会把它管理的内存划分为多个不同的数据区域.这些区域各有用途,以及创建和销毁的时间 ...

  6. Autotestplat.com 更新了!

    1 提供测试发帖留言功能 2 自动化平台体验功能 3 提供招聘资讯功能       4 提供推荐书籍功能

  7. Python——5函数

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

  8. Zookeeper的使用场景和集群配置

    Zookeeper的介绍 ZK在分布式系统的应用 Zookeeper搭建 集群角色介绍 ZK的常用命令 一.Zookeeper的介绍 官方:ZooKeeper是一个分布式的,开放源码的分布式应用程序协 ...

  9. CSS 实现元素较宽不能被完全展示时将其隐藏

    首发于本人的博客 varnull.cn 遇到一个需求,需要实现的样式是固定宽度的容器里一排显示若干个标签,数量不定,每个标签的长度也不定.当到了某个标签不能被完全展示下时则不显示.大致效果如下,标签只 ...

  10. Java的三魂七魄 —— 高级多线程

    目录 Java的三魂七魄 -- 高级多线程 一.多线程的创建 二.线程安全问题 三.线程通信问题 四.更多实例 1.用线程同步的方法解决单例模式的线程安全问题 2.银行存钱问题(线程安全问题) 3.生 ...