Golang 微框架 Gin


框架是敏捷开发的利器 借助框架 省时间精力 利于团队的编码风格和形成规范
Gin是Go写的微框架 封装优雅 API友好 源码注释明确 有快速灵活 容错方便等特点
Gin安装
go get github.com/gin-gonic/gin
用Gin实现Hello world 创建router 使用其Run的方法
package main
import (
   "github.com/gin-gonic/gin"
   "net/http"
)
func main(){   
   router := gin.Default()
   router.GET("/", func(c *gin.Context) {
         c.String(http.StatusOK, "Hello World")
   })
   router.Run(":8080")
}
gin.Default()方法 创建路由 通过HTTP方法绑定路由规则和路由函数
gin把 request 和 response 都封装到 gin.Context 上下文环境
最后是启动路由的Run方法监听端口 gin 支持 GET POST PUT DELETE PATCH HEAD OPTIONS  7个方法 和  Any 函数 常用的restful方法

restful路由

gin 路由来自 httprouter 库 具有httprouter的功能  gin不支持路由正则表达式
func main(){
   router := gin.Default()   
   router.GET("/user/:name", func(c *gin.Context) {
         name := c.Param("name")
         c.String(http.StatusOK, "Hello %s", name)
   })
} 冒号:加上一个参数名组成路由参数
用 c.Params 方法读取其值
值是字串string 诸如/user/i9527 和/user/hello都可以匹配 而/user/和/user/i9527/不会被匹配 用curl测试
curl http://127.0.0.1:8000/user/i9527
Hello i9527%
curl http://127.0.0.1:8000/user/i9527/
404 page not found%
curl http://127.0.0.1:8000/user/
404 page not found%

gin 提供*号处理参数

*号能匹配的规则更多
func main(){
   router := gin.Default()   
   router.GET("/user/:name/*action", func(c *gin.Context) {
         name := c.Param("name")
         action := c.Param("action")
         message := name + " is " + action
         c.String(http.StatusOK, message)
   })
}
访问效果
curl http://127.0.0.1:8000/user/i9527/
i9527 is /%
curl http://127.0.0.1:8000/user/i9527/中国
i9527 is /中国%

query string参数 与 body 参数

web提供的服务通常是 client和server 交互
客户端向服务器发送请求 除了路由参数
其他两种参数 是 查询字符串query string和报文体body参数

查询字符串query string

即路由用 ?以后连接的key1=value2&key2=value2的形式的参数  这个key-value是经过urlencode编码
对于参数的处理 经常会出现参数不存在的情况  gin提供默认值
func main(){
   router := gin.Default()
   router.GET("/welcome", func(c *gin.Context) {
         firstname := c.DefaultQuery("firstname", "Guest")
         lastname := c.Query("lastname")
         c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
   })
 router.Run()
}
c.DefaultQuery 方法读取参数 其中当参数不存在的时候 提供一个默认值
Query 方法读取正常参数 当参数不存在的时候 返回空字串
 curl http://127.0.0.1:8000/welcome
Hello Guest %
curl http://127.0.0.1:8000/welcome\?firstname\=中国
Hello 中国 %
curl http://127.0.0.1:8000/welcome\?firstname\=中国\&lastname\=天朝
Hello 中国 天朝%
curl http://127.0.0.1:8000/welcome\?firstname\=\&lastname\=天朝
Hello  天朝%
curl http://127.0.0.1:8000/welcome\?firstname\=%E4%B8%AD%E5%9B%BD
Hello 中国 % 中文是为了说明 urlencode 当firstname为空字串的时候 并不会使用默认的Guest值 空值也是值
DefaultQuery只作用于key不存在的时候 提供默认值

gin 报文体body参数

