简介

Gin 源码解读, 基于 v1.5.0 版本.

Context 初始化

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

    // 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
}

注释中说到, Context 用于中间件中的变量传递, 流程控制, 验证请求的 JSON 格式以及返回 JSON 响应等.

Context 是在每次接受请求的时候初始化的:

// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    c := engine.pool.Get().(*Context)
    c.writermem.reset(w)
    c.Request = req
    c.reset()

    engine.handleHTTPRequest(c)

    engine.pool.Put(c)
}

里面用到了 sync.Pool, sync.Pool 适用于缓存已分配但未使用的 items, 以便后续重用, 并减轻垃圾回收的压力.

type Pool struct {

    // New optionally specifies a function to generate
    // a value when Get would otherwise return nil.
    // It may not be changed concurrently with calls to Get.
    New func() interface{}
    // contains filtered or unexported fields
}

sync.Pool 需要实现一个名为 New 的方法, 这其实在初始化 Engine 的时候就已经完成了.

// New returns a new blank Engine instance without any middleware attached.
// By default the configuration is:
// - RedirectTrailingSlash:  true
// - RedirectFixedPath:      false
// - HandleMethodNotAllowed: false
// - ForwardedByClientIP:    true
// - UseRawPath:             false
// - UnescapePathValues:     true
func New() *Engine {
    debugPrintWARNINGNew()
    engine := &Engine{
        RouterGroup: RouterGroup{
            Handlers: nil,
            basePath: "/",
            root:     true,
        },
        FuncMap:                template.FuncMap{},
        RedirectTrailingSlash:  true,
        RedirectFixedPath:      false,
        HandleMethodNotAllowed: false,
        ForwardedByClientIP:    true,
        AppEngine:              defaultAppEngine,
        UseRawPath:             false,
        UnescapePathValues:     true,
        MaxMultipartMemory:     defaultMultipartMemory,
        trees:                  make(methodTrees, 0, 9),
        delims:                 render.Delims{Left: "{{", Right: "}}"},
        secureJsonPrefix:       "while(1);",
    }
    engine.RouterGroup.engine = engine
    engine.pool.New = func() interface{} {
        return engine.allocateContext()
    }
    return engine
}

func (engine *Engine) allocateContext() *Context {
    return &Context{engine: engine}
}

由此, 我们已经知道了 Context 是如何初始化的了.

Context 之请求参数获取

Context 肩负着很重要的使命, 所有的处理函数的唯一参数就是 Context.

// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)

在探究中间件的原理时, 我们已经看过了流程控制, 即 context.Next() 方法:

// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
// See example in GitHub.
func (c *Context) Next() {
    c.index++
    for c.index < int8(len(c.handlers)) {
        c.handlers[c.index](c)
        c.index++
    }
}

接着看一下如何获取请求参数, 比如 URL 中的参数, GET 中的 query, 或者是 POST 中的 data.

// However, this one will match /user/john/ and also /user/john/send
// If no other routers match /user/john, it will redirect to /user/john/
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)
})

// Query string parameters are parsed using the existing underlying request object.
// The request responds to a url matching:  /welcome?firstname=Jane&lastname=Doe
router.GET("/welcome", func(c *gin.Context) {
  firstname := c.DefaultQuery("firstname", "Guest")
  lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname")

  c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
})

router.POST("/form_post", func(c *gin.Context) {
  message := c.PostForm("message")
  nick := c.DefaultPostForm("nick", "anonymous")

  c.JSON(200, gin.H{
    "status":  "posted",
    "message": message,
    "nick":    nick,
  })
})

router.POST("/post", func(c *gin.Context) {

  ids := c.QueryMap("ids")
  names := c.PostFormMap("names")

  fmt.Printf("ids: %v; names: %v", ids, names)
})

上面的示例来自官方文档, 看一下其中涉及到的方法是如何实现的.

func (c *Context) Param(key string) string {
    return c.Params.ByName(key)
}

func (c *Context) Query(key string) string {
    value, _ := c.GetQuery(key)
    return value
}

func (c *Context) DefaultQuery(key, defaultValue string) string {
    if value, ok := c.GetQuery(key); ok {
        return value
    }
    return defaultValue
}

