From 10979dd8627c2a91d729fbb65cdb91e032cb417b Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 2 Jul 2014 20:17:57 +0200 Subject: [PATCH 01/45] Adds new context caching system - Reduces allocation overhead - Reduces Garbage Collection stress - Reduces memory fragmentation --- gin.go | 49 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/gin.go b/gin.go index 847df19..91ebcae 100644 --- a/gin.go +++ b/gin.go @@ -56,9 +56,10 @@ type ( // Represents the web framework, it wrappers the blazing fast httprouter multiplexer and a list of global middlewares. Engine struct { *RouterGroup + HTMLTemplates *template.Template + cache chan *Context handlers404 []HandlerFunc router *httprouter.Router - HTMLTemplates *template.Template } ) @@ -74,10 +75,18 @@ func (a ErrorMsgs) String() string { // Returns a new blank Engine instance without any middleware attached. // The most basic configuration func New() *Engine { + cacheSize := 1024 + engine := &Engine{} engine.RouterGroup = &RouterGroup{nil, "/", nil, engine} engine.router = httprouter.New() engine.router.NotFound = engine.handle404 + engine.cache = make(chan *Context, cacheSize) + + // Fill it with empty contexts + for i := 0; i < cacheSize/2; i++ { + engine.cache <- &Context{engine: engine} + } return engine } @@ -107,6 +116,7 @@ func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { } c.Next() + engine.reuseContext(c) } // ServeFiles serves files from the given file system root. @@ -136,14 +146,31 @@ func (engine *Engine) Run(addr string) { /********** ROUTES GROUPING *********/ /************************************/ -func (group *RouterGroup) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context { - return &Context{ - Writer: w, - Req: req, - index: -1, - engine: group.engine, - Params: params, - handlers: handlers, +func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context { + select { + case c := <-engine.cache: + c.Writer = w + c.Req = req + c.Params = params + c.handlers = handlers + c.index = -1 + return c + default: + return &Context{ + Writer: w, + Req: req, + Params: params, + handlers: handlers, + index: -1, + engine: engine, + } + } +} + +func (engine *Engine) reuseContext(c *Context) { + select { + case engine.cache <- c: + default: } } @@ -178,7 +205,9 @@ func (group *RouterGroup) Handle(method, p string, handlers []HandlerFunc) { p = path.Join(group.prefix, p) handlers = group.combineHandlers(handlers) group.engine.router.Handle(method, p, func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { - group.createContext(w, req, params, handlers).Next() + c := group.engine.createContext(w, req, params, handlers) + c.Next() + group.engine.reuseContext(c) }) } From 30ea9c06fc9cac893f10d6ad0847a510cf35aeaf Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Thu, 3 Jul 2014 14:11:42 +0200 Subject: [PATCH 02/45] Adds NewWithConfig() and CacheStress() --- gin.go | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/gin.go b/gin.go index 91ebcae..f60901c 100644 --- a/gin.go +++ b/gin.go @@ -31,6 +31,11 @@ type ( ErrorMsgs []ErrorMsg + Config struct { + CacheSize int + Preallocated int + } + // Context is the most important part of gin. It allows us to pass variables between middleware, // manage the flow, validate the JSON of a request and render a JSON response for example. Context struct { @@ -39,8 +44,8 @@ type ( Keys map[string]interface{} Errors ErrorMsgs Params httprouter.Params + Engine *Engine handlers []HandlerFunc - engine *Engine index int8 } @@ -72,24 +77,35 @@ func (a ErrorMsgs) String() string { return buffer.String() } -// Returns a new blank Engine instance without any middleware attached. -// The most basic configuration -func New() *Engine { - cacheSize := 1024 - +func NewWithConfig(config Config) *Engine { + if config.CacheSize < 2 { + panic("CacheSize must be at least 2") + } + if config.Preallocated > config.CacheSize { + panic("Preallocated must be less or equal to CacheSize") + } engine := &Engine{} engine.RouterGroup = &RouterGroup{nil, "/", nil, engine} engine.router = httprouter.New() engine.router.NotFound = engine.handle404 - engine.cache = make(chan *Context, cacheSize) + engine.cache = make(chan *Context, config.CacheSize) // Fill it with empty contexts - for i := 0; i < cacheSize/2; i++ { - engine.cache <- &Context{engine: engine} + for i := 0; i < config.Preallocated; i++ { + engine.cache <- &Context{Engine: engine} } return engine } +// Returns a new blank Engine instance without any middleware attached. +// The most basic configuration +func New() *Engine { + return NewWithConfig(Config{ + CacheSize: 1024, + Preallocated: 512, + }) +} + // Returns a Engine instance with the Logger and Recovery already attached. func Default() *Engine { engine := New() @@ -106,6 +122,10 @@ func (engine *Engine) NotFound404(handlers ...HandlerFunc) { engine.handlers404 = handlers } +func (engine *Engine) CacheStress() float32 { + return 1.0 - float32(len(engine.cache))/float32(cap(engine.cache)) +} + func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { handlers := engine.combineHandlers(engine.handlers404) c := engine.createContext(w, req, nil, handlers) @@ -162,7 +182,7 @@ func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, pa Params: params, handlers: handlers, index: -1, - engine: engine, + Engine: engine, } } } @@ -385,7 +405,7 @@ func (c *Context) HTML(code int, name string, data interface{}) { if code >= 0 { c.Writer.WriteHeader(code) } - if err := c.engine.HTMLTemplates.ExecuteTemplate(c.Writer, name, data); err != nil { + if err := c.Engine.HTMLTemplates.ExecuteTemplate(c.Writer, name, data); err != nil { c.Error(err, map[string]interface{}{ "name": name, "data": data, From d9573b45c71db971477d5eea9189497c1a7b2a20 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Thu, 3 Jul 2014 15:59:39 +0200 Subject: [PATCH 03/45] Adds Keep() and Release() to gin.Context --- gin.go | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/gin.go b/gin.go index f60901c..fea800f 100644 --- a/gin.go +++ b/gin.go @@ -45,6 +45,7 @@ type ( Errors ErrorMsgs Params httprouter.Params Engine *Engine + keep bool handlers []HandlerFunc index int8 } @@ -180,6 +181,7 @@ func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, pa Writer: w, Req: req, Params: params, + keep: false, handlers: handlers, index: -1, Engine: engine, @@ -188,9 +190,11 @@ func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, pa } func (engine *Engine) reuseContext(c *Context) { - select { - case engine.cache <- c: - default: + if c.keep == false { + select { + case engine.cache <- c: + default: + } } } @@ -268,6 +272,23 @@ func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc /****** FLOW AND ERROR MANAGEMENT****/ /************************************/ +func (c *Context) Keep() { + if c.keep == false { + c.keep = true + } else { + log.Println("gin: trying to Keep same context several times") + } +} + +func (c *Context) Release() { + if c.keep == true { + c.keep = false + c.Engine.reuseContext(c) + } else { + log.Println("gin: bug: trying to Release same context several times") + } +} + // Next should be used only in the middlewares. // It executes the pending handlers in the chain inside the calling handler. // See example in github. From 5e0d560dd6e3042f0fbd51adf9fb9ab8b54ecebf Mon Sep 17 00:00:00 2001 From: Alexander Nyquist Date: Thu, 3 Jul 2014 16:14:33 +0200 Subject: [PATCH 04/45] Added support for OPTIONS verb --- gin.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gin.go b/gin.go index 847df19..b855ca6 100644 --- a/gin.go +++ b/gin.go @@ -207,6 +207,11 @@ func (group *RouterGroup) PUT(path string, handlers ...HandlerFunc) { group.Handle("PUT", path, handlers) } +// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle) +func (group *RouterGroup) OPTIONS(path string, handlers ...HandlerFunc) { + group.Handle("OPTIONS", path, handlers) +} + func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc { s := len(group.Handlers) + len(handlers) h := make([]HandlerFunc, 0, s) From fd86812e816290ab3bd55e7ce0be4efecc31c5b2 Mon Sep 17 00:00:00 2001 From: Alexander Nyquist Date: Thu, 3 Jul 2014 16:16:40 +0200 Subject: [PATCH 05/45] Added OPTIONS to README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c2a5f11..d80f07a 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ func main() { } ``` -#### Using GET, POST, PUT, PATCH and DELETE +#### Using GET, POST, PUT, PATCH, DELETE and OPTIONS ```go func main() { @@ -67,6 +67,7 @@ func main() { r.PUT("/somePut", putting) r.DELETE("/someDelete", deleting) r.PATCH("/somePatch", patching) + r.OPTIONS("/someOptions", options) // Listen and server on 0.0.0.0:8080 r.Run(":8080") From d7a3fdcd8fa5a38111578c254a38616e6af5a76c Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Thu, 3 Jul 2014 16:31:27 +0200 Subject: [PATCH 06/45] Testing Copy() instead of Keep() and Release() --- gin.go | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/gin.go b/gin.go index fea800f..ffc9581 100644 --- a/gin.go +++ b/gin.go @@ -45,7 +45,6 @@ type ( Errors ErrorMsgs Params httprouter.Params Engine *Engine - keep bool handlers []HandlerFunc index int8 } @@ -174,6 +173,7 @@ func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, pa c.Req = req c.Params = params c.handlers = handlers + c.Keys = nil c.index = -1 return c default: @@ -181,7 +181,6 @@ func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, pa Writer: w, Req: req, Params: params, - keep: false, handlers: handlers, index: -1, Engine: engine, @@ -190,11 +189,9 @@ func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, pa } func (engine *Engine) reuseContext(c *Context) { - if c.keep == false { - select { - case engine.cache <- c: - default: - } + select { + case engine.cache <- c: + default: } } @@ -272,21 +269,12 @@ func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc /****** FLOW AND ERROR MANAGEMENT****/ /************************************/ -func (c *Context) Keep() { - if c.keep == false { - c.keep = true - } else { - log.Println("gin: trying to Keep same context several times") - } -} - -func (c *Context) Release() { - if c.keep == true { - c.keep = false - c.Engine.reuseContext(c) - } else { - log.Println("gin: bug: trying to Release same context several times") - } +func (c *Context) Copy() *Context { + cp := &Context{} + *cp = *c + cp.index = AbortIndex + cp.handlers = nil + return cp } // Next should be used only in the middlewares. From 12fa9fe4993674bd64a3fcbcb298b6e7741aedb5 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Thu, 3 Jul 2014 16:35:20 +0200 Subject: [PATCH 07/45] Improves Copy() in gin.Context --- gin.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/gin.go b/gin.go index ffc9581..3c8e49e 100644 --- a/gin.go +++ b/gin.go @@ -270,11 +270,10 @@ func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc /************************************/ func (c *Context) Copy() *Context { - cp := &Context{} - *cp = *c + var cp Context = *c cp.index = AbortIndex cp.handlers = nil - return cp + return &cp } // Next should be used only in the middlewares. From b8053b284df3db3d6e16bb6aa03d3a8d0026ebbf Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Thu, 3 Jul 2014 16:57:45 +0200 Subject: [PATCH 08/45] Adds HEAD --- gin.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gin.go b/gin.go index dcb826f..d1d4262 100644 --- a/gin.go +++ b/gin.go @@ -264,6 +264,11 @@ func (group *RouterGroup) OPTIONS(path string, handlers ...HandlerFunc) { group.Handle("OPTIONS", path, handlers) } +// HEAD is a shortcut for router.Handle("HEAD", path, handle) +func (group *RouterGroup) HEAD(path string, handlers ...HandlerFunc) { + group.Handle("HEAD", path, handlers) +} + func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc { s := len(group.Handlers) + len(handlers) h := make([]HandlerFunc, 0, s) From 1aa3216303697b27eb57ab321942a116a47217db Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Thu, 3 Jul 2014 19:19:06 +0200 Subject: [PATCH 09/45] Some work around bindings. it may do not compile --- binding/binding.go | 34 ++++++++++++++++++++++++++++++ examples/example_basic.go | 3 ++- gin.go | 44 ++++++++++++++++++++++++++++++++------- 3 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 binding/binding.go diff --git a/binding/binding.go b/binding/binding.go new file mode 100644 index 0000000..667b3d0 --- /dev/null +++ b/binding/binding.go @@ -0,0 +1,34 @@ +package binding + +import ( + "encoding/json" + "encoding/xml" + "io" +) + +type ( + Binding interface { + Bind(io.Reader, interface{}) error + } + + // JSON binding + jsonBinding struct{} + + // JSON binding + xmlBinding struct{} +) + +var ( + JSON = jsonBinding{} + XML = xmlBinding{} +) + +func (_ jsonBinding) Bind(r io.Reader, obj interface{}) error { + decoder := json.NewDecoder(r) + return decoder.Decode(&obj) +} + +func (_ xmlBinding) Bind(r io.Reader, obj interface{}) error { + decoder := xml.NewDecoder(r) + return decoder.Decode(&obj) +} diff --git a/examples/example_basic.go b/examples/example_basic.go index ea34343..7959a6c 100644 --- a/examples/example_basic.go +++ b/examples/example_basic.go @@ -44,7 +44,8 @@ func main() { var json struct { Value string `json:"value" binding:"required"` } - if c.EnsureBody(&json) { + + if c.Bind(&json) { DB[user] = json.Value c.JSON(200, gin.H{"status": "ok"}) } diff --git a/gin.go b/gin.go index d1d4262..2c4d640 100644 --- a/gin.go +++ b/gin.go @@ -6,6 +6,7 @@ import ( "encoding/xml" "errors" "fmt" + "github.com/gin-gonic/gin/binding" "github.com/julienschmidt/httprouter" "html/template" "log" @@ -371,22 +372,49 @@ func (c *Context) Get(key string) interface{} { /************************************/ // Like ParseBody() but this method also writes a 400 error if the json is not valid. +// DEPRECATED, use Bind() instead. func (c *Context) EnsureBody(item interface{}) bool { - if err := c.ParseBody(item); err != nil { + return c.Bind(item) +} + +// DEPRECATED use bindings directly +// Parses the body content as a JSON input. It decodes the json payload into the struct specified as a pointer. +func (c *Context) ParseBody(item interface{}) error { + return binding.JSON.Bind(c.Req.Body, item) +} + +// This function checks the Content-Type to select a binding engine automatically, +// Depending the "Content-Type" header different bindings are used: +// "application/json" --> JSON binding +// "application/xml" --> XML binding +// else --> returns an error +// if Parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. It decodes the json payload into the struct specified as a pointer.Like ParseBody() but this method also writes a 400 error if the json is not valid. +func (c *Context) Bind(obj interface{}) bool { + var err error + switch c.Req.Header.Get("Content-Type") { + case "application/json": + err = binding.JSON.Bind(c.Req.Body, obj) + case "application/xml": + err = binding.XML.Bind(c.Req.Body, obj) + default: + err = errors.New("unknown content-type: " + c.Req.Header.Get("Content-Type")) + } + if err == nil { + err = Validate(c, obj) + } + if err != nil { c.Fail(400, err) return false } return true } -// Parses the body content as a JSON input. It decodes the json payload into the struct specified as a pointer. -func (c *Context) ParseBody(item interface{}) error { - decoder := json.NewDecoder(c.Req.Body) - if err := decoder.Decode(&item); err == nil { - return Validate(c, item) - } else { - return err +func (c *Context) BindWith(obj interface{}, b binding.Binding) bool { + if err := b.Bind(c.Req.Body, obj); err != nil { + c.Fail(400, err) + return false } + return true } // Serializes the given struct as a JSON into the response body in a fast and efficient way. From 108bfb44704ce68ca5c6e05972445965edd247e0 Mon Sep 17 00:00:00 2001 From: Nick Gerakines Date: Thu, 3 Jul 2014 15:17:24 -0400 Subject: [PATCH 10/45] Setting Get metadata method to return both an interface as well as an error to remove panic. --- examples/example_basic.go | 4 ++-- gin.go | 17 ++++++----------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/examples/example_basic.go b/examples/example_basic.go index ea34343..4586907 100644 --- a/examples/example_basic.go +++ b/examples/example_basic.go @@ -38,14 +38,14 @@ func main() { })) authorized.POST("admin", func(c *gin.Context) { - user := c.Get("user").(string) + user, _ := c.Get("user") // Parse JSON var json struct { Value string `json:"value" binding:"required"` } if c.EnsureBody(&json) { - DB[user] = json.Value + DB[user.(string)] = json.Value c.JSON(200, gin.H{"status": "ok"}) } }) diff --git a/gin.go b/gin.go index 197b98c..e29fe9d 100644 --- a/gin.go +++ b/gin.go @@ -8,7 +8,6 @@ import ( "fmt" "github.com/julienschmidt/httprouter" "html/template" - "log" "math" "net/http" "path" @@ -283,18 +282,14 @@ func (c *Context) Set(key string, item interface{}) { // Returns the value for the given key. // It panics if the value doesn't exist. -func (c *Context) Get(key string) interface{} { - var ok bool - var item interface{} +func (c *Context) Get(key string) (interface{}, error) { if c.Keys != nil { - item, ok = c.Keys[key] - } else { - item, ok = nil, false + item, ok := c.Keys[key] + if ok { + return item, nil + } } - if !ok || item == nil { - log.Panicf("Key %s doesn't exist", key) - } - return item + return nil, errors.New("Key does not exist.") } /************************************/ From d42aa6d868c84c83737779f2c0446513efdca317 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Fri, 4 Jul 2014 00:01:28 +0200 Subject: [PATCH 11/45] Fixes wrap around http.ResponseWriter --- README.md | 4 ++++ gin.go | 8 ++++---- response_writer.go | 47 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 response_writer.go diff --git a/README.md b/README.md index d80f07a..97e80af 100644 --- a/README.md +++ b/README.md @@ -286,6 +286,10 @@ func Logger() gin.HandlerFunc { // after request latency := time.Since(t) log.Print(latency) + + // access the status we are sending + status := c.Writer.Status() + log.Println(status) } } diff --git a/gin.go b/gin.go index d1d4262..314894f 100644 --- a/gin.go +++ b/gin.go @@ -40,7 +40,7 @@ type ( // manage the flow, validate the JSON of a request and render a JSON response for example. Context struct { Req *http.Request - Writer http.ResponseWriter + Writer ResponseWriter Keys map[string]interface{} Errors ErrorMsgs Params httprouter.Params @@ -92,7 +92,7 @@ func NewWithConfig(config Config) *Engine { // Fill it with empty contexts for i := 0; i < config.Preallocated; i++ { - engine.cache <- &Context{Engine: engine} + engine.cache <- &Context{Engine: engine, Writer: &responseWriter{}} } return engine } @@ -171,7 +171,7 @@ func (engine *Engine) Run(addr string) { func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context { select { case c := <-engine.cache: - c.Writer = w + c.Writer.reset(w) c.Req = req c.Params = params c.handlers = handlers @@ -180,7 +180,7 @@ func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, pa return c default: return &Context{ - Writer: w, + Writer: &responseWriter{w, -1, false}, Req: req, Params: params, handlers: handlers, diff --git a/response_writer.go b/response_writer.go new file mode 100644 index 0000000..88c1b20 --- /dev/null +++ b/response_writer.go @@ -0,0 +1,47 @@ +package gin + +import ( + "net/http" +) + +type ( + ResponseWriter interface { + http.ResponseWriter + Status() int + Written() bool + + // private + reset(http.ResponseWriter) + setStatus(int) + } + + responseWriter struct { + http.ResponseWriter + status int + written bool + } +) + +func (w *responseWriter) reset(writer http.ResponseWriter) { + w.ResponseWriter = writer + w.status = 0 + w.written = false +} + +func (w *responseWriter) setStatus(code int) { + w.status = code +} + +func (w *responseWriter) WriteHeader(code int) { + w.status = code + w.written = true + w.ResponseWriter.WriteHeader(code) +} + +func (w *responseWriter) Status() int { + return w.status +} + +func (w *responseWriter) Written() bool { + return w.written +} From e1781e2db10b1c3580837491198ea2f7ef3c0ab4 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Fri, 4 Jul 2014 00:12:35 +0200 Subject: [PATCH 12/45] Fixes "http: multiple response.WriteHeader calls" in 404 [Issue #29] --- gin.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/gin.go b/gin.go index 8a4ed8c..3b584d9 100644 --- a/gin.go +++ b/gin.go @@ -129,13 +129,11 @@ func (engine *Engine) CacheStress() float32 { func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { handlers := engine.combineHandlers(engine.handlers404) c := engine.createContext(w, req, nil, handlers) - if engine.handlers404 == nil { - http.NotFound(c.Writer, c.Req) - } else { - c.Writer.WriteHeader(404) - } - + c.Writer.setStatus(404) c.Next() + if !c.Writer.Written() { + c.String(404, "404 page not found") + } engine.reuseContext(c) } From abe076b8f8961378daecca4d8309929302f3c37a Mon Sep 17 00:00:00 2001 From: Nick Gerakines Date: Thu, 3 Jul 2014 18:16:41 -0400 Subject: [PATCH 13/45] Adding MustGet method. Updating README. --- README.md | 14 +++++++------- gin.go | 13 +++++++++++-- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 53efb20..7c36793 100644 --- a/README.md +++ b/README.md @@ -275,14 +275,14 @@ func main() { func Logger() gin.HandlerFunc { return func(c *gin.Context) { t := time.Now() - + // Set example variable c.Set("example", "12345") - + // before request - + c.Next() - + // after request latency := time.Since(t) log.Print(latency) @@ -292,10 +292,10 @@ func Logger() gin.HandlerFunc { func main() { r := gin.New() r.Use(Logger()) - + r.GET("/test", func(c *gin.Context){ - example := r.Get("example").(string) - + example := c.MustGet("example").(string) + // it would print: "12345" log.Println(example) }) diff --git a/gin.go b/gin.go index e29fe9d..48a2b6d 100644 --- a/gin.go +++ b/gin.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/julienschmidt/httprouter" "html/template" + "log" "math" "net/http" "path" @@ -280,8 +281,7 @@ func (c *Context) Set(key string, item interface{}) { c.Keys[key] = item } -// Returns the value for the given key. -// It panics if the value doesn't exist. +// Get returns the value for the given key or an error if the key does not exist. func (c *Context) Get(key string) (interface{}, error) { if c.Keys != nil { item, ok := c.Keys[key] @@ -292,6 +292,15 @@ func (c *Context) Get(key string) (interface{}, error) { return nil, errors.New("Key does not exist.") } +// MustGet returns the value for the given key or panics if the value doesn't exist. +func (c *Context) MustGet(key string) interface{} { + value, err := c.Get(key) + if err != nil || value == nil { + log.Panicf("Key %s doesn't exist", key) + } + return value +} + /************************************/ /******** ENCOGING MANAGEMENT********/ /************************************/ From 78536abb5846bff1950a2ebd9d864de042f2020a Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Fri, 4 Jul 2014 00:25:12 +0200 Subject: [PATCH 14/45] Better logger - Includes http method - Includes coloured status code - Better formatting --- logger.go | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/logger.go b/logger.go index 7f0abdb..dde0d5d 100644 --- a/logger.go +++ b/logger.go @@ -3,6 +3,7 @@ package gin import ( "fmt" "log" + "os" "time" ) @@ -17,17 +18,44 @@ func ErrorLogger() HandlerFunc { } } -func Logger() HandlerFunc { - return func(c *Context) { +var ( + green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109}) + white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109}) + yellow = string([]byte{27, 91, 57, 55, 59, 52, 51, 109}) + red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109}) + reset = string([]byte{27, 91, 48, 109}) +) +func Logger() HandlerFunc { + logger := log.New(os.Stdout, "", 0) + return func(c *Context) { // Start timer - t := time.Now() + start := time.Now() // Process request c.Next() + var color string + code := c.Writer.Status() + switch { + case code >= 200 && code <= 299: + color = green + case code >= 300 && code <= 399: + color = white + case code >= 400 && code <= 499: + color = yellow + default: + color = red + } + latency := time.Since(start) + logger.Printf("[GIN] %v |%s %3d %s| %12v | %3s %s\n", + time.Now().Format("2006/01/02 - 15:04:05"), + color, c.Writer.Status(), reset, + latency, + c.Req.Method, c.Req.URL.Path, + ) + // Calculate resolution time - log.Printf("%s in %v", c.Req.RequestURI, time.Since(t)) if len(c.Errors) > 0 { fmt.Println(c.Errors.String()) } From 6da7374af5c776343ab8ef61c9146f8e0aa2e0fd Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Fri, 4 Jul 2014 00:37:54 +0200 Subject: [PATCH 15/45] Do not update status code in Abort() if code is a negative number --- gin.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gin.go b/gin.go index 3b584d9..4b61918 100644 --- a/gin.go +++ b/gin.go @@ -301,7 +301,9 @@ func (c *Context) Next() { // For example, the first handler checks if the request is authorized. If it's not, context.Abort(401) should be called. // The rest of pending handlers would never be called for that request. func (c *Context) Abort(code int) { - c.Writer.WriteHeader(code) + if code >= 0 { + c.Writer.WriteHeader(code) + } c.index = AbortIndex } From c8cb943ef78aa554ec3bccf39dd7b15b2afb0559 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Fri, 4 Jul 2014 02:55:01 +0200 Subject: [PATCH 16/45] Adds a console image --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 02ffaa4..344390c 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,7 @@ [![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin) Gin is a web framework written in Golang. It features a martini-like API with much better performance, up to 40 times faster. If you need performance and good productivity, you will love Gin. -[Check out the official web site](http://gin-gonic.github.io/gin/) - +![Gin console logger](http://gin-gonic.github.io/gin/other/console.png) ##Gin is new, will it be supported? From 661398ca53c85c89a96be4ea5874872d60484af8 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Fri, 4 Jul 2014 04:28:25 +0200 Subject: [PATCH 17/45] Nicer BasicAuth API --- auth.go | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/auth.go b/auth.go index eab1d94..a8720c4 100644 --- a/auth.go +++ b/auth.go @@ -16,12 +16,7 @@ type ( Code string User string } - Account struct { - User string - Password string - } - - Accounts []Account + Accounts map[string]string Pairs []BasicAuthPair ) @@ -34,13 +29,13 @@ func processCredentials(accounts Accounts) (Pairs, error) { return nil, errors.New("Empty list of authorized credentials.") } pairs := make(Pairs, 0, len(accounts)) - for _, account := range accounts { - if len(account.User) == 0 || len(account.Password) == 0 { + for user, password := range accounts { + if len(user) == 0 || len(password) == 0 { return nil, errors.New("User or password is empty") } - base := account.User + ":" + account.Password + base := user + ":" + password code := "Basic " + base64.StdEncoding.EncodeToString([]byte(base)) - pairs = append(pairs, BasicAuthPair{code, account.User}) + pairs = append(pairs, BasicAuthPair{code, user}) } // We have to sort the credentials in order to use bsearch later. sort.Sort(pairs) From 7582b9077ca20d05204d18081bdff3528b5282fd Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Fri, 4 Jul 2014 04:30:30 +0200 Subject: [PATCH 18/45] Adds more samples in README - Goroutines inside a request - Basic HTTP authorization --- README.md | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 344390c..e81f5c9 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Gin is a web framework written in Golang. It features a martini-like API with mu Yes, Gin is an internal project of [my](https://github.com/manucorporat) upcoming startup. We developed it and we are going to continue using and improve it. -##Roadmap +##Roadmap for v0.2 - Performance improments, reduce allocation and garbage collection overhead - Fix bugs - Ask our designer for a cool logo @@ -26,12 +26,13 @@ Yes, Gin is an internal project of [my](https://github.com/manucorporat) upcomin ## Start using it -Run: +Obviously, you need to have Git and Go! already installed to run Gin. +Run this in your terminal ``` go get github.com/gin-gonic/gin ``` -Then import it in your Golang code: +Then import it in your Go! code: ``` import "github.com/gin-gonic/gin" @@ -67,6 +68,7 @@ func main() { r.PUT("/somePut", putting) r.DELETE("/someDelete", deleting) r.PATCH("/somePatch", patching) + r.HEAD("/someHead", head) r.OPTIONS("/someOptions", options) // Listen and server on 0.0.0.0:8080 @@ -86,6 +88,13 @@ func main() { c.String(200, message) }) + r.GET("/user/:name/:action", func(c *gin.Context) { + name := c.Params.ByName("name") + action := c.Params.ByName("action") + message := name + " is " + action + c.String(200, message) + }) + // Listen and server on 0.0.0.0:8080 r.Run(":8080") } @@ -309,8 +318,78 @@ func main() { } ``` +#### Using BasicAuth() middleware +```go +func main() { + r := gin.Default() + // note than: type gin.H map[string]interface{} + secrets := gin.H{ + "foo": gin.H{"email": "foo@bar.com", "phone": "123433"}, + "austin": gin.H{"email": "austin@example.com", "phone": "666"}, + "lena": gin.H{"email": "lena@guapa.com", "phone": "523443"} + } + + authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{ + "foo": "bar", + "austin": "1234", + "lena": "hello2", + "manu": "4321" + } + authorized.GET("/secrets", func(c *gin.Context) { + // get user, it was setted by the BasicAuth middleware + user := c.GET(gin.AuthUserKey).(string) + if secret, ok := secrets[user]; ok { + c.JSON(200, gin.H{ + "user": user, + "secret": secret + } + } else { + c.JSON(200, gin.H{ + "user": user, + "secret": "NO SECRET :(" + } + } + } + // hit "localhost:8080/admin/secrets + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") +} +``` +#### Goroutines inside a middleware +When starting inside a middleware or handler, you **SHOULD NOT** use the original context inside it, you have to use a read-only copy. + +```go +func main() { + r := gin.Default() + + r.GET("/long_async", func(c *gin.Context) { + // create copy to be used inside the goroutine + c_cp := c.Copy() + go func() { + // simulate a long task with time.Sleep(). 5 seconds + time.Sleep(5 * time.Second) + + // note than you are using the copied context "c_cp", IMPORTANT + log.Println("Done! in path " + c_cp.Req.URL.Path) + }() + }) + + + r.GET("/long_sync", func(c *gin.Context) { + // simulate a long task with time.Sleep(). 5 seconds + time.Sleep(5 * time.Second) + + // since we are NOT using a goroutine, we do not have to copy the context + log.Println("Done! in path " + c.Req.URL.Path) + }) + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") +} +``` #### Custom HTTP configuration From 91df54a5c20205492538d17007d5a8ff05a344a5 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Fri, 4 Jul 2014 04:31:11 +0200 Subject: [PATCH 19/45] Better print formatting for Errors --- gin.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gin.go b/gin.go index 4b61918..d76eae1 100644 --- a/gin.go +++ b/gin.go @@ -71,9 +71,10 @@ type ( func (a ErrorMsgs) String() string { var buffer bytes.Buffer for i, msg := range a { - text := fmt.Sprintf("Error #%02d: %s \n Meta: %v\n\n", (i + 1), msg.Err, msg.Meta) + text := fmt.Sprintf("Error #%02d: %s \n Meta: %v\n", (i + 1), msg.Err, msg.Meta) buffer.WriteString(text) } + buffer.WriteString("\n") return buffer.String() } From fa80b047f903350600fc1cc682a89519306f31b6 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Fri, 4 Jul 2014 04:32:43 +0200 Subject: [PATCH 20/45] The Recovery() middleware should not print the errors (only panics) --- recovery.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/recovery.go b/recovery.go index 15066c8..4716522 100644 --- a/recovery.go +++ b/recovery.go @@ -82,9 +82,6 @@ func function(pc uintptr) []byte { func Recovery() HandlerFunc { return func(c *Context) { defer func() { - if len(c.Errors) > 0 { - log.Println(c.Errors.String()) - } if err := recover(); err != nil { stack := stack(3) log.Printf("PANIC: %s\n%s", err, stack) From 9efd207a048f83efd198d4be525589e84210154e Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Fri, 4 Jul 2014 04:40:00 +0200 Subject: [PATCH 21/45] Fixes BasicAuth test in example_basic.go --- examples/example_basic.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/example_basic.go b/examples/example_basic.go index ea34343..77a8cec 100644 --- a/examples/example_basic.go +++ b/examples/example_basic.go @@ -33,12 +33,12 @@ func main() { // "manu": "123", //})) authorized := r.Group("/", gin.BasicAuth(gin.Accounts{ - {"foo", "bar"}, //1. user:foo password:bar - {"manu", "123"}, //2. user:manu password:123 + "foo": "bar", // user:foo password:bar + "manu": "123", // user:manu password:123 })) authorized.POST("admin", func(c *gin.Context) { - user := c.Get("user").(string) + user := c.Get(gin.AuthUserKey).(string) // Parse JSON var json struct { From 493008ea3c72b8588f5f3a87bcb32274f49837d3 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Fri, 4 Jul 2014 04:47:34 +0200 Subject: [PATCH 22/45] Improved BasicAuth() example --- README.md | 49 ++++++++++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index e81f5c9..72a87a1 100644 --- a/README.md +++ b/README.md @@ -320,40 +320,39 @@ func main() { #### Using BasicAuth() middleware ```go -func main() { - r := gin.Default() - // note than: type gin.H map[string]interface{} - secrets := gin.H{ - "foo": gin.H{"email": "foo@bar.com", "phone": "123433"}, - "austin": gin.H{"email": "austin@example.com", "phone": "666"}, - "lena": gin.H{"email": "lena@guapa.com", "phone": "523443"} - } +// similate some private data +var secrets = gin.H{ + "foo": gin.H{"email": "foo@bar.com", "phone": "123433"}, + "austin": gin.H{"email": "austin@example.com", "phone": "666"}, + "lena": gin.H{"email": "lena@guapa.com", "phone": "523443"}, +} +func main() { + r := gin.Default() + + // Group using gin.BasicAuth() middleware + // gin.Accounts is a shortcut for map[string]string authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{ - "foo": "bar", + "foo": "bar", "austin": "1234", - "lena": "hello2", - "manu": "4321" - } + "lena": "hello2", + "manu": "4321", + })) + + // /admin/secrets endpoint + // hit "localhost:8080/admin/secrets authorized.GET("/secrets", func(c *gin.Context) { // get user, it was setted by the BasicAuth middleware - user := c.GET(gin.AuthUserKey).(string) + user := c.Get(gin.AuthUserKey).(string) if secret, ok := secrets[user]; ok { - c.JSON(200, gin.H{ - "user": user, - "secret": secret - } + c.JSON(200, gin.H{"user": user, "secret": secret}) } else { - c.JSON(200, gin.H{ - "user": user, - "secret": "NO SECRET :(" - } + c.JSON(200, gin.H{"user": user, "secret": "NO SECRET :("}) } - } - // hit "localhost:8080/admin/secrets + }) - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") } ``` From b5db76b87a6a430c80c6467b62837a52099fb7f9 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Fri, 4 Jul 2014 16:59:57 +0200 Subject: [PATCH 23/45] Adds default config variable --- gin.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/gin.go b/gin.go index d76eae1..03bf8e5 100644 --- a/gin.go +++ b/gin.go @@ -68,6 +68,13 @@ type ( } ) +var ( + DefaultConfig = Config{ + CacheSize: 1024, + Preallocated: 512, + } +) + func (a ErrorMsgs) String() string { var buffer bytes.Buffer for i, msg := range a { @@ -101,10 +108,7 @@ func NewWithConfig(config Config) *Engine { // Returns a new blank Engine instance without any middleware attached. // The most basic configuration func New() *Engine { - return NewWithConfig(Config{ - CacheSize: 1024, - Preallocated: 512, - }) + return NewWithConfig(DefaultConfig) } // Returns a Engine instance with the Logger and Recovery already attached. From 592af3026c09cf2110b3295806b903d6cf4f7be5 Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Fri, 4 Jul 2014 19:44:07 +0200 Subject: [PATCH 24/45] README: gofmt the code examples --- README.md | 343 +++++++++++++++++++++++++++--------------------------- 1 file changed, 173 insertions(+), 170 deletions(-) diff --git a/README.md b/README.md index 53efb20..20a2a03 100644 --- a/README.md +++ b/README.md @@ -43,16 +43,18 @@ import "github.com/gin-gonic/gin" #### Create most basic PING/PONG HTTP endpoint ```go +package main + import "github.com/gin-gonic/gin" func main() { - r := gin.Default() - r.GET("/ping", func(c *gin.Context){ - c.String(200, "pong") - }) - - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + r := gin.Default() + r.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") } ``` @@ -60,17 +62,17 @@ func main() { ```go func main() { - // Creates a gin router + logger and recovery (crash-free) middlewares - r := gin.Default() - - r.GET("/someGet", getting) - r.POST("/somePost", posting) - r.PUT("/somePut", putting) - r.DELETE("/someDelete", deleting) - r.PATCH("/somePatch", patching) + // Creates a gin router + logger and recovery (crash-free) middlewares + r := gin.Default() - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + r.GET("/someGet", getting) + r.POST("/somePost", posting) + r.PUT("/somePut", putting) + r.DELETE("/someDelete", deleting) + r.PATCH("/somePatch", patching) + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") } ``` @@ -78,16 +80,16 @@ func main() { ```go func main() { - r := gin.Default() - - r.GET("/user/:name", func(c *gin.Context) { - name := c.Params.ByName("name") - message := "Hello "+name - c.String(200, message) - }) + r := gin.Default() - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + r.GET("/user/:name", func(c *gin.Context) { + name := c.Params.ByName("name") + message := "Hello " + name + c.String(200, message) + }) + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") } ``` @@ -95,26 +97,26 @@ func main() { #### Grouping routes ```go func main() { - r := gin.Default() - - // Simple group: v1 - v1 := r.Group("/v1") - { - v1.POST("/login", loginEndpoint) - v1.POST("/submit", submitEndpoint) - v1.POST("/read", readEndpoint) - } - - // Simple group: v2 - v2 := r.Group("/v2") - { - v2.POST("/login", loginEndpoint) - v2.POST("/submit", submitEndpoint) - v2.POST("/read", readEndpoint) - } + r := gin.Default() - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + // Simple group: v1 + v1 := r.Group("/v1") + { + v1.POST("/login", loginEndpoint) + v1.POST("/submit", submitEndpoint) + v1.POST("/read", readEndpoint) + } + + // Simple group: v2 + v2 := r.Group("/v2") + { + v2.POST("/login", loginEndpoint) + v2.POST("/submit", submitEndpoint) + v2.POST("/read", readEndpoint) + } + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") } ``` @@ -136,35 +138,35 @@ r := gin.Default() #### Using middlewares ```go func main() { - // Creates a router without any middleware by default - r := gin.New() - - // Global middlewares - r.Use(gin.Logger()) - r.Use(gin.Recovery()) - - // Per route middlewares, you can add as many as you desire. - r.GET("/benchmark", MyBenchLogger(), benchEndpoint) + // Creates a router without any middleware by default + r := gin.New() - // Authorization group - // authorized := r.Group("/", AuthRequired()) - // exactly the same than: - authorized := r.Group("/") - // per group middlewares! in this case we use the custom created - // AuthRequired() middleware just in the "authorized" group. - authorized.Use(AuthRequired()) - { - authorized.POST("/login", loginEndpoint) - authorized.POST("/submit", submitEndpoint) - authorized.POST("/read", readEndpoint) - - // nested group - testing := authorized.Group("testing") - testing.GET("/analytics", analyticsEndpoint) - } - - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + // Global middlewares + r.Use(gin.Logger()) + r.Use(gin.Recovery()) + + // Per route middlewares, you can add as many as you desire. + r.GET("/benchmark", MyBenchLogger(), benchEndpoint) + + // Authorization group + // authorized := r.Group("/", AuthRequired()) + // exactly the same than: + authorized := r.Group("/") + // per group middlewares! in this case we use the custom created + // AuthRequired() middleware just in the "authorized" group. + authorized.Use(AuthRequired()) + { + authorized.POST("/login", loginEndpoint) + authorized.POST("/submit", submitEndpoint) + authorized.POST("/read", readEndpoint) + + // nested group + testing := authorized.Group("testing") + testing.GET("/analytics", analyticsEndpoint) + } + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") } ``` @@ -173,30 +175,30 @@ func main() { ```go type LoginJSON struct { - User string `json:"user" binding:"required"` - Password string `json:"password" binding:"required"` + User string `json:"user" binding:"required"` + Password string `json:"password" binding:"required"` } func main() { - r := gin.Default() - - r.POST("/login", func(c *gin.Context) { - var json LoginJSON - - // If EnsureBody returns false, it will write automatically the error - // in the HTTP stream and return a 400 error. If you want custom error - // handling you should use: c.ParseBody(interface{}) error - if c.EnsureBody(&json) { - if json.User=="manu" && json.Password=="123" { - c.JSON(200, gin.H{"status": "you are logged in"}) - }else{ - c.JSON(401, gin.H{"status": "unauthorized"}) - } - } - }) + r := gin.Default() - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + r.POST("/login", func(c *gin.Context) { + var json LoginJSON + + // If EnsureBody returns false, it will write automatically the error + // in the HTTP stream and return a 400 error. If you want custom error + // handling you should use: c.ParseBody(interface{}) error + if c.EnsureBody(&json) { + if json.User == "manu" && json.Password == "123" { + c.JSON(200, gin.H{"status": "you are logged in"}) + } else { + c.JSON(401, gin.H{"status": "unauthorized"}) + } + } + }) + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") } ``` @@ -204,34 +206,34 @@ func main() { ```go func main() { - r := gin.Default() - - // gin.H is a shortcup for map[string]interface{} - r.GET("/someJSON", func(c *gin.Context) { - c.JSON(200, gin.H{"message": "hey", "status": 200}) - }) - - r.GET("/moreJSON", func(c *gin.Context) { - // You also can use a struct - var msg struct { - Name string `json:"user"` - Message string - Number int - } - msg.Name = "Lena" - msg.Message = "hey" - msg.Number = 123 - // Note that msg.Name becomes "user" in the JSON - // Will output : {"user": "Lena", "Message": "hey", "Number": 123} - c.JSON(200, msg) - }) - - r.GET("/someXML", func(c *gin.Context) { - c.XML(200, gin.H{"message": "hey", "status": 200}) - }) + r := gin.Default() - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + // gin.H is a shortcup for map[string]interface{} + r.GET("/someJSON", func(c *gin.Context) { + c.JSON(200, gin.H{"message": "hey", "status": 200}) + }) + + r.GET("/moreJSON", func(c *gin.Context) { + // You also can use a struct + var msg struct { + Name string `json:"user"` + Message string + Number int + } + msg.Name = "Lena" + msg.Message = "hey" + msg.Number = 123 + // Note that msg.Name becomes "user" in the JSON + // Will output : {"user": "Lena", "Message": "hey", "Number": 123} + c.JSON(200, msg) + }) + + r.GET("/someXML", func(c *gin.Context) { + c.XML(200, gin.H{"message": "hey", "status": 200}) + }) + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") } ``` @@ -242,15 +244,15 @@ Using LoadHTMLTemplates() ```go func main() { - r := gin.Default() - r.LoadHTMLTemplates("templates/*") - r.GET("/index", func(c *gin.Context) { - obj := gin.H{"title": "Main website"} - c.HTML(200, "index.tmpl", obj) - }) + r := gin.Default() + r.LoadHTMLTemplates("templates/*") + r.GET("/index", func(c *gin.Context) { + obj := gin.H{"title": "Main website"} + c.HTML(200, "index.tmpl", obj) + }) - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") } ``` @@ -258,13 +260,14 @@ You can also use your own html template render ```go import "html/template" -func main() { - r := gin.Default() - html := template.Must(template.ParseFiles("file1", "file2")) - r.HTMLTemplates = html - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") +func main() { + r := gin.Default() + html := template.Must(template.ParseFiles("file1", "file2")) + r.HTMLTemplates = html + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") } ``` @@ -273,35 +276,35 @@ func main() { ```go func Logger() gin.HandlerFunc { - return func(c *gin.Context) { - t := time.Now() - - // Set example variable - c.Set("example", "12345") - - // before request - - c.Next() - - // after request - latency := time.Since(t) - log.Print(latency) - } + return func(c *gin.Context) { + t := time.Now() + + // Set example variable + c.Set("example", "12345") + + // before request + + c.Next() + + // after request + latency := time.Since(t) + log.Print(latency) + } } func main() { - r := gin.New() - r.Use(Logger()) - - r.GET("/test", func(c *gin.Context){ - example := r.Get("example").(string) - - // it would print: "12345" - log.Println(example) - }) + r := gin.New() + r.Use(Logger()) - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + r.GET("/test", func(c *gin.Context) { + example := r.Get("example").(string) + + // it would print: "12345" + log.Println(example) + }) + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") } ``` @@ -314,23 +317,23 @@ Use `http.ListenAndServe()` directly, like this: ```go func main() { - router := gin.Default() - http.ListenAndServe(":8080", router) + router := gin.Default() + http.ListenAndServe(":8080", router) } ``` or ```go func main() { - router := gin.Default() + router := gin.Default() - s := &http.Server{ - Addr: ":8080", - Handler: router, - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, - MaxHeaderBytes: 1 << 20, - } - s.ListenAndServe() + s := &http.Server{ + Addr: ":8080", + Handler: router, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxHeaderBytes: 1 << 20, + } + s.ListenAndServe() } ``` From 9634a387040757ef933b1fad98471ea81c622dda Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Fri, 4 Jul 2014 23:28:50 +0200 Subject: [PATCH 25/45] New bindings for JSON, XML and form parsing and validation --- binding/binding.go | 75 ++++++++++++++++++++++++++++++++++++++++------ deprecated.go | 17 +++++++++++ gin.go | 49 +++++++++++++++--------------- validation.go | 45 ---------------------------- 4 files changed, 107 insertions(+), 79 deletions(-) create mode 100644 deprecated.go delete mode 100644 validation.go diff --git a/binding/binding.go b/binding/binding.go index 667b3d0..cb831f3 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -3,32 +3,89 @@ package binding import ( "encoding/json" "encoding/xml" - "io" + "errors" + "net/http" + "reflect" + "strings" ) type ( Binding interface { - Bind(io.Reader, interface{}) error + Bind(*http.Request, interface{}) error } // JSON binding jsonBinding struct{} - // JSON binding + // XML binding xmlBinding struct{} + + // // form binding + formBinding struct{} ) var ( JSON = jsonBinding{} XML = xmlBinding{} + Form = formBinding{} // todo ) -func (_ jsonBinding) Bind(r io.Reader, obj interface{}) error { - decoder := json.NewDecoder(r) - return decoder.Decode(&obj) +func (_ jsonBinding) Bind(req *http.Request, obj interface{}) error { + decoder := json.NewDecoder(req.Body) + if err := decoder.Decode(obj); err == nil { + return Validate(obj) + } else { + return err + } } -func (_ xmlBinding) Bind(r io.Reader, obj interface{}) error { - decoder := xml.NewDecoder(r) - return decoder.Decode(&obj) +func (_ xmlBinding) Bind(req *http.Request, obj interface{}) error { + decoder := xml.NewDecoder(req.Body) + if err := decoder.Decode(obj); err == nil { + return Validate(obj) + } else { + return err + } +} + +func (_ formBinding) Bind(req *http.Request, obj interface{}) error { + return nil +} + +func Validate(obj interface{}) error { + + typ := reflect.TypeOf(obj) + val := reflect.ValueOf(obj) + + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + val = val.Elem() + } + + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + fieldValue := val.Field(i).Interface() + zero := reflect.Zero(field.Type).Interface() + + // Validate nested and embedded structs (if pointer, only do so if not nil) + if field.Type.Kind() == reflect.Struct || + (field.Type.Kind() == reflect.Ptr && !reflect.DeepEqual(zero, fieldValue)) { + if err := Validate(fieldValue); err != nil { + return err + } + } + + if strings.Index(field.Tag.Get("binding"), "required") > -1 { + if reflect.DeepEqual(zero, fieldValue) { + name := field.Name + if j := field.Tag.Get("json"); j != "" { + name = j + } else if f := field.Tag.Get("form"); f != "" { + name = f + } + return errors.New("Required " + name) + } + } + } + return nil } diff --git a/deprecated.go b/deprecated.go new file mode 100644 index 0000000..b629098 --- /dev/null +++ b/deprecated.go @@ -0,0 +1,17 @@ +package gin + +import ( + "github.com/gin-gonic/gin/binding" +) + +// DEPRECATED, use Bind() instead. +// Like ParseBody() but this method also writes a 400 error if the json is not valid. +func (c *Context) EnsureBody(item interface{}) bool { + return c.Bind(item) +} + +// DEPRECATED use bindings directly +// Parses the body content as a JSON input. It decodes the json payload into the struct specified as a pointer. +func (c *Context) ParseBody(item interface{}) error { + return binding.JSON.Bind(c.Req, item) +} diff --git a/gin.go b/gin.go index 2c4d640..716d2f4 100644 --- a/gin.go +++ b/gin.go @@ -17,6 +17,11 @@ import ( const ( AbortIndex = math.MaxInt8 / 2 + MIMEJSON = "application/json" + MIMEHTML = "text/html" + MIMEXML = "application/xml" + MIMEXML2 = "text/xml" + MIMEPlain = "text/plain" ) type ( @@ -371,16 +376,13 @@ func (c *Context) Get(key string) interface{} { /******** ENCOGING MANAGEMENT********/ /************************************/ -// Like ParseBody() but this method also writes a 400 error if the json is not valid. -// DEPRECATED, use Bind() instead. -func (c *Context) EnsureBody(item interface{}) bool { - return c.Bind(item) -} - -// DEPRECATED use bindings directly -// Parses the body content as a JSON input. It decodes the json payload into the struct specified as a pointer. -func (c *Context) ParseBody(item interface{}) error { - return binding.JSON.Bind(c.Req.Body, item) +func filterFlags(content string) string { + for i, a := range content { + if a == ' ' || a == ';' { + return content[:i] + } + } + return content } // This function checks the Content-Type to select a binding engine automatically, @@ -390,27 +392,24 @@ func (c *Context) ParseBody(item interface{}) error { // else --> returns an error // if Parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. It decodes the json payload into the struct specified as a pointer.Like ParseBody() but this method also writes a 400 error if the json is not valid. func (c *Context) Bind(obj interface{}) bool { - var err error - switch c.Req.Header.Get("Content-Type") { - case "application/json": - err = binding.JSON.Bind(c.Req.Body, obj) - case "application/xml": - err = binding.XML.Bind(c.Req.Body, obj) + var b binding.Binding + ctype := filterFlags(c.Req.Header.Get("Content-Type")) + switch { + case c.Req.Method == "GET": + b = binding.Form + case ctype == MIMEJSON: + b = binding.JSON + case ctype == MIMEXML || ctype == MIMEXML2: + b = binding.XML default: - err = errors.New("unknown content-type: " + c.Req.Header.Get("Content-Type")) - } - if err == nil { - err = Validate(c, obj) - } - if err != nil { - c.Fail(400, err) + c.Fail(400, errors.New("unknown content-type: "+ctype)) return false } - return true + return c.BindWith(obj, b) } func (c *Context) BindWith(obj interface{}, b binding.Binding) bool { - if err := b.Bind(c.Req.Body, obj); err != nil { + if err := b.Bind(c.Req, obj); err != nil { c.Fail(400, err) return false } diff --git a/validation.go b/validation.go deleted file mode 100644 index 501ee50..0000000 --- a/validation.go +++ /dev/null @@ -1,45 +0,0 @@ -package gin - -import ( - "errors" - "reflect" - "strings" -) - -func Validate(c *Context, obj interface{}) error { - - var err error - typ := reflect.TypeOf(obj) - val := reflect.ValueOf(obj) - - if typ.Kind() == reflect.Ptr { - typ = typ.Elem() - val = val.Elem() - } - - for i := 0; i < typ.NumField(); i++ { - field := typ.Field(i) - fieldValue := val.Field(i).Interface() - zero := reflect.Zero(field.Type).Interface() - - // Validate nested and embedded structs (if pointer, only do so if not nil) - if field.Type.Kind() == reflect.Struct || - (field.Type.Kind() == reflect.Ptr && !reflect.DeepEqual(zero, fieldValue)) { - err = Validate(c, fieldValue) - } - - if strings.Index(field.Tag.Get("binding"), "required") > -1 { - if reflect.DeepEqual(zero, fieldValue) { - name := field.Name - if j := field.Tag.Get("json"); j != "" { - name = j - } else if f := field.Tag.Get("form"); f != "" { - name = f - } - err = errors.New("Required " + name) - c.Error(err, "json validation") - } - } - } - return err -} From d90868e5bdc4091840bebe78c97ed923455abb21 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Sat, 5 Jul 2014 02:44:32 +0200 Subject: [PATCH 26/45] Adds FORM bindings --- binding/binding.go | 99 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/binding/binding.go b/binding/binding.go index cb831f3..c826073 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -6,6 +6,7 @@ import ( "errors" "net/http" "reflect" + "strconv" "strings" ) @@ -49,9 +50,107 @@ func (_ xmlBinding) Bind(req *http.Request, obj interface{}) error { } func (_ formBinding) Bind(req *http.Request, obj interface{}) error { + if err := req.ParseForm(); err != nil { + return err + } + if err := mapForm(obj, req.Form); err != nil { + return err + } + return Validate(obj) +} + +func mapForm(ptr interface{}, form map[string][]string) error { + typ := reflect.TypeOf(ptr).Elem() + formStruct := reflect.ValueOf(ptr).Elem() + for i := 0; i < typ.NumField(); i++ { + typeField := typ.Field(i) + if inputFieldName := typeField.Tag.Get("form"); inputFieldName != "" { + structField := formStruct.Field(i) + if !structField.CanSet() { + continue + } + + inputValue, exists := form[inputFieldName] + if !exists { + continue + } + numElems := len(inputValue) + if structField.Kind() == reflect.Slice && numElems > 0 { + sliceOf := structField.Type().Elem().Kind() + slice := reflect.MakeSlice(structField.Type(), numElems, numElems) + for i := 0; i < numElems; i++ { + if err := setWithProperType(sliceOf, inputValue[i], slice.Index(i)); err != nil { + return err + } + } + formStruct.Elem().Field(i).Set(slice) + } else { + if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil { + return err + } + } + } + } return nil } +func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error { + switch valueKind { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if val == "" { + val = "0" + } + intVal, err := strconv.Atoi(val) + if err != nil { + return err + } else { + structField.SetInt(int64(intVal)) + } + case reflect.Bool: + if val == "" { + val = "false" + } + boolVal, err := strconv.ParseBool(val) + if err != nil { + return err + } else { + structField.SetBool(boolVal) + } + case reflect.Float32: + if val == "" { + val = "0.0" + } + floatVal, err := strconv.ParseFloat(val, 32) + if err != nil { + return err + } else { + structField.SetFloat(floatVal) + } + case reflect.Float64: + if val == "" { + val = "0.0" + } + floatVal, err := strconv.ParseFloat(val, 64) + if err != nil { + return err + } else { + structField.SetFloat(floatVal) + } + case reflect.String: + structField.SetString(val) + } + return nil +} + +// Don't pass in pointers to bind to. Can lead to bugs. See: +// https://github.com/codegangsta/martini-contrib/issues/40 +// https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659 +func ensureNotPointer(obj interface{}) { + if reflect.TypeOf(obj).Kind() == reflect.Ptr { + panic("Pointers are not accepted as binding models") + } +} + func Validate(obj interface{}) error { typ := reflect.TypeOf(obj) From cde876395f3d297698938c1d82171aa61a94563d Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Sat, 5 Jul 2014 02:51:14 +0200 Subject: [PATCH 27/45] Using constant values for Content-Type --- gin.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gin.go b/gin.go index f470fc8..30b1afb 100644 --- a/gin.go +++ b/gin.go @@ -428,7 +428,7 @@ func (c *Context) BindWith(obj interface{}, b binding.Binding) bool { // 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.Writer.Header().Set("Content-Type", "application/json") + c.Writer.Header().Set("Content-Type", MIMEJSON) if code >= 0 { c.Writer.WriteHeader(code) } @@ -442,7 +442,7 @@ func (c *Context) JSON(code int, obj interface{}) { // 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.Writer.Header().Set("Content-Type", "application/xml") + c.Writer.Header().Set("Content-Type", MIMEXML) if code >= 0 { c.Writer.WriteHeader(code) } @@ -457,7 +457,7 @@ func (c *Context) XML(code int, obj interface{}) { // 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, data interface{}) { - c.Writer.Header().Set("Content-Type", "text/html") + c.Writer.Header().Set("Content-Type", MIMEHTML) if code >= 0 { c.Writer.WriteHeader(code) } @@ -475,7 +475,7 @@ func (c *Context) String(code int, msg string) { if code >= 0 { c.Writer.WriteHeader(code) } - c.Writer.Header().Set("Content-Type", "text/plain") + c.Writer.Header().Set("Content-Type", MIMEPlain) c.Writer.Write([]byte(msg)) } From 3c2da979192acd7570d201999556e21076ed8b98 Mon Sep 17 00:00:00 2001 From: Chad Russell Date: Fri, 4 Jul 2014 20:51:25 -0400 Subject: [PATCH 28/45] Fix XML Marshal to work with gin.H --- gin.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/gin.go b/gin.go index 197b98c..bf2338e 100644 --- a/gin.go +++ b/gin.go @@ -62,6 +62,17 @@ type ( } ) +// Allows type H to be used with xml.Marshal +func (h H) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + e.EncodeToken(start) + for key, value := range h { + elem := xml.StartElement{Name: xml.Name{Local: key}} + e.EncodeElement(value, elem) + } + e.EncodeToken(xml.EndElement{start.Name}) + return nil +} + func (a ErrorMsgs) String() string { var buffer bytes.Buffer for i, msg := range a { From f387bdda9e165aee139233a1a8a5cb79ea9ea84d Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Sat, 5 Jul 2014 02:51:52 +0200 Subject: [PATCH 29/45] Fixes context.String() --- gin.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gin.go b/gin.go index 30b1afb..fc49c83 100644 --- a/gin.go +++ b/gin.go @@ -472,10 +472,10 @@ func (c *Context) HTML(code int, name string, data interface{}) { // Writes the given string into the response body and sets the Content-Type to "text/plain". func (c *Context) String(code int, msg string) { + c.Writer.Header().Set("Content-Type", MIMEPlain) if code >= 0 { c.Writer.WriteHeader(code) } - c.Writer.Header().Set("Content-Type", MIMEPlain) c.Writer.Write([]byte(msg)) } From aadd33af2c277718817d90de5ef935ff06923815 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Sat, 5 Jul 2014 02:53:51 +0200 Subject: [PATCH 30/45] Context.Data() takes content-type --- gin.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/gin.go b/gin.go index fc49c83..a407ddb 100644 --- a/gin.go +++ b/gin.go @@ -480,7 +480,12 @@ func (c *Context) String(code int, msg string) { } // Writes some data into the body stream and updates the HTTP code. -func (c *Context) Data(code int, data []byte) { - c.Writer.WriteHeader(code) +func (c *Context) Data(code int, contentType string, data []byte) { + if len(contentType) > 0 { + c.Writer.Header().Set("Content-Type", contentType) + } + if code >= 0 { + c.Writer.WriteHeader(code) + } c.Writer.Write(data) } From c978efa42f3e072a1969c42bfa5305131d0a7935 Mon Sep 17 00:00:00 2001 From: Chad Russell Date: Fri, 4 Jul 2014 23:18:05 -0400 Subject: [PATCH 31/45] added error handling --- gin.go | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/gin.go b/gin.go index bf2338e..308a34c 100644 --- a/gin.go +++ b/gin.go @@ -64,12 +64,21 @@ type ( // Allows type H to be used with xml.Marshal func (h H) MarshalXML(e *xml.Encoder, start xml.StartElement) error { - e.EncodeToken(start) - for key, value := range h { - elem := xml.StartElement{Name: xml.Name{Local: key}} - e.EncodeElement(value, elem) + if err := e.EncodeToken(start); err != nil { + return err + } + for key, value := range h { + elem := xml.StartElement{ + xml.Name{"", key}, + []xml.Attr{}, + } + if err = e.EncodeElement(value, elem); err != nil { + return err + } + } + if err = e.EncodeToken(xml.EndElement{start.Name}); err != nil { + return err } - e.EncodeToken(xml.EndElement{start.Name}) return nil } From ee1406bc4f57fea441dbcd6ad9907a13523cb59e Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Sat, 5 Jul 2014 16:14:43 +0200 Subject: [PATCH 32/45] ServeFiles should not be part of Gin core. We have to create a middleware to handle static files --- gin.go | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/gin.go b/gin.go index a407ddb..f150af3 100644 --- a/gin.go +++ b/gin.go @@ -148,20 +148,6 @@ func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { engine.reuseContext(c) } -// ServeFiles serves files from the given file system root. -// The path must end with "/*filepath", files are then served from the local -// path /defined/root/dir/*filepath. -// For example if root is "/etc" and *filepath is "passwd", the local file -// "/etc/passwd" would be served. -// Internally a http.FileServer is used, therefore http.NotFound is used instead -// of the Router's NotFound handler. -// To use the operating system's file system implementation, -// use http.Dir: -// router.ServeFiles("/src/*filepath", http.Dir("/var/www")) -func (engine *Engine) ServeFiles(path string, root http.FileSystem) { - engine.router.ServeFiles(path, root) -} - // ServeHTTP makes the router implement the http.Handler interface. func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { engine.router.ServeHTTP(w, req) @@ -471,12 +457,12 @@ func (c *Context) HTML(code int, name string, data interface{}) { } // Writes the given string into the response body and sets the Content-Type to "text/plain". -func (c *Context) String(code int, msg string) { +func (c *Context) String(code int, format string, values ...interface{}) { c.Writer.Header().Set("Content-Type", MIMEPlain) if code >= 0 { c.Writer.WriteHeader(code) } - c.Writer.Write([]byte(msg)) + c.Writer.Write([]byte(fmt.Sprintf(format, values...))) } // Writes some data into the body stream and updates the HTTP code. From f19cca070e962d3a7126feb97bd3f18a15bd821e Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Sat, 5 Jul 2014 19:23:40 +0200 Subject: [PATCH 33/45] Using c.Data() to write 404 error --- gin.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gin.go b/gin.go index f150af3..a4871f4 100644 --- a/gin.go +++ b/gin.go @@ -143,7 +143,7 @@ func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { c.Writer.setStatus(404) c.Next() if !c.Writer.Written() { - c.String(404, "404 page not found") + c.Data(404, MIMEPlain, []byte("404 page not found")) } engine.reuseContext(c) } From 8ad554dac3fd1e958bafc72f1dc9dd795dc6db05 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Sat, 5 Jul 2014 19:27:33 +0200 Subject: [PATCH 34/45] Adds task list --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index b035fdf..9c8a729 100644 --- a/README.md +++ b/README.md @@ -12,16 +12,16 @@ Yes, Gin is an internal project of [my](https://github.com/manucorporat) upcomin ##Roadmap for v0.2 -- Performance improments, reduce allocation and garbage collection overhead -- Fix bugs -- Ask our designer for a cool logo -- Add tons of unit tests and benchmarks -- Improve logging system -- Improve JSON/XML validation using bindings -- Improve XML support -- Improve documentation -- Add more cool middlewares, for example redis catching (this also helps developers to understand the framework) -- Continuous integration +- [x] Performance improments, reduce allocation and garbage collection overhead +- [] Fix bugs +- [ ] Ask our designer for a cool logo +- [ ] Add tons of unit tests and benchmarks +- [x] Improve logging system +- [x] Improve JSON/XML validation using bindings +- [ ] Improve XML support +- [ ]Improve documentation +- [ ]Add more cool middlewares, for example redis catching (this also helps developers to understand the framework) +- [x]Continuous integration From db259897f4a3bc488a627ce4925387f972601145 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Sat, 5 Jul 2014 19:28:45 +0200 Subject: [PATCH 35/45] Adds task list (part2) --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9c8a729..f920543 100644 --- a/README.md +++ b/README.md @@ -13,15 +13,15 @@ Yes, Gin is an internal project of [my](https://github.com/manucorporat) upcomin ##Roadmap for v0.2 - [x] Performance improments, reduce allocation and garbage collection overhead -- [] Fix bugs +- [ ] Fix bugs - [ ] Ask our designer for a cool logo - [ ] Add tons of unit tests and benchmarks - [x] Improve logging system - [x] Improve JSON/XML validation using bindings - [ ] Improve XML support -- [ ]Improve documentation -- [ ]Add more cool middlewares, for example redis catching (this also helps developers to understand the framework) -- [x]Continuous integration +- [ ] Improve documentation +- [ ] Add more cool middlewares, for example redis catching (this also helps developers to understand the framework) +- [x] Continuous integration From 027341c80b63905372b4c92584399a9673983d18 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Sat, 5 Jul 2014 19:31:26 +0200 Subject: [PATCH 36/45] Adds cache pressure in logger --- logger.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/logger.go b/logger.go index dde0d5d..a593b5f 100644 --- a/logger.go +++ b/logger.go @@ -48,10 +48,11 @@ func Logger() HandlerFunc { color = red } latency := time.Since(start) - logger.Printf("[GIN] %v |%s %3d %s| %12v | %3s %s\n", + logger.Printf("[GIN] %v |%s %3d %s| %12v | %3.1f%% | %3s | %s\n", time.Now().Format("2006/01/02 - 15:04:05"), color, c.Writer.Status(), reset, latency, + c.Engine.CacheStress()*100, c.Req.Method, c.Req.URL.Path, ) From f8d85c1b4de8a6987446e984885fc111ae4a001b Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Sun, 6 Jul 2014 18:25:17 +0200 Subject: [PATCH 37/45] Fixes MarshalXML() and renames initial "H" tag to "map". --- gin.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gin.go b/gin.go index cc37508..33459cb 100644 --- a/gin.go +++ b/gin.go @@ -83,6 +83,7 @@ var ( // Allows type H to be used with xml.Marshal func (h H) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + start.Name = xml.Name{"", "map"} if err := e.EncodeToken(start); err != nil { return err } @@ -91,11 +92,11 @@ func (h H) MarshalXML(e *xml.Encoder, start xml.StartElement) error { xml.Name{"", key}, []xml.Attr{}, } - if err = e.EncodeElement(value, elem); err != nil { + if err := e.EncodeElement(value, elem); err != nil { return err } } - if err = e.EncodeToken(xml.EndElement{start.Name}); err != nil { + if err := e.EncodeToken(xml.EndElement{start.Name}); err != nil { return err } return nil From c1775e85cc39bbc32f8433e7971fca01bdbe031e Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Sun, 6 Jul 2014 18:26:40 +0200 Subject: [PATCH 38/45] Adds source IP in built-in logger --- logger.go | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/logger.go b/logger.go index a593b5f..7bd87b7 100644 --- a/logger.go +++ b/logger.go @@ -1,7 +1,6 @@ package gin import ( - "fmt" "log" "os" "time" @@ -27,7 +26,9 @@ var ( ) func Logger() HandlerFunc { - logger := log.New(os.Stdout, "", 0) + stdlogger := log.New(os.Stdout, "", 0) + //errlogger := log.New(os.Stderr, "", 0) + return func(c *Context) { // Start timer start := time.Now() @@ -35,6 +36,18 @@ func Logger() HandlerFunc { // Process request c.Next() + // save the IP of the requester + requester := c.Req.Header.Get("X-Real-IP") + // if the requester-header is empty, check the forwarded-header + if requester == "" { + requester = c.Req.Header.Get("X-Forwarded-For") + } + + // if the requester is still empty, use the hard-coded address from the socket + if requester == "" { + requester = c.Req.RemoteAddr + } + var color string code := c.Writer.Status() switch { @@ -48,17 +61,18 @@ func Logger() HandlerFunc { color = red } latency := time.Since(start) - logger.Printf("[GIN] %v |%s %3d %s| %12v | %3.1f%% | %3s | %s\n", + stdlogger.Printf("[GIN] %v |%s %3d %s| %12v | %3.1f%% | %s %4s %s\n", time.Now().Format("2006/01/02 - 15:04:05"), color, c.Writer.Status(), reset, latency, c.Engine.CacheStress()*100, + requester, c.Req.Method, c.Req.URL.Path, ) // Calculate resolution time if len(c.Errors) > 0 { - fmt.Println(c.Errors.String()) + stdlogger.Println(c.Errors.String()) } } } From 99cc7ca362c41c63f3b2237df01e782cfa7fbef6 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Sun, 6 Jul 2014 18:41:07 +0200 Subject: [PATCH 39/45] Go1.1 is not supported anymore --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 57bd4a7..1b1a9d0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: go go: - - 1.1 - 1.2 - 1.3 - tip From 31417dbc63643fd26c05302f29713728a1ac947d Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Sun, 6 Jul 2014 18:57:58 +0200 Subject: [PATCH 40/45] Updates task list. --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f920543..6ebc4ac 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,15 @@ Yes, Gin is an internal project of [my](https://github.com/manucorporat) upcomin ##Roadmap for v0.2 - [x] Performance improments, reduce allocation and garbage collection overhead -- [ ] Fix bugs +- [x] Fix bugs - [ ] Ask our designer for a cool logo -- [ ] Add tons of unit tests and benchmarks +- [ ] Add tons of unit tests +- [ ] Add internal benchmarks suite - [x] Improve logging system - [x] Improve JSON/XML validation using bindings -- [ ] Improve XML support +- [x] Improve XML support - [ ] Improve documentation -- [ ] Add more cool middlewares, for example redis catching (this also helps developers to understand the framework) +- [ ] Add more cool middlewares, for example redis catching (this also helps developers to understand the framework). - [x] Continuous integration From 3e4033673eb26f63e1b38b3870afc8e1fdc31469 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Sun, 6 Jul 2014 21:09:23 +0200 Subject: [PATCH 41/45] Using sync.Pool instead of a channel --- gin.go | 80 ++++++++++++------------------------------------------- logger.go | 3 +-- 2 files changed, 18 insertions(+), 65 deletions(-) diff --git a/gin.go b/gin.go index 33459cb..8521654 100644 --- a/gin.go +++ b/gin.go @@ -13,6 +13,7 @@ import ( "math" "net/http" "path" + "sync" ) const ( @@ -37,11 +38,6 @@ type ( ErrorMsgs []ErrorMsg - Config struct { - CacheSize int - Preallocated int - } - // Context is the most important part of gin. It allows us to pass variables between middleware, // manage the flow, validate the JSON of a request and render a JSON response for example. Context struct { @@ -68,19 +64,12 @@ type ( Engine struct { *RouterGroup HTMLTemplates *template.Template - cache chan *Context + cache sync.Pool handlers404 []HandlerFunc router *httprouter.Router } ) -var ( - DefaultConfig = Config{ - CacheSize: 1024, - Preallocated: 512, - } -) - // Allows type H to be used with xml.Marshal func (h H) MarshalXML(e *xml.Encoder, start xml.StartElement) error { start.Name = xml.Name{"", "map"} @@ -112,32 +101,19 @@ func (a ErrorMsgs) String() string { return buffer.String() } -func NewWithConfig(config Config) *Engine { - if config.CacheSize < 2 { - panic("CacheSize must be at least 2") - } - if config.Preallocated > config.CacheSize { - panic("Preallocated must be less or equal to CacheSize") - } +// Returns a new blank Engine instance without any middleware attached. +// The most basic configuration +func New() *Engine { engine := &Engine{} engine.RouterGroup = &RouterGroup{nil, "/", nil, engine} engine.router = httprouter.New() engine.router.NotFound = engine.handle404 - engine.cache = make(chan *Context, config.CacheSize) - - // Fill it with empty contexts - for i := 0; i < config.Preallocated; i++ { - engine.cache <- &Context{Engine: engine, Writer: &responseWriter{}} + engine.cache.New = func() interface{} { + return &Context{Engine: engine, Writer: &responseWriter{}} } return engine } -// Returns a new blank Engine instance without any middleware attached. -// The most basic configuration -func New() *Engine { - return NewWithConfig(DefaultConfig) -} - // Returns a Engine instance with the Logger and Recovery already attached. func Default() *Engine { engine := New() @@ -154,10 +130,6 @@ func (engine *Engine) NotFound404(handlers ...HandlerFunc) { engine.handlers404 = handlers } -func (engine *Engine) CacheStress() float32 { - return 1.0 - float32(len(engine.cache))/float32(cap(engine.cache)) -} - func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { handlers := engine.combineHandlers(engine.handlers404) c := engine.createContext(w, req, nil, handlers) @@ -166,7 +138,7 @@ func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { if !c.Writer.Written() { c.Data(404, MIMEPlain, []byte("404 page not found")) } - engine.reuseContext(c) + engine.cache.Put(c) } // ServeHTTP makes the router implement the http.Handler interface. @@ -185,32 +157,14 @@ func (engine *Engine) Run(addr string) { /************************************/ func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context { - select { - case c := <-engine.cache: - c.Writer.reset(w) - c.Req = req - c.Params = params - c.handlers = handlers - c.Keys = nil - c.index = -1 - return c - default: - return &Context{ - Writer: &responseWriter{w, -1, false}, - Req: req, - Params: params, - handlers: handlers, - index: -1, - Engine: engine, - } - } -} - -func (engine *Engine) reuseContext(c *Context) { - select { - case engine.cache <- c: - default: - } + c := engine.cache.Get().(*Context) + c.Writer.reset(w) + c.Req = req + c.Params = params + c.handlers = handlers + c.Keys = nil + c.index = -1 + return c } // Adds middlewares to the group, see example code in github. @@ -246,7 +200,7 @@ func (group *RouterGroup) Handle(method, p string, handlers []HandlerFunc) { group.engine.router.Handle(method, p, func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { c := group.engine.createContext(w, req, params, handlers) c.Next() - group.engine.reuseContext(c) + group.engine.cache.Put(c) }) } diff --git a/logger.go b/logger.go index 7bd87b7..3120cbe 100644 --- a/logger.go +++ b/logger.go @@ -61,11 +61,10 @@ func Logger() HandlerFunc { color = red } latency := time.Since(start) - stdlogger.Printf("[GIN] %v |%s %3d %s| %12v | %3.1f%% | %s %4s %s\n", + stdlogger.Printf("[GIN] %v |%s %3d %s| %12v | %s %4s %s\n", time.Now().Format("2006/01/02 - 15:04:05"), color, c.Writer.Status(), reset, latency, - c.Engine.CacheStress()*100, requester, c.Req.Method, c.Req.URL.Path, ) From b5ddd484de8ff3a6ebf4ef2295a1013db1ad3156 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Mon, 7 Jul 2014 03:04:06 +0200 Subject: [PATCH 42/45] Timestamp is calculated once --- logger.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/logger.go b/logger.go index 7bd87b7..ff843a1 100644 --- a/logger.go +++ b/logger.go @@ -60,9 +60,10 @@ func Logger() HandlerFunc { default: color = red } - latency := time.Since(start) + end := time.Now() + latency := end.Sub(start) stdlogger.Printf("[GIN] %v |%s %3d %s| %12v | %3.1f%% | %s %4s %s\n", - time.Now().Format("2006/01/02 - 15:04:05"), + end.Format("2006/01/02 - 15:04:05"), color, c.Writer.Status(), reset, latency, c.Engine.CacheStress()*100, From 3295c6e9c47878cd3682325681a8e148d23bbb4c Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 8 Jul 2014 00:16:41 +0200 Subject: [PATCH 43/45] Improves error management --- gin.go | 54 +++++++++++++++++++++++++++++++++++++++--------------- logger.go | 7 ++++++- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/gin.go b/gin.go index 33459cb..d6c7a6a 100644 --- a/gin.go +++ b/gin.go @@ -24,18 +24,25 @@ const ( MIMEPlain = "text/plain" ) +const ( + ErrorTypeInternal = 1 << iota + ErrorTypeExternal = 1 << iota + ErrorTypeAll = 0xffffffff +) + type ( HandlerFunc func(*Context) H map[string]interface{} // Used internally to collect errors that occurred during an http request. - ErrorMsg struct { + errorMsg struct { Err string `json:"error"` + Type uint32 `json:"-"` Meta interface{} `json:"meta"` } - ErrorMsgs []ErrorMsg + errorMsgs []errorMsg Config struct { CacheSize int @@ -48,7 +55,7 @@ type ( Req *http.Request Writer ResponseWriter Keys map[string]interface{} - Errors ErrorMsgs + Errors errorMsgs Params httprouter.Params Engine *Engine handlers []HandlerFunc @@ -102,13 +109,25 @@ func (h H) MarshalXML(e *xml.Encoder, start xml.StartElement) error { return nil } -func (a ErrorMsgs) String() string { +func (a errorMsgs) ByType(typ uint32) errorMsgs { + if len(a) == 0 { + return a + } + result := make(errorMsgs, 0, len(a)) + for _, msg := range a { + if msg.Type&typ > 0 { + result = append(result, msg) + } + } + return result +} + +func (a errorMsgs) String() string { var buffer bytes.Buffer for i, msg := range a { text := fmt.Sprintf("Error #%02d: %s \n Meta: %v\n", (i + 1), msg.Err, msg.Meta) buffer.WriteString(text) } - buffer.WriteString("\n") return buffer.String() } @@ -336,14 +355,19 @@ func (c *Context) Fail(code int, err error) { c.Abort(code) } +func (c *Context) ErrorTyped(err error, typ uint32, meta interface{}) { + c.Errors = append(c.Errors, errorMsg{ + Err: err.Error(), + Type: typ, + Meta: meta, + }) +} + // Attaches an error to the current context. The error is pushed to a list of errors. // It's a good idea to call Error for each error that occurred during the resolution of a request. // A middleware can be used to collect all the errors and push them to a database together, print a log, or append it in the HTTP response. func (c *Context) Error(err error, meta interface{}) { - c.Errors = append(c.Errors, ErrorMsg{ - Err: err.Error(), - Meta: meta, - }) + c.ErrorTyped(err, ErrorTypeExternal, meta) } func (c *Context) LastError() error { @@ -441,8 +465,8 @@ func (c *Context) JSON(code int, obj interface{}) { } encoder := json.NewEncoder(c.Writer) if err := encoder.Encode(obj); err != nil { - c.Error(err, obj) - http.Error(c.Writer, err.Error(), 500) + c.ErrorTyped(err, ErrorTypeInternal, obj) + c.Abort(500) } } @@ -455,8 +479,8 @@ func (c *Context) XML(code int, obj interface{}) { } encoder := xml.NewEncoder(c.Writer) if err := encoder.Encode(obj); err != nil { - c.Error(err, obj) - http.Error(c.Writer, err.Error(), 500) + c.ErrorTyped(err, ErrorTypeInternal, obj) + c.Abort(500) } } @@ -469,11 +493,11 @@ func (c *Context) HTML(code int, name string, data interface{}) { c.Writer.WriteHeader(code) } if err := c.Engine.HTMLTemplates.ExecuteTemplate(c.Writer, name, data); err != nil { - c.Error(err, map[string]interface{}{ + c.ErrorTyped(err, ErrorTypeInternal, H{ "name": name, "data": data, }) - http.Error(c.Writer, err.Error(), 500) + c.Abort(500) } } diff --git a/logger.go b/logger.go index ff843a1..f7d41d7 100644 --- a/logger.go +++ b/logger.go @@ -7,10 +7,15 @@ import ( ) func ErrorLogger() HandlerFunc { + return ErrorLoggerT(ErrorTypeAll) +} + +func ErrorLoggerT(typ uint32) HandlerFunc { return func(c *Context) { c.Next() - if len(c.Errors) > 0 { + errs := c.Errors.ByType(typ) + if len(errs) > 0 { // -1 status code = do not change current one c.JSON(-1, c.Errors) } From 058201713b5badcabe7a5f200bb9008b590e3307 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 8 Jul 2014 01:09:48 +0200 Subject: [PATCH 44/45] New static file serving --- deprecated.go | 16 ++++++++++++++++ gin.go | 18 ++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/deprecated.go b/deprecated.go index b629098..ca572d7 100644 --- a/deprecated.go +++ b/deprecated.go @@ -2,6 +2,7 @@ package gin import ( "github.com/gin-gonic/gin/binding" + "net/http" ) // DEPRECATED, use Bind() instead. @@ -15,3 +16,18 @@ func (c *Context) EnsureBody(item interface{}) bool { func (c *Context) ParseBody(item interface{}) error { return binding.JSON.Bind(c.Req, item) } + +// DEPRECATED use gin.Static() instead +// ServeFiles serves files from the given file system root. +// The path must end with "/*filepath", files are then served from the local +// path /defined/root/dir/*filepath. +// For example if root is "/etc" and *filepath is "passwd", the local file +// "/etc/passwd" would be served. +// Internally a http.FileServer is used, therefore http.NotFound is used instead +// of the Router's NotFound handler. +// To use the operating system's file system implementation, +// use http.Dir: +// router.ServeFiles("/src/*filepath", http.Dir("/var/www")) +func (engine *Engine) ServeFiles(path string, root http.FileSystem) { + engine.router.ServeFiles(path, root) +} diff --git a/gin.go b/gin.go index d6c7a6a..8cae8ba 100644 --- a/gin.go +++ b/gin.go @@ -304,6 +304,24 @@ func (group *RouterGroup) HEAD(path string, handlers ...HandlerFunc) { group.Handle("HEAD", path, handlers) } +// Static serves files from the given file system root. +// Internally a http.FileServer is used, therefore http.NotFound is used instead +// of the Router's NotFound handler. +// To use the operating system's file system implementation, +// use : +// router.Static("/static", "/var/www") +func (group *RouterGroup) Static(p, root string) { + p = path.Join(p, "/*filepath") + fileServer := http.FileServer(http.Dir(root)) + + group.GET(p, func(c *Context) { + original := c.Req.URL.Path + c.Req.URL.Path = c.Params.ByName("filepath") + fileServer.ServeHTTP(c.Writer, c.Req) + c.Req.URL.Path = original + }) +} + func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc { s := len(group.Handlers) + len(handlers) h := make([]HandlerFunc, 0, s) From b6be4ba58cad0863b712329b2b7be69f4c8e7e81 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 8 Jul 2014 01:16:51 +0200 Subject: [PATCH 45/45] Updates travis. Only compile for Go1.3 --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1b1a9d0..98b4346 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: go go: - - 1.2 - 1.3 - tip