Pārlūkot izejas kodu

Performance improvements when rendering

- Fast path for JSON, XML and plain text rendering
Manu Mtz-Almeida 10 gadi atpakaļ
vecāks
revīzija
2d8f0a4801
10 mainītis faili ar 294 papildinājumiem un 158 dzēšanām
  1. 26 26
      context.go
  2. 20 0
      render/data.go
  3. 66 0
      render/html.go
  4. 0 38
      render/html_debug.go
  5. 31 0
      render/json.go
  6. 22 0
      render/redirect.go
  7. 13 92
      render/render.go
  8. 74 2
      render/render_test.go
  9. 25 0
      render/text.go
  10. 17 0
      render/xml.go

+ 26 - 26
context.go

@@ -6,7 +6,6 @@ package gin
 
 import (
 	"errors"
-	"fmt"
 	"math"
 	"net/http"
 	"strings"
@@ -314,61 +313,62 @@ 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)
+// 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{}) {
+	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{}) {
-	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/
-func (c *Context) HTML(code int, name string, obj interface{}) {
-	c.Render(code, c.Engine.HTMLRender, name, obj)
+	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 - 0
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)
+}

+ 66 - 0
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))
+	}
+}

+ 0 - 38
render/html_debug.go

@@ -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 - 0
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)
+}

+ 22 - 0
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)
+}

+ 13 - 92
render/render.go

@@ -4,103 +4,24 @@
 
 package render
 
-import (
-	"encoding/json"
-	"encoding/xml"
-	"fmt"
-	"html/template"
-	"net/http"
-)
-
-type (
-	Render interface {
-		Render(http.ResponseWriter, int, ...interface{}) error
-	}
-
-	jsonRender struct{}
-
-	indentedJSON struct{}
-
-	xmlRender struct{}
-
-	plainTextRender struct{}
-
-	htmlPlainRender struct{}
+import "net/http"
 
-	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)

+ 74 - 2
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(), "<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()
+	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 - 0
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))
+	}
+}

+ 17 - 0
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)
+}