func (c *Context) GetQuery(key string) (string, bool) {
    if values, ok := c.GetQueryArray(key); ok {
        return values[0], ok
    }
    return "", false
}

func (c *Context) getQueryCache() {
    if c.queryCache == nil {
        c.queryCache = c.Request.URL.Query()
    }
}

func (c *Context) GetQueryArray(key string) ([]string, bool) {
    c.getQueryCache()
    if values, ok := c.queryCache[key]; ok && len(values) > 0 {
        return values, true
    }
    return []string{}, false
}

func (c *Context) PostForm(key string) string {
    value, _ := c.GetPostForm(key)
    return value
}

func (c *Context) DefaultPostForm(key, defaultValue string) string {
    if value, ok := c.GetPostForm(key); ok {
        return value
    }
    return defaultValue
}

func (c *Context) GetPostForm(key string) (string, bool) {
    if values, ok := c.GetPostFormArray(key); ok {
        return values[0], ok
    }
    return "", false
}

func (c *Context) PostFormArray(key string) []string {
    values, _ := c.GetPostFormArray(key)
    return values
}

func (c *Context) getFormCache() {
    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
    }
}

func (c *Context) GetPostFormArray(key string) ([]string, bool) {
    c.getFormCache()
    if values := c.formCache[key]; len(values) > 0 {
        return values, true
    }
    return []string{}, false
}

从上面的代码可以看出 GetQueryArrayGetPostFormArray 的实现非常相似, 都使用内部缓存.

func (c *Context) QueryMap(key string) map[string]string {
    dicts, _ := c.GetQueryMap(key)
    return dicts
}

func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
    c.getQueryCache()
    return c.get(c.queryCache, key)
}

func (c *Context) PostFormMap(key string) map[string]string {
    dicts, _ := c.GetPostFormMap(key)
    return dicts
}

func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
    c.getFormCache()
    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
}

上面的代码实现了参数的 map 化, 可以看下具体的请求参数, 下面的例子中 ids 就是一个 map, 它有两个 key.

POST /post?ids[a]=1234&ids[b]=hello HTTP/1.1
Content-Type: application/x-www-form-urlencoded

names[first]=thinkerou&names[second]=tianou

这不是 HTTP 中定义的内容, 使用的时候必须遵从这种规范, 可能在特定的场景下比较有用.
但一般不太会这么使用, 因为如果是公开的 API, 则其他语言都要实现这种类型的解析.
解析的代码倒是没有什么特别的,

再看一下文件类型的如何实现的, 即 FormFile.

// 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
}

稍微包装了一下 c.Request.MultipartForm, 对于单个文件而言更方便些, 保存文件的方法也有了.

Context 之模型绑定和验证

模型绑定是一个非常有用的能力, 尤其是和验证结合在一起. 处理请求参数时, 一大重点就是验证.

Gin 支持两种类型的绑定, Must bindShould bind. 请求类型则支持 JSON, XML, YAML 和标准表单绑定.

先来看一下 Must bind:

// 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
}

从上面的代码可以发现, MustBindWith 其实是 ShouldBindWith 的包装, 具体内容还是要看 ShouldBindWith.
另一点是绑定支持多种数据类型, 比如 BindQuery, BindHeader, BindUri.

// 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)
}

但实际上 ShouldBindWith 也只是调用了 binding.Binding 上的方法而言.

// Binding describes the interface which needs to be implemented for binding the
// data present in the request such as JSON request body, query parameters or
// the form POST.
type Binding interface {
    Name() string
    Bind(*http.Request, interface{}) error
}

// BindingBody adds BindBody method to Binding. BindBody is similar with Bind,
// but it reads the body from supplied bytes instead of req.Body.
type BindingBody interface {
    Binding
    BindBody([]byte, interface{}) error
}

// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
// but it read the Params.
type BindingUri interface {
    Name() string
    BindUri(map[string][]string, interface{}) error
}

// These implement the Binding interface and can be used to bind the data
// present in the request to struct instances.
var (
    JSON          = jsonBinding{}
    XML           = xmlBinding{}
    Form          = formBinding{}
    Query         = queryBinding{}
    FormPost      = formPostBinding{}
    FormMultipart = formMultipartBinding{}
    ProtoBuf      = protobufBinding{}
    MsgPack       = msgpackBinding{}
    YAML          = yamlBinding{}
    Uri           = uriBinding{}
    Header        = headerBinding{}
)