http 报文体传输数据 比 query string 复杂
常见四种格式
application/json
application/x-www-form-urlencoded
application/xml
multipart/form-data //用于图片上传
json格式的好理解
urlencode 是把query string的内容 放到了body体里
同样也需要 urlencode 默认情况下 c.PostFROM 解析的是 x-www-form-urlencoded 或 from-data 的参数
func main(){
   router := gin.Default()
   router.POST("/form_post", func(c *gin.Context) {
      message := c.PostForm("message")
      nick := c.DefaultPostForm("nick", "anonymous")
      c.JSON(http.StatusOK, gin.H{
            "status":  gin.H{
                  "status_code": http.StatusOK,
                  "status": "ok",
            },
            "message": message,
            "nick":   nick,
      })
   })
}
与get处理query参数一样 post方法也提供了处理默认参数的情况 同理 如果参数不存在 将会得到空字串
curl -X POST http://127.0.0.1:8000/form_post -H "Content-Type:application/x-www-form-urlencoded" -d "message=hello&nick=i9527" | python -m json.tool
% Total % Received % Xferd  Average Speed  Time   Time   Time  Current
Dload  Upload  Total  Spent   Left  Speed
100  104  100   79  100   25  48555  15365 --:--:-- --:--:-- --:--:-- 79000
{   "message": "hello",
   "nick": "i9527",
   "status": {"status": "ok","status_code": 200}
}
c.String 返回响应  返回string类型 content-type是plain或者text
调用c.JSON则返回json数据
其中gin.H封装了生成json的方式 使用golang可以像动态语言一样写字面量的json 对于嵌套json的实现 嵌套gin.H即可
发送数据给服务端 querystring 和 body 不是分开的 两个同时发送也可以
func main(){
   router := gin.Default()   
   router.PUT("/post", func(c *gin.Context) {
      id := c.Query("id")
      page := c.DefaultQuery("page", "0")
      name := c.PostForm("name")
      message := c.PostForm("message")
      fmt.Printf("id: %s; page: %s; name: %s; message: %s \n", id, page, name, message)
      c.JSON(http.StatusOK, gin.H{
            "status_code": http.StatusOK,
      })
   })
}同时使用查询字串和body参数发送数据给服务器

gin文件上传

上传单个文件 multipart/form-data 用于文件上传 gin把原生的request封装到了 c.Request 中
func main(){
   router := gin.Default()   
   router.POST("/upload", func(c *gin.Context) {
         name := c.PostForm("name")
         fmt.Println(name)
         file, header, err := c.Request.FormFile("upload")
         if err != nil {
               c.String(http.StatusBadRequest, "Bad request")
               return
         }
         filename := header.Filename
         fmt.Println(file, err, filename)
         out, err := os.Create(filename)
         if err != nil {
               log.Fatal(err)
         }
         defer out.Close()
         _, err = io.Copy(out, file)
         if err != nil {
               log.Fatal(err)
         }
         c.String(http.StatusCreated, "upload successful")
   })
   router.Run(":8000")
}用c.Request.FormFile 解析客户端文件name属性
不传文件 会抛错 因此需要处理这个错误 一种方式是直接返回 然后使用os的操作 把文件数据复制到硬盘上
使用下面的命令可以测试上传 upload 为 c.Request.FormFile 指定的参数 其值必须要是绝对路径
curl -X POST http://127.0.0.1:8000/upload -F "upload=@/Users/ghost/Desktop/pic.jpg" -H "Content-Type: multipart/form-data"

gin上传多个文件

多个文件 就是多一次遍历文件 然后一次copy数据存储即可
router.POST("/multi/upload", func(c *gin.Context) {
   err := c.Request.ParseMultipartForm(200000)
   if err != nil {
         log.Fatal(err)
   }
   formdata := c.Request.MultipartForm
   files := formdata.File["upload"]
   for i, _ := range files {
         file, err := files[i].Open()
         defer file.Close()
         if err != nil {
               log.Fatal(err)
         }
         out, err := os.Create(files[i].Filename)
         defer out.Close()
         if err != nil {
               log.Fatal(err)
         }
         _, err = io.Copy(out, file)
         if err != nil {
               log.Fatal(err)
         }
         c.String(http.StatusCreated, "upload successful")
   }
}) 使用 c.Request.MultipartForm 得到文件句柄 再获取文件数据 然后遍历读写
使用curl上传
curl -X POST http://127.0.0.1:8000/multi/upload -F "upload=@/Users/ghost/Desktop/pic.jpg" -F "upload=@/Users/ghost/Desktop/journey.png" -H "Content-Type: multipart/form-data"

gin 表单上传

