Performance improvements when rendering

- Fast path for JSON, XML and plain text rendering
This commit is contained in:
Manu Mtz-Almeida 2015-05-07 12:44:52 +02:00
parent eb3e9293ed
commit 2d8f0a4801
10 changed files with 299 additions and 163 deletions

View File

@ -6,7 +6,6 @@ package gin
import (
"errors"
"fmt"
"math"
"net/http"
"strings"
@ -314,27 +313,15 @@ func (c *Context) BindWith(obj interface{}, b binding.Binding) bool {
/******** RESPONSE RENDERING ********/
/************************************/
func (c *Context) Render(code int, render render.Render, obj ...interface{}) {
if err := render.Render(c.Writer, code, obj...); err != nil {
c.ErrorTyped(err, ErrorTypeInternal, obj)
func (c *Context) renderingError(err error, meta ...interface{}) {
c.ErrorTyped(err, ErrorTypeInternal, meta)
c.AbortWithStatus(500)
}
}
// Serializes the given struct as JSON into the response body in a fast and efficient way.
// It also sets the Content-Type as "application/json".
func (c *Context) JSON(code int, obj interface{}) {
c.Render(code, render.JSON, obj)
func (c *Context) Render(code int, render render.Render, obj ...interface{}) {
if err := render.Render(c.Writer, code, obj...); err != nil {
c.renderingError(err, obj)
}
func (c *Context) IndentedJSON(code int, obj interface{}) {
c.Render(code, render.IndentedJSON, obj)
}
// Serializes the given struct as XML into the response body in a fast and efficient way.
// It also sets the Content-Type as "application/xml".
func (c *Context) XML(code int, obj interface{}) {
c.Render(code, render.XML, obj)
}
// Renders the HTTP template specified by its file name.
@ -344,31 +331,44 @@ func (c *Context) HTML(code int, name string, obj interface{}) {
c.Render(code, c.Engine.HTMLRender, name, obj)
}
func (c *Context) IndentedJSON(code int, obj interface{}) {
c.Render(code, render.IndentedJSON, obj)
}
// Serializes the given struct as JSON into the response body in a fast and efficient way.
// It also sets the Content-Type as "application/json".
func (c *Context) JSON(code int, obj interface{}) {
if err := render.WriteJSON(c.Writer, code, obj); err != nil {
c.renderingError(err, obj)
}
}
// Serializes the given struct as XML into the response body in a fast and efficient way.
// It also sets the Content-Type as "application/xml".
func (c *Context) XML(code int, obj interface{}) {
if err := render.WriteXML(c.Writer, code, obj); err != nil {
c.renderingError(err, obj)
}
}
// Writes the given string into the response body and sets the Content-Type to "text/plain".
func (c *Context) String(code int, format string, values ...interface{}) {
c.Render(code, render.Plain, format, values)
render.WritePlainText(c.Writer, code, format, values)
}
// Writes the given string into the response body and sets the Content-Type to "text/html" without template.
func (c *Context) HTMLString(code int, format string, values ...interface{}) {
c.Render(code, render.HTMLPlain, format, values)
render.WriteHTMLString(c.Writer, code, format, values)
}
// Returns a HTTP redirect to the specific location.
func (c *Context) Redirect(code int, location string) {
if code < 300 || code > 308 {
panic(fmt.Sprintf("Cannot redirect with status code %d", code))
}
c.Render(code, render.Redirect, c.Request, location)
render.WriteRedirect(c.Writer, code, c.Request, location)
}
// Writes some data into the body stream and updates the HTTP code.
func (c *Context) Data(code int, contentType string, data []byte) {
if len(contentType) > 0 {
c.Writer.Header().Set("Content-Type", contentType)
}
c.Writer.WriteHeader(code)
c.Writer.Write(data)
render.WriteData(c.Writer, code, contentType, data)
}
// Writes the specified file into the body stream

20
render/data.go Normal file
View File

@ -0,0 +1,20 @@
package render
import "net/http"
type dataRender struct{}
func (_ dataRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
contentType := data[0].(string)
bytes := data[1].([]byte)
WriteData(w, code, contentType, bytes)
return nil
}
func WriteData(w http.ResponseWriter, code int, contentType string, data []byte) {
if len(contentType) > 0 {
w.Header().Set("Content-Type", contentType)
}
w.WriteHeader(code)
w.Write(data)
}

66
render/html.go Normal file
View File

@ -0,0 +1,66 @@
package render
import (
"errors"
"fmt"
"html/template"
"net/http"
)
type (
HTMLRender struct {
Template *template.Template
}
htmlPlainRender struct{}
HTMLDebugRender struct {
Files []string
Glob string
}
)
func (html HTMLRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
WriteHeader(w, code, "text/html")
file := data[0].(string)
args := data[1]
return html.Template.ExecuteTemplate(w, file, args)
}
func (r *HTMLDebugRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
WriteHeader(w, code, "text/html")
file := data[0].(string)
obj := data[1]
if t, err := r.loadTemplate(); err == nil {
return t.ExecuteTemplate(w, file, obj)
} else {
return err
}
}
func (r *HTMLDebugRender) loadTemplate() (*template.Template, error) {
if len(r.Files) > 0 {
return template.ParseFiles(r.Files...)
}
if len(r.Glob) > 0 {
return template.ParseGlob(r.Glob)
}
return nil, errors.New("the HTML debug render was created without files or glob pattern")
}
func (_ htmlPlainRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
format := data[0].(string)
values := data[1].([]interface{})
WriteHTMLString(w, code, format, values)
return nil
}
func WriteHTMLString(w http.ResponseWriter, code int, format string, values []interface{}) {
WriteHeader(w, code, "text/html")
if len(values) > 0 {
fmt.Fprintf(w, format, values...)
} else {
w.Write([]byte(format))
}
}

View File

@ -1,38 +0,0 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package render
import (
"errors"
"html/template"
"net/http"
)
type HTMLDebugRender struct {
Files []string
Glob string
}
func (r *HTMLDebugRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
WriteHeader(w, code, "text/html")
file := data[0].(string)
obj := data[1]
if t, err := r.loadTemplate(); err == nil {
return t.ExecuteTemplate(w, file, obj)
} else {
return err
}
}
func (r *HTMLDebugRender) loadTemplate() (*template.Template, error) {
if len(r.Files) > 0 {
return template.ParseFiles(r.Files...)
}
if len(r.Glob) > 0 {
return template.ParseGlob(r.Glob)
}
return nil, errors.New("the HTML debug render was created without files or glob pattern")
}

31
render/json.go Normal file
View File

@ -0,0 +1,31 @@
package render
import (
"encoding/json"
"net/http"
)
type (
jsonRender struct{}
indentedJSON struct{}
)
func (_ jsonRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
return WriteJSON(w, code, data[0])
}
func (_ indentedJSON) Render(w http.ResponseWriter, code int, data ...interface{}) error {
WriteHeader(w, code, "application/json")
jsonData, err := json.MarshalIndent(data[0], "", " ")
if err != nil {
return err
}
_, err = w.Write(jsonData)
return err
}
func WriteJSON(w http.ResponseWriter, code int, data interface{}) error {
WriteHeader(w, code, "application/json")
return json.NewEncoder(w).Encode(data)
}

22
render/redirect.go Normal file
View File

@ -0,0 +1,22 @@
package render
import (
"fmt"
"net/http"
)
type redirectRender struct{}
func (_ redirectRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
req := data[0].(*http.Request)
location := data[1].(string)
WriteRedirect(w, code, req, location)
return nil
}
func WriteRedirect(w http.ResponseWriter, code int, req *http.Request, location string) {
if code < 300 || code > 308 {
panic(fmt.Sprintf("Cannot redirect with status code %d", code))
}
http.Redirect(w, req, location, code)
}

View File

@ -4,103 +4,24 @@
package render
import (
"encoding/json"
"encoding/xml"
"fmt"
"html/template"
"net/http"
)
import "net/http"
type (
Render interface {
type Render interface {
Render(http.ResponseWriter, int, ...interface{}) error
}
jsonRender struct{}
indentedJSON struct{}
xmlRender struct{}
plainTextRender struct{}
htmlPlainRender struct{}
redirectRender struct{}
HTMLRender struct {
Template *template.Template
}
)
var (
JSON = jsonRender{}
IndentedJSON = indentedJSON{}
XML = xmlRender{}
HTMLPlain = htmlPlainRender{}
Plain = plainTextRender{}
Redirect = redirectRender{}
JSON Render = jsonRender{}
IndentedJSON Render = indentedJSON{}
XML Render = xmlRender{}
HTMLPlain Render = htmlPlainRender{}
Plain Render = plainTextRender{}
Redirect Render = redirectRender{}
Data Render = dataRender{}
_ Render = HTMLRender{}
_ Render = &HTMLDebugRender{}
)
func (_ redirectRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
req := data[0].(*http.Request)
location := data[1].(string)
http.Redirect(w, req, location, code)
return nil
}
func (_ jsonRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
WriteHeader(w, code, "application/json")
return json.NewEncoder(w).Encode(data[0])
}
func (_ indentedJSON) Render(w http.ResponseWriter, code int, data ...interface{}) error {
WriteHeader(w, code, "application/json")
jsonData, err := json.MarshalIndent(data[0], "", " ")
if err != nil {
return err
}
_, err = w.Write(jsonData)
return err
}
func (_ xmlRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
WriteHeader(w, code, "application/xml")
return xml.NewEncoder(w).Encode(data[0])
}
func (_ plainTextRender) Render(w http.ResponseWriter, code int, data ...interface{}) (err error) {
WriteHeader(w, code, "text/plain")
format := data[0].(string)
args := data[1].([]interface{})
if len(args) > 0 {
_, err = fmt.Fprintf(w, format, args...)
} else {
_, err = w.Write([]byte(format))
}
return
}
func (_ htmlPlainRender) Render(w http.ResponseWriter, code int, data ...interface{}) (err error) {
WriteHeader(w, code, "text/html")
format := data[0].(string)
args := data[1].([]interface{})
if len(args) > 0 {
_, err = fmt.Fprintf(w, format, args...)
} else {
_, err = w.Write([]byte(format))
}
return
}
func (html HTMLRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
WriteHeader(w, code, "text/html")
file := data[0].(string)
args := data[1]
return html.Template.ExecuteTemplate(w, file, args)
}
func WriteHeader(w http.ResponseWriter, code int, contentType string) {
contentType = joinStrings(contentType, "; charset=utf-8")
w.Header().Set("Content-Type", contentType)

View File

@ -5,6 +5,7 @@
package render
import (
"encoding/xml"
"html/template"
"net/http/httptest"
"testing"
@ -14,10 +15,15 @@ import (
func TestRenderJSON(t *testing.T) {
w := httptest.NewRecorder()
err := JSON.Render(w, 201, map[string]interface{}{
w2 := httptest.NewRecorder()
data := map[string]interface{}{
"foo": "bar",
})
}
err := JSON.Render(w, 201, data)
WriteJSON(w2, 201, data)
assert.Equal(t, w, w2)
assert.NoError(t, err)
assert.Equal(t, w.Code, 201)
assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n")
@ -37,10 +43,76 @@ func TestRenderIndentedJSON(t *testing.T) {
assert.Equal(t, w.Header().Get("Content-Type"), "application/json; charset=utf-8")
}
type xmlmap map[string]interface{}
// Allows type H to be used with xml.Marshal
func (h xmlmap) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
start.Name = xml.Name{
Space: "",
Local: "map",
}
if err := e.EncodeToken(start); err != nil {
return err
}
for key, value := range h {
elem := xml.StartElement{
Name: xml.Name{Space: "", Local: key},
Attr: []xml.Attr{},
}
if err := e.EncodeElement(value, elem); err != nil {
return err
}
}
if err := e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil {
return err
}
return nil
}
func TestRenderXML(t *testing.T) {
w := httptest.NewRecorder()
w2 := httptest.NewRecorder()
data := xmlmap{
"foo": "bar",
}
err := XML.Render(w, 200, data)
WriteXML(w2, 200, data)
assert.Equal(t, w, w2)
assert.NoError(t, err)
assert.Equal(t, w.Code, 200)
assert.Equal(t, w.Body.String(), "<map><foo>bar</foo></map>")
assert.Equal(t, w.Header().Get("Content-Type"), "application/xml; charset=utf-8")
}
func TestRenderRedirect(t *testing.T) {
// TODO
}
func TestRenderData(t *testing.T) {
w := httptest.NewRecorder()
w2 := httptest.NewRecorder()
data := []byte("#!PNG some raw data")
err := Data.Render(w, 400, "image/png", data)
WriteData(w2, 400, "image/png", data)
assert.Equal(t, w, w2)
assert.NoError(t, err)
assert.Equal(t, w.Code, 400)
assert.Equal(t, w.Body.String(), "#!PNG some raw data")
assert.Equal(t, w.Header().Get("Content-Type"), "image/png")
}
func TestRenderPlain(t *testing.T) {
w := httptest.NewRecorder()
err := Plain.Render(w, 400, "hola %s %d", []interface{}{"manu", 2})
w2 := httptest.NewRecorder()
err := Plain.Render(w, 400, "hola %s %d", []interface{}{"manu", 2})
WritePlainText(w2, 400, "hola %s %d", []interface{}{"manu", 2})
assert.Equal(t, w, w2)
assert.NoError(t, err)
assert.Equal(t, w.Code, 400)
assert.Equal(t, w.Body.String(), "hola manu 2")

25
render/text.go Normal file
View File

@ -0,0 +1,25 @@
package render
import (
"fmt"
"net/http"
)
type plainTextRender struct{}
func (_ plainTextRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
format := data[0].(string)
values := data[1].([]interface{})
WritePlainText(w, code, format, values)
return nil
}
func WritePlainText(w http.ResponseWriter, code int, format string, values []interface{}) {
WriteHeader(w, code, "text/plain")
// we assume w.Write can not fail, is that right?
if len(values) > 0 {
fmt.Fprintf(w, format, values...)
} else {
w.Write([]byte(format))
}
}

17
render/xml.go Normal file
View File

@ -0,0 +1,17 @@
package render
import (
"encoding/xml"
"net/http"
)
type xmlRender struct{}
func (_ xmlRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
return WriteXML(w, code, data[0])
}
func WriteXML(w http.ResponseWriter, code int, data interface{}) error {
WriteHeader(w, code, "application/xml")
return xml.NewEncoder(w).Encode(data)
}