上面的代码显示了 Binding 接口, 以及实现了 Binding 接口的类型, 具体以 JSON 为例, 看一下 jsonBinding 是如何实现的.

package binding

import (
    "bytes"
    "fmt"
    "io"
    "net/http"

    "github.com/gin-gonic/gin/internal/json"
)

// EnableDecoderUseNumber is used to call the UseNumber method on the JSON
// Decoder instance. UseNumber causes the Decoder to unmarshal a number into an
// interface{} as a Number instead of as a float64.
var EnableDecoderUseNumber = false

// EnableDecoderDisallowUnknownFields is used to call the DisallowUnknownFields method
// on the JSON Decoder instance. DisallowUnknownFields causes the Decoder to
// return an error when the destination is a struct and the input contains object
// keys which do not match any non-ignored, exported fields in the destination.
var EnableDecoderDisallowUnknownFields = false

type jsonBinding struct{}

func (jsonBinding) Name() string {
    return "json"
}

func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
    if req == nil || req.Body == nil {
        return fmt.Errorf("invalid request")
    }
    return decodeJSON(req.Body, obj)
}

func (jsonBinding) BindBody(body []byte, obj interface{}) error {
    return decodeJSON(bytes.NewReader(body), obj)
}

func decodeJSON(r io.Reader, obj interface{}) error {
    decoder := json.NewDecoder(r)
    if EnableDecoderUseNumber {
        decoder.UseNumber()
    }
    if EnableDecoderDisallowUnknownFields {
        decoder.DisallowUnknownFields()
    }
    if err := decoder.Decode(obj); err != nil {
        return err
    }
    return validate(obj)
}

代码也不长, 内部用了自定义的 json 接口, 以便实现可替换的 JSON 编解码.

解码的最后一步是验证, 调用了 validate 函数:

func validate(obj interface{}) error {
    if Validator == nil {
        return nil
    }
    return Validator.ValidateStruct(obj)
}

由此, 可以引申到验证方面, 看一下是如何结合验证的.

// StructValidator is the minimal interface which needs to be implemented in
// order for it to be used as the validator engine for ensuring the correctness
// of the request. Gin provides a default implementation for this using
// https://github.com/go-playground/validator/tree/v8.18.2.
type StructValidator interface {
    // ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
    // If the received type is not a struct, any validation should be skipped and nil must be returned.
    // If the received type is a struct or pointer to a struct, the validation should be performed.
    // If the struct is not valid or the validation itself fails, a descriptive error should be returned.
    // Otherwise nil must be returned.
    ValidateStruct(interface{}) error

    // Engine returns the underlying validator engine which powers the
    // StructValidator implementation.
    Engine() interface{}
}

// Validator is the default validator which implements the StructValidator
// interface. It uses https://github.com/go-playground/validator/tree/v8.18.2
// under the hood.
var Validator StructValidator = &defaultValidator{}

验证器需要实现 StructValidator 接口, 看一下默认的验证器的实现.

package binding

import (
    "reflect"
    "sync"

    "gopkg.in/go-playground/validator.v9"
)

type defaultValidator struct {
    once     sync.Once
    validate *validator.Validate
}

var _ StructValidator = &defaultValidator{}

// ValidateStruct receives any kind of type, but only performed struct or pointer to struct type.
func (v *defaultValidator) ValidateStruct(obj interface{}) error {
    value := reflect.ValueOf(obj)
    valueType := value.Kind()
    if valueType == reflect.Ptr {
        valueType = value.Elem().Kind()
    }
    if valueType == reflect.Struct {
        v.lazyinit()
        if err := v.validate.Struct(obj); err != nil {
            return err
        }
    }
    return nil
}

// Engine returns the underlying validator engine which powers the default
// Validator instance. This is useful if you want to register custom validations
// or struct level validations. See validator GoDoc for more info -
// https://godoc.org/gopkg.in/go-playground/validator.v8
func (v *defaultValidator) Engine() interface{} {
    v.lazyinit()
    return v.validate
}

