gin-gonic / gin

Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API with much better performance -- up to 40 times faster. If you need smashing performance, get yourself some Gin.
https://gin-gonic.com/
MIT License
78.88k stars 8.02k forks source link

Question with render.Render And effective JSON. #2092

Open ShiinaOrez opened 5 years ago

ShiinaOrez commented 5 years ago

First, why call writeContentType() in twice?

When I do the research for source code of go-gonic/gin for building more EFFECTIVE json way, render.Render interface got my attention: Because when you call c.JSON() method, you'll call the writeContentType() function in render.go twice, though the "content-type" header just be replaced, but I think it is unnecessary.

Second, build json in effictive way

Cuz encoding/json using a lot of reflect, so the performance for using encoding/json is unsatisfactory, now I see a solution of serializing to json using strongly typed way, so I want to build a new Type for render.Render, and I could use github.com/darjun/json-gen to make the performance upper.

github.com/darjun/json-gen

My Solution:

Assume the new Type is Response, so we create the Render() and WriteContentType() methods on it: WriteContentType() is simple, so we talk about the Render() method.

In the source code for gin.render.JSON.Render(), it build json by this way:

encoder := json.NewEncoder(w) // w type is http.ResponseWriter
err := encoder.Encode(&obj) // obj is interface{}

Now I build a new Interface called JSONify, it just contain only one method:

type JSONify interface {
    Map()  *jsongen.Map
}

So the nested data structure need to implemente this interface too, we just call the top-level data structure's Map() method in Render() method, performance improvements can range from 75% to 90%.

Demo:

go version go1.10.1 linux/amd64

package main

import (
    "net/http"
    "fmt"
    "time"
    "encoding/json"
    jsongen "github.com/darjun/json-gen"
)

type JSONify interface {
    Map() *jsongen.Map
}

type Response struct {
    Code    int         `json:"code"`
    Message string      `json:"message"`
    Data    JSONify     `json:"data"`
}

func (response Response) Map() *jsongen.Map {
    m := jsongen.NewMap() // m type *jsongen.Map
    m.PutInt("code", int64(response.Code)) // response.Code's type decide by CPU(runtime.GOARCH).
    m.PutString("message", response.Message)
    m.PutMap("data", response.Data.Map())
    return m
}

type ResponseData struct {
    Name      string   `json:"name"`
    Score     float64  `json:"score"`
    Friends   []string `json:"friends"`
}

func (data ResponseData) Map() *jsongen.Map {
    m := jsongen.NewMap()
    m.PutString("name", data.Name)
    m.PutFloat("score", data.Score)
    m.PutStringArray("friends", data.Friends)
    return m
}

func (response Response) WriteContentType(w http.ResponseWriter) {
    writeContentType(w, jsonContentType)
}

func (response Response) Render(w http.ResponseWriter) error {
    writeContentType(w, jsonContentType)

    m := response.Map()
    w.Write(m.Serialize(nil))

    return nil
}

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

var (
    _ JSONify = Response{}
    _ JSONify = ResponseData{}

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

func main() {
    responseData := ResponseData {
        Name: "Bob",
        Score: 1.2223,
        Friends: []string{"Alice", "Cherry"},
    }
    response := Response {
        Code: 0,
        Message: "OK",
        Data: responseData,
    }

    time1 := time.Now()
    m := response.Map()
    bytes := m.Serialize(nil)
    fmt.Println(time.Now().Sub(time1))

    time2 := time.Now()
    bytes, _ = json.Marshal(response)
    fmt.Println(time.Now().Sub(time2))
    fmt.Println(string(bytes))
}

Chinese Version中文版本

为什么要调用WriteContentType()两次?

最近在研究序列化, 然后有阅读c.JSON()的代码(因为我的业务逻辑中经常性的使用c.JSON()方法), 然后深挖下去发现gin重复调用了writeContentType()方法(这个方法在gin-gonic/render/render.go)中, 这看上去挺奇怪的, 不是吗?

render.Render接口包含了Render()和WriteContentType()两个方法, 但是却分别在两个方法中都进行了对于Content-Type这个响应头的写操作, 虽然只是对于Map的一次插入而已, 但是我认为这是不必要的, 如果是为了保险, 那么为什么接口还要包含WriteContentType()方法呢?

以上就是我的第一个问题.

更加高效的构建JSON的方法

众所周知encoding/json的序列化是低效的, 因为使用了过多的反射和类型断言, 因此在网上看到了使用强类型的方法进行序列化JSON生成的时候, 我就在想能不能使用这种方法在gin中多加入一个强类型JSON生成的接口类型.

使用该库的方法不再赘述, 链接上面有, 只是讲一下我的解决方案的大体思想:

构建一个类似gin.render.JSON的接口类型, 同样实现WriteContentType和Render两个方法, 在Render中调用该类型的Map()方法, (任何自定义数据类型都应该满足这个接口, 目的是返回一个json-gen库中的*Map类型, 可以直接导出序列化数据), 这样就可以完成递归的高效序列化操作, 在Render中只需要调用顶层数据结构的Map()方法, 就可以获取到JSON串了.

我写的demo在上面有, 在结构很简单的时候大概可以有2/3到3/4的性能提升, 面对比较复杂的JSON可以有90%的提升.

guonaihong commented 5 years ago

Good, can have test data?

ShiinaOrez commented 5 years ago

事实上, 测试数据不论是在我给出的demo中还是在json-gen中都有, 因此请自行查找