From b1d607a8991147c4f3c905cd4e766eb35c83fdfa Mon Sep 17 00:00:00 2001 From: Kirill Motkov Date: Tue, 21 May 2019 18:08:52 +0300 Subject: [PATCH 01/23] Some code improvements (#1909) * strings.ToLower comparison changed to strings.EqualFold. * Rewrite switch statement with only one case as if. --- binding/form_mapping.go | 4 +--- context.go | 2 +- tree.go | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/binding/form_mapping.go b/binding/form_mapping.go index 32c5b668..ebf3b199 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -126,9 +126,7 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter for len(opts) > 0 { opt, opts = head(opts, ",") - k, v := head(opt, "=") - switch k { - case "default": + if k, v := head(opt, "="); k == "default" { setOpt.isDefaultExists = true setOpt.defaultValue = v } diff --git a/context.go b/context.go index af747a1e..a3a7bc47 100644 --- a/context.go +++ b/context.go @@ -671,7 +671,7 @@ func (c *Context) ContentType() string { // handshake is being initiated by the client. func (c *Context) IsWebsocket() bool { if strings.Contains(strings.ToLower(c.requestHeader("Connection")), "upgrade") && - strings.ToLower(c.requestHeader("Upgrade")) == "websocket" { + strings.EqualFold(c.requestHeader("Upgrade"), "websocket") { return true } return false diff --git a/tree.go b/tree.go index ada62ceb..07d6b4be 100644 --- a/tree.go +++ b/tree.go @@ -514,7 +514,7 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa ciPath = make([]byte, 0, len(path)+1) // preallocate enough memory // Outer loop for walking the tree - for len(path) >= len(n.path) && strings.ToLower(path[:len(n.path)]) == strings.ToLower(n.path) { + for len(path) >= len(n.path) && strings.EqualFold(path[:len(n.path)], n.path) { path = path[len(n.path):] ciPath = append(ciPath, n.path...) @@ -618,7 +618,7 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa return ciPath, true } if len(path)+1 == len(n.path) && n.path[len(path)] == '/' && - strings.ToLower(path) == strings.ToLower(n.path[:len(path)]) && + strings.EqualFold(path, n.path[:len(path)]) && n.handlers != nil { return append(ciPath, n.path...), true } From 0cbf290302ae06b647989effaf5e5a23028670d7 Mon Sep 17 00:00:00 2001 From: itcloudy <272685110@qq.com> Date: Wed, 22 May 2019 07:48:50 +0800 Subject: [PATCH 02/23] use encode replace json marshal increase json encoder speed (#1546) --- context_test.go | 10 +++++----- logger_test.go | 6 +++--- middleware_test.go | 2 +- render/json.go | 7 ++----- render/render_test.go | 2 +- 5 files changed, 12 insertions(+), 15 deletions(-) diff --git a/context_test.go b/context_test.go index 490e4490..e8dcd3dc 100644 --- a/context_test.go +++ b/context_test.go @@ -660,7 +660,7 @@ func TestContextRenderJSON(t *testing.T) { c.JSON(http.StatusCreated, H{"foo": "bar", "html": ""}) assert.Equal(t, http.StatusCreated, w.Code) - assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String()) + assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}\n", w.Body.String()) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) } @@ -688,7 +688,7 @@ func TestContextRenderJSONPWithoutCallback(t *testing.T) { c.JSONP(http.StatusCreated, H{"foo": "bar"}) assert.Equal(t, http.StatusCreated, w.Code) - assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String()) + assert.Equal(t, "{\"foo\":\"bar\"}\n", w.Body.String()) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) } @@ -714,7 +714,7 @@ func TestContextRenderAPIJSON(t *testing.T) { c.JSON(http.StatusCreated, H{"foo": "bar"}) assert.Equal(t, http.StatusCreated, w.Code) - assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String()) + assert.Equal(t, "{\"foo\":\"bar\"}\n", w.Body.String()) assert.Equal(t, "application/vnd.api+json", w.Header().Get("Content-Type")) } @@ -1117,7 +1117,7 @@ func TestContextNegotiationWithJSON(t *testing.T) { }) assert.Equal(t, http.StatusOK, w.Code) - assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String()) + assert.Equal(t, "{\"foo\":\"bar\"}\n", w.Body.String()) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) } @@ -1281,7 +1281,7 @@ func TestContextAbortWithStatusJSON(t *testing.T) { _, err := buf.ReadFrom(w.Body) assert.NoError(t, err) jsonStringBody := buf.String() - assert.Equal(t, fmt.Sprint(`{"foo":"fooValue","bar":"barValue"}`), jsonStringBody) + assert.Equal(t, fmt.Sprint("{\"foo\":\"fooValue\",\"bar\":\"barValue\"}\n"), jsonStringBody) } func TestContextError(t *testing.T) { diff --git a/logger_test.go b/logger_test.go index 56bb3a00..9177e1d9 100644 --- a/logger_test.go +++ b/logger_test.go @@ -369,15 +369,15 @@ func TestErrorLogger(t *testing.T) { w := performRequest(router, "GET", "/error") assert.Equal(t, http.StatusOK, w.Code) - assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String()) + assert.Equal(t, "{\"error\":\"this is an error\"}\n", w.Body.String()) w = performRequest(router, "GET", "/abort") assert.Equal(t, http.StatusUnauthorized, w.Code) - assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String()) + assert.Equal(t, "{\"error\":\"no authorized\"}\n", w.Body.String()) w = performRequest(router, "GET", "/print") assert.Equal(t, http.StatusInternalServerError, w.Code) - assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String()) + assert.Equal(t, "hola!{\"error\":\"this is an error\"}\n", w.Body.String()) } func TestLoggerWithWriterSkippingPaths(t *testing.T) { diff --git a/middleware_test.go b/middleware_test.go index fca1c530..2ae9e889 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -246,5 +246,5 @@ func TestMiddlewareWrite(t *testing.T) { w := performRequest(router, "GET", "/") assert.Equal(t, http.StatusBadRequest, w.Code) - assert.Equal(t, strings.Replace("hola\nbar{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1)) + assert.Equal(t, strings.Replace("hola\nbar{\"foo\":\"bar\"}\n{\"foo\":\"bar\"}\nevent:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1)) } diff --git a/render/json.go b/render/json.go index 18f27fa9..2b07cba0 100644 --- a/render/json.go +++ b/render/json.go @@ -68,11 +68,8 @@ func (r JSON) WriteContentType(w http.ResponseWriter) { // WriteJSON marshals the given interface object and writes it with custom ContentType. func WriteJSON(w http.ResponseWriter, obj interface{}) error { writeContentType(w, jsonContentType) - jsonBytes, err := json.Marshal(obj) - if err != nil { - return err - } - _, err = w.Write(jsonBytes) + encoder := json.NewEncoder(w) + err := encoder.Encode(&obj) return err } diff --git a/render/render_test.go b/render/render_test.go index 3aa5dbcc..9d7eaeef 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -62,7 +62,7 @@ func TestRenderJSON(t *testing.T) { err := (JSON{data}).Render(w) assert.NoError(t, err) - assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String()) + assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}\n", w.Body.String()) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) } From 78a8b5c9d58ed7a81f3e55aaf7e4b1825f2cfecd Mon Sep 17 00:00:00 2001 From: ZYunH Date: Thu, 23 May 2019 11:37:34 +0800 Subject: [PATCH 03/23] Fix typo (#1913) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 10fb1d45..2737e9ad 100644 --- a/README.md +++ b/README.md @@ -1694,7 +1694,7 @@ func main() { quit := make(chan os.Signal) // kill (no param) default send syscall.SIGTERM // kill -2 is syscall.SIGINT - // kill -9 is syscall.SIGKILL but can"t be catch, so don't need add it + // kill -9 is syscall.SIGKILL but can't be catch, so don't need add it signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("Shutdown Server ...") From 35e33d3638f9b5a1246dd9c72a99740f5ad4b43b Mon Sep 17 00:00:00 2001 From: Roman Zaynetdinov <627197+zaynetro@users.noreply.github.com> Date: Sun, 26 May 2019 03:20:21 +0300 Subject: [PATCH 04/23] Hold matched route full path in the Context (#1826) * Return nodeValue from getValue method * Hold route full path in the Context * Add small example --- README.md | 5 ++++ context.go | 11 ++++++++ gin.go | 14 +++++----- routes_test.go | 35 ++++++++++++++++++++++++ tree.go | 72 +++++++++++++++++++++++++++++++------------------- tree_test.go | 26 +++++++++--------- 6 files changed, 117 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 2737e9ad..092e91ff 100644 --- a/README.md +++ b/README.md @@ -252,6 +252,11 @@ func main() { c.String(http.StatusOK, message) }) + // For each matched request Context will hold the route definition + router.POST("/user/:name/*action", func(c *gin.Context) { + c.FullPath() == "/user/:name/*action" // true + }) + router.Run(":8080") } ``` diff --git a/context.go b/context.go index a3a7bc47..425b627f 100644 --- a/context.go +++ b/context.go @@ -48,6 +48,7 @@ type Context struct { Params Params handlers HandlersChain index int8 + fullPath string engine *Engine @@ -70,6 +71,7 @@ func (c *Context) reset() { c.Params = c.Params[0:0] c.handlers = nil c.index = -1 + c.fullPath = "" c.Keys = nil c.Errors = c.Errors[0:0] c.Accepted = nil @@ -111,6 +113,15 @@ func (c *Context) Handler() HandlerFunc { return c.handlers.Last() } +// FullPath returns a matched route full path. For not found routes +// returns an empty string. +// router.GET("/user/:id", func(c *gin.Context) { +// c.FullPath() == "/user/:id" // true +// }) +func (c *Context) FullPath() string { + return c.fullPath +} + /************************************/ /*********** FLOW CONTROL ***********/ /************************************/ diff --git a/gin.go b/gin.go index 4dbe9836..220f0401 100644 --- a/gin.go +++ b/gin.go @@ -252,6 +252,7 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) { root := engine.trees.get(method) if root == nil { root = new(node) + root.fullPath = "/" engine.trees = append(engine.trees, methodTree{method: method, root: root}) } root.addRoute(path, handlers) @@ -382,16 +383,17 @@ func (engine *Engine) handleHTTPRequest(c *Context) { } root := t[i].root // Find route in tree - handlers, params, tsr := root.getValue(rPath, c.Params, unescape) - if handlers != nil { - c.handlers = handlers - c.Params = params + value := root.getValue(rPath, c.Params, unescape) + if value.handlers != nil { + c.handlers = value.handlers + c.Params = value.params + c.fullPath = value.fullPath c.Next() c.writermem.WriteHeaderNow() return } if httpMethod != "CONNECT" && rPath != "/" { - if tsr && engine.RedirectTrailingSlash { + if value.tsr && engine.RedirectTrailingSlash { redirectTrailingSlash(c) return } @@ -407,7 +409,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) { if tree.method == httpMethod { continue } - if handlers, _, _ := tree.root.getValue(rPath, nil, unescape); handlers != nil { + if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil { c.handlers = engine.allNoMethod serveError(c, http.StatusMethodNotAllowed, default405Body) return diff --git a/routes_test.go b/routes_test.go index e16c1376..457c923e 100644 --- a/routes_test.go +++ b/routes_test.go @@ -554,3 +554,38 @@ func TestRouteServeErrorWithWriteHeader(t *testing.T) { assert.Equal(t, 421, w.Code) assert.Equal(t, 0, w.Body.Len()) } + +func TestRouteContextHoldsFullPath(t *testing.T) { + router := New() + + // Test routes + routes := []string{ + "/", + "/simple", + "/project/:name", + "/project/:name/build/*params", + } + + for _, route := range routes { + actualRoute := route + router.GET(route, func(c *Context) { + // For each defined route context should contain its full path + assert.Equal(t, actualRoute, c.FullPath()) + c.AbortWithStatus(http.StatusOK) + }) + } + + for _, route := range routes { + w := performRequest(router, "GET", route) + assert.Equal(t, http.StatusOK, w.Code) + } + + // Test not found + router.Use(func(c *Context) { + // For not found routes full path is empty + assert.Equal(t, "", c.FullPath()) + }) + + w := performRequest(router, "GET", "/not-found") + assert.Equal(t, http.StatusNotFound, w.Code) +} diff --git a/tree.go b/tree.go index 07d6b4be..9a789f2f 100644 --- a/tree.go +++ b/tree.go @@ -94,6 +94,7 @@ type node struct { nType nodeType maxParams uint8 wildChild bool + fullPath string } // increments priority of the given child and reorders if necessary. @@ -154,6 +155,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) { children: n.children, handlers: n.handlers, priority: n.priority - 1, + fullPath: fullPath, } // Update maxParams (max of all children) @@ -229,6 +231,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) { n.indices += string([]byte{c}) child := &node{ maxParams: numParams, + fullPath: fullPath, } n.children = append(n.children, child) n.incrementChildPrio(len(n.indices) - 1) @@ -296,6 +299,7 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle child := &node{ nType: param, maxParams: numParams, + fullPath: fullPath, } n.children = []*node{child} n.wildChild = true @@ -312,6 +316,7 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle child := &node{ maxParams: numParams, priority: 1, + fullPath: fullPath, } n.children = []*node{child} n = child @@ -339,6 +344,7 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle wildChild: true, nType: catchAll, maxParams: 1, + fullPath: fullPath, } n.children = []*node{child} n.indices = string(path[i]) @@ -352,6 +358,7 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle maxParams: 1, handlers: handlers, priority: 1, + fullPath: fullPath, } n.children = []*node{child} @@ -364,13 +371,21 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle n.handlers = handlers } +// nodeValue holds return values of (*Node).getValue method +type nodeValue struct { + handlers HandlersChain + params Params + tsr bool + fullPath string +} + // getValue returns the handle registered with the given path (key). The values of // wildcards are saved to a map. // If no handle can be found, a TSR (trailing slash redirect) recommendation is // made if a handle exists with an extra (without the) trailing slash for the // given path. -func (n *node) getValue(path string, po Params, unescape bool) (handlers HandlersChain, p Params, tsr bool) { - p = po +func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) { + value.params = po walk: // Outer loop for walking the tree for { if len(path) > len(n.path) { @@ -391,7 +406,7 @@ walk: // Outer loop for walking the tree // Nothing found. // We can recommend to redirect to the same URL without a // trailing slash if a leaf exists for that path. - tsr = path == "/" && n.handlers != nil + value.tsr = path == "/" && n.handlers != nil return } @@ -406,20 +421,20 @@ walk: // Outer loop for walking the tree } // save param value - if cap(p) < int(n.maxParams) { - p = make(Params, 0, n.maxParams) + if cap(value.params) < int(n.maxParams) { + value.params = make(Params, 0, n.maxParams) } - i := len(p) - p = p[:i+1] // expand slice within preallocated capacity - p[i].Key = n.path[1:] + i := len(value.params) + value.params = value.params[:i+1] // expand slice within preallocated capacity + value.params[i].Key = n.path[1:] val := path[:end] if unescape { var err error - if p[i].Value, err = url.QueryUnescape(val); err != nil { - p[i].Value = val // fallback, in case of error + if value.params[i].Value, err = url.QueryUnescape(val); err != nil { + value.params[i].Value = val // fallback, in case of error } } else { - p[i].Value = val + value.params[i].Value = val } // we need to go deeper! @@ -431,40 +446,42 @@ walk: // Outer loop for walking the tree } // ... but we can't - tsr = len(path) == end+1 + value.tsr = len(path) == end+1 return } - if handlers = n.handlers; handlers != nil { + if value.handlers = n.handlers; value.handlers != nil { + value.fullPath = n.fullPath return } if len(n.children) == 1 { // No handle found. Check if a handle for this path + a // trailing slash exists for TSR recommendation n = n.children[0] - tsr = n.path == "/" && n.handlers != nil + value.tsr = n.path == "/" && n.handlers != nil } return case catchAll: // save param value - if cap(p) < int(n.maxParams) { - p = make(Params, 0, n.maxParams) + if cap(value.params) < int(n.maxParams) { + value.params = make(Params, 0, n.maxParams) } - i := len(p) - p = p[:i+1] // expand slice within preallocated capacity - p[i].Key = n.path[2:] + i := len(value.params) + value.params = value.params[:i+1] // expand slice within preallocated capacity + value.params[i].Key = n.path[2:] if unescape { var err error - if p[i].Value, err = url.QueryUnescape(path); err != nil { - p[i].Value = path // fallback, in case of error + if value.params[i].Value, err = url.QueryUnescape(path); err != nil { + value.params[i].Value = path // fallback, in case of error } } else { - p[i].Value = path + value.params[i].Value = path } - handlers = n.handlers + value.handlers = n.handlers + value.fullPath = n.fullPath return default: @@ -474,12 +491,13 @@ walk: // Outer loop for walking the tree } else if path == n.path { // We should have reached the node containing the handle. // Check if this node has a handle registered. - if handlers = n.handlers; handlers != nil { + if value.handlers = n.handlers; value.handlers != nil { + value.fullPath = n.fullPath return } if path == "/" && n.wildChild && n.nType != root { - tsr = true + value.tsr = true return } @@ -488,7 +506,7 @@ walk: // Outer loop for walking the tree for i := 0; i < len(n.indices); i++ { if n.indices[i] == '/' { n = n.children[i] - tsr = (len(n.path) == 1 && n.handlers != nil) || + value.tsr = (len(n.path) == 1 && n.handlers != nil) || (n.nType == catchAll && n.children[0].handlers != nil) return } @@ -499,7 +517,7 @@ walk: // Outer loop for walking the tree // Nothing found. We can recommend to redirect to the same URL with an // extra trailing slash if a leaf exists for that path - tsr = (path == "/") || + value.tsr = (path == "/") || (len(n.path) == len(path)+1 && n.path[len(path)] == '/' && path == n.path[:len(n.path)-1] && n.handlers != nil) return diff --git a/tree_test.go b/tree_test.go index dbb0352b..e6e28865 100644 --- a/tree_test.go +++ b/tree_test.go @@ -35,22 +35,22 @@ func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes .. } for _, request := range requests { - handler, ps, _ := tree.getValue(request.path, nil, unescape) + value := tree.getValue(request.path, nil, unescape) - if handler == nil { + if value.handlers == nil { if !request.nilHandler { t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path) } } else if request.nilHandler { t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path) } else { - handler[0](nil) + value.handlers[0](nil) if fakeHandlerValue != request.route { t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route) } } - if !reflect.DeepEqual(ps, request.ps) { + if !reflect.DeepEqual(value.params, request.ps) { t.Errorf("Params mismatch for route '%s'", request.path) } } @@ -454,10 +454,10 @@ func TestTreeTrailingSlashRedirect(t *testing.T) { "/doc/", } for _, route := range tsrRoutes { - handler, _, tsr := tree.getValue(route, nil, false) - if handler != nil { + value := tree.getValue(route, nil, false) + if value.handlers != nil { t.Fatalf("non-nil handler for TSR route '%s", route) - } else if !tsr { + } else if !value.tsr { t.Errorf("expected TSR recommendation for route '%s'", route) } } @@ -471,10 +471,10 @@ func TestTreeTrailingSlashRedirect(t *testing.T) { "/api/world/abc", } for _, route := range noTsrRoutes { - handler, _, tsr := tree.getValue(route, nil, false) - if handler != nil { + value := tree.getValue(route, nil, false) + if value.handlers != nil { t.Fatalf("non-nil handler for No-TSR route '%s", route) - } else if tsr { + } else if value.tsr { t.Errorf("expected no TSR recommendation for route '%s'", route) } } @@ -490,10 +490,10 @@ func TestTreeRootTrailingSlashRedirect(t *testing.T) { t.Fatalf("panic inserting test route: %v", recv) } - handler, _, tsr := tree.getValue("/", nil, false) - if handler != nil { + value := tree.getValue("/", nil, false) + if value.handlers != nil { t.Fatalf("non-nil handler") - } else if tsr { + } else if value.tsr { t.Errorf("expected no TSR recommendation") } } From 6e320c97e83a61df3daa0f28695a736bece3104b Mon Sep 17 00:00:00 2001 From: Samuel Abreu Date: Mon, 27 May 2019 03:04:30 -0300 Subject: [PATCH 05/23] Fix context.Params race condition on Copy() (#1841) * Fix context.Params race condition on Copy() Using context.Param(key) on a context.Copy inside a goroutine may lead to incorrect value on a high load, where another request overwrite a Param * Using waitgroup to wait asynchronous test case --- context.go | 3 +++ context_test.go | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/context.go b/context.go index 425b627f..3f1f8ca0 100644 --- a/context.go +++ b/context.go @@ -89,6 +89,9 @@ func (c *Context) Copy() *Context { for k, v := range c.Keys { cp.Keys[k] = v } + paramCopy := make([]Param, len(cp.Params)) + copy(paramCopy, cp.Params) + cp.Params = paramCopy return &cp } diff --git a/context_test.go b/context_test.go index e8dcd3dc..89cfb446 100644 --- a/context_test.go +++ b/context_test.go @@ -13,8 +13,10 @@ import ( "mime/multipart" "net/http" "net/http/httptest" + "os" "reflect" "strings" + "sync" "testing" "time" @@ -1821,3 +1823,24 @@ func TestContextResetInHandler(t *testing.T) { c.Next() }) } + +func TestRaceParamsContextCopy(t *testing.T) { + DefaultWriter = os.Stdout + router := Default() + nameGroup := router.Group("/:name") + var wg sync.WaitGroup + wg.Add(2) + { + nameGroup.GET("/api", func(c *Context) { + go func(c *Context, param string) { + defer wg.Done() + // First assert must be executed after the second request + time.Sleep(50 * time.Millisecond) + assert.Equal(t, c.Param("name"), param) + }(c.Copy(), c.Param("name")) + }) + } + performRequest(router, "GET", "/name1/api") + performRequest(router, "GET", "/name2/api") + wg.Wait() +} From 233a3e493d2adac62141371c983b55e71d805ca3 Mon Sep 17 00:00:00 2001 From: ijaa Date: Wed, 29 May 2019 11:25:02 +0800 Subject: [PATCH 06/23] add context param query cache (#1450) --- context.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/context.go b/context.go index 3f1f8ca0..64810d54 100644 --- a/context.go +++ b/context.go @@ -60,6 +60,9 @@ type Context struct { // Accepted defines a list of manually accepted formats for content negotiation. Accepted []string + + // queryCache use url.ParseQuery cached the param query result from c.Request.URL.Query() + queryCache url.Values } /************************************/ @@ -75,6 +78,7 @@ func (c *Context) reset() { c.Keys = nil c.Errors = c.Errors[0:0] c.Accepted = nil + c.queryCache = nil } // Copy returns a copy of the current context that can be safely used outside the request's scope. @@ -385,7 +389,13 @@ func (c *Context) QueryArray(key string) []string { // GetQueryArray returns a slice of strings for a given query key, plus // a boolean value whether at least one value exists for the given key. func (c *Context) GetQueryArray(key string) ([]string, bool) { - if values, ok := c.Request.URL.Query()[key]; ok && len(values) > 0 { + + if c.queryCache == nil { + c.queryCache = make(url.Values) + c.queryCache, _ = url.ParseQuery(c.Request.URL.RawQuery) + } + + if values, ok := c.queryCache[key]; ok && len(values) > 0 { return values, true } return []string{}, false From 4b6df417e4bda80a698a64aa085779a7ad1269c0 Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Wed, 29 May 2019 14:54:55 +0800 Subject: [PATCH 07/23] chore: improve GetQueryMap performance. (#1918) Signed-off-by: Bo-Yi Wu --- context.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/context.go b/context.go index 64810d54..71337acd 100644 --- a/context.go +++ b/context.go @@ -386,15 +386,17 @@ func (c *Context) QueryArray(key string) []string { return values } -// GetQueryArray returns a slice of strings for a given query key, plus -// a boolean value whether at least one value exists for the given key. -func (c *Context) GetQueryArray(key string) ([]string, bool) { - +func (c *Context) getQueryCache() { if c.queryCache == nil { c.queryCache = make(url.Values) c.queryCache, _ = url.ParseQuery(c.Request.URL.RawQuery) } +} +// GetQueryArray returns a slice of strings for a given query key, plus +// a boolean value whether at least one value exists for the given key. +func (c *Context) GetQueryArray(key string) ([]string, bool) { + c.getQueryCache() if values, ok := c.queryCache[key]; ok && len(values) > 0 { return values, true } @@ -410,7 +412,8 @@ func (c *Context) QueryMap(key string) map[string]string { // GetQueryMap returns a map for a given query key, plus a boolean value // whether at least one value exists for the given key. func (c *Context) GetQueryMap(key string) (map[string]string, bool) { - return c.get(c.Request.URL.Query(), key) + c.getQueryCache() + return c.get(c.queryCache, key) } // PostForm returns the specified key from a POST urlencoded form or multipart form From 08b52e5394099db4c2399357e060619c1545083e Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Sun, 2 Jun 2019 17:24:41 +0800 Subject: [PATCH 08/23] feat: improve get post data. (#1920) Signed-off-by: Bo-Yi Wu --- context.go | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/context.go b/context.go index 71337acd..ffb9a2de 100644 --- a/context.go +++ b/context.go @@ -61,8 +61,12 @@ type Context struct { // Accepted defines a list of manually accepted formats for content negotiation. Accepted []string - // queryCache use url.ParseQuery cached the param query result from c.Request.URL.Query() + // queryCache use url.ParseQuery cached the param query result from c.Request.URL.Query() queryCache url.Values + + // formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH, + // or PUT body parameters. + formCache url.Values } /************************************/ @@ -454,16 +458,24 @@ func (c *Context) PostFormArray(key string) []string { return values } +func (c *Context) getFormCache() { + if c.formCache == nil { + c.formCache = make(url.Values) + req := c.Request + if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil { + if err != http.ErrNotMultipart { + debugPrint("error on parse multipart form array: %v", err) + } + } + c.formCache = req.PostForm + } +} + // GetPostFormArray returns a slice of strings for a given form key, plus // a boolean value whether at least one value exists for the given key. func (c *Context) GetPostFormArray(key string) ([]string, bool) { - req := c.Request - if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil { - if err != http.ErrNotMultipart { - debugPrint("error on parse multipart form array: %v", err) - } - } - if values := req.PostForm[key]; len(values) > 0 { + c.getFormCache() + if values := c.formCache[key]; len(values) > 0 { return values, true } return []string{}, false From bfecd88fc4c22acdc93585ecc44b3a10d9702e2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Mon, 3 Jun 2019 22:42:25 +0800 Subject: [PATCH 09/23] use sse v0.1.0 (#1923) --- go.mod | 2 +- go.sum | 4 ++-- vendor/vendor.json | 10 ++++++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 1c5e995c..7680d4f7 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/gin-gonic/gin go 1.12 require ( - github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 + github.com/gin-contrib/sse v0.1.0 github.com/golang/protobuf v1.3.1 github.com/json-iterator/go v1.1.6 github.com/mattn/go-isatty v0.0.7 diff --git a/go.sum b/go.sum index 58104682..8610eae2 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g= -github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= diff --git a/vendor/vendor.json b/vendor/vendor.json index 4de0bfd1..3e9d13b7 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -11,10 +11,12 @@ "versionExact": "v1.1.1" }, { - "checksumSHA1": "QeKwBtN2df+j+4stw3bQJ6yO4EY=", + "checksumSHA1": "qlEzrgKgIkh7y0ePm9BNo1cNdXo=", "path": "github.com/gin-contrib/sse", - "revision": "5545eab6dad3bbbd6c5ae9186383c2a9d23c0dae", - "revisionTime": "2019-03-01T06:25:29Z" + "revision": "54d8467d122d380a14768b6b4e5cd7ca4755938f", + "revisionTime": "2019-06-02T15:02:53Z", + "version": "v0.1", + "versionExact": "v0.1.0" }, { "checksumSHA1": "Y2MOwzNZfl4NRNDbLCZa6sgx7O0=", @@ -118,4 +120,4 @@ } ], "rootPath": "github.com/gin-gonic/gin" -} \ No newline at end of file +} From 73c4633943d596bdbeaa7d02cebdd4bd0c4f4630 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Mon, 3 Jun 2019 22:52:33 +0800 Subject: [PATCH 10/23] use context instead of x/net/context (#1922) --- context_test.go | 2 +- vendor/vendor.json | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/context_test.go b/context_test.go index 89cfb446..b6ecb280 100644 --- a/context_test.go +++ b/context_test.go @@ -6,6 +6,7 @@ package gin import ( "bytes" + "context" "errors" "fmt" "html/template" @@ -24,7 +25,6 @@ import ( "github.com/gin-gonic/gin/binding" "github.com/golang/protobuf/proto" "github.com/stretchr/testify/assert" - "golang.org/x/net/context" testdata "github.com/gin-gonic/gin/testdata/protoexample" ) diff --git a/vendor/vendor.json b/vendor/vendor.json index 3e9d13b7..a225eb57 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -90,12 +90,6 @@ "version": "v1.1", "versionExact": "v1.1.4" }, - { - "checksumSHA1": "GtamqiJoL7PGHsN454AoffBFMa8=", - "path": "golang.org/x/net/context", - "revision": "f4e77d36d62c17c2336347bb2670ddbd02d092b7", - "revisionTime": "2019-05-02T22:26:14Z" - }, { "checksumSHA1": "2gaep1KNRDNyDA3O+KgPTQsGWvs=", "path": "golang.org/x/sys/unix", From 75b9d2bed75a2d96198738ac3974211924dbde6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Wed, 12 Jun 2019 21:07:15 +0800 Subject: [PATCH 11/23] Attempt to fix PostForm cache bug (#1931) --- context.go | 1 + 1 file changed, 1 insertion(+) diff --git a/context.go b/context.go index ffb9a2de..77cdc182 100644 --- a/context.go +++ b/context.go @@ -83,6 +83,7 @@ func (c *Context) reset() { c.Errors = c.Errors[0:0] c.Accepted = nil c.queryCache = nil + c.formCache = nil } // Copy returns a copy of the current context that can be safely used outside the request's scope. From 09a3650c97ca7ef3a542d428a9fb2c8da8c18002 Mon Sep 17 00:00:00 2001 From: Dmitry Kutakov Date: Tue, 18 Jun 2019 14:49:10 +0300 Subject: [PATCH 12/23] binding: add support of multipart multi files (#1878) (#1949) * binding: add support of multipart multi files (#1878) * update readme: add multipart file binding --- README.md | 38 ++++--- binding/form.go | 26 ----- binding/multipart_form_mapping.go | 66 ++++++++++++ binding/multipart_form_mapping_test.go | 138 +++++++++++++++++++++++++ 4 files changed, 225 insertions(+), 43 deletions(-) create mode 100644 binding/multipart_form_mapping.go create mode 100644 binding/multipart_form_mapping_test.go diff --git a/README.md b/README.md index 092e91ff..49b044aa 100644 --- a/README.md +++ b/README.md @@ -959,32 +959,36 @@ result: ### Multipart/Urlencoded binding ```go -package main +type ProfileForm struct { + Name string `form:"name" binding:"required"` + Avatar *multipart.FileHeader `form:"avatar" binding:"required"` -import ( - "github.com/gin-gonic/gin" -) - -type LoginForm struct { - User string `form:"user" binding:"required"` - Password string `form:"password" binding:"required"` + // or for multiple files + // Avatars []*multipart.FileHeader `form:"avatar" binding:"required"` } func main() { router := gin.Default() - router.POST("/login", func(c *gin.Context) { + router.POST("/profile", func(c *gin.Context) { // you can bind multipart form with explicit binding declaration: // c.ShouldBindWith(&form, binding.Form) // or you can simply use autobinding with ShouldBind method: - var form LoginForm + var form ProfileForm // in this case proper binding will be automatically selected - if c.ShouldBind(&form) == nil { - if form.User == "user" && form.Password == "password" { - c.JSON(200, gin.H{"status": "you are logged in"}) - } else { - c.JSON(401, gin.H{"status": "unauthorized"}) - } + if err := c.ShouldBind(&form); err != nil { + c.String(http.StatusBadRequest, "bad request") + return } + + err := c.SaveUploadedFile(form.Avatar, form.Avatar.Filename) + if err != nil { + c.String(http.StatusInternalServerError, "unknown error") + return + } + + // db.Save(&form) + + c.String(http.StatusOK, "ok") }) router.Run(":8080") } @@ -992,7 +996,7 @@ func main() { Test it with: ```sh -$ curl -v --form user=user --form password=password http://localhost:8080/login +$ curl -X POST -v --form name=user --form "avatar=@./avatar.png" http://localhost:8080/profile ``` ### XML, JSON, YAML and ProtoBuf rendering diff --git a/binding/form.go b/binding/form.go index 0b28aa8a..9e9fc3de 100644 --- a/binding/form.go +++ b/binding/form.go @@ -5,9 +5,7 @@ package binding import ( - "mime/multipart" "net/http" - "reflect" ) const defaultMemory = 32 * 1024 * 1024 @@ -63,27 +61,3 @@ func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error { return validate(obj) } - -type multipartRequest http.Request - -var _ setter = (*multipartRequest)(nil) - -var ( - multipartFileHeaderStructType = reflect.TypeOf(multipart.FileHeader{}) -) - -// TrySet tries to set a value by the multipart request with the binding a form file -func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) { - if value.Type() == multipartFileHeaderStructType { - _, file, err := (*http.Request)(r).FormFile(key) - if err != nil { - return false, err - } - if file != nil { - value.Set(reflect.ValueOf(*file)) - return true, nil - } - } - - return setByForm(value, field, r.MultipartForm.Value, key, opt) -} diff --git a/binding/multipart_form_mapping.go b/binding/multipart_form_mapping.go new file mode 100644 index 00000000..f85a1aa6 --- /dev/null +++ b/binding/multipart_form_mapping.go @@ -0,0 +1,66 @@ +// Copyright 2019 Gin Core Team. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import ( + "errors" + "mime/multipart" + "net/http" + "reflect" +) + +type multipartRequest http.Request + +var _ setter = (*multipartRequest)(nil) + +// TrySet tries to set a value by the multipart request with the binding a form file +func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) { + if files := r.MultipartForm.File[key]; len(files) != 0 { + return setByMultipartFormFile(value, field, files) + } + + return setByForm(value, field, r.MultipartForm.Value, key, opt) +} + +func setByMultipartFormFile(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) { + switch value.Kind() { + case reflect.Ptr: + switch value.Interface().(type) { + case *multipart.FileHeader: + value.Set(reflect.ValueOf(files[0])) + return true, nil + } + case reflect.Struct: + switch value.Interface().(type) { + case multipart.FileHeader: + value.Set(reflect.ValueOf(*files[0])) + return true, nil + } + case reflect.Slice: + slice := reflect.MakeSlice(value.Type(), len(files), len(files)) + isSetted, err = setArrayOfMultipartFormFiles(slice, field, files) + if err != nil || !isSetted { + return isSetted, err + } + value.Set(slice) + return true, nil + case reflect.Array: + return setArrayOfMultipartFormFiles(value, field, files) + } + return false, errors.New("unsupported field type for multipart.FileHeader") +} + +func setArrayOfMultipartFormFiles(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) { + if value.Len() != len(files) { + return false, errors.New("unsupported len of array for []*multipart.FileHeader") + } + for i := range files { + setted, err := setByMultipartFormFile(value.Index(i), field, files[i:i+1]) + if err != nil || !setted { + return setted, err + } + } + return true, nil +} diff --git a/binding/multipart_form_mapping_test.go b/binding/multipart_form_mapping_test.go new file mode 100644 index 00000000..4c75d1fe --- /dev/null +++ b/binding/multipart_form_mapping_test.go @@ -0,0 +1,138 @@ +// Copyright 2019 Gin Core Team. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import ( + "bytes" + "io/ioutil" + "mime/multipart" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFormMultipartBindingBindOneFile(t *testing.T) { + var s struct { + FileValue multipart.FileHeader `form:"file"` + FilePtr *multipart.FileHeader `form:"file"` + SliceValues []multipart.FileHeader `form:"file"` + SlicePtrs []*multipart.FileHeader `form:"file"` + ArrayValues [1]multipart.FileHeader `form:"file"` + ArrayPtrs [1]*multipart.FileHeader `form:"file"` + } + file := testFile{"file", "file1", []byte("hello")} + + req := createRequestMultipartFiles(t, file) + err := FormMultipart.Bind(req, &s) + assert.NoError(t, err) + + assertMultipartFileHeader(t, &s.FileValue, file) + assertMultipartFileHeader(t, s.FilePtr, file) + assert.Len(t, s.SliceValues, 1) + assertMultipartFileHeader(t, &s.SliceValues[0], file) + assert.Len(t, s.SlicePtrs, 1) + assertMultipartFileHeader(t, s.SlicePtrs[0], file) + assertMultipartFileHeader(t, &s.ArrayValues[0], file) + assertMultipartFileHeader(t, s.ArrayPtrs[0], file) +} + +func TestFormMultipartBindingBindTwoFiles(t *testing.T) { + var s struct { + SliceValues []multipart.FileHeader `form:"file"` + SlicePtrs []*multipart.FileHeader `form:"file"` + ArrayValues [2]multipart.FileHeader `form:"file"` + ArrayPtrs [2]*multipart.FileHeader `form:"file"` + } + files := []testFile{ + {"file", "file1", []byte("hello")}, + {"file", "file2", []byte("world")}, + } + + req := createRequestMultipartFiles(t, files...) + err := FormMultipart.Bind(req, &s) + assert.NoError(t, err) + + assert.Len(t, s.SliceValues, len(files)) + assert.Len(t, s.SlicePtrs, len(files)) + assert.Len(t, s.ArrayValues, len(files)) + assert.Len(t, s.ArrayPtrs, len(files)) + + for i, file := range files { + assertMultipartFileHeader(t, &s.SliceValues[i], file) + assertMultipartFileHeader(t, s.SlicePtrs[i], file) + assertMultipartFileHeader(t, &s.ArrayValues[i], file) + assertMultipartFileHeader(t, s.ArrayPtrs[i], file) + } +} + +func TestFormMultipartBindingBindError(t *testing.T) { + files := []testFile{ + {"file", "file1", []byte("hello")}, + {"file", "file2", []byte("world")}, + } + + for _, tt := range []struct { + name string + s interface{} + }{ + {"wrong type", &struct { + Files int `form:"file"` + }{}}, + {"wrong array size", &struct { + Files [1]*multipart.FileHeader `form:"file"` + }{}}, + {"wrong slice type", &struct { + Files []int `form:"file"` + }{}}, + } { + req := createRequestMultipartFiles(t, files...) + err := FormMultipart.Bind(req, tt.s) + assert.Error(t, err) + } +} + +type testFile struct { + Fieldname string + Filename string + Content []byte +} + +func createRequestMultipartFiles(t *testing.T, files ...testFile) *http.Request { + var body bytes.Buffer + + mw := multipart.NewWriter(&body) + for _, file := range files { + fw, err := mw.CreateFormFile(file.Fieldname, file.Filename) + assert.NoError(t, err) + + n, err := fw.Write(file.Content) + assert.NoError(t, err) + assert.Equal(t, len(file.Content), n) + } + err := mw.Close() + assert.NoError(t, err) + + req, err := http.NewRequest("POST", "/", &body) + assert.NoError(t, err) + + req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+mw.Boundary()) + return req +} + +func assertMultipartFileHeader(t *testing.T, fh *multipart.FileHeader, file testFile) { + assert.Equal(t, file.Filename, fh.Filename) + // assert.Equal(t, int64(len(file.Content)), fh.Size) // fh.Size does not exist on go1.8 + + fl, err := fh.Open() + assert.NoError(t, err) + + body, err := ioutil.ReadAll(fl) + assert.NoError(t, err) + assert.Equal(t, string(file.Content), string(body)) + + err = fl.Close() + assert.NoError(t, err) +} From f98b339b773105aad77f321d0baaa30475bf875d Mon Sep 17 00:00:00 2001 From: guonaihong Date: Thu, 27 Jun 2019 12:47:45 +0800 Subject: [PATCH 13/23] support bind http header param #1956 (#1957) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * support bind http header param #1956 update #1956 ``` package main import ( "fmt" "github.com/gin-gonic/gin" ) type testHeader struct { Rate int `header:"Rate"` Domain string `header:"Domain"` } func main() { r := gin.Default() r.GET("/", func(c *gin.Context) { h := testHeader{} if err := c.ShouldBindHeader(&h); err != nil { c.JSON(200, err) } fmt.Printf("%#v\n", h) c.JSON(200, gin.H{"Rate": h.Rate, "Domain": h.Domain}) }) r.Run() // client // curl -H "rate:300" -H "domain:music" 127.0.0.1:8080/ // output // {"Domain":"music","Rate":300} } ``` * add unit test * Modify the code to get the http header When the http header is obtained in the standard library, the key value will be modified by the CanonicalMIMEHeaderKey function, and finally the value of the http header will be obtained from the map. As follows. ```go func (h MIMEHeader) Get(key string) string {         // ...          v := h[CanonicalMIMEHeaderKey(key)]         // ... } ``` This pr also follows this modification * Thanks to vkd for suggestions, modifying code * Increase test coverage env GOPATH=`pwd` go test github.com/gin-gonic/gin/binding -coverprofile=cover.prof ok github.com/gin-gonic/gin/binding 0.015s coverage: 100.0% of statements * Rollback check code * add use case to README.md --- README.md | 38 +++++++++++++++++++++++++++++++++++ binding/binding.go | 1 + binding/binding_test.go | 25 +++++++++++++++++++++++ binding/header.go | 34 +++++++++++++++++++++++++++++++ context.go | 10 ++++++++++ context_test.go | 44 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 152 insertions(+) create mode 100644 binding/header.go diff --git a/README.md b/README.md index 49b044aa..aa5043aa 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi - [Only Bind Query String](#only-bind-query-string) - [Bind Query String or Post Data](#bind-query-string-or-post-data) - [Bind Uri](#bind-uri) + - [Bind Header](#bind-header) - [Bind HTML checkboxes](#bind-html-checkboxes) - [Multipart/Urlencoded binding](#multiparturlencoded-binding) - [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering) @@ -910,6 +911,43 @@ $ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3 $ curl -v localhost:8088/thinkerou/not-uuid ``` +### Bind Header + +```go +package main + +import ( + "fmt" + "github.com/gin-gonic/gin" +) + +type testHeader struct { + Rate int `header:"Rate"` + Domain string `header:"Domain"` +} + +func main() { + r := gin.Default() + r.GET("/", func(c *gin.Context) { + h := testHeader{} + + if err := c.ShouldBindHeader(&h); err != nil { + c.JSON(200, err) + } + + fmt.Printf("%#v\n", h) + c.JSON(200, gin.H{"Rate": h.Rate, "Domain": h.Domain}) + }) + + r.Run() + +// client +// curl -H "rate:300" -H "domain:music" 127.0.0.1:8080/ +// output +// {"Domain":"music","Rate":300} +} +``` + ### Bind HTML checkboxes See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092) diff --git a/binding/binding.go b/binding/binding.go index 520c5109..6d58c3cd 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -78,6 +78,7 @@ var ( MsgPack = msgpackBinding{} YAML = yamlBinding{} Uri = uriBinding{} + Header = headerBinding{} ) // Default returns the appropriate Binding instance based on the HTTP method diff --git a/binding/binding_test.go b/binding/binding_test.go index 6710e42b..827518f9 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -667,6 +667,31 @@ func TestExistsFails(t *testing.T) { assert.Error(t, err) } +func TestHeaderBinding(t *testing.T) { + h := Header + assert.Equal(t, "header", h.Name()) + + type tHeader struct { + Limit int `header:"limit"` + } + + var theader tHeader + req := requestWithBody("GET", "/", "") + req.Header.Add("limit", "1000") + assert.NoError(t, h.Bind(req, &theader)) + assert.Equal(t, 1000, theader.Limit) + + req = requestWithBody("GET", "/", "") + req.Header.Add("fail", `{fail:fail}`) + + type failStruct struct { + Fail map[string]interface{} `header:"fail"` + } + + err := h.Bind(req, &failStruct{}) + assert.Error(t, err) +} + func TestUriBinding(t *testing.T) { b := Uri assert.Equal(t, "uri", b.Name()) diff --git a/binding/header.go b/binding/header.go new file mode 100644 index 00000000..179ce4ea --- /dev/null +++ b/binding/header.go @@ -0,0 +1,34 @@ +package binding + +import ( + "net/http" + "net/textproto" + "reflect" +) + +type headerBinding struct{} + +func (headerBinding) Name() string { + return "header" +} + +func (headerBinding) Bind(req *http.Request, obj interface{}) error { + + if err := mapHeader(obj, req.Header); err != nil { + return err + } + + return validate(obj) +} + +func mapHeader(ptr interface{}, h map[string][]string) error { + return mappingByPtr(ptr, headerSource(h), "header") +} + +type headerSource map[string][]string + +var _ setter = headerSource(nil) + +func (hs headerSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSetted bool, err error) { + return setByForm(value, field, hs, textproto.CanonicalMIMEHeaderKey(tagValue), opt) +} diff --git a/context.go b/context.go index 77cdc182..d9fcc285 100644 --- a/context.go +++ b/context.go @@ -583,6 +583,11 @@ func (c *Context) BindYAML(obj interface{}) error { return c.MustBindWith(obj, binding.YAML) } +// BindHeader is a shortcut for c.MustBindWith(obj, binding.Header). +func (c *Context) BindHeader(obj interface{}) error { + return c.MustBindWith(obj, binding.Header) +} + // BindUri binds the passed struct pointer using binding.Uri. // It will abort the request with HTTP 400 if any error occurs. func (c *Context) BindUri(obj interface{}) error { @@ -637,6 +642,11 @@ func (c *Context) ShouldBindYAML(obj interface{}) error { return c.ShouldBindWith(obj, binding.YAML) } +// ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header). +func (c *Context) ShouldBindHeader(obj interface{}) error { + return c.ShouldBindWith(obj, binding.Header) +} + // ShouldBindUri binds the passed struct pointer using the specified binding engine. func (c *Context) ShouldBindUri(obj interface{}) error { m := make(map[string][]string) diff --git a/context_test.go b/context_test.go index b6ecb280..439e8ee6 100644 --- a/context_test.go +++ b/context_test.go @@ -1436,6 +1436,28 @@ func TestContextBindWithXML(t *testing.T) { assert.Equal(t, 0, w.Body.Len()) } +func TestContextBindHeader(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.Request, _ = http.NewRequest("POST", "/", nil) + c.Request.Header.Add("rate", "8000") + c.Request.Header.Add("domain", "music") + c.Request.Header.Add("limit", "1000") + + var testHeader struct { + Rate int `header:"Rate"` + Domain string `header:"Domain"` + Limit int `header:"limit"` + } + + assert.NoError(t, c.BindHeader(&testHeader)) + assert.Equal(t, 8000, testHeader.Rate) + assert.Equal(t, "music", testHeader.Domain) + assert.Equal(t, 1000, testHeader.Limit) + assert.Equal(t, 0, w.Body.Len()) +} + func TestContextBindWithQuery(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) @@ -1543,6 +1565,28 @@ func TestContextShouldBindWithXML(t *testing.T) { assert.Equal(t, 0, w.Body.Len()) } +func TestContextShouldBindHeader(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.Request, _ = http.NewRequest("POST", "/", nil) + c.Request.Header.Add("rate", "8000") + c.Request.Header.Add("domain", "music") + c.Request.Header.Add("limit", "1000") + + var testHeader struct { + Rate int `header:"Rate"` + Domain string `header:"Domain"` + Limit int `header:"limit"` + } + + assert.NoError(t, c.ShouldBindHeader(&testHeader)) + assert.Equal(t, 8000, testHeader.Rate) + assert.Equal(t, "music", testHeader.Domain) + assert.Equal(t, 1000, testHeader.Limit) + assert.Equal(t, 0, w.Body.Len()) +} + func TestContextShouldBindWithQuery(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) From 31342fc03febd3fb35a269191a40d01311ba87a6 Mon Sep 17 00:00:00 2001 From: guonaihong Date: Fri, 28 Jun 2019 09:25:19 +0800 Subject: [PATCH 14/23] fix README.md code bug and Change map to gin.H (#1963) ``` go func main() { r := gin.Default() // r.GET("/JSONP?callback=x", func(c *gin.Context) { // old r.GET("/JSONP", func(c *gin.Context) { // new data := gin.H{ "foo": "bar", } //callback is x // Will output : x({\"foo\":\"bar\"}) c.JSONP(http.StatusOK, data) }) // Listen and serve on 0.0.0.0:8080 r.Run(":8080") } // client // curl http://127.0.0.1:8080/JSONP?callback=x // old output // 404 page not found // new output // x({"foo":"bar"}) ``` Most of the sample code in the documentation map[string]interface{} is represented by gin.H. gin.H is a very important place for me to like gin, can write a lot less code --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index aa5043aa..aa546e58 100644 --- a/README.md +++ b/README.md @@ -1119,8 +1119,8 @@ Using JSONP to request data from a server in a different domain. Add callback t func main() { r := gin.Default() - r.GET("/JSONP?callback=x", func(c *gin.Context) { - data := map[string]interface{}{ + r.GET("/JSONP", func(c *gin.Context) { + data := gin.H{ "foo": "bar", } @@ -1131,6 +1131,9 @@ func main() { // Listen and serve on 0.0.0.0:8080 r.Run(":8080") + + // client + // curl http://127.0.0.1:8080/JSONP?callback=x } ``` @@ -1143,7 +1146,7 @@ func main() { r := gin.Default() r.GET("/someJSON", func(c *gin.Context) { - data := map[string]interface{}{ + data := gin.H{ "lang": "GO语言", "tag": "
", } @@ -1352,7 +1355,7 @@ func main() { router.LoadHTMLFiles("./testdata/template/raw.tmpl") router.GET("/raw", func(c *gin.Context) { - c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{ + c.HTML(http.StatusOK, "raw.tmpl", gin.H{ "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), }) }) From 46acb91996921079112470de9068cd4cea503a70 Mon Sep 17 00:00:00 2001 From: srt180 <30768686+srt180@users.noreply.github.com> Date: Fri, 28 Jun 2019 09:34:14 +0800 Subject: [PATCH 15/23] modify readme example code (#1961) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aa546e58..f79c0c1c 100644 --- a/README.md +++ b/README.md @@ -756,7 +756,7 @@ func bookableDate( ) bool { if date, ok := field.Interface().(time.Time); ok { today := time.Now() - if today.Year() > date.Year() || today.YearDay() > date.YearDay() { + if today.After(date) { return false } } From fc920dc56141d11599201b2b35838cc9449d5a2a Mon Sep 17 00:00:00 2001 From: Dan Markham Date: Fri, 28 Jun 2019 08:43:07 -0700 Subject: [PATCH 16/23] Drop Support for go1.8 and go1.9 (#1933) --- .travis.yml | 2 -- README.md | 2 +- debug.go | 4 ++-- debug_test.go | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index f6ec8a82..27c80ef8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,6 @@ language: go matrix: fast_finish: true include: - - go: 1.8.x - - go: 1.9.x - go: 1.10.x - go: 1.11.x env: GO111MODULE=on diff --git a/README.md b/README.md index f79c0c1c..4257df3e 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi To install Gin package, you need to install Go and set your Go workspace first. -1. The first need [Go](https://golang.org/) installed (**version 1.8+ is required**), then you can use the below Go command to install Gin. +1. The first need [Go](https://golang.org/) installed (**version 1.10+ is required**), then you can use the below Go command to install Gin. ```sh $ go get -u github.com/gin-gonic/gin diff --git a/debug.go b/debug.go index 19e380fb..64777c25 100644 --- a/debug.go +++ b/debug.go @@ -13,7 +13,7 @@ import ( "strings" ) -const ginSupportMinGoVer = 8 +const ginSupportMinGoVer = 10 // IsDebugging returns true if the framework is running in debug mode. // Use SetMode(gin.ReleaseMode) to disable debug mode. @@ -68,7 +68,7 @@ func getMinVer(v string) (uint64, error) { func debugPrintWARNINGDefault() { if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer { - debugPrint(`[WARNING] Now Gin requires Go 1.8 or later and Go 1.9 will be required soon. + debugPrint(`[WARNING] Now Gin requires Go 1.10 or later and Go 1.11 will be required soon. `) } diff --git a/debug_test.go b/debug_test.go index 9ace2989..d6f320ef 100644 --- a/debug_test.go +++ b/debug_test.go @@ -91,7 +91,7 @@ func TestDebugPrintWARNINGDefault(t *testing.T) { }) m, e := getMinVer(runtime.Version()) if e == nil && m <= ginSupportMinGoVer { - assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.8 or later and Go 1.9 will be required soon.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re) + assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.10 or later and Go 1.11 will be required soon.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re) } else { assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re) } From f65018d7b1f1a5d54e5791e9e9ac76e9946eae36 Mon Sep 17 00:00:00 2001 From: bbiao Date: Fri, 28 Jun 2019 23:54:52 +0800 Subject: [PATCH 17/23] Bugfix for the FullPath feature (#1919) * worked with more complex situations * the original pr not work when and a short route with the same prefix to some already added routes --- routes_test.go | 7 ++++++- tree.go | 9 ++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/routes_test.go b/routes_test.go index 457c923e..0c2f9a0c 100644 --- a/routes_test.go +++ b/routes_test.go @@ -560,10 +560,15 @@ func TestRouteContextHoldsFullPath(t *testing.T) { // Test routes routes := []string{ - "/", "/simple", "/project/:name", + "/", + "/news/home", + "/news", + "/simple-two/one", + "/simple-two/one-two", "/project/:name/build/*params", + "/project/:name/bui", } for _, route := range routes { diff --git a/tree.go b/tree.go index 9a789f2f..371d5ad1 100644 --- a/tree.go +++ b/tree.go @@ -128,6 +128,8 @@ func (n *node) addRoute(path string, handlers HandlersChain) { n.priority++ numParams := countParams(path) + parentFullPathIndex := 0 + // non-empty tree if len(n.path) > 0 || len(n.children) > 0 { walk: @@ -155,7 +157,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) { children: n.children, handlers: n.handlers, priority: n.priority - 1, - fullPath: fullPath, + fullPath: n.fullPath, } // Update maxParams (max of all children) @@ -171,6 +173,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) { n.path = path[:i] n.handlers = nil n.wildChild = false + n.fullPath = fullPath[:parentFullPathIndex+i] } // Make new node a child of this node @@ -178,6 +181,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) { path = path[i:] if n.wildChild { + parentFullPathIndex += len(n.path) n = n.children[0] n.priority++ @@ -211,6 +215,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) { // slash after param if n.nType == param && c == '/' && len(n.children) == 1 { + parentFullPathIndex += len(n.path) n = n.children[0] n.priority++ continue walk @@ -219,6 +224,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) { // Check if a child with the next path byte exists for i := 0; i < len(n.indices); i++ { if c == n.indices[i] { + parentFullPathIndex += len(n.path) i = n.incrementChildPrio(i) n = n.children[i] continue walk @@ -369,6 +375,7 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle // insert remaining path part and handle to the leaf n.path = path[offset:] n.handlers = handlers + n.fullPath = fullPath } // nodeValue holds return values of (*Node).getValue method From 3f53a58d4ad33bf881c028936b0b44f2c3210d56 Mon Sep 17 00:00:00 2001 From: thinkerou Date: Sat, 29 Jun 2019 00:09:53 +0800 Subject: [PATCH 18/23] Add user case: brigade (#1937) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4257df3e..8d7705d3 100644 --- a/README.md +++ b/README.md @@ -2114,3 +2114,4 @@ Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framewor * [photoprism](https://github.com/photoprism/photoprism): Personal photo management powered by Go and Google TensorFlow. * [krakend](https://github.com/devopsfaith/krakend): Ultra performant API Gateway with middlewares. * [picfit](https://github.com/thoas/picfit): An image resizing server written in Go. +* [brigade](https://github.com/brigadecore/brigade): Event-based Scripting for Kubernetes. From b67bc8f00502b1f555b9db64cd2dfc03098cfc8f Mon Sep 17 00:00:00 2001 From: guonaihong Date: Sat, 29 Jun 2019 20:43:32 +0800 Subject: [PATCH 19/23] Gin1.5 bytes.Buffer to strings.Builder (#1939) * Replace bytes.Buffer to strings.Builder * Merge the latest changes * Update errors.go --- debug.go | 3 +-- errors.go | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/debug.go b/debug.go index 64777c25..49080dbf 100644 --- a/debug.go +++ b/debug.go @@ -5,7 +5,6 @@ package gin import ( - "bytes" "fmt" "html/template" "runtime" @@ -38,7 +37,7 @@ func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) { func debugPrintLoadTemplate(tmpl *template.Template) { if IsDebugging() { - var buf bytes.Buffer + var buf strings.Builder for _, tmpl := range tmpl.Templates() { buf.WriteString("\t- ") buf.WriteString(tmpl.Name()) diff --git a/errors.go b/errors.go index 6070ff55..25e8ff60 100644 --- a/errors.go +++ b/errors.go @@ -5,9 +5,9 @@ package gin import ( - "bytes" "fmt" "reflect" + "strings" "github.com/gin-gonic/gin/internal/json" ) @@ -158,7 +158,7 @@ func (a errorMsgs) String() string { if len(a) == 0 { return "" } - var buffer bytes.Buffer + var buffer strings.Builder for i, msg := range a { fmt.Fprintf(&buffer, "Error #%02d: %s\n", i+1, msg.Err) if msg.Meta != nil { From 6f7276fdc1d3cfad2052ba770382afd2f4d41dbf Mon Sep 17 00:00:00 2001 From: Alan Wang Date: Sun, 30 Jun 2019 08:55:09 +0800 Subject: [PATCH 20/23] Update CHANGELOG.md (#1966) typo fix --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ea2495d..15dfb1a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### Gin 1.4.0 - [NEW] Support for [Go Modules](https://github.com/golang/go/wiki/Modules) [#1569](https://github.com/gin-gonic/gin/pull/1569) -- [NEW] Refactor of form mapping multipart requesta [#1829](https://github.com/gin-gonic/gin/pull/1829) +- [NEW] Refactor of form mapping multipart request [#1829](https://github.com/gin-gonic/gin/pull/1829) - [FIX] Truncate Latency precision in long running request [#1830](https://github.com/gin-gonic/gin/pull/1830) - [FIX] IsTerm flag should not be affected by DisableConsoleColor method. [#1802](https://github.com/gin-gonic/gin/pull/1802) - [NEW] Supporting file binding [#1264](https://github.com/gin-gonic/gin/pull/1264) From e602d524cccad90261e10bbb5ca41e9a81e467d4 Mon Sep 17 00:00:00 2001 From: Rafal Zajac Date: Thu, 4 Jul 2019 01:57:52 +0200 Subject: [PATCH 21/23] Typo (#1971) --- utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils.go b/utils.go index f4532d56..71b80de7 100644 --- a/utils.go +++ b/utils.go @@ -146,6 +146,6 @@ func resolveAddress(addr []string) string { case 1: return addr[0] default: - panic("too much parameters") + panic("too many parameters") } } From 0349de518b3bd862f664d1e58565a3d3bff6a771 Mon Sep 17 00:00:00 2001 From: thinkerou Date: Wed, 10 Jul 2019 06:20:20 +0800 Subject: [PATCH 22/23] upgrade github.com/ugorji/go/codec (#1969) --- go.mod | 5 ++--- go.sum | 15 ++++++--------- vendor/vendor.json | 8 ++++---- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index 7680d4f7..dbe8afc5 100644 --- a/go.mod +++ b/go.mod @@ -6,12 +6,11 @@ require ( github.com/gin-contrib/sse v0.1.0 github.com/golang/protobuf v1.3.1 github.com/json-iterator/go v1.1.6 - github.com/mattn/go-isatty v0.0.7 + github.com/mattn/go-isatty v0.0.8 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.1 // indirect github.com/stretchr/testify v1.3.0 - github.com/ugorji/go v1.1.4 - golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c + github.com/ugorji/go/codec v1.1.7 gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/go-playground/validator.v8 v8.18.2 gopkg.in/yaml.v2 v2.2.2 diff --git a/go.sum b/go.sum index 8610eae2..c1e9f221 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= @@ -17,15 +17,12 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= diff --git a/vendor/vendor.json b/vendor/vendor.json index a225eb57..fa8fd13a 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -83,12 +83,12 @@ "versionExact": "v1.2.2" }, { - "checksumSHA1": "csplo594qomjp2IZj82y7mTueOw=", + "checksumSHA1": "S4ei9eSqVThDio0Jn2sav6yUbvg=", "path": "github.com/ugorji/go/codec", - "revision": "2adff0894ba3bc2eeb9f9aea45fefd49802e1a13", - "revisionTime": "2019-04-08T19:08:48Z", + "revision": "82dbfaf494e3b01d2d481376f11f6a5c8cf9599f", + "revisionTime": "2019-07-02T14:15:27Z", "version": "v1.1", - "versionExact": "v1.1.4" + "versionExact": "v1.1.6" }, { "checksumSHA1": "2gaep1KNRDNyDA3O+KgPTQsGWvs=", From 502c898d755b2156b295ea8bdbbecf9ba374d067 Mon Sep 17 00:00:00 2001 From: guonaihong Date: Wed, 10 Jul 2019 13:02:40 +0800 Subject: [PATCH 23/23] binding: support unix time (#1980) * binding: support unix time ref:#1979 * binding: support unix time add test file modify readme ```golang package main import ( "fmt" "github.com/gin-gonic/gin" "time" ) type shareTime struct { CreateTime time.Time `form:"createTime" time_format:"unixNano"` UnixTime time.Time `form:"unixTime" time_format:"unix"` } func main() { r := gin.Default() unix := r.Group("/unix") testCT := time.Date(2019, 7, 6, 16, 0, 33, 123, time.Local) fmt.Printf("%d\n", testCT.UnixNano()) testUT := time.Date(2019, 7, 6, 16, 0, 33, 0, time.Local) fmt.Printf("%d\n", testUT.Unix()) unix.GET("/nano", func(c *gin.Context) { s := shareTime{} c.ShouldBindQuery(&s) if !testCT.Equal(s.CreateTime) { c.String(500, "want %d got %d", testCT.UnixNano(), s.CreateTime) return } c.JSON(200, s) }) unix.GET("/sec", func(c *gin.Context) { s := shareTime{} c.ShouldBindQuery(&s) if !testUT.Equal(s.UnixTime) { c.String(500, "want %d got %d", testCT.Unix(), s.UnixTime) return } c.JSON(200, s) }) r.Run() } ``` * Contraction variable scope --- README.md | 22 +++++++++++++--------- binding/binding_test.go | 41 +++++++++++++++++++++++++++++++++++++---- binding/form_mapping.go | 18 ++++++++++++++++++ 3 files changed, 68 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 8d7705d3..2761259e 100644 --- a/README.md +++ b/README.md @@ -846,9 +846,11 @@ import ( ) type Person struct { - Name string `form:"name"` - Address string `form:"address"` - Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"` + Name string `form:"name"` + Address string `form:"address"` + Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"` + CreateTime time.Time `form:"createTime" time_format:"unixNano"` + UnixTime time.Time `form:"unixTime" time_format:"unix"` } func main() { @@ -862,11 +864,13 @@ func startPage(c *gin.Context) { // If `GET`, only `Form` binding engine (`query`) used. // If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`). // See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48 - if c.ShouldBind(&person) == nil { - log.Println(person.Name) - log.Println(person.Address) - log.Println(person.Birthday) - } + if c.ShouldBind(&person) == nil { + log.Println(person.Name) + log.Println(person.Address) + log.Println(person.Birthday) + log.Println(person.CreateTime) + log.Println(person.UnixTime) + } c.String(200, "Success") } @@ -874,7 +878,7 @@ func startPage(c *gin.Context) { Test it with: ```sh -$ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15" +$ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033" ``` ### Bind Uri diff --git a/binding/binding_test.go b/binding/binding_test.go index 827518f9..806f3ac9 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -65,8 +65,15 @@ type FooStructUseNumber struct { } type FooBarStructForTimeType struct { - TimeFoo time.Time `form:"time_foo" time_format:"2006-01-02" time_utc:"1" time_location:"Asia/Chongqing"` - TimeBar time.Time `form:"time_bar" time_format:"2006-01-02" time_utc:"1"` + TimeFoo time.Time `form:"time_foo" time_format:"2006-01-02" time_utc:"1" time_location:"Asia/Chongqing"` + TimeBar time.Time `form:"time_bar" time_format:"2006-01-02" time_utc:"1"` + CreateTime time.Time `form:"createTime" time_format:"unixNano"` + UnixTime time.Time `form:"unixTime" time_format:"unix"` +} + +type FooStructForTimeTypeNotUnixFormat struct { + CreateTime time.Time `form:"createTime" time_format:"unixNano"` + UnixTime time.Time `form:"unixTime" time_format:"unix"` } type FooStructForTimeTypeNotFormat struct { @@ -226,7 +233,10 @@ func TestBindingFormDefaultValue2(t *testing.T) { func TestBindingFormForTime(t *testing.T) { testFormBindingForTime(t, "POST", "/", "/", - "time_foo=2017-11-15&time_bar=", "bar2=foo") + "time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "bar2=foo") + testFormBindingForTimeNotUnixFormat(t, "POST", + "/", "/", + "time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo") testFormBindingForTimeNotFormat(t, "POST", "/", "/", "time_foo=2017-11-15", "bar2=foo") @@ -240,8 +250,11 @@ func TestBindingFormForTime(t *testing.T) { func TestBindingFormForTime2(t *testing.T) { testFormBindingForTime(t, "GET", - "/?time_foo=2017-11-15&time_bar=", "/?bar2=foo", + "/?time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "/?bar2=foo", "", "") + testFormBindingForTimeNotUnixFormat(t, "POST", + "/", "/", + "time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo") testFormBindingForTimeNotFormat(t, "GET", "/?time_foo=2017-11-15", "/?bar2=foo", "", "") @@ -849,6 +862,8 @@ func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody s assert.Equal(t, "Asia/Chongqing", obj.TimeFoo.Location().String()) assert.Equal(t, int64(-62135596800), obj.TimeBar.Unix()) assert.Equal(t, "UTC", obj.TimeBar.Location().String()) + assert.Equal(t, int64(1562400033000000123), obj.CreateTime.UnixNano()) + assert.Equal(t, int64(1562400033), obj.UnixTime.Unix()) obj = FooBarStructForTimeType{} req = requestWithBody(method, badPath, badBody) @@ -856,6 +871,24 @@ func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody s assert.Error(t, err) } +func testFormBindingForTimeNotUnixFormat(t *testing.T, method, path, badPath, body, badBody string) { + b := Form + assert.Equal(t, "form", b.Name()) + + obj := FooStructForTimeTypeNotUnixFormat{} + req := requestWithBody(method, path, body) + if method == "POST" { + req.Header.Add("Content-Type", MIMEPOSTForm) + } + err := b.Bind(req, &obj) + assert.Error(t, err) + + obj = FooStructForTimeTypeNotUnixFormat{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) +} + func testFormBindingForTimeNotFormat(t *testing.T, method, path, badPath, body, badBody string) { b := Form assert.Equal(t, "form", b.Name()) diff --git a/binding/form_mapping.go b/binding/form_mapping.go index ebf3b199..80b1d15a 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -266,6 +266,24 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val timeFormat = time.RFC3339 } + switch tf := strings.ToLower(timeFormat); tf { + case "unix", "unixnano": + tv, err := strconv.ParseInt(val, 10, 0) + if err != nil { + return err + } + + d := time.Duration(1) + if tf == "unixnano" { + d = time.Second + } + + t := time.Unix(tv/int64(d), tv%int64(d)) + value.Set(reflect.ValueOf(t)) + return nil + + } + if val == "" { value.Set(reflect.ValueOf(time.Time{})) return nil