func (v *defaultValidator) lazyinit() {
    v.once.Do(func() {
        v.validate = validator.New()
        v.validate.SetTagName("binding")
    })
}

默认的验证器是 validator.v9, 使用了懒初始化, 以及使用 reflect 判断数据类型, 只验证结构体.

Context 之响应

看完了请求参数的获取和模型绑定之后, 来看看响应是如何发送的.

先来看一下 Context 中用到的 responseWriter 类型和 ResponseWriter 类型.

type Context struct {
    writermem responseWriter
    Request   *http.Request
    Writer    ResponseWriter
// ResponseWriter ...
type ResponseWriter interface {
    http.ResponseWriter
    http.Hijacker
    http.Flusher
    http.CloseNotifier

    // Returns the HTTP response status code of the current request.
    Status() int

    // Returns the number of bytes already written into the response http body.
    // See Written()
    Size() int

    // Writes the string into the response body.
    WriteString(string) (int, error)

    // Returns true if the response body was already written.
    Written() bool

    // Forces to write the http header (status code + headers).
    WriteHeaderNow()

    // get the http.Pusher for server push
    Pusher() http.Pusher
}

type responseWriter struct {
    http.ResponseWriter
    size   int
    status int
}

var _ ResponseWriter = &responseWriter{}

ResponseWriter 接口组合了 http 包中用于响应的数据结构, 所有的方法上都有注释.
responseWriter 实际上就是实现了 ResponseWriter 接口的结构体.

在继续之前, 先来了解下 Context 中 writermem 的作用.
Writer 是用于写入响应的, 而从 writermem 名字的后缀, 可以推断出这和内存有关.
再寻找一下它的用处.

// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    c := engine.pool.Get().(*Context)
    c.writermem.reset(w)
    c.Request = req
    c.reset()

    engine.handleHTTPRequest(c)

    engine.pool.Put(c)
}

func (c *Context) reset() {
  c.Writer = &c.writermem
  ...
}

func (w *responseWriter) reset(writer http.ResponseWriter) {
    w.ResponseWriter = writer
    w.size = noWritten
    w.status = defaultStatus
}

所以, 可以推断出 writermem 是每次请求时 w http.ResponseWriter 的拥有者, 而 c.Writer 是它的指针.

继续看 Context 是如何处理响应的.

func (c *Context) requestHeader(key string) string {
    return c.Request.Header.Get(key)
}

// 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)
}

上面是和 Header 有关的部分, 实际上是内部的 Writer.Header() 的代理.

接着看和 Cookie 有关的部分:

// 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,
        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
}

整合了 Cookie 的读取与设置.

看完 Header 和 Cookie 之后, 接下来就是重点了, 看一下如何渲染内容, 即返回的响应.

Gin 支持 XML, JSON, YAML and ProtoBuf rendering, 看一下具体的实现方式.

// 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)
    }
}

主要的方法就是 Render, 而内部使用了 render.Render 接口中的 Render 方法.

// 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 add 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,
    })
}

看一下这些迥异的 render.Render 接口的实现者.

package render

import "net/http"

// Render interface is to be implemented by JSON, XML, HTML, YAML and so on.
type Render interface {
    // Render writes data with custom ContentType.
    Render(http.ResponseWriter) error
    // WriteContentType writes custom ContentType.
    WriteContentType(w http.ResponseWriter)
}

var (
    _ Render     = JSON{}
    _ Render     = IndentedJSON{}
    _ Render     = SecureJSON{}
    _ Render     = JsonpJSON{}
    _ Render     = XML{}
    _ Render     = String{}
    _ Render     = Redirect{}
    _ Render     = Data{}
    _ Render     = HTML{}
    _ HTMLRender = HTMLDebug{}
    _ HTMLRender = HTMLProduction{}
    _ Render     = YAML{}
    _ Render     = MsgPack{}
    _ Render     = Reader{}
    _ Render     = AsciiJSON{}
    _ Render     = ProtoBuf{}
)

func writeContentType(w http.ResponseWriter, value []string) {
    header := w.Header()
    if val := header["Content-Type"]; len(val) == 0 {
        header["Content-Type"] = value
    }
}

上面是 Render 接口的定义, 主要需要实现 Render 方法.
WriteContentType 方法实际上已经被 writeContentType 函数实现得差不多了,
只是每种渲染方式对应的 Content-Type 值不同.

