手记

Go web 框架 gin 的使用

gin 是 golang 中最流行的 web 框架拥有高性能的路由官网中介绍的主要特点包括快速、支持中间件、crash 处理、json 验证、支持路由组等这些特性可以类比 node 的 koa 框架。

快速开始

安装

go get -u github.com/gin-gonic/gin
复制代码

返回一个 json 的路由

package main

import (

   "github.com/gin-gonic/gin"

   "net/http"

)



func main() {

   r := gin.Default()

   r.GET("/someJson", func(c *gin.Context) {

      data := map[string]interface{}{

         "lang": "go lang",

         "tag": "<br>",

      }

      c.JSON(http.StatusOK, data)

   })

   r.Run(":8000")

}
复制代码

其中 gin.Default 是默认开启 logger 和 recovery 两个中间件从源码中可以看到相当于是调用 New 函数之后使用 Use 开启两个中间件

改写成使用 New 方法就是

package main

import (

   "github.com/gin-gonic/gin"

   "net/http"

)



func main() {

   r := gin.New()

   r.Use(gin.Logger(), gin.Recovery())

   r.GET("/someJson", func(c *gin.Context) {

      data := map[string]interface{}{

         "lang": "go lang",

         "tag": "<br>",

      }

      c.JSON(http.StatusOK, data)

   })

   r.Run(":8000")

}
复制代码

路由和路由组

Gin 支持 get、post、patch、delete、put、options、head、any其中 any 是支持get、post、patch、delete、put、options、head 这7种方法gin 提供了这些 http 方法的的大写形式的方法从 gin 的源码中可以看到这些是 RouterGroup 结构体的方法

这些方法都是 Handle 的快捷方法这个从源码可以看到都同样使用了 group.handle 方法Handle 中对传入的 httpMethod 方法做了字符串校验

我们将上面的 r.GET 修改为 Handle 方法如下

package main

import (

   "github.com/gin-gonic/gin"

   "net/http"

)



func main() {

   r := gin.New()

   r.Use(gin.Logger(), gin.Recovery())

 r.Handle(http.MethodGet, "/someJson", func(c *gin.Context) {

      data := map[string]interface{}{

         "lang": "go lang",

         "tag": "<br>",

      }

      c.JSON(http.StatusOK, data)

   })

   r.Run(":8000")

}
复制代码

在实际开发中我们经常有对 api 版本、业务模块划分路由的场景在 gin 中可以通过路由组来实现即 Group 方法

package main

import (

   "github.com/gin-gonic/gin"

   "net/http"

)





func main() {

   r := gin.Default()

   v1 := r.Group("/api/v1")

   v1.GET("/getUser", func(c *gin.Context) {

      c.JSON(http.StatusOK, gin.H{

         "name": "golang",

         "id": "1",

      })

   })

   r.Run(":8000")

}
复制代码

从源码中可以看到 Group 方法返回了一个新的 RouterGroup同时将对应的 routePath 计算作为一个 basePath

在处理路由的时候 handle 方法会 调用 calculateAbsolutePath 方法计算出最后的路由路径

路由参数

Gin 的路由基于的是 httprouter和 koa 一样以 :param 的方式作为一个路由参数通过 context 的 Param 方法获取对应的值如

package main



import (

   "github.com/gin-gonic/gin"

   "net/http"

)



func main() {

   r := gin.Default()

   r.GET("/user/:id", func(c *gin.Context) {

      id := c.Param("id")

      c.JSON(http.StatusOK, gin.H{

         "user": id,

      })

   })

   r.Run(":8000")

}
复制代码

通过源码我们可以看到是通过 c.Params.ByName 获取的c.Params 本质上是一个存储参数的 slice

路由参数还支持以 * 开头匹配所有如

package main



import (

   "fmt"

   "github.com/gin-gonic/gin"

   "net/http"

)



func main() {

   r := gin.Default()

   r.GET("/article/*id", func(c *gin.Context) {

      id := c.Param("id")

      c.JSON(http.StatusOK, gin.H{

         "article": id,

      })

   })

   r.Run(":8000")

}
复制代码

这段代码可以匹配以下所有路由

/aritcle/123

/article/123/info

/aritcle/123/author/info
复制代码

Get 和 Post 参数

在开发中最常见的就是通过 getquery string、post 参数http body来向服务端传递数据gin 通过 Context 的 Query 获取对应的 get 参数

package main



import (

   "github.com/gin-gonic/gin"

   "net/http"

)



func main()  {

   r := gin.Default()

   r.GET("/user", func(c *gin.Context) {

      id := c.Query("id")

      c.JSON(http.StatusOK, gin.H{

         "id": id,

      })

   })

   r.Run(":8000")

}
复制代码

在源码中我们可以看到调用的是 context 的 GetQuery 方法

query 在 gin 内部是通过一个 map 来存储map 定义为 map[string][]string本质上是通过 context 的 c.Request.URL.Query() 方法拿到的

在 gin 内部是通过 parseQuery 方法来解析的从返回值也可以看出是一个以 string 为 keystring 数组为值的 map

如果想要为 query 不存在时设置一个默认值可以使用 DefaultQuery 方法在这个方法内部也是使用 GetQuery 方法当不存在时使用默认值