实际用户上传图片 多是通过表单 或者ajax和一些requests的请求完成
web的form表单上传 先写表单页面 因此需要引入gin如何render模板
c.HTML方法
定义模板的文件夹 调用c.HTML渲染模板  通过gin.H给模板传值  无论是String JSON还是HTML 以及后面的XML和YAML 都可以看到Gin封装的接口简明易用
创建文件夹 templates  里面创建 upload.html 文件
<!DOCTYPE html><html><head><meta charset="UTF-8"><title>upload</title></head>
<body><h3>Single Upload</h3><form action="/upload", method="post" enctype="multipart/form-data"><input type="text" value="hello gin" />
<input type="file" name="upload" /><input type="submit" value="upload" /></form><h3>Multi Upload</h3>
<form action="/multi/upload", method="post" enctype="multipart/form-data">
<input type="text" value="hello gin" /><input type="file" name="upload" />
<input type="file" name="upload" /><input type="submit" value="upload" /></form></body></html>
upload 没有参数 一个用于单个文件上传 一个用于多个文件上传
router.LoadHTMLGlob("templates/*")
router.GET("/upload", func(c *gin.Context) {
   c.HTML(http.StatusOK, "upload.html", gin.H{})
})

LoadHTMLGlob 模板文件路径

参数绑定 x-www-form-urlencoded 类型的参数处理  越来越多的应用习惯使用JSON来通信
无论返回的 response 还是提交的 request 其 content-type 类型都是 application/json 的格式
旧的web表单页还是 x-www-form-urlencoded 的形式 这就需要 服务器能改hold住这多种content-type的参数了
写一个装饰器将两个格式的数据封装成一个数据模型 go 处理并非易事 有gin  的 model bind 功能非常强大
type User struct {
   Username string `form:"username" json:"username" binding:"required"`
   Passwd  string `form:"passwd" json:"passwd" bdinding:"required"`
   Age    int   `form:"age" json:"age"`
}
func main(){
   router := gin.Default()   
   router.POST("/login", func(c *gin.Context) {
      var user User
      var err error
      contentType := c.Request.Header.Get("Content-Type")
      switch contentType {
      case "application/json":
            err = c.BindJSON(&user)
      case "application/x-www-form-urlencoded":
            err = c.BindWith(&user, binding.Form)
      }
      if err != nil {
            fmt.Println(err)
            log.Fatal(err)
      }
      c.JSON(http.StatusOK, gin.H{
            "user":  user.Username,
            "passwd": user.Passwd,
            "age":   user.Age,
      })

   })
}定义 User模型结构体  客户端 content-type 一次使 BindJSON 和 BindWith 方法
curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/x-www-form-urlencoded" -d "username=i9527&passwd=1&age=21" | python -m json.tool
{
"age": 21,
"passwd": "123",
"username": "i9527"
}
curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/x-www-form-urlencoded" -d "username=i9527&passwd=1&new=21" | python -m json.tool
{
"age": 0,
"passwd": "123",
"username": "i9527"
}
curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/x-www-form-urlencoded" -d "username=i9527&new=21" | python -m json.tool
No JSON object could be decoded
结构体中 设置了binding标签的字段 username和passwd
如果没传会抛错误 非banding的字段 age
对于客户端没有传 User结构会用零值填充
对于User结构没有的参数 会自动被忽略
改成json的效果类似:
curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/json" -d '{"username": "i9527", "passwd": "123", "age": 21}' | python -m json.tool
{
   "age": 21,
   "passwd": "123",
   "username": "i9527"
}
curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/json" -d '{"username": "i9527", "passwd": "123", "new": 21}' | python -m json.tool
{
   "age": 0,
   "passwd": "123",
   "username": "i9527"
}
curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/json" -d '{"username": "i9527", "new": 21}' | python -m json.tool
No JSON object could be decoded
json有数据类型 对于 {"passwd": "123"} 和 {"passwd": 123} 是不同的数据类型 解析需要符合对应的数据类型 否则会出错

gin 高级方法 c.Bind

  更加 content-type 自动推断是 bind 表单还是 json 的参数
router.POST("/login", func(c *gin.Context) {
   var user User   
   err := c.Bind(&user)
   if err != nil {
         fmt.Println(err)
         log.Fatal(err)
   }
   c.JSON(http.StatusOK, gin.H{
         "username":  user.Username,
         "passwd":   user.Passwd,
         "age":     user.Age,
   })
})

多格式渲染 

请求可使用不同的 content-type 响应也如此 通常响应会有html text plain json和xml等
gin 优雅的渲染方法 c.String  c.JSON  c.HTML  c.XML
router.GET("/render", func(c *gin.Context) {
   contentType := c.DefaultQuery("content_type", "json")
   if contentType == "json" {
      c.JSON(http.StatusOK, gin.H{
            "user":  "i9527",
            "passwd": "123",
      })
   } else if contentType == "xml" {
      c.XML(http.StatusOK, gin.H{
            "user":  "i9527",
            "passwd": "123",
      })
   }
})//
curl  http://127.0.0.1:8000/render\?content_type\=json
{"passwd":"123","user":"i9527"}
curl  http://127.0.0.1:8000/render\?content_type\=xml
<map><user>i9527</user><passwd>123</passwd></map>%