以 JSON 为例, 看一下具体是如何实现的.

// JSON contains the given interface object.
type JSON struct {
    Data interface{}
}

var jsonContentType = []string{"application/json; charset=utf-8"}

// Render (JSON) writes data with custom ContentType.
func (r JSON) Render(w http.ResponseWriter) (err error) {
    if err = WriteJSON(w, r.Data); err != nil {
        panic(err)
    }
    return
}

// WriteContentType (JSON) writes JSON ContentType.
func (r JSON) WriteContentType(w http.ResponseWriter) {
    writeContentType(w, jsonContentType)
}

// WriteJSON marshals the given interface object and writes it with custom ContentType.
func WriteJSON(w http.ResponseWriter, obj interface{}) error {
    writeContentType(w, jsonContentType)
    encoder := json.NewEncoder(w)
    err := encoder.Encode(&obj)
    return err
}

看上去非常简洁, 实现也不复杂.

RenderBinding 非常相似, 都是通过定义接口, 然后用不同的结构体实现具体的功能.

Context 之高级响应

// File writes the specified file into the body stream in a efficient way.
func (c *Context) File(filepath string) {
    http.ServeFile(c.Writer, c.Request, filepath)
}

// 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)
}

托管静态文件, 使用的是 http.ServeFile, 也实现了附件下载的功能, 还是挺方便的,
虽然只是 content-disposition 这个 Header 的功能.

// 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,
    })
}

SSEvent 实现了服务端推送事件的功能, 具体看一下它的实现.

package sse

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "reflect"
    "strconv"
    "strings"
)

// Server-Sent Events
// W3C Working Draft 29 October 2009
// http://www.w3.org/TR/2009/WD-eventsource-20091029/

const ContentType = "text/event-stream"

var contentType = []string{ContentType}
var noCache = []string{"no-cache"}

var fieldReplacer = strings.NewReplacer(
    "\n", "\\n",
    "\r", "\\r")

var dataReplacer = strings.NewReplacer(
    "\n", "\ndata:",
    "\r", "\\r")

type Event struct {
    Event string
    Id    string
    Retry uint
    Data  interface{}
}

func Encode(writer io.Writer, event Event) error {
    w := checkWriter(writer)
    writeId(w, event.Id)
    writeEvent(w, event.Event)
    writeRetry(w, event.Retry)
    return writeData(w, event.Data)
}

func writeId(w stringWriter, id string) {
    if len(id) > 0 {
        w.WriteString("id:")
        fieldReplacer.WriteString(w, id)
        w.WriteString("\n")
    }
}

func writeEvent(w stringWriter, event string) {
    if len(event) > 0 {
        w.WriteString("event:")
        fieldReplacer.WriteString(w, event)
        w.WriteString("\n")
    }
}

func writeRetry(w stringWriter, retry uint) {
    if retry > 0 {
        w.WriteString("retry:")
        w.WriteString(strconv.FormatUint(uint64(retry), 10))
        w.WriteString("\n")
    }
}

func writeData(w stringWriter, data interface{}) error {
    w.WriteString("data:")
    switch kindOfData(data) {
    case reflect.Struct, reflect.Slice, reflect.Map:
        err := json.NewEncoder(w).Encode(data)
        if err != nil {
            return err
        }
        w.WriteString("\n")
    default:
        dataReplacer.WriteString(w, fmt.Sprint(data))
        w.WriteString("\n\n")
    }
    return nil
}

func (r Event) Render(w http.ResponseWriter) error {
    r.WriteContentType(w)
    return Encode(w, r)
}

func (r Event) WriteContentType(w http.ResponseWriter) {
    header := w.Header()
    header["Content-Type"] = contentType

    if _, exist := header["Cache-Control"]; !exist {
        header["Cache-Control"] = noCache
    }
}

func kindOfData(data interface{}) reflect.Kind {
    value := reflect.ValueOf(data)
    valueType := value.Kind()
    if valueType == reflect.Ptr {
        valueType = value.Elem().Kind()
    }
    return valueType
}

SSEvent 是作为扩展实现的, 代码并不在 Gin 的源码中. 先看一下 Event 结构体.