package main



import (

   "github.com/gin-gonic/gin"

   "net/http"

)



func main()  {

   r := gin.Default()

   r.GET("/user", func(c *gin.Context) {

      id := c.DefaultQuery("id", "456")

      c.JSON(http.StatusOK, gin.H{

         "id": id,

      })

   })

   r.Run(":8000")

}
复制代码

向服务端发数据的时候常常使用 post 方法以 form-data 的形式存放在 http body 内在 gin 中可以通过 PostForm 方法获取对应的值

package main

import (

   "github.com/gin-gonic/gin"

   "net/http"

)



func main() {

   r := gin.Default()

   r.POST("/user", func(c *gin.Context) {

      id := c.PostForm("id")

      c.JSON(http.StatusOK, gin.H{

         "code": 200,

         "id": id,

      })

   })

   r.Run(":8000")

}
复制代码

和 query 类似可以通过 DefaultPostForm 来设置对应的默认值内部都是通过 GetPostForm 方法来获取的

id := c.DefaultPostForm("id", "456")
复制代码

cookie 和 http header

开发过程中常常需要获取和设置 cookie可以通过 c.Cookie 获取对应的 cookie 值使用 c.SetCookie 来设置 cookie

package main

import (

   "github.com/gin-gonic/gin"

   "net/http"

)



func main()  {

   r := gin.Default()

   r.GET("/user", func(c *gin.Context) {

      session, _ := c.Cookie("session")

      c.SetCookie("site_cookie", "cookie1", 3600, "/", "localhost", false, true)

      c.JSON(http.StatusOK, gin.H{

         "code": 200,

         "session": session,

      })

   })

   r.Run(":8000")

}
复制代码

获取 cookie 内部也是通过 c.Request.Cookie 来获取的c.Request.Cookie 会读取解析 http 头 cookie 字段

Http header 可以通过 c.GetHeader(key) 的形式获取

package main

import (

   "github.com/gin-gonic/gin"

   "net/http"

)



func main() {

   r := gin.Default()

   r.GET("/user", func(c *gin.Context) {

      lang := c.GetHeader("lang")

      c.JSON(http.StatusOK, gin.H{

         "code": 200,

         "lang": lang,

      })

   })

   r.Run(":8000")

}
复制代码

本质上是通过 request 的 header.get 方法获取的即 c.Request.Header.Get源码如下

设置 HTTP 响应头通过 Header 方法即可

c.Header("user", "golang")
复制代码

实际上是通过 http 包的 Header struct 来设置的

重定向

gin 框架的重定向 context 下有 Redirect 方法帮助我们重定向也可以直接修改 context request 的 url 信息然后继续处理 context

package main

import (

   "github.com/gin-gonic/gin"

   "net/http"

)



func main() {

   r := gin.Default()

   r.GET("/info", func(c *gin.Context) {

      c.Redirect(http.StatusMovedPermanently, "/user")

   })

   r.GET("/article", func(c *gin.Context) {

      c.Request.URL.Path = "/user"

      r.HandleContext(c)

   })

   r.GET("/user", func(c *gin.Context) {

      c.JSON(http.StatusOK, gin.H{

         "code": 200,

         "data": "user",

      })

   })

   r.Run(":8000")

}
复制代码

Redirect 本质上也是调用的 http 包的 Redirect 方法

静态资源和模版引擎

设置静态资源和模版引擎是一个 web 服务器最基本的能力gin 通过路由的 Static、StaticFS、StaticFile 三种方法设置

package main

import (

   "github.com/gin-gonic/gin"

   "net/http"

)



func main() {

   r := gin.Default()

   r.Static("/", "./public")

   r.StaticFile("/", "./public")

   r.StaticFS("/", http.Dir("./public"))

   r.Run(":8000")

}
复制代码

Static 本质上内部也是通过 StaticFS 方法来实现的

golang 有一个模版引擎标准库 http/templategin 内部默认也是使用这个标准库这个库和我们常用的模版引擎类似使用胡子表达式作为变量gin 中使用 LoadHTMLGlob 方法加载模版使用 c.HTML 表示使用模版引擎处理源代码可以在 gin 的 render/html.go 中

package main

import (

   "github.com/gin-gonic/gin"

   "net/http"

)



func main() {

   r := gin.Default()



   r.LoadHTMLGlob("./template/*")



   r.GET("/index", func(c *gin.Context) {

      c.HTML(http.StatusOK, "index.tmpl", gin.H{

         "title": "golang",

      })

   })



   r.Run(":8000")

}
复制代码
<html>

<h1>

    {{ .title }}

</h1>

</html>
复制代码

Context

gin 的 context 贯穿一次 http 请求的全流程可以类比 koa 的 contextcontext 也是 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

}
复制代码

上面对 query、postForm、param 、header、cookie 等的操作都是挂在 context 上的context 也提供了基本的元数据存取Get 和 Set这样我们可以把需要的尤其是跨中间件使用的数据挂在 context 上本质上是存储在 c.Keys 上的

package main

import (

   "github.com/gin-gonic/gin"

   "net/http"

)