重定向

gin对于重定向的请求 调用上下文的Redirect方法
router.GET("/redict/google", func(c *gin.Context) {
      c.Redirect(http.StatusMovedPermanently, "https://google.com")
})

分组路由

gin  让代码逻辑更加模块化 分组 易于定义中间件的使用范围
v1 := router.Group("/v1")
v1.GET("/login", func(c *gin.Context) {
   c.String(http.StatusOK, "v1 login")
})
v2 := router.Group("/v2")
v2.GET("/login", func(c *gin.Context) {
   c.String(http.StatusOK, "v2 login")
})
访问效果
curl http://127.0.0.1:8000/v1/login
v1 login%
curl http://127.0.0.1:8000/v2/login
v2 login%

middleware中间件

golang的net/http 一大特点 是容易构建中间件 gin提供了类似的中间件 中间件只对注册过的路由函数起作用
对于分组路由 嵌套使用中间件 可以限定中间件的作用范围 中间件分为 全局中间件 单个路由中间件和群组中间件
全局中间件
先定义一个中间件函数
func MiddleWare() gin.HandlerFunc {
   return func(c *gin.Context) {
         fmt.Println("before middleware")
         c.Set("request", "clinet_request")
         c.Next()
         fmt.Println("before middleware")
   }
}给c上下文添加一个属性 并赋值 后面的路由处理器 可以根据被中间件装饰后提取其值
虽然名为全局中间件 只要注册中间件的过程之前设置的路由 将不会受注册的中间件所影响
只有注册了中间件 代码的路由函数规则 才会被中间件装饰
router.Use(MiddleWare()){
   router.GET("/middleware", func(c *gin.Context) {
         request := c.MustGet("request").(string)
         req, _ := c.Get("request")
         c.JSON(http.StatusOK, gin.H{
               "middile_request": request,
               "request": req,
         })
   })
}
使用 router 装饰中间件 然后在 /middlerware 读取 request 的值
在 router.Use(MiddleWare()) 代码以上的路由函数 将不会有被中间件装饰的效果
花括号包含被装饰的路由函数只是一个代码规范
即使没有被包含在内的路由函数 只要使用router进行路由 都等于被装饰了
想要区分权限范围 可以使用组返回的对象注册中间件
curl  http://127.0.0.1:8000/middleware
{"middile_request":"clinet_request","request":"clinet_request"}
如果没有注册就使用MustGet方法读取c的值将会抛错 可以使用Get方法取而代之
上面的注册装饰方式 会让所有下面所写的代码都默认使用了router的注册过的中间件

单个路由中间件 

gin提供了针对指定的路由函数进行注册
router.GET("/before", MiddleWare(), func(c *gin.Context) {
   request := c.MustGet("request").(string)
   c.JSON(http.StatusOK, gin.H{
         "middile_request": request,
   })
})把上述代码写在 router.Use(Middleware())之前 同样也能看见/before被装饰了中间件

群组中间件

群组的中间件也类似 只要在对于的群组路由上注册中间件函数即可:
authorized := router.Group("/", MyMiddelware())// 或者这样用:
authorized := router.Group("/")
authorized.Use(MyMiddelware()){
   authorized.POST("/login", loginEndpoint)
}群组可以嵌套 因为中间件也可以根据群组的嵌套规则嵌套
中间件实践 中间件最大的作用 记录log 错误handler 对部分接口的鉴权
router.GET("/auth/signin", func(c *gin.Context) {
cookie := &http.Cookie{Name:"session_id",   Value:"123",Path:"/",   HttpOnly: true,}
http.SetCookie(c.Writer, cookie)
c.String(http.StatusOK, "Login successful")
})
router.GET("/home", AuthMiddleWare(), func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"data": "home"})
})

登录函数设置 session_id 的cookie