type Event struct {
    Event string
    Id    string
    Retry uint
    Data  interface{}
}

func (r Event) Render(w http.ResponseWriter) error {
    r.WriteContentType(w)
    return Encode(w, r)
}

Event 实现了 Render 接口, 看一下内部的 Encode 函数.

func Encode(writer io.Writer, event Event) error {
    w := checkWriter(writer)
    writeId(w, event.Id)
    writeEvent(w, event.Event)
    writeRetry(w, event.Retry)
    return writeData(w, event.Data)
}

过程并不复杂, 分为四步写入, 分别是事件 ID, 事件名 Event, 重连时间 Retry, 消息体 Data.
如果对服务端推送事件不太了解, 可以参考
MDN-使用服务器发送事件..

事件流仅仅是一个简单的文本数据流,文本应该使用 UTF- 8 格式的编码.每条消息后面都由一个空行作为分隔符.以冒号开头的行为注释行,会被忽略.
注:注释行可以用来防止连接超时,服务器可以定期发送一条消息注释行,以保持连接不断.
每条消息是由多个字段组成的,每个字段由字段名,一个冒号,以及字段值组成.

实际上并没有对消息体的格式做任何要求, 这属于前后端协定的范围.

func writeData(w stringWriter, data interface{}) error {
    w.WriteString("data:")
    switch kindOfData(data) {
    case reflect.Struct, reflect.Slice, reflect.Map:
        err := json.NewEncoder(w).Encode(data)
        if err != nil {
            return err
        }
        w.WriteString("\n")
    default:
        dataReplacer.WriteString(w, fmt.Sprint(data))
        w.WriteString("\n\n")
    }
    return nil
}

该实现中, 主要使用了 JSON 格式, 但对其他类型的数据直接写入纯文本.

接着看一下流式响应是如何实现的.

// 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
            }
        }
    }
}

这是一个非常常见的模式, 使用 for 和 select 以及 channel 实现无限循环.

Context 之内容协商

内容协商通过 Accept Header 实现, 用于为不同类型的客户端提供不同类型的资源,
比如协商网页语言或响应格式等.

具体可以参考 MDN-内容协商.

// Negotiate contains all negotiations data.
type Negotiate struct {
    Offered  []string
    HTMLName string
    HTMLData interface{}
    JSONData interface{}
    XMLData  interface{}
    Data     interface{}
}

// Negotiate calls different Render according acceptable Accept format.
func (c *Context) Negotiate(code int, config Negotiate) {
    switch c.NegotiateFormat(config.Offered...) {
    case binding.MIMEJSON:
        data := chooseData(config.JSONData, config.Data)
        c.JSON(code, data)

    case binding.MIMEHTML:
        data := chooseData(config.HTMLData, config.Data)
        c.HTML(code, config.HTMLName, data)

    case binding.MIMEXML:
        data := chooseData(config.XMLData, config.Data)
        c.XML(code, data)

    default:
        c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) // nolint: errcheck
    }
}

// NegotiateFormat returns an acceptable Accept format.
func (c *Context) NegotiateFormat(offered ...string) string {
    assert1(len(offered) > 0, "you must provide at least one offer")

    if c.Accepted == nil {
        c.Accepted = parseAccept(c.requestHeader("Accept"))
    }
    if len(c.Accepted) == 0 {
        return offered[0]
    }
    for _, accepted := range c.Accepted {
        for _, offert := range offered {
            // According to RFC 2616 and RFC 2396, non-ASCII characters are not allowed in headers,
            // therefore we can just iterate over the string without casting it into []rune
            i := 0
            for ; i < len(accepted); i++ {
                if accepted[i] == '*' || offert[i] == '*' {
                    return offert
                }
                if accepted[i] != offert[i] {
                    break
                }
            }
            if i == len(accepted) {
                return offert
            }
        }
    }
    return ""
}

// SetAccepted sets Accept header data.
func (c *Context) SetAccepted(formats ...string) {
    c.Accepted = formats
}

总结

Context 的内容就到这里了, 虽然源文件有点长, 但配合注释还是挺清晰的.


帅气猫咪
105 声望13 粉丝

« 上一篇
02Gin源码解读
下一篇 »
04Gin源码解读