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.
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.gotwice, 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.
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))
}
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 usegithub.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()
andWriteContentType()
methods on it:WriteContentType()
is simple, so we talk about theRender()
method.In the source code for
gin.render.JSON.Render()
, it build json by this way:Now I build a new Interface called JSONify, it just contain only one method:
So the nested data structure need to implemente this interface too, we just call the top-level data structure's
Map()
method inRender()
method, performance improvements can range from 75% to 90%.Demo:
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%的提升.