func main()  {

   r := gin.Default()

   r.GET("/user", func(c *gin.Context) {

      c.Set("key", "value")

      val, _ := c.Get("key")

      c.JSON(http.StatusOK, gin.H{

         "code": 200,

         "key": val,

      })

   })

   r.Run(":8000")

}
复制代码

除了 Get 存储外gin 还内置了 MustGet不存在就出发 panic、对类型断言后的取值如 GetString、GetBool、GetInt 等

对元数据存取的方法有以下



 /************************************/

 /******** METADATA MANAGEMENT********/

 /************************************/



func (c *Context) Set(key string, value interface{}) {}



 // Get returns the value for the given key, ie: (value, true).

 // If the value does not exists it returns (nil, false)

func (c *Context) Get(key string) (value interface{}, exists bool) {}



 // MustGet returns the value for the given key if it exists, otherwise it panics.

func (c *Context) MustGet(key string) interface{} {}



 // GetString returns the value associated with the key as a string.

func (c *Context) GetString(key string) (s string) {}



 // GetBool returns the value associated with the key as a boolean.

func (c *Context) GetBool(key string) (b bool) {}



 // GetInt returns the value associated with the key as an integer.

func (c *Context) GetInt(key string) (i int) {}



 // GetInt64 returns the value associated with the key as an integer.

func (c *Context) GetInt64(key string) (i64 int64) {}



 // GetUint returns the value associated with the key as an unsigned integer.

func (c *Context) GetUint(key string) (ui uint) {

}



 // GetUint64 returns the value associated with the key as an unsigned integer.

func (c *Context) GetUint64(key string) (ui64 uint64) {}



 // GetFloat64 returns the value associated with the key as a float64.

func (c *Context) GetFloat64(key string) (f64 float64) {}



 // GetTime returns the value associated with the key as time.

func (c *Context) GetTime(key string) (t time.Time) {}



 // GetDuration returns the value associated with the key as a duration.

func (c *Context) GetDuration(key string) (d time.Duration) {}



 // GetStringSlice returns the value associated with the key as a slice of strings.

func (c *Context) GetStringSlice(key string) (ss []string) {}



 // GetStringMap returns the value associated with the key as a map of interfaces.

func (c *Context) GetStringMap(key string) (sm map[string]interface{}) {}



 // GetStringMapString returns the value associated with the key as a map of strings.

func (c *Context) GetStringMapString(key string) (sms map[string]string) {}



 // GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings.

func (c *Context) GetStringMapStringSlice(key string) (smss map[string][]string) {}
复制代码

gin 中把对 query、postForm、param 等请求数据称为 Input Data从 gin 的源码中可以看到这部分的方法主要有对 query、postForm、param 等信息的获取和参数绑定的方法源码如下



 /************************************/

 /************ INPUT DATA ************/

 /************************************/



 // Param returns the value of the URL param.

 // It is a shortcut for c.Params.ByName(key)

 //     router.GET("/user/:id", func(c *gin.Context) {

 //         // a GET request to /user/john

 //         id := c.Param("id") // id == "john"

 //     })

func (c *Context) Param(key string) string {

   return c.Params.ByName(key)

}



 // Query returns the keyed url query value if it exists,

 // otherwise it returns an empty string `("")`.

 // It is shortcut for `c.Request.URL.Query().Get(key)`

 //     GET /path?id=1234&name=Manu&value=

 //        c.Query("id") == "1234"

 //        c.Query("name") == "Manu"

 //        c.Query("value") == ""

 //        c.Query("wtf") == ""

func (c *Context) Query(key string) string {

   value, _ := c.GetQuery(key)

   return value

}



 // DefaultQuery returns the keyed url query value if it exists,

 // otherwise it returns the specified defaultValue string.

 // See: Query() and GetQuery() for further information.

 //     GET /?name=Manu&lastname=

 //     c.DefaultQuery("name", "unknown") == "Manu"

 //     c.DefaultQuery("id", "none") == "none"

 //     c.DefaultQuery("lastname", "none") == ""

func (c *Context) DefaultQuery(key, defaultValue string) string {

   if value, ok := c.GetQuery(key); ok {

      return value

   }

   return defaultValue

}



 // GetQuery is like Query(), it returns the keyed url query value

 // if it exists `(value, true)` (even when the value is an empty string),

 // otherwise it returns `("", false)`.

 // It is shortcut for `c.Request.URL.Query().Get(key)`

 //     GET /?name=Manu&lastname=

 //     ("Manu", true) == c.GetQuery("name")

 //     ("", false) == c.GetQuery("id")

 //     ("", true) == c.GetQuery("lastname")

func (c *Context) GetQuery(key string) (string, bool) {

   if values, ok := c.GetQueryArray(key); ok {

      return values[0], ok

   }

   return "", false

}



 // QueryArray returns a slice of strings for a given query key.

 // The length of the slice depends on the number of params with the given key.

func (c *Context) QueryArray(key string) []string {

   values, _ := c.GetQueryArray(key)

   return values

}



func (c *Context) initQueryCache() {

   if c.queryCache == nil {

      if c.Request != nil {

         c.queryCache = c.Request.URL.Query()

      } else {

         c.queryCache = url.Values{}

      }

   }

}



 // GetQueryArray returns a slice of strings for a given query key, plus

 // a boolean value whether at least one value exists for the given key.