这里需要指定path为/ 不然gin会自动设置cookie的path为/auth
/homne的逻辑很简单 使用中间件AuthMiddleWare注册之后
将会先执行AuthMiddleWare的逻辑 然后才到/home的逻辑
AuthMiddleWare的代码
func AuthMiddleWare() gin.HandlerFunc {
   return func(c *gin.Context) {
         if cookie, err := c.Request.Cookie("session_id"); err == nil {
               value := cookie.Value
               fmt.Println(value)
               if value == "123" {
                     c.Next()
                     return
               }
         }
         c.JSON(http.StatusUnauthorized, gin.H{
               "error": "Unauthorized",
         })
         c.Abort()
         return
   }
}从上下文的请求中读取cookie 然后校对cookie 如果有问题 则终止请求 直接返回 这里使用了c.Abort()方法
In [7]: resp = requests.get('http://127.0.0.1:8000/home')
In [8]: resp.json()
Out[8]: {u'error': u'Unauthorized'}
In [9]: login = requests.get('http://127.0.0.1:8000/auth/signin')
In [10]: login.cookies
Out[10]: <RequestsCookieJar[Cookie(version=0, name='session_id', value='123', port=None, port_specified=False, domain='127.0.0.1', domain_specified=False, domain_initial_dot=False, path='/', path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={'HttpOnly': None}, rfc2109=False)]>
In [11]: resp = requests.get('http://127.0.0.1:8000/home', cookies=login.cookies)
In [12]: resp.json()
Out[12]: {u'data': u'home'}

异步协程

go 高并发一大利器就是协程  gin借助协程实现异步任务 因为涉及异步过程 请求的上下文需要copy到异步的上下文 且上下文只读
router.GET("/sync", func(c *gin.Context) {
   time.Sleep(5 * time.Second)
   log.Println("Done! in path" + c.Request.URL.Path)
})
router.GET("/async", func(c *gin.Context) {
   cCp := c.Copy()
   go func() {
         time.Sleep(5 * time.Second)
         log.Println("Done! in path" + cCp.Request.URL.Path)
   }()
})在请求的时候 sleep5秒钟 同步的逻辑可以看到 服务的进程睡眠了 异步的逻辑则看到响应返回了 程序还在后台的协程处理

自定义router

gin 使用框架本身的router进行Run  也可以配合使用net/http本身的功能
func main() {
   router := gin.Default()
   http.ListenAndServe(":8080", router)
}
或者
func main() {
   router := gin.Default()

   s := &http.Server{
         Addr:      ":8000",
         Handler:     router,
         ReadTimeout:   10 * time.Second,
         WriteTimeout:  10 * time.Second,
         MaxHeaderBytes: 1 << 20,
   }
   s.ListenAndServe()
}

gin 获取post请求的所有参数

用结构体接收参数 假如事先不清楚参数名 或 参数是不固定的 就要动态获取
ctx *gin.Context
form方式的请求 ctx.Request.ParseForm()
for k, v := range ctx.Request.PostForm {
   fmt.Printf("k:%v\n", k)
   fmt.Printf("v:%v\n", v)
}
json方式的请求
data, _ := ioutil.ReadAll(ctx.Request.Body)
fmt.Printf("ctx.Request.body: %v", string(data))
然后从data里解析出来
logging.Debugf("c.Request.Method: %v", ctx.Request.Method)
logging.Debugf("c.Request.ContentType: %v", ctx.ContentType())
logging.Debugf("c.Request.Body: %v", ctx.Request.Body)
ctx.Request.ParseForm()
logging.Debugf("c.Request.Form: %v", ctx.Request.PostForm)
for k, v := range ctx.Request.PostForm {
   logging.Debugf("k:%v\n", k)
   logging.Debugf("v:%v\n", v)
}
logging.Debugf("c.Request.ContentLength: %v", ctx.Request.ContentLength)
data, _ := ioutil.ReadAll(ctx.Request.Body)
logging.Debugf("c.Request.GetBody: %v", string(data))
var role entity.AuthGroup
ctx.ShouldBindWith(&role, binding.FormPost)   
Gin是一个轻巧而强大的golang web框架 涉及常见开发的功能 gin的源码注释很详细 可以阅读源码了解更多详细的功能和魔法特性
package main
import (
   "fmt"
   "strings"
)
func main() {
   s := []string{"hello", "word", "xiaowei"}
   fmt.Println(strings.Join(s, "-")) // hello-word-xiaowei
   
   role := "hello,world"
   if strings.Contains(role, ",") {
      s := "ab,cd,ef"
      result := strings.FieldsFunc(s, func(c rune) bool {
         if c == ',' {
            return true
         }
         return false
      })
      fmt.Printf("%+v", result) // hello-word-xiaowei
   }
}



Go 框架 Gin和gin相关