From 2d8f0a48017ce647b625a0789238bdb5926738a2 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Thu, 7 May 2015 12:44:52 +0200 Subject: [PATCH] Performance improvements when rendering - Fast path for JSON, XML and plain text rendering --- context.go | 60 ++++++++++++------------ render/data.go | 20 ++++++++ render/html.go | 66 ++++++++++++++++++++++++++ render/html_debug.go | 38 --------------- render/json.go | 31 +++++++++++++ render/redirect.go | 22 +++++++++ render/render.go | 105 ++++++------------------------------------ render/render_test.go | 78 +++++++++++++++++++++++++++++-- render/text.go | 25 ++++++++++ render/xml.go | 17 +++++++ 10 files changed, 299 insertions(+), 163 deletions(-) create mode 100644 render/data.go create mode 100644 render/html.go delete mode 100644 render/html_debug.go create mode 100644 render/json.go create mode 100644 render/redirect.go create mode 100644 render/text.go create mode 100644 render/xml.go diff --git a/context.go b/context.go index b99e54cc..44b82e73 100644 --- a/context.go +++ b/context.go @@ -6,7 +6,6 @@ package gin import ( "errors" - "fmt" "math" "net/http" "strings" @@ -314,29 +313,17 @@ func (c *Context) BindWith(obj interface{}, b binding.Binding) bool { /******** RESPONSE RENDERING ********/ /************************************/ +func (c *Context) renderingError(err error, meta ...interface{}) { + c.ErrorTyped(err, ErrorTypeInternal, meta) + c.AbortWithStatus(500) +} + 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) - c.AbortWithStatus(500) + c.renderingError(err, 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{}) { - c.Render(code, render.JSON, 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. // It also updates the HTTP code and sets the Content-Type as "text/html". // See http://golang.org/doc/articles/wiki/ @@ -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 diff --git a/render/data.go b/render/data.go new file mode 100644 index 00000000..42f14d53 --- /dev/null +++ b/render/data.go @@ -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) +} diff --git a/render/html.go b/render/html.go new file mode 100644 index 00000000..139a8aea --- /dev/null +++ b/render/html.go @@ -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)) + } +} diff --git a/render/html_debug.go b/render/html_debug.go deleted file mode 100644 index 2a5a6971..00000000 --- a/render/html_debug.go +++ /dev/null @@ -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") -} diff --git a/render/json.go b/render/json.go new file mode 100644 index 00000000..a6bab248 --- /dev/null +++ b/render/json.go @@ -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) +} diff --git a/render/redirect.go b/render/redirect.go new file mode 100644 index 00000000..6f6e60a7 --- /dev/null +++ b/render/redirect.go @@ -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) +} diff --git a/render/render.go b/render/render.go index 53b9978f..694f4007 100644 --- a/render/render.go +++ b/render/render.go @@ -4,103 +4,24 @@ package render -import ( - "encoding/json" - "encoding/xml" - "fmt" - "html/template" - "net/http" -) +import "net/http" -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 - } -) +type Render interface { + Render(http.ResponseWriter, int, ...interface{}) error +} 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) diff --git a/render/render_test.go b/render/render_test.go index 88ee24f5..6f27f041 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -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(), "bar") + 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") diff --git a/render/text.go b/render/text.go new file mode 100644 index 00000000..bfcfc6ff --- /dev/null +++ b/render/text.go @@ -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)) + } +} diff --git a/render/xml.go b/render/xml.go new file mode 100644 index 00000000..3792d5fa --- /dev/null +++ b/render/xml.go @@ -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) +}