func (c *Context) GetQueryArray(key string) ([]string, bool) {

   c.initQueryCache()

   if values, ok := c.queryCache[key]; ok && len(values) > 0 {

      return values, true

   }

   return []string{}, false

}



 // QueryMap returns a map for a given query key.

func (c *Context) QueryMap(key string) map[string]string {

   dicts, _ := c.GetQueryMap(key)

   return dicts

}



 // GetQueryMap returns a map for a given query key, plus a boolean value

 // whether at least one value exists for the given key.

func (c *Context) GetQueryMap(key string) (map[string]string, bool) {

   c.initQueryCache()

   return c.get(c.queryCache, key)

}



 // PostForm returns the specified key from a POST urlencoded form or multipart form

 // when it exists, otherwise it returns an empty string `("")`.

func (c *Context) PostForm(key string) string {

   value, _ := c.GetPostForm(key)

   return value

}



 // DefaultPostForm returns the specified key from a POST urlencoded form or multipart form

 // when it exists, otherwise it returns the specified defaultValue string.

 // See: PostForm() and GetPostForm() for further information.

func (c *Context) DefaultPostForm(key, defaultValue string) string {

   if value, ok := c.GetPostForm(key); ok {

      return value

   }

   return defaultValue

}



 // GetPostForm is like PostForm(key). It returns the specified key from a POST urlencoded

 // form or multipart form when it exists `(value, true)` (even when the value is an empty string),

 // otherwise it returns ("", false).

 // For example, during a PATCH request to update the user's email:

 //     email=mail@example.com  -->  ("mail@example.com", true) := GetPostForm("email") // set email to "mail@example.com"

 //        email=                  -->  ("", true) := GetPostForm("email") // set email to ""

 //                             -->  ("", false) := GetPostForm("email") // do nothing with email

func (c *Context) GetPostForm(key string) (string, bool) {

   if values, ok := c.GetPostFormArray(key); ok {

      return values[0], ok

   }

   return "", false

}



 // PostFormArray returns a slice of strings for a given form key.

 // The length of the slice depends on the number of params with the given key.

func (c *Context) PostFormArray(key string) []string {

   values, _ := c.GetPostFormArray(key)

   return values

}



func (c *Context) initFormCache() {

   if c.formCache == nil {

      c.formCache = make(url.Values)

      req := c.Request

 if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {

         if err != http.ErrNotMultipart {

            debugPrint("error on parse multipart form array: %v", err)

         }

      }

      c.formCache = req.PostForm

 }

}



 // GetPostFormArray returns a slice of strings for a given form key, plus

 // a boolean value whether at least one value exists for the given key.

func (c *Context) GetPostFormArray(key string) ([]string, bool) {

   c.initFormCache()

   if values := c.formCache[key]; len(values) > 0 {

      return values, true

   }

   return []string{}, false

}



 // PostFormMap returns a map for a given form key.

func (c *Context) PostFormMap(key string) map[string]string {

   dicts, _ := c.GetPostFormMap(key)

   return dicts

}



 // GetPostFormMap returns a map for a given form key, plus a boolean value

 // whether at least one value exists for the given key.

func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {

   c.initFormCache()

   return c.get(c.formCache, key)

}



 // get is an internal method and returns a map which satisfy conditions.

func (c *Context) get(m map[string][]string, key string) (map[string]string, bool) {

   dicts := make(map[string]string)

   exist := false

   for k, v := range m {

      if i := strings.IndexByte(k, '['); i >= 1 && k[0:i] == key {

         if j := strings.IndexByte(k[i+1:], ']'); j >= 1 {

            exist = true

            dicts[k[i+1:][:j]] = v[0]

         }

      }

   }

   return dicts, exist

}



 // FormFile returns the first file for the provided form key.

func (c *Context) FormFile(name string) (*multipart.FileHeader, error) {

   if c.Request.MultipartForm == nil {

      if err := c.Request.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {

         return nil, err

      }

   }

   f, fh, err := c.Request.FormFile(name)

   if err != nil {

      return nil, err

   }

   f.Close()

   return fh, err

}



 // MultipartForm is the parsed multipart form, including file uploads.

func (c *Context) MultipartForm() (*multipart.Form, error) {

   err := c.Request.ParseMultipartForm(c.engine.MaxMultipartMemory)

   return c.Request.MultipartForm, err

}



 // SaveUploadedFile uploads the form file to specific dst.

func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error {

   src, err := file.Open()

   if err != nil {

      return err

   }

   defer src.Close()



   out, err := os.Create(dst)

   if err != nil {

      return err

   }

   defer out.Close()



   _, err = io.Copy(out, src)

   return err

}



 // Bind checks the Content-Type to select a binding engine automatically,

 // Depending the "Content-Type" header different bindings are used:

 //     "application/json" --> JSON binding

 //     "application/xml"  --> XML binding

 // otherwise --> returns an error.

 // It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input.

 // It decodes the json payload into the struct specified as a pointer.

 // It writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid.

func (c *Context) Bind(obj interface{}) error {

   b := binding.Default(c.Request.Method, c.ContentType())

   return c.MustBindWith(obj, b)

}



 // BindJSON is a shortcut for c.MustBindWith(obj, binding.JSON).

func (c *Context) BindJSON(obj interface{}) error {

   return c.MustBindWith(obj, binding.JSON)

}



 // BindXML is a shortcut for c.MustBindWith(obj, binding.BindXML).

func (c *Context) BindXML(obj interface{}) error {

   return c.MustBindWith(obj, binding.XML)

}



 // BindQuery is a shortcut for c.MustBindWith(obj, binding.Query).

func (c *Context) BindQuery(obj interface{}) error {

   return c.MustBindWith(obj, binding.Query)

}



 // BindYAML is a shortcut for c.MustBindWith(obj, binding.YAML).

func (c *Context) BindYAML(obj interface{}) error {

   return c.MustBindWith(obj, binding.YAML)

}



 // BindHeader is a shortcut for c.MustBindWith(obj, binding.Header).

func (c *Context) BindHeader(obj interface{}) error {

   return c.MustBindWith(obj, binding.Header)

}



 // BindUri binds the passed struct pointer using binding.Uri.

 // It will abort the request with HTTP 400 if any error occurs.

func (c *Context) BindUri(obj interface{}) error {

   if err := c.ShouldBindUri(obj); err != nil {

      c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // nolint: errcheck

 return err

   }

   return nil

}



 // MustBindWith binds the passed struct pointer using the specified binding engine.

 // It will abort the request with HTTP 400 if any error occurs.

 // See the binding package.

func (c *Context) MustBindWith(obj interface{}, b binding.Binding) error {

   if err := c.ShouldBindWith(obj, b); err != nil {

      c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // nolint: errcheck

 return err

   }

   return nil

}



 // ShouldBind checks the Content-Type to select a binding engine automatically,

 // Depending the "Content-Type" header different bindings are used:

 //     "application/json" --> JSON binding

 //     "application/xml"  --> XML binding

 // otherwise --> returns an error

 // It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input.

 // It decodes the json payload into the struct specified as a pointer.

 // Like c.Bind() but this method does not set the response status code to 400 and abort if the json is not valid.

func (c *Context) ShouldBind(obj interface{}) error {

   b := binding.Default(c.Request.Method, c.ContentType())

   return c.ShouldBindWith(obj, b)

}



 // ShouldBindJSON is a shortcut for c.ShouldBindWith(obj, binding.JSON).

func (c *Context) ShouldBindJSON(obj interface{}) error {

   return c.ShouldBindWith(obj, binding.JSON)

}



 // ShouldBindXML is a shortcut for c.ShouldBindWith(obj, binding.XML).

func (c *Context) ShouldBindXML(obj interface{}) error {

   return c.ShouldBindWith(obj, binding.XML)

}



 // ShouldBindQuery is a shortcut for c.ShouldBindWith(obj, binding.Query).

func (c *Context) ShouldBindQuery(obj interface{}) error {

   return c.ShouldBindWith(obj, binding.Query)

}



 // ShouldBindYAML is a shortcut for c.ShouldBindWith(obj, binding.YAML).

func (c *Context) ShouldBindYAML(obj interface{}) error {

   return c.ShouldBindWith(obj, binding.YAML)

}



 // ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header).

func (c *Context) ShouldBindHeader(obj interface{}) error {

   return c.ShouldBindWith(obj, binding.Header)

}



 // ShouldBindUri binds the passed struct pointer using the specified binding engine.

func (c *Context) ShouldBindUri(obj interface{}) error {

   m := make(map[string][]string)

   for _, v := range c.Params {

      m[v.Key] = []string{v.Value}

   }

   return binding.Uri.BindUri(m, obj)

}



 // ShouldBindWith binds the passed struct pointer using the specified binding engine.

 // See the binding package.

func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {

   return b.Bind(c.Request, obj)

}



 // ShouldBindBodyWith is similar with ShouldBindWith, but it stores the request

 // body into the context, and reuse when it is called again.

 //

 // NOTE: This method reads the body before binding. So you should use

 // ShouldBindWith for better performance if you need to call only once.

func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (err error) {

   var body []byte

   if cb, ok := c.Get(BodyBytesKey); ok {

      if cbb, ok := cb.([]byte); ok {

         body = cbb

      }

   }

   if body == nil {

      body, err = ioutil.ReadAll(c.Request.Body)

      if err != nil {

         return err

      }

      c.Set(BodyBytesKey, body)

   }

   return bb.BindBody(body, obj)

}



 // ClientIP implements a best effort algorithm to return the real client IP.

 // It called c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not.

 // If it's it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-Ip]).

 // If the headers are nots syntactically valid OR the remote IP does not correspong to a trusted proxy,

 // the remote IP (coming form Request.RemoteAddr) is returned.

func (c *Context) ClientIP() string {

   if c.engine.AppEngine {

      if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" {

         return addr

      }

   }



   remoteIP, trusted := c.RemoteIP()

   if remoteIP == nil {

      return ""

   }



   if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {

      for _, headerName := range c.engine.RemoteIPHeaders {

         ip, valid := validateHeader(c.requestHeader(headerName))

         if valid {

            return ip

         }

      }

   }

   return remoteIP.String()

}



 // RemoteIP parses the IP from Request.RemoteAddr, normalizes and returns the IP (without the port).

 // It also checks if the remoteIP is a trusted proxy or not.

 // In order to perform this validation, it will see if the IP is contained within at least one of the CIDR blocks

 // defined in Engine.TrustedProxies

func (c *Context) RemoteIP() (net.IP, bool) {

   ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr))

   if err != nil {

      return nil, false

   }

   remoteIP := net.ParseIP(ip)

   if remoteIP == nil {

      return nil, false

   }



   if c.engine.trustedCIDRs != nil {

      for _, cidr := range c.engine.trustedCIDRs {

         if cidr.Contains(remoteIP) {

            return remoteIP, true

         }

      }

   }



   return remoteIP, false

}



func validateHeader(header string) (clientIP string, valid bool) {

   if header == "" {

      return "", false

   }

   items := strings.Split(header, ",")

   for i, ipStr := range items {

      ipStr = strings.TrimSpace(ipStr)

      ip := net.ParseIP(ipStr)

      if ip == nil {

         return "", false

      }



      // We need to return the first IP in the list, but,

 // we should not early return since we need to validate that

 // the rest of the header is syntactically valid

 if i == 0 {

         clientIP = ipStr

         valid = true

      }

   }

   return

}



 // ContentType returns the Content-Type header of the request.

func (c *Context) ContentType() string {

   return filterFlags(c.requestHeader("Content-Type"))

}



 // IsWebsocket returns true if the request headers indicate that a websocket

 // handshake is being initiated by the client.

func (c *Context) IsWebsocket() bool {

   if strings.Contains(strings.ToLower(c.requestHeader("Connection")), "upgrade") &&

      strings.EqualFold(c.requestHeader("Upgrade"), "websocket") {

      return true

   }

   return false

}



func (c *Context) requestHeader(key string) string {

   return c.Request.Header.Get(key)

}
复制代码

gin 中像上面的对 cookie 、header、body 的存取和 c.JSON、c.HTML 等决定输出渲染类型统归类为 RESPONSE RENDERING响应渲染各类方法的源代码如下



 /************************************/

 /******** RESPONSE RENDERING ********/

 /************************************/



 // bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function.

func bodyAllowedForStatus(status int) bool {

   switch {

   case status >= 100 && status <= 199:

      return false

   case status == http.StatusNoContent:

      return false

   case status == http.StatusNotModified:

      return false

   }

   return true

}



 // Status sets the HTTP response code.

func (c *Context) Status(code int) {

   c.Writer.WriteHeader(code)

}



 // Header is a intelligent shortcut for c.Writer.Header().Set(key, value).

 // It writes a header in the response.

 // If value == "", this method removes the header `c.Writer.Header().Del(key)`

func (c *Context) Header(key, value string) {

   if value == "" {

      c.Writer.Header().Del(key)

      return

   }

   c.Writer.Header().Set(key, value)

}



 // GetHeader returns value from request headers.

func (c *Context) GetHeader(key string) string {

   return c.requestHeader(key)

}



 // GetRawData return stream data.

func (c *Context) GetRawData() ([]byte, error) {

   return ioutil.ReadAll(c.Request.Body)

}



 // SetSameSite with cookie

func (c *Context) SetSameSite(samesite http.SameSite) {

   c.sameSite = samesite

}



 // SetCookie adds a Set-Cookie header to the ResponseWriter's headers.

 // The provided cookie must have a valid Name. Invalid cookies may be

 // silently dropped.

func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool) {

   if path == "" {

      path = "/"

   }

   http.SetCookie(c.Writer, &http.Cookie{

      Name:     name,

      Value:    url.QueryEscape(value),

      MaxAge:   maxAge,

      Path:     path,

      Domain:   domain,

      SameSite: c.sameSite,

      Secure:   secure,

      HttpOnly: httpOnly,

   })

}



 // Cookie returns the named cookie provided in the request or

 // ErrNoCookie if not found. And return the named cookie is unescaped.

 // If multiple cookies match the given name, only one cookie will

 // be returned.

func (c *Context) Cookie(name string) (string, error) {

   cookie, err := c.Request.Cookie(name)

   if err != nil {

      return "", err

   }

   val, _ := url.QueryUnescape(cookie.Value)

   return val, nil

}



 // Render writes the response headers and calls render.Render to render data.

func (c *Context) Render(code int, r render.Render) {

   c.Status(code)



   if !bodyAllowedForStatus(code) {

      r.WriteContentType(c.Writer)

      c.Writer.WriteHeaderNow()

      return

   }



   if err := r.Render(c.Writer); err != nil {

      panic(err)

   }

}



 // HTML renders the HTTP template specified by its file name.

 // It also updates the HTTP code and sets the Content-Type as "text/html".

 // See http://golang.org/doc/articles/wiki/

func (c *Context) HTML(code int, name string, obj interface{}) {

   instance := c.engine.HTMLRender.Instance(name, obj)

   c.Render(code, instance)

}



 // IndentedJSON serializes the given struct as pretty JSON (indented + endlines) into the response body.

 // It also sets the Content-Type as "application/json".

 // WARNING: we recommend to use this only for development purposes since printing pretty JSON is

 // more CPU and bandwidth consuming. Use Context.JSON() instead.

func (c *Context) IndentedJSON(code int, obj interface{}) {

   c.Render(code, render.IndentedJSON{Data: obj})

}



 // SecureJSON serializes the given struct as Secure JSON into the response body.

 // Default prepends "while(1)," to response body if the given struct is array values.

 // It also sets the Content-Type as "application/json".

func (c *Context) SecureJSON(code int, obj interface{}) {

   c.Render(code, render.SecureJSON{Prefix: c.engine.secureJSONPrefix, Data: obj})

}



 // JSONP serializes the given struct as JSON into the response body.

 // It adds padding to response body to request data from a server residing in a different domain than the client.

 // It also sets the Content-Type as "application/javascript".

func (c *Context) JSONP(code int, obj interface{}) {

   callback := c.DefaultQuery("callback", "")

   if callback == "" {

      c.Render(code, render.JSON{Data: obj})

      return

   }

   c.Render(code, render.JsonpJSON{Callback: callback, Data: obj})

}



 // JSON serializes the given struct as JSON into the response body.

 // It also sets the Content-Type as "application/json".

func (c *Context) JSON(code int, obj interface{}) {

   c.Render(code, render.JSON{Data: obj})

}



 // AsciiJSON serializes the given struct as JSON into the response body with unicode to ASCII string.

 // It also sets the Content-Type as "application/json".

func (c *Context) AsciiJSON(code int, obj interface{}) {

   c.Render(code, render.AsciiJSON{Data: obj})

}



 // PureJSON serializes the given struct as JSON into the response body.

 // PureJSON, unlike JSON, does not replace special html characters with their unicode entities.

func (c *Context) PureJSON(code int, obj interface{}) {

   c.Render(code, render.PureJSON{Data: obj})

}



 // XML serializes the given struct as XML into the response body.

 // It also sets the Content-Type as "application/xml".

func (c *Context) XML(code int, obj interface{}) {

   c.Render(code, render.XML{Data: obj})

}



 // YAML serializes the given struct as YAML into the response body.

func (c *Context) YAML(code int, obj interface{}) {

   c.Render(code, render.YAML{Data: obj})

}



 // ProtoBuf serializes the given struct as ProtoBuf into the response body.

func (c *Context) ProtoBuf(code int, obj interface{}) {

   c.Render(code, render.ProtoBuf{Data: obj})

}



 // String writes the given string into the response body.

func (c *Context) String(code int, format string, values ...interface{}) {

   c.Render(code, render.String{Format: format, Data: values})

}



 // Redirect returns a HTTP redirect to the specific location.

func (c *Context) Redirect(code int, location string) {

   c.Render(-1, render.Redirect{

      Code:     code,

      Location: location,

      Request:  c.Request,

   })

}



 // Data writes some data into the body stream and updates the HTTP code.

func (c *Context) Data(code int, contentType string, data []byte) {

   c.Render(code, render.Data{

      ContentType: contentType,

      Data:        data,

   })

}



 // DataFromReader writes the specified reader into the body stream and updates the HTTP code.

func (c *Context) DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string) {

   c.Render(code, render.Reader{

      Headers:       extraHeaders,

      ContentType:   contentType,

      ContentLength: contentLength,

      Reader:        reader,

   })

}



 // File writes the specified file into the body stream in an efficient way.

func (c *Context) File(filepath string) {

   http.ServeFile(c.Writer, c.Request, filepath)

}



 // FileFromFS writes the specified file from http.FileSystem into the body stream in an efficient way.

func (c *Context) FileFromFS(filepath string, fs http.FileSystem) {

   defer func(old string) {

      c.Request.URL.Path = old

   }(c.Request.URL.Path)



   c.Request.URL.Path = filepath



   http.FileServer(fs).ServeHTTP(c.Writer, c.Request)

}



 // FileAttachment writes the specified file into the body stream in an efficient way

 // On the client side, the file will typically be downloaded with the given filename

func (c *Context) FileAttachment(filepath, filename string) {

   c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename="%s"", filename))

   http.ServeFile(c.Writer, c.Request, filepath)

}



 // SSEvent writes a Server-Sent Event into the body stream.

func (c *Context) SSEvent(name string, message interface{}) {

   c.Render(-1, sse.Event{

      Event: name,

      Data:  message,

   })

}



 // Stream sends a streaming response and returns a boolean

 // indicates "Is client disconnected in middle of stream"

func (c *Context) Stream(step func(w io.Writer) bool) bool {

   w := c.Writer

 clientGone := w.CloseNotify()

   for {

      select {

      case <-clientGone:

         return true

      default:

         keepOpen := step(w)

         w.Flush()

         if !keepOpen {

            return false

         }

      }

   }

}
复制代码

Http Request

在 golang 中使用 http 包可以很方便的实现一个简单的服务器如下

package main



import (

   "fmt"

   "log"

   "net/http"

)



func main() {

   http.HandleFunc("/user", func(writer http.ResponseWriter, request *http.Request) {

      //writer.Write()

 _, err :=writer.Write([]byte("hello world"))

      if err!= nil {

         fmt.Println(err)

      }

   })

   err := http.ListenAndServe(":8000",nil)

   if err != nil {

      log.Fatal("ListenAndServe: ", err)

   }

}
复制代码

和 gin 一样路由的 handler 中都涉及到 http.ResponseWriter 和 http.Request 这两个对象在 gin 中 http.ResponseWriter 被包裹在了一个 *ResponseWriter 对象中http.Request 则是直接挂在 context 上的

中间件

gin 的中间件可以类比 koa 的中间件是一种洋葱模型这个模型的中心是最终处理请求的 handler称之为 main handler其他为称为 middleware handler每一个 middleware handle 可以分为两部分随着 request 的流动左边是入右边为出而分割点就是 next本质就是通过这个next来执行函数链 各个中间件符合先进后出原则

如下自定义两个全局使用的中间件可以反映出这个模型

package main



import (

"fmt"

"github.com/gin-gonic/gin"

"net/http"

)



func m1() gin.HandlerFunc{

   return func(c *gin.Context) {

      fmt.Println("m1 start")

      c.Next()

      fmt.Println("m1 end")

   }

}



func m2() gin.HandlerFunc {

   return func(c *gin.Context) {

      fmt.Println("m2 start")

      c.Next()

      fmt.Println("m2 end")

   }

}



func main() {

   r := gin.New()

   r.Use(m1())

   r.Use(m2())

   r.GET("/user", func(c *gin.Context) {

      c.JSON(http.StatusOK, gin.H{

         "code": 200,

         "data": "user",

      })

   })



   r.Run(":8000")

}
复制代码

当请求打进来的时候输出将是 m1 start、m2 start、m2 end、m1 start

在上文最常用的 gin.Default 中默认使用了 Logger 和 Recovery 作为全局中间件分别作为日志和 panic 处理这种直接使用 engine 的 Use 方法来挂载中间件的方法会使得中间件在全局起作用

和 koa 类似我们可以把中间件挂载在路由和路由组上这样这些中间件就只会在匹配的路由中生效如下m1 只会在 user 这个路由组下生效m2 则只会在 /article 这个路由下生效

package main



import (

   "fmt"

   "github.com/gin-gonic/gin"

   "net/http"

)



func m1() gin.HandlerFunc{

   return func(c *gin.Context) {

      fmt.Println("m1 start")

      c.Next()

      fmt.Println("m1 end")

   }

}



func m2() gin.HandlerFunc {

   return func(c *gin.Context) {

      fmt.Println("m2 start")

      c.Next()

      fmt.Println("m2 end")

   }

}



func main() {

   r := gin.New()

   userRouter := r.Group("/user", m1())

   userRouter.GET("/info", func(c *gin.Context) {

      c.JSON(http.StatusOK, gin.H{

         "code": 200,

         "data": "info",

      })

   })

   r.GET("/article", m2(), func(c *gin.Context) {

      c.JSON(http.StatusOK, gin.H{

         "code": 200,

         "data": "article",

      })

   })



   r.Run(":8000")

}
复制代码

在实际开发中经常需要自定义中间件一个中间件本质上就是一个 handler 函数即是一个以 *gin.Context 为参数的函数在实际开发中中间件可以是一个返回 handler 的函数这样使用的时候调用这个函数即可如果本身就是一个 handler则不需要调用直接作为参数即可

package main



import (

   "fmt"

   "github.com/gin-gonic/gin"

   "net/http"

)



func middle1() gin.HandlerFunc{

   return func(c *gin.Context) {

      fmt.Println("middleware 1")

      c.Next()

   }

}



func middle2(c *gin.Context) {

   fmt.Println("middleware 2")

   c.Next()

}



func main()  {

   r := gin.Default()

   r.Use(middle1())

   r.Use(middle2)

   r.GET("/user", func(c *gin.Context) {

      c.JSON(http.StatusOK, gin.H{

         "data": "user",

      })

   })



   r.Run(":8000")

}
复制代码

中间件中除了 Next 外还可以使用 Abort、AbortWithStatus、AbortWithStatusJSON、AbortWithError 方法拦截请求这种常常可以用于鉴权、鉴参数等前置流程如下当请求的 header 没有 token 时会拦截请求直接返回没权限

package main



import (

   "github.com/gin-gonic/gin"

   "net/http"

)



func auth() gin.HandlerFunc{

   return func(c *gin.Context) {

      token := c.GetHeader("token")

      if token == "" {

         c.AbortWithStatusJSON(401, gin.H{

            "message": "没有权限",

         })

      }

      c.Next()

   }

}



func main()  {

   r := gin.Default()

   r.Use(auth())

   r.GET("/user", func(c *gin.Context) {

      c.JSON(http.StatusOK, gin.H{

         "data": "user",

      })

   })

   r.Run(":8000")

}
复制代码

在 gin 的 官方文档下可以看到官方维护和外部维护的中间件列表github.com/gin-gonic/c…

脚手架工程

可参考的开源代码仓库

参考

作者_忽如寄
链接https://juejin.cn/post/6957982755527344158
著作权归作者所有。商业转载请联系作者获得授权非商业转载请注明出处。

0人推荐
随时随地看视频
慕课网APP