diff --git a/.travis.yml b/.travis.yml
index 2fd9c8a2..f6ec8a82 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,8 +3,6 @@ language: go
matrix:
fast_finish: true
include:
- - go: 1.6.x
- - go: 1.7.x
- go: 1.8.x
- go: 1.9.x
- go: 1.10.x
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e6a108ca..8ea2495d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,60 @@
-# CHANGELOG
+
+### 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)
+- [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)
+- [NEW] Add support for mapping arrays [#1797](https://github.com/gin-gonic/gin/pull/1797)
+- [FIX] Readme updates [#1793](https://github.com/gin-gonic/gin/pull/1793) [#1788](https://github.com/gin-gonic/gin/pull/1788) [1789](https://github.com/gin-gonic/gin/pull/1789)
+- [FIX] StaticFS: Fixed Logging two log lines on 404. [#1805](https://github.com/gin-gonic/gin/pull/1805), [#1804](https://github.com/gin-gonic/gin/pull/1804)
+- [NEW] Make context.Keys available as LogFormatterParams [#1779](https://github.com/gin-gonic/gin/pull/1779)
+- [NEW] Use internal/json for Marshal/Unmarshal [#1791](https://github.com/gin-gonic/gin/pull/1791)
+- [NEW] Support mapping time.Duration [#1794](https://github.com/gin-gonic/gin/pull/1794)
+- [NEW] Refactor form mappings [#1749](https://github.com/gin-gonic/gin/pull/1749)
+- [NEW] Added flag to context.Stream indicates if client disconnected in middle of stream [#1252](https://github.com/gin-gonic/gin/pull/1252)
+- [FIX] Moved [examples](https://github.com/gin-gonic/examples) to stand alone Repo [#1775](https://github.com/gin-gonic/gin/pull/1775)
+- [NEW] Extend context.File to allow for the content-dispositon attachments via a new method context.Attachment [#1260](https://github.com/gin-gonic/gin/pull/1260)
+- [FIX] Support HTTP content negotiation wildcards [#1112](https://github.com/gin-gonic/gin/pull/1112)
+- [NEW] Add prefix from X-Forwarded-Prefix in redirectTrailingSlash [#1238](https://github.com/gin-gonic/gin/pull/1238)
+- [FIX] context.Copy() race condition [#1020](https://github.com/gin-gonic/gin/pull/1020)
+- [NEW] Add context.HandlerNames() [#1729](https://github.com/gin-gonic/gin/pull/1729)
+- [FIX] Change color methods to public in the defaultLogger. [#1771](https://github.com/gin-gonic/gin/pull/1771)
+- [FIX] Update writeHeaders method to use http.Header.Set [#1722](https://github.com/gin-gonic/gin/pull/1722)
+- [NEW] Add response size to LogFormatterParams [#1752](https://github.com/gin-gonic/gin/pull/1752)
+- [NEW] Allow ignoring field on form mapping [#1733](https://github.com/gin-gonic/gin/pull/1733)
+- [NEW] Add a function to force color in console output. [#1724](https://github.com/gin-gonic/gin/pull/1724)
+- [FIX] Context.Next() - recheck len of handlers on every iteration. [#1745](https://github.com/gin-gonic/gin/pull/1745)
+- [FIX] Fix all errcheck warnings [#1739](https://github.com/gin-gonic/gin/pull/1739) [#1653](https://github.com/gin-gonic/gin/pull/1653)
+- [NEW] context: inherits context cancellation and deadline from http.Request context for Go>=1.7 [#1690](https://github.com/gin-gonic/gin/pull/1690)
+- [NEW] Binding for URL Params [#1694](https://github.com/gin-gonic/gin/pull/1694)
+- [NEW] Add LoggerWithFormatter method [#1677](https://github.com/gin-gonic/gin/pull/1677)
+- [FIX] CI testing updates [#1671](https://github.com/gin-gonic/gin/pull/1671) [#1670](https://github.com/gin-gonic/gin/pull/1670) [#1682](https://github.com/gin-gonic/gin/pull/1682) [#1669](https://github.com/gin-gonic/gin/pull/1669)
+- [FIX] StaticFS(): Send 404 when path does not exist [#1663](https://github.com/gin-gonic/gin/pull/1663)
+- [FIX] Handle nil body for JSON binding [#1638](https://github.com/gin-gonic/gin/pull/1638)
+- [FIX] Support bind uri param [#1612](https://github.com/gin-gonic/gin/pull/1612)
+- [FIX] recovery: fix issue with syscall import on google app engine [#1640](https://github.com/gin-gonic/gin/pull/1640)
+- [FIX] Make sure the debug log contains line breaks [#1650](https://github.com/gin-gonic/gin/pull/1650)
+- [FIX] Panic stack trace being printed during recovery of broken pipe [#1089](https://github.com/gin-gonic/gin/pull/1089) [#1259](https://github.com/gin-gonic/gin/pull/1259)
+- [NEW] RunFd method to run http.Server through a file descriptor [#1609](https://github.com/gin-gonic/gin/pull/1609)
+- [NEW] Yaml binding support [#1618](https://github.com/gin-gonic/gin/pull/1618)
+- [FIX] Pass MaxMultipartMemory when FormFile is called [#1600](https://github.com/gin-gonic/gin/pull/1600)
+- [FIX] LoadHTML* tests [#1559](https://github.com/gin-gonic/gin/pull/1559)
+- [FIX] Removed use of sync.pool from HandleContext [#1565](https://github.com/gin-gonic/gin/pull/1565)
+- [FIX] Format output log to os.Stderr [#1571](https://github.com/gin-gonic/gin/pull/1571)
+- [FIX] Make logger use a yellow background and a darkgray text for legibility [#1570](https://github.com/gin-gonic/gin/pull/1570)
+- [FIX] Remove sensitive request information from panic log. [#1370](https://github.com/gin-gonic/gin/pull/1370)
+- [FIX] log.Println() does not print timestamp [#829](https://github.com/gin-gonic/gin/pull/829) [#1560](https://github.com/gin-gonic/gin/pull/1560)
+- [NEW] Add PureJSON renderer [#694](https://github.com/gin-gonic/gin/pull/694)
+- [FIX] Add missing copyright and update if/else [#1497](https://github.com/gin-gonic/gin/pull/1497)
+- [FIX] Update msgpack usage [#1498](https://github.com/gin-gonic/gin/pull/1498)
+- [FIX] Use protobuf on render [#1496](https://github.com/gin-gonic/gin/pull/1496)
+- [FIX] Add support for Protobuf format response [#1479](https://github.com/gin-gonic/gin/pull/1479)
+- [NEW] Set default time format in form binding [#1487](https://github.com/gin-gonic/gin/pull/1487)
+- [FIX] Add BindXML and ShouldBindXML [#1485](https://github.com/gin-gonic/gin/pull/1485)
+- [NEW] Upgrade dependency libraries [#1491](https://github.com/gin-gonic/gin/pull/1491)
+
### Gin 1.3.0
diff --git a/README.md b/README.md
index d3433ed2..2737e9ad 100644
--- a/README.md
+++ b/README.md
@@ -13,35 +13,118 @@
Gin is a web framework written in Go (Golang). It features a martini-like API with much better performance, up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin.
-**The key features of Gin are:**
-- Zero allocation router
-- Fast
-- Middleware support
-- Crash-free
-- JSON validation
-- Routes grouping
-- Error management
-- Rendering built-in
-- Extendable
+## Contents
-For more feature details, please see the [Gin website introduction](https://gin-gonic.com/docs/introduction/).
+- [Installation](#installation)
+- [Prerequisite](#prerequisite)
+- [Quick start](#quick-start)
+- [Benchmarks](#benchmarks)
+- [Gin v1.stable](#gin-v1-stable)
+- [Build with jsoniter](#build-with-jsoniter)
+- [API Examples](#api-examples)
+ - [Using GET,POST,PUT,PATCH,DELETE and OPTIONS](#using-get-post-put-patch-delete-and-options)
+ - [Parameters in path](#parameters-in-path)
+ - [Querystring parameters](#querystring-parameters)
+ - [Multipart/Urlencoded Form](#multiparturlencoded-form)
+ - [Another example: query + post form](#another-example-query--post-form)
+ - [Map as querystring or postform parameters](#map-as-querystring-or-postform-parameters)
+ - [Upload files](#upload-files)
+ - [Grouping routes](#grouping-routes)
+ - [Blank Gin without middleware by default](#blank-gin-without-middleware-by-default)
+ - [Using middleware](#using-middleware)
+ - [How to write log file](#how-to-write-log-file)
+ - [Custom Log Format](#custom-log-format)
+ - [Model binding and validation](#model-binding-and-validation)
+ - [Custom Validators](#custom-validators)
+ - [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 HTML checkboxes](#bind-html-checkboxes)
+ - [Multipart/Urlencoded binding](#multiparturlencoded-binding)
+ - [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering)
+ - [JSONP rendering](#jsonp)
+ - [Serving static files](#serving-static-files)
+ - [Serving data from reader](#serving-data-from-reader)
+ - [HTML rendering](#html-rendering)
+ - [Multitemplate](#multitemplate)
+ - [Redirects](#redirects)
+ - [Custom Middleware](#custom-middleware)
+ - [Using BasicAuth() middleware](#using-basicauth-middleware)
+ - [Goroutines inside a middleware](#goroutines-inside-a-middleware)
+ - [Custom HTTP configuration](#custom-http-configuration)
+ - [Support Let's Encrypt](#support-lets-encrypt)
+ - [Run multiple service using Gin](#run-multiple-service-using-gin)
+ - [Graceful restart or stop](#graceful-restart-or-stop)
+ - [Build a single binary with templates](#build-a-single-binary-with-templates)
+ - [Bind form-data request with custom struct](#bind-form-data-request-with-custom-struct)
+ - [Try to bind body into different structs](#try-to-bind-body-into-different-structs)
+ - [http2 server push](#http2-server-push)
+ - [Define format for the log of routes](#define-format-for-the-log-of-routes)
+ - [Set and get a cookie](#set-and-get-a-cookie)
+- [Testing](#testing)
+- [Users](#users)
-## Getting started
+## Installation
-### Getting Gin
+To install Gin package, you need to install Go and set your Go workspace first.
-The first need [Go](https://golang.org/) installed (**version 1.6+ is required**), then you can use the below Go command to install Gin.
+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.
```sh
$ go get -u github.com/gin-gonic/gin
```
-For more installation guides such as vendor tool, please check out [Gin quickstart](https://gin-gonic.com/docs/quickstart/).
+2. Import it in your code:
-### Running Gin
+```go
+import "github.com/gin-gonic/gin"
+```
-First you need to import Gin package for using Gin, one simplest example likes the follow `example.go`:
+3. (Optional) Import `net/http`. This is required for example if using constants such as `http.StatusOK`.
+
+```go
+import "net/http"
+```
+
+### Use a vendor tool like [Govendor](https://github.com/kardianos/govendor)
+
+1. `go get` govendor
+
+```sh
+$ go get github.com/kardianos/govendor
+```
+2. Create your project folder and `cd` inside
+
+```sh
+$ mkdir -p $GOPATH/src/github.com/myusername/project && cd "$_"
+```
+
+3. Vendor init your project and add gin
+
+```sh
+$ govendor init
+$ govendor fetch github.com/gin-gonic/gin@v1.3
+```
+
+4. Copy a starting template inside your project
+
+```sh
+$ curl https://raw.githubusercontent.com/gin-gonic/examples/master/basic/main.go > main.go
+```
+
+5. Run your project
+
+```sh
+$ go run main.go
+```
+
+## Quick start
+
+```sh
+# assume the following codes in example.go file
+$ cat example.go
+```
```go
package main
@@ -59,8 +142,6 @@ func main() {
}
```
-And use the Go command to run the demo:
-
```
# run example.go and visit 0.0.0.0:8080/ping on browser
$ go run example.go
@@ -68,7 +149,9 @@ $ go run example.go
## Benchmarks
-Please see all benchmarks details from [Gin website](https://gin-gonic.com/docs/benchmarks/).
+Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httprouter)
+
+[See all benchmarks](/BENCHMARKS.md)
Benchmark name | (1) | (2) | (3) | (4)
--------------------------------------------|-----------:|------------:|-----------:|---------:
@@ -105,32 +188,1879 @@ BenchmarkVulcan_GithubAll | 5000 | 394253 | 19894
- (3): Heap Memory (B/op), lower is better
- (4): Average Allocations per Repetition (allocs/op), lower is better
-## Middlewares
+## Gin v1. stable
-You can find many useful Gin middlewares at [gin-contrib](https://github.com/gin-contrib).
+- [x] Zero allocation router.
+- [x] Still the fastest http router and framework. From routing to writing.
+- [x] Complete suite of unit tests
+- [x] Battle tested
+- [x] API frozen, new releases will not break your code.
-## Documentation
+## Build with [jsoniter](https://github.com/json-iterator/go)
-See [API documentation and descriptions](https://godoc.org/github.com/gin-gonic/gin) for package.
+Gin uses `encoding/json` as default json package but you can change to [jsoniter](https://github.com/json-iterator/go) by build from other tags.
-All documentation is available on the Gin website.
+```sh
+$ go build -tags=jsoniter .
+```
-- [English](https://gin-gonic.com/docs/)
-- [简体中文](https://gin-gonic.com/zh-cn/docs/)
-- [繁體中文](https://gin-gonic.com/zh-tw/docs/)
-- [日本語](https://gin-gonic.com/ja/docs/)
+## API Examples
-## Examples
+You can find a number of ready-to-run examples at [Gin examples repository](https://github.com/gin-gonic/examples).
-A number of ready-to-run examples demonstrating various use cases of Gin on the [Gin examples](https://github.com/gin-gonic/examples) repository.
+### Using GET, POST, PUT, PATCH, DELETE and OPTIONS
+
+```go
+func main() {
+ // Creates a gin router with default middleware:
+ // logger and recovery (crash-free) middleware
+ router := gin.Default()
+
+ router.GET("/someGet", getting)
+ router.POST("/somePost", posting)
+ router.PUT("/somePut", putting)
+ router.DELETE("/someDelete", deleting)
+ router.PATCH("/somePatch", patching)
+ router.HEAD("/someHead", head)
+ router.OPTIONS("/someOptions", options)
+
+ // By default it serves on :8080 unless a
+ // PORT environment variable was defined.
+ router.Run()
+ // router.Run(":3000") for a hard coded port
+}
+```
+
+### Parameters in path
+
+```go
+func main() {
+ router := gin.Default()
+
+ // This handler will match /user/john but will not match /user/ or /user
+ router.GET("/user/:name", func(c *gin.Context) {
+ name := c.Param("name")
+ c.String(http.StatusOK, "Hello %s", name)
+ })
+
+ // However, this one will match /user/john/ and also /user/john/send
+ // If no other routers match /user/john, it will redirect to /user/john/
+ router.GET("/user/:name/*action", func(c *gin.Context) {
+ name := c.Param("name")
+ action := c.Param("action")
+ message := name + " is " + action
+ c.String(http.StatusOK, message)
+ })
+
+ router.Run(":8080")
+}
+```
+
+### Querystring parameters
+
+```go
+func main() {
+ router := gin.Default()
+
+ // Query string parameters are parsed using the existing underlying request object.
+ // The request responds to a url matching: /welcome?firstname=Jane&lastname=Doe
+ router.GET("/welcome", func(c *gin.Context) {
+ firstname := c.DefaultQuery("firstname", "Guest")
+ lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname")
+
+ c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
+ })
+ router.Run(":8080")
+}
+```
+
+### Multipart/Urlencoded Form
+
+```go
+func main() {
+ router := gin.Default()
+
+ router.POST("/form_post", func(c *gin.Context) {
+ message := c.PostForm("message")
+ nick := c.DefaultPostForm("nick", "anonymous")
+
+ c.JSON(200, gin.H{
+ "status": "posted",
+ "message": message,
+ "nick": nick,
+ })
+ })
+ router.Run(":8080")
+}
+```
+
+### Another example: query + post form
+
+```
+POST /post?id=1234&page=1 HTTP/1.1
+Content-Type: application/x-www-form-urlencoded
+
+name=manu&message=this_is_great
+```
+
+```go
+func main() {
+ router := gin.Default()
+
+ router.POST("/post", func(c *gin.Context) {
+
+ id := c.Query("id")
+ page := c.DefaultQuery("page", "0")
+ name := c.PostForm("name")
+ message := c.PostForm("message")
+
+ fmt.Printf("id: %s; page: %s; name: %s; message: %s", id, page, name, message)
+ })
+ router.Run(":8080")
+}
+```
+
+```
+id: 1234; page: 1; name: manu; message: this_is_great
+```
+
+### Map as querystring or postform parameters
+
+```
+POST /post?ids[a]=1234&ids[b]=hello HTTP/1.1
+Content-Type: application/x-www-form-urlencoded
+
+names[first]=thinkerou&names[second]=tianou
+```
+
+```go
+func main() {
+ router := gin.Default()
+
+ router.POST("/post", func(c *gin.Context) {
+
+ ids := c.QueryMap("ids")
+ names := c.PostFormMap("names")
+
+ fmt.Printf("ids: %v; names: %v", ids, names)
+ })
+ router.Run(":8080")
+}
+```
+
+```
+ids: map[b:hello a:1234], names: map[second:tianou first:thinkerou]
+```
+
+### Upload files
+
+#### Single file
+
+References issue [#774](https://github.com/gin-gonic/gin/issues/774) and detail [example code](https://github.com/gin-gonic/examples/tree/master/upload-file/single).
+
+`file.Filename` **SHOULD NOT** be trusted. See [`Content-Disposition` on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#Directives) and [#1693](https://github.com/gin-gonic/gin/issues/1693)
+
+> The filename is always optional and must not be used blindly by the application: path information should be stripped, and conversion to the server file system rules should be done.
+
+```go
+func main() {
+ router := gin.Default()
+ // Set a lower memory limit for multipart forms (default is 32 MiB)
+ // router.MaxMultipartMemory = 8 << 20 // 8 MiB
+ router.POST("/upload", func(c *gin.Context) {
+ // single file
+ file, _ := c.FormFile("file")
+ log.Println(file.Filename)
+
+ // Upload the file to specific dst.
+ // c.SaveUploadedFile(file, dst)
+
+ c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
+ })
+ router.Run(":8080")
+}
+```
+
+How to `curl`:
+
+```bash
+curl -X POST http://localhost:8080/upload \
+ -F "file=@/Users/appleboy/test.zip" \
+ -H "Content-Type: multipart/form-data"
+```
+
+#### Multiple files
+
+See the detail [example code](https://github.com/gin-gonic/examples/tree/master/upload-file/multiple).
+
+```go
+func main() {
+ router := gin.Default()
+ // Set a lower memory limit for multipart forms (default is 32 MiB)
+ // router.MaxMultipartMemory = 8 << 20 // 8 MiB
+ router.POST("/upload", func(c *gin.Context) {
+ // Multipart form
+ form, _ := c.MultipartForm()
+ files := form.File["upload[]"]
+
+ for _, file := range files {
+ log.Println(file.Filename)
+
+ // Upload the file to specific dst.
+ // c.SaveUploadedFile(file, dst)
+ }
+ c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files)))
+ })
+ router.Run(":8080")
+}
+```
+
+How to `curl`:
+
+```bash
+curl -X POST http://localhost:8080/upload \
+ -F "upload[]=@/Users/appleboy/test1.zip" \
+ -F "upload[]=@/Users/appleboy/test2.zip" \
+ -H "Content-Type: multipart/form-data"
+```
+
+### Grouping routes
+
+```go
+func main() {
+ router := gin.Default()
+
+ // Simple group: v1
+ v1 := router.Group("/v1")
+ {
+ v1.POST("/login", loginEndpoint)
+ v1.POST("/submit", submitEndpoint)
+ v1.POST("/read", readEndpoint)
+ }
+
+ // Simple group: v2
+ v2 := router.Group("/v2")
+ {
+ v2.POST("/login", loginEndpoint)
+ v2.POST("/submit", submitEndpoint)
+ v2.POST("/read", readEndpoint)
+ }
+
+ router.Run(":8080")
+}
+```
+
+### Blank Gin without middleware by default
+
+Use
+
+```go
+r := gin.New()
+```
+
+instead of
+
+```go
+// Default With the Logger and Recovery middleware already attached
+r := gin.Default()
+```
+
+
+### Using middleware
+```go
+func main() {
+ // Creates a router without any middleware by default
+ r := gin.New()
+
+ // Global middleware
+ // Logger middleware will write the logs to gin.DefaultWriter even if you set with GIN_MODE=release.
+ // By default gin.DefaultWriter = os.Stdout
+ r.Use(gin.Logger())
+
+ // Recovery middleware recovers from any panics and writes a 500 if there was one.
+ r.Use(gin.Recovery())
+
+ // Per route middleware, you can add as many as you desire.
+ r.GET("/benchmark", MyBenchLogger(), benchEndpoint)
+
+ // Authorization group
+ // authorized := r.Group("/", AuthRequired())
+ // exactly the same as:
+ authorized := r.Group("/")
+ // per group middleware! 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 serve on 0.0.0.0:8080
+ r.Run(":8080")
+}
+```
+
+### How to write log file
+```go
+func main() {
+ // Disable Console Color, you don't need console color when writing the logs to file.
+ gin.DisableConsoleColor()
+
+ // Logging to a file.
+ f, _ := os.Create("gin.log")
+ gin.DefaultWriter = io.MultiWriter(f)
+
+ // Use the following code if you need to write the logs to file and console at the same time.
+ // gin.DefaultWriter = io.MultiWriter(f, os.Stdout)
+
+ router := gin.Default()
+ router.GET("/ping", func(c *gin.Context) {
+ c.String(200, "pong")
+ })
+
+ router.Run(":8080")
+}
+```
+
+### Custom Log Format
+```go
+func main() {
+ router := gin.New()
+
+ // LoggerWithFormatter middleware will write the logs to gin.DefaultWriter
+ // By default gin.DefaultWriter = os.Stdout
+ router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
+
+ // your custom format
+ return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
+ param.ClientIP,
+ param.TimeStamp.Format(time.RFC1123),
+ param.Method,
+ param.Path,
+ param.Request.Proto,
+ param.StatusCode,
+ param.Latency,
+ param.Request.UserAgent(),
+ param.ErrorMessage,
+ )
+ }))
+ router.Use(gin.Recovery())
+
+ router.GET("/ping", func(c *gin.Context) {
+ c.String(200, "pong")
+ })
+
+ router.Run(":8080")
+}
+```
+
+**Sample Output**
+```
+::1 - [Fri, 07 Dec 2018 17:04:38 JST] "GET /ping HTTP/1.1 200 122.767µs "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36" "
+```
+
+### Controlling Log output coloring
+
+By default, logs output on console should be colorized depending on the detected TTY.
+
+Never colorize logs:
+
+```go
+func main() {
+ // Disable log's color
+ gin.DisableConsoleColor()
+
+ // Creates a gin router with default middleware:
+ // logger and recovery (crash-free) middleware
+ router := gin.Default()
+
+ router.GET("/ping", func(c *gin.Context) {
+ c.String(200, "pong")
+ })
+
+ router.Run(":8080")
+}
+```
+
+Always colorize logs:
+
+```go
+func main() {
+ // Force log's color
+ gin.ForceConsoleColor()
+
+ // Creates a gin router with default middleware:
+ // logger and recovery (crash-free) middleware
+ router := gin.Default()
+
+ router.GET("/ping", func(c *gin.Context) {
+ c.String(200, "pong")
+ })
+
+ router.Run(":8080")
+}
+```
+
+### Model binding and validation
+
+To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML and standard form values (foo=bar&boo=baz).
+
+Gin uses [**go-playground/validator.v8**](https://github.com/go-playground/validator) for validation. Check the full docs on tags usage [here](http://godoc.org/gopkg.in/go-playground/validator.v8#hdr-Baked_In_Validators_and_Tags).
+
+Note that you need to set the corresponding binding tag on all fields you want to bind. For example, when binding from JSON, set `json:"fieldname"`.
+
+Also, Gin provides two sets of methods for binding:
+- **Type** - Must bind
+ - **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`, `BindYAML`
+ - **Behavior** - These methods use `MustBindWith` under the hood. If there is a binding error, the request is aborted with `c.AbortWithError(400, err).SetType(ErrorTypeBind)`. This sets the response status code to 400 and the `Content-Type` header is set to `text/plain; charset=utf-8`. Note that if you try to set the response code after this, it will result in a warning `[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422`. If you wish to have greater control over the behavior, consider using the `ShouldBind` equivalent method.
+- **Type** - Should bind
+ - **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindXML`, `ShouldBindQuery`, `ShouldBindYAML`
+ - **Behavior** - These methods use `ShouldBindWith` under the hood. If there is a binding error, the error is returned and it is the developer's responsibility to handle the request and error appropriately.
+
+When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use `MustBindWith` or `ShouldBindWith`.
+
+You can also specify that specific fields are required. If a field is decorated with `binding:"required"` and has a empty value when binding, an error will be returned.
+
+```go
+// Binding from JSON
+type Login struct {
+ User string `form:"user" json:"user" xml:"user" binding:"required"`
+ Password string `form:"password" json:"password" xml:"password" binding:"required"`
+}
+
+func main() {
+ router := gin.Default()
+
+ // Example for binding JSON ({"user": "manu", "password": "123"})
+ router.POST("/loginJSON", func(c *gin.Context) {
+ var json Login
+ if err := c.ShouldBindJSON(&json); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ if json.User != "manu" || json.Password != "123" {
+ c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
+ })
+
+ // Example for binding XML (
+ //
+ //
+ // user
+ // 123
+ // )
+ router.POST("/loginXML", func(c *gin.Context) {
+ var xml Login
+ if err := c.ShouldBindXML(&xml); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ if xml.User != "manu" || xml.Password != "123" {
+ c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
+ })
+
+ // Example for binding a HTML form (user=manu&password=123)
+ router.POST("/loginForm", func(c *gin.Context) {
+ var form Login
+ // This will infer what binder to use depending on the content-type header.
+ if err := c.ShouldBind(&form); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ if form.User != "manu" || form.Password != "123" {
+ c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
+ })
+
+ // Listen and serve on 0.0.0.0:8080
+ router.Run(":8080")
+}
+```
+
+**Sample request**
+```shell
+$ curl -v -X POST \
+ http://localhost:8080/loginJSON \
+ -H 'content-type: application/json' \
+ -d '{ "user": "manu" }'
+> POST /loginJSON HTTP/1.1
+> Host: localhost:8080
+> User-Agent: curl/7.51.0
+> Accept: */*
+> content-type: application/json
+> Content-Length: 18
+>
+* upload completely sent off: 18 out of 18 bytes
+< HTTP/1.1 400 Bad Request
+< Content-Type: application/json; charset=utf-8
+< Date: Fri, 04 Aug 2017 03:51:31 GMT
+< Content-Length: 100
+<
+{"error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'required' tag"}
+```
+
+**Skip validate**
+
+When running the above example using the above the `curl` command, it returns error. Because the example use `binding:"required"` for `Password`. If use `binding:"-"` for `Password`, then it will not return error when running the above example again.
+
+### Custom Validators
+
+It is also possible to register custom validators. See the [example code](https://github.com/gin-gonic/examples/tree/master/custom-validation/server.go).
+
+```go
+package main
+
+import (
+ "net/http"
+ "reflect"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ "github.com/gin-gonic/gin/binding"
+ "gopkg.in/go-playground/validator.v8"
+)
+
+// Booking contains binded and validated data.
+type Booking struct {
+ CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
+ CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
+}
+
+func bookableDate(
+ v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
+ field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
+) bool {
+ if date, ok := field.Interface().(time.Time); ok {
+ today := time.Now()
+ if today.Year() > date.Year() || today.YearDay() > date.YearDay() {
+ return false
+ }
+ }
+ return true
+}
+
+func main() {
+ route := gin.Default()
+
+ if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
+ v.RegisterValidation("bookabledate", bookableDate)
+ }
+
+ route.GET("/bookable", getBookable)
+ route.Run(":8085")
+}
+
+func getBookable(c *gin.Context) {
+ var b Booking
+ if err := c.ShouldBindWith(&b, binding.Query); err == nil {
+ c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
+ } else {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ }
+}
+```
+
+```console
+$ curl "localhost:8085/bookable?check_in=2018-04-16&check_out=2018-04-17"
+{"message":"Booking dates are valid!"}
+
+$ curl "localhost:8085/bookable?check_in=2018-03-08&check_out=2018-03-09"
+{"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag"}
+```
+
+[Struct level validations](https://github.com/go-playground/validator/releases/tag/v8.7) can also be registered this way.
+See the [struct-lvl-validation example](https://github.com/gin-gonic/examples/tree/master/struct-lvl-validations) to learn more.
+
+### Only Bind Query String
+
+`ShouldBindQuery` function only binds the query params and not the post data. See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-315953017).
+
+```go
+package main
+
+import (
+ "log"
+
+ "github.com/gin-gonic/gin"
+)
+
+type Person struct {
+ Name string `form:"name"`
+ Address string `form:"address"`
+}
+
+func main() {
+ route := gin.Default()
+ route.Any("/testing", startPage)
+ route.Run(":8085")
+}
+
+func startPage(c *gin.Context) {
+ var person Person
+ if c.ShouldBindQuery(&person) == nil {
+ log.Println("====== Only Bind By Query String ======")
+ log.Println(person.Name)
+ log.Println(person.Address)
+ }
+ c.String(200, "Success")
+}
+
+```
+
+### Bind Query String or Post Data
+
+See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-264681292).
+
+```go
+package main
+
+import (
+ "log"
+ "time"
+
+ "github.com/gin-gonic/gin"
+)
+
+type Person struct {
+ Name string `form:"name"`
+ Address string `form:"address"`
+ Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
+}
+
+func main() {
+ route := gin.Default()
+ route.GET("/testing", startPage)
+ route.Run(":8085")
+}
+
+func startPage(c *gin.Context) {
+ var person Person
+ // 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)
+ }
+
+ c.String(200, "Success")
+}
+```
+
+Test it with:
+```sh
+$ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15"
+```
+
+### Bind Uri
+
+See the [detail information](https://github.com/gin-gonic/gin/issues/846).
+
+```go
+package main
+
+import "github.com/gin-gonic/gin"
+
+type Person struct {
+ ID string `uri:"id" binding:"required,uuid"`
+ Name string `uri:"name" binding:"required"`
+}
+
+func main() {
+ route := gin.Default()
+ route.GET("/:name/:id", func(c *gin.Context) {
+ var person Person
+ if err := c.ShouldBindUri(&person); err != nil {
+ c.JSON(400, gin.H{"msg": err})
+ return
+ }
+ c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID})
+ })
+ route.Run(":8088")
+}
+```
+
+Test it with:
+```sh
+$ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3
+$ curl -v localhost:8088/thinkerou/not-uuid
+```
+
+### Bind HTML checkboxes
+
+See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092)
+
+main.go
+
+```go
+...
+
+type myForm struct {
+ Colors []string `form:"colors[]"`
+}
+
+...
+
+func formHandler(c *gin.Context) {
+ var fakeForm myForm
+ c.ShouldBind(&fakeForm)
+ c.JSON(200, gin.H{"color": fakeForm.Colors})
+}
+
+...
+
+```
+
+form.html
+
+```html
+
+```
+
+result:
+
+```
+{"color":["red","green","blue"]}
+```
+
+### Multipart/Urlencoded binding
+
+```go
+package main
+
+import (
+ "github.com/gin-gonic/gin"
+)
+
+type LoginForm struct {
+ User string `form:"user" binding:"required"`
+ Password string `form:"password" binding:"required"`
+}
+
+func main() {
+ router := gin.Default()
+ router.POST("/login", 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
+ // 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"})
+ }
+ }
+ })
+ router.Run(":8080")
+}
+```
+
+Test it with:
+```sh
+$ curl -v --form user=user --form password=password http://localhost:8080/login
+```
+
+### XML, JSON, YAML and ProtoBuf rendering
+
+```go
+func main() {
+ r := gin.Default()
+
+ // gin.H is a shortcut for map[string]interface{}
+ r.GET("/someJSON", func(c *gin.Context) {
+ c.JSON(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
+ })
+
+ 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(http.StatusOK, msg)
+ })
+
+ r.GET("/someXML", func(c *gin.Context) {
+ c.XML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
+ })
+
+ r.GET("/someYAML", func(c *gin.Context) {
+ c.YAML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
+ })
+
+ r.GET("/someProtoBuf", func(c *gin.Context) {
+ reps := []int64{int64(1), int64(2)}
+ label := "test"
+ // The specific definition of protobuf is written in the testdata/protoexample file.
+ data := &protoexample.Test{
+ Label: &label,
+ Reps: reps,
+ }
+ // Note that data becomes binary data in the response
+ // Will output protoexample.Test protobuf serialized data
+ c.ProtoBuf(http.StatusOK, data)
+ })
+
+ // Listen and serve on 0.0.0.0:8080
+ r.Run(":8080")
+}
+```
+
+#### SecureJSON
+
+Using SecureJSON to prevent json hijacking. Default prepends `"while(1),"` to response body if the given struct is array values.
+
+```go
+func main() {
+ r := gin.Default()
+
+ // You can also use your own secure json prefix
+ // r.SecureJsonPrefix(")]}',\n")
+
+ r.GET("/someJSON", func(c *gin.Context) {
+ names := []string{"lena", "austin", "foo"}
+
+ // Will output : while(1);["lena","austin","foo"]
+ c.SecureJSON(http.StatusOK, names)
+ })
+
+ // Listen and serve on 0.0.0.0:8080
+ r.Run(":8080")
+}
+```
+#### JSONP
+
+Using JSONP to request data from a server in a different domain. Add callback to response body if the query parameter callback exists.
+
+```go
+func main() {
+ r := gin.Default()
+
+ r.GET("/JSONP?callback=x", func(c *gin.Context) {
+ data := map[string]interface{}{
+ "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")
+}
+```
+
+#### AsciiJSON
+
+Using AsciiJSON to Generates ASCII-only JSON with escaped non-ASCII chracters.
+
+```go
+func main() {
+ r := gin.Default()
+
+ r.GET("/someJSON", func(c *gin.Context) {
+ data := map[string]interface{}{
+ "lang": "GO语言",
+ "tag": "
",
+ }
+
+ // will output : {"lang":"GO\u8bed\u8a00","tag":"\u003cbr\u003e"}
+ c.AsciiJSON(http.StatusOK, data)
+ })
+
+ // Listen and serve on 0.0.0.0:8080
+ r.Run(":8080")
+}
+```
+
+#### PureJSON
+
+Normally, JSON replaces special HTML characters with their unicode entities, e.g. `<` becomes `\u003c`. If you want to encode such characters literally, you can use PureJSON instead.
+This feature is unavailable in Go 1.6 and lower.
+
+```go
+func main() {
+ r := gin.Default()
+
+ // Serves unicode entities
+ r.GET("/json", func(c *gin.Context) {
+ c.JSON(200, gin.H{
+ "html": "Hello, world!",
+ })
+ })
+
+ // Serves literal characters
+ r.GET("/purejson", func(c *gin.Context) {
+ c.PureJSON(200, gin.H{
+ "html": "Hello, world!",
+ })
+ })
+
+ // listen and serve on 0.0.0.0:8080
+ r.Run(":8080")
+}
+```
+
+### Serving static files
+
+```go
+func main() {
+ router := gin.Default()
+ router.Static("/assets", "./assets")
+ router.StaticFS("/more_static", http.Dir("my_file_system"))
+ router.StaticFile("/favicon.ico", "./resources/favicon.ico")
+
+ // Listen and serve on 0.0.0.0:8080
+ router.Run(":8080")
+}
+```
+
+### Serving data from reader
+
+```go
+func main() {
+ router := gin.Default()
+ router.GET("/someDataFromReader", func(c *gin.Context) {
+ response, err := http.Get("https://raw.githubusercontent.com/gin-gonic/logo/master/color.png")
+ if err != nil || response.StatusCode != http.StatusOK {
+ c.Status(http.StatusServiceUnavailable)
+ return
+ }
+
+ reader := response.Body
+ contentLength := response.ContentLength
+ contentType := response.Header.Get("Content-Type")
+
+ extraHeaders := map[string]string{
+ "Content-Disposition": `attachment; filename="gopher.png"`,
+ }
+
+ c.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders)
+ })
+ router.Run(":8080")
+}
+```
+
+### HTML rendering
+
+Using LoadHTMLGlob() or LoadHTMLFiles()
+
+```go
+func main() {
+ router := gin.Default()
+ router.LoadHTMLGlob("templates/*")
+ //router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")
+ router.GET("/index", func(c *gin.Context) {
+ c.HTML(http.StatusOK, "index.tmpl", gin.H{
+ "title": "Main website",
+ })
+ })
+ router.Run(":8080")
+}
+```
+
+templates/index.tmpl
+
+```html
+
+
+ {{ .title }}
+
+
+```
+
+Using templates with same name in different directories
+
+```go
+func main() {
+ router := gin.Default()
+ router.LoadHTMLGlob("templates/**/*")
+ router.GET("/posts/index", func(c *gin.Context) {
+ c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{
+ "title": "Posts",
+ })
+ })
+ router.GET("/users/index", func(c *gin.Context) {
+ c.HTML(http.StatusOK, "users/index.tmpl", gin.H{
+ "title": "Users",
+ })
+ })
+ router.Run(":8080")
+}
+```
+
+templates/posts/index.tmpl
+
+```html
+{{ define "posts/index.tmpl" }}
+
+ {{ .title }}
+
+Using posts/index.tmpl
+
+{{ end }}
+```
+
+templates/users/index.tmpl
+
+```html
+{{ define "users/index.tmpl" }}
+
+ {{ .title }}
+
+Using users/index.tmpl
+
+{{ end }}
+```
+
+#### Custom Template renderer
+
+You can also use your own html template render
+
+```go
+import "html/template"
+
+func main() {
+ router := gin.Default()
+ html := template.Must(template.ParseFiles("file1", "file2"))
+ router.SetHTMLTemplate(html)
+ router.Run(":8080")
+}
+```
+
+#### Custom Delimiters
+
+You may use custom delims
+
+```go
+ r := gin.Default()
+ r.Delims("{[{", "}]}")
+ r.LoadHTMLGlob("/path/to/templates")
+```
+
+#### Custom Template Funcs
+
+See the detail [example code](https://github.com/gin-gonic/examples/tree/master/template).
+
+main.go
+
+```go
+import (
+ "fmt"
+ "html/template"
+ "net/http"
+ "time"
+
+ "github.com/gin-gonic/gin"
+)
+
+func formatAsDate(t time.Time) string {
+ year, month, day := t.Date()
+ return fmt.Sprintf("%d%02d/%02d", year, month, day)
+}
+
+func main() {
+ router := gin.Default()
+ router.Delims("{[{", "}]}")
+ router.SetFuncMap(template.FuncMap{
+ "formatAsDate": formatAsDate,
+ })
+ router.LoadHTMLFiles("./testdata/template/raw.tmpl")
+
+ router.GET("/raw", func(c *gin.Context) {
+ c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{
+ "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
+ })
+ })
+
+ router.Run(":8080")
+}
+
+```
+
+raw.tmpl
+
+```html
+Date: {[{.now | formatAsDate}]}
+```
+
+Result:
+```
+Date: 2017/07/01
+```
+
+### Multitemplate
+
+Gin allow by default use only one html.Template. Check [a multitemplate render](https://github.com/gin-contrib/multitemplate) for using features like go 1.6 `block template`.
+
+### Redirects
+
+Issuing a HTTP redirect is easy. Both internal and external locations are supported.
+
+```go
+r.GET("/test", func(c *gin.Context) {
+ c.Redirect(http.StatusMovedPermanently, "http://www.google.com/")
+})
+```
+
+
+Issuing a Router redirect, use `HandleContext` like below.
+
+``` go
+r.GET("/test", func(c *gin.Context) {
+ c.Request.URL.Path = "/test2"
+ r.HandleContext(c)
+})
+r.GET("/test2", func(c *gin.Context) {
+ c.JSON(200, gin.H{"hello": "world"})
+})
+```
+
+
+### Custom Middleware
+
+```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)
+
+ // access the status we are sending
+ status := c.Writer.Status()
+ log.Println(status)
+ }
+}
+
+func main() {
+ r := gin.New()
+ r.Use(Logger())
+
+ r.GET("/test", func(c *gin.Context) {
+ example := c.MustGet("example").(string)
+
+ // it would print: "12345"
+ log.Println(example)
+ })
+
+ // Listen and serve on 0.0.0.0:8080
+ r.Run(":8080")
+}
+```
+
+### Using BasicAuth() middleware
+
+```go
+// simulate 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",
+ "austin": "1234",
+ "lena": "hello2",
+ "manu": "4321",
+ }))
+
+ // /admin/secrets endpoint
+ // hit "localhost:8080/admin/secrets
+ authorized.GET("/secrets", func(c *gin.Context) {
+ // get user, it was set by the BasicAuth middleware
+ user := c.MustGet(gin.AuthUserKey).(string)
+ if secret, ok := secrets[user]; ok {
+ c.JSON(http.StatusOK, gin.H{"user": user, "secret": secret})
+ } else {
+ c.JSON(http.StatusOK, gin.H{"user": user, "secret": "NO SECRET :("})
+ }
+ })
+
+ // Listen and serve on 0.0.0.0:8080
+ r.Run(":8080")
+}
+```
+
+### Goroutines inside a middleware
+
+When starting new Goroutines 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
+ cCp := c.Copy()
+ go func() {
+ // simulate a long task with time.Sleep(). 5 seconds
+ time.Sleep(5 * time.Second)
+
+ // note that you are using the copied context "cCp", IMPORTANT
+ log.Println("Done! in path " + cCp.Request.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.Request.URL.Path)
+ })
+
+ // Listen and serve on 0.0.0.0:8080
+ r.Run(":8080")
+}
+```
+
+### Custom HTTP configuration
+
+Use `http.ListenAndServe()` directly, like this:
+
+```go
+func main() {
+ router := gin.Default()
+ http.ListenAndServe(":8080", router)
+}
+```
+or
+
+```go
+func main() {
+ router := gin.Default()
+
+ s := &http.Server{
+ Addr: ":8080",
+ Handler: router,
+ ReadTimeout: 10 * time.Second,
+ WriteTimeout: 10 * time.Second,
+ MaxHeaderBytes: 1 << 20,
+ }
+ s.ListenAndServe()
+}
+```
+
+### Support Let's Encrypt
+
+example for 1-line LetsEncrypt HTTPS servers.
+
+```go
+package main
+
+import (
+ "log"
+
+ "github.com/gin-gonic/autotls"
+ "github.com/gin-gonic/gin"
+)
+
+func main() {
+ r := gin.Default()
+
+ // Ping handler
+ r.GET("/ping", func(c *gin.Context) {
+ c.String(200, "pong")
+ })
+
+ log.Fatal(autotls.Run(r, "example1.com", "example2.com"))
+}
+```
+
+example for custom autocert manager.
+
+```go
+package main
+
+import (
+ "log"
+
+ "github.com/gin-gonic/autotls"
+ "github.com/gin-gonic/gin"
+ "golang.org/x/crypto/acme/autocert"
+)
+
+func main() {
+ r := gin.Default()
+
+ // Ping handler
+ r.GET("/ping", func(c *gin.Context) {
+ c.String(200, "pong")
+ })
+
+ m := autocert.Manager{
+ Prompt: autocert.AcceptTOS,
+ HostPolicy: autocert.HostWhitelist("example1.com", "example2.com"),
+ Cache: autocert.DirCache("/var/www/.cache"),
+ }
+
+ log.Fatal(autotls.RunWithManager(r, &m))
+}
+```
+
+### Run multiple service using Gin
+
+See the [question](https://github.com/gin-gonic/gin/issues/346) and try the following example:
+
+```go
+package main
+
+import (
+ "log"
+ "net/http"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ "golang.org/x/sync/errgroup"
+)
+
+var (
+ g errgroup.Group
+)
+
+func router01() http.Handler {
+ e := gin.New()
+ e.Use(gin.Recovery())
+ e.GET("/", func(c *gin.Context) {
+ c.JSON(
+ http.StatusOK,
+ gin.H{
+ "code": http.StatusOK,
+ "error": "Welcome server 01",
+ },
+ )
+ })
+
+ return e
+}
+
+func router02() http.Handler {
+ e := gin.New()
+ e.Use(gin.Recovery())
+ e.GET("/", func(c *gin.Context) {
+ c.JSON(
+ http.StatusOK,
+ gin.H{
+ "code": http.StatusOK,
+ "error": "Welcome server 02",
+ },
+ )
+ })
+
+ return e
+}
+
+func main() {
+ server01 := &http.Server{
+ Addr: ":8080",
+ Handler: router01(),
+ ReadTimeout: 5 * time.Second,
+ WriteTimeout: 10 * time.Second,
+ }
+
+ server02 := &http.Server{
+ Addr: ":8081",
+ Handler: router02(),
+ ReadTimeout: 5 * time.Second,
+ WriteTimeout: 10 * time.Second,
+ }
+
+ g.Go(func() error {
+ return server01.ListenAndServe()
+ })
+
+ g.Go(func() error {
+ return server02.ListenAndServe()
+ })
+
+ if err := g.Wait(); err != nil {
+ log.Fatal(err)
+ }
+}
+```
+
+### Graceful restart or stop
+
+Do you want to graceful restart or stop your web server?
+There are some ways this can be done.
+
+We can use [fvbock/endless](https://github.com/fvbock/endless) to replace the default `ListenAndServe`. Refer issue [#296](https://github.com/gin-gonic/gin/issues/296) for more details.
+
+```go
+router := gin.Default()
+router.GET("/", handler)
+// [...]
+endless.ListenAndServe(":4242", router)
+```
+
+An alternative to endless:
+
+* [manners](https://github.com/braintree/manners): A polite Go HTTP server that shuts down gracefully.
+* [graceful](https://github.com/tylerb/graceful): Graceful is a Go package enabling graceful shutdown of an http.Handler server.
+* [grace](https://github.com/facebookgo/grace): Graceful restart & zero downtime deploy for Go servers.
+
+If you are using Go 1.8, you may not need to use this library! Consider using http.Server's built-in [Shutdown()](https://golang.org/pkg/net/http/#Server.Shutdown) method for graceful shutdowns. See the full [graceful-shutdown](https://github.com/gin-gonic/examples/tree/master/graceful-shutdown) example with gin.
+
+```go
+// +build go1.8
+
+package main
+
+import (
+ "context"
+ "log"
+ "net/http"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+
+ "github.com/gin-gonic/gin"
+)
+
+func main() {
+ router := gin.Default()
+ router.GET("/", func(c *gin.Context) {
+ time.Sleep(5 * time.Second)
+ c.String(http.StatusOK, "Welcome Gin Server")
+ })
+
+ srv := &http.Server{
+ Addr: ":8080",
+ Handler: router,
+ }
+
+ go func() {
+ // service connections
+ if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+ log.Fatalf("listen: %s\n", err)
+ }
+ }()
+
+ // Wait for interrupt signal to gracefully shutdown the server with
+ // a timeout of 5 seconds.
+ 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
+ signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
+ <-quit
+ log.Println("Shutdown Server ...")
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ if err := srv.Shutdown(ctx); err != nil {
+ log.Fatal("Server Shutdown:", err)
+ }
+ // catching ctx.Done(). timeout of 5 seconds.
+ select {
+ case <-ctx.Done():
+ log.Println("timeout of 5 seconds.")
+ }
+ log.Println("Server exiting")
+}
+```
+
+### Build a single binary with templates
+
+You can build a server into a single binary containing templates by using [go-assets][].
+
+[go-assets]: https://github.com/jessevdk/go-assets
+
+```go
+func main() {
+ r := gin.New()
+
+ t, err := loadTemplate()
+ if err != nil {
+ panic(err)
+ }
+ r.SetHTMLTemplate(t)
+
+ r.GET("/", func(c *gin.Context) {
+ c.HTML(http.StatusOK, "/html/index.tmpl",nil)
+ })
+ r.Run(":8080")
+}
+
+// loadTemplate loads templates embedded by go-assets-builder
+func loadTemplate() (*template.Template, error) {
+ t := template.New("")
+ for name, file := range Assets.Files {
+ if file.IsDir() || !strings.HasSuffix(name, ".tmpl") {
+ continue
+ }
+ h, err := ioutil.ReadAll(file)
+ if err != nil {
+ return nil, err
+ }
+ t, err = t.New(name).Parse(string(h))
+ if err != nil {
+ return nil, err
+ }
+ }
+ return t, nil
+}
+```
+
+See a complete example in the `https://github.com/gin-gonic/examples/tree/master/assets-in-binary` directory.
+
+### Bind form-data request with custom struct
+
+The follow example using custom struct:
+
+```go
+type StructA struct {
+ FieldA string `form:"field_a"`
+}
+
+type StructB struct {
+ NestedStruct StructA
+ FieldB string `form:"field_b"`
+}
+
+type StructC struct {
+ NestedStructPointer *StructA
+ FieldC string `form:"field_c"`
+}
+
+type StructD struct {
+ NestedAnonyStruct struct {
+ FieldX string `form:"field_x"`
+ }
+ FieldD string `form:"field_d"`
+}
+
+func GetDataB(c *gin.Context) {
+ var b StructB
+ c.Bind(&b)
+ c.JSON(200, gin.H{
+ "a": b.NestedStruct,
+ "b": b.FieldB,
+ })
+}
+
+func GetDataC(c *gin.Context) {
+ var b StructC
+ c.Bind(&b)
+ c.JSON(200, gin.H{
+ "a": b.NestedStructPointer,
+ "c": b.FieldC,
+ })
+}
+
+func GetDataD(c *gin.Context) {
+ var b StructD
+ c.Bind(&b)
+ c.JSON(200, gin.H{
+ "x": b.NestedAnonyStruct,
+ "d": b.FieldD,
+ })
+}
+
+func main() {
+ r := gin.Default()
+ r.GET("/getb", GetDataB)
+ r.GET("/getc", GetDataC)
+ r.GET("/getd", GetDataD)
+
+ r.Run()
+}
+```
+
+Using the command `curl` command result:
+
+```
+$ curl "http://localhost:8080/getb?field_a=hello&field_b=world"
+{"a":{"FieldA":"hello"},"b":"world"}
+$ curl "http://localhost:8080/getc?field_a=hello&field_c=world"
+{"a":{"FieldA":"hello"},"c":"world"}
+$ curl "http://localhost:8080/getd?field_x=hello&field_d=world"
+{"d":"world","x":{"FieldX":"hello"}}
+```
+
+### Try to bind body into different structs
+
+The normal methods for binding request body consumes `c.Request.Body` and they
+cannot be called multiple times.
+
+```go
+type formA struct {
+ Foo string `json:"foo" xml:"foo" binding:"required"`
+}
+
+type formB struct {
+ Bar string `json:"bar" xml:"bar" binding:"required"`
+}
+
+func SomeHandler(c *gin.Context) {
+ objA := formA{}
+ objB := formB{}
+ // This c.ShouldBind consumes c.Request.Body and it cannot be reused.
+ if errA := c.ShouldBind(&objA); errA == nil {
+ c.String(http.StatusOK, `the body should be formA`)
+ // Always an error is occurred by this because c.Request.Body is EOF now.
+ } else if errB := c.ShouldBind(&objB); errB == nil {
+ c.String(http.StatusOK, `the body should be formB`)
+ } else {
+ ...
+ }
+}
+```
+
+For this, you can use `c.ShouldBindBodyWith`.
+
+```go
+func SomeHandler(c *gin.Context) {
+ objA := formA{}
+ objB := formB{}
+ // This reads c.Request.Body and stores the result into the context.
+ if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {
+ c.String(http.StatusOK, `the body should be formA`)
+ // At this time, it reuses body stored in the context.
+ } else if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
+ c.String(http.StatusOK, `the body should be formB JSON`)
+ // And it can accepts other formats
+ } else if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil {
+ c.String(http.StatusOK, `the body should be formB XML`)
+ } else {
+ ...
+ }
+}
+```
+
+* `c.ShouldBindBodyWith` stores body into the context before binding. This has
+a slight impact to performance, so you should not use this method if you are
+enough to call binding at once.
+* This feature is only needed for some formats -- `JSON`, `XML`, `MsgPack`,
+`ProtoBuf`. For other formats, `Query`, `Form`, `FormPost`, `FormMultipart`,
+can be called by `c.ShouldBind()` multiple times without any damage to
+performance (See [#1341](https://github.com/gin-gonic/gin/pull/1341)).
+
+### http2 server push
+
+http.Pusher is supported only **go1.8+**. See the [golang blog](https://blog.golang.org/h2push) for detail information.
+
+```go
+package main
+
+import (
+ "html/template"
+ "log"
+
+ "github.com/gin-gonic/gin"
+)
+
+var html = template.Must(template.New("https").Parse(`
+
+
+ Https Test
+
+
+
+ Welcome, Ginner!
+
+
+`))
+
+func main() {
+ r := gin.Default()
+ r.Static("/assets", "./assets")
+ r.SetHTMLTemplate(html)
+
+ r.GET("/", func(c *gin.Context) {
+ if pusher := c.Writer.Pusher(); pusher != nil {
+ // use pusher.Push() to do server push
+ if err := pusher.Push("/assets/app.js", nil); err != nil {
+ log.Printf("Failed to push: %v", err)
+ }
+ }
+ c.HTML(200, "https", gin.H{
+ "status": "success",
+ })
+ })
+
+ // Listen and Server in https://127.0.0.1:8080
+ r.RunTLS(":8080", "./testdata/server.pem", "./testdata/server.key")
+}
+```
+
+### Define format for the log of routes
+
+The default log of routes is:
+```
+[GIN-debug] POST /foo --> main.main.func1 (3 handlers)
+[GIN-debug] GET /bar --> main.main.func2 (3 handlers)
+[GIN-debug] GET /status --> main.main.func3 (3 handlers)
+```
+
+If you want to log this information in given format (e.g. JSON, key values or something else), then you can define this format with `gin.DebugPrintRouteFunc`.
+In the example below, we log all routes with standard log package but you can use another log tools that suits of your needs.
+```go
+import (
+ "log"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+)
+
+func main() {
+ r := gin.Default()
+ gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
+ log.Printf("endpoint %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)
+ }
+
+ r.POST("/foo", func(c *gin.Context) {
+ c.JSON(http.StatusOK, "foo")
+ })
+
+ r.GET("/bar", func(c *gin.Context) {
+ c.JSON(http.StatusOK, "bar")
+ })
+
+ r.GET("/status", func(c *gin.Context) {
+ c.JSON(http.StatusOK, "ok")
+ })
+
+ // Listen and Server in http://0.0.0.0:8080
+ r.Run()
+}
+```
+
+### Set and get a cookie
+
+```go
+import (
+ "fmt"
+
+ "github.com/gin-gonic/gin"
+)
+
+func main() {
+
+ router := gin.Default()
+
+ router.GET("/cookie", func(c *gin.Context) {
+
+ cookie, err := c.Cookie("gin_cookie")
+
+ if err != nil {
+ cookie = "NotSet"
+ c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
+ }
+
+ fmt.Printf("Cookie value: %s \n", cookie)
+ })
+
+ router.Run()
+}
+```
+
+
+## Testing
+
+The `net/http/httptest` package is preferable way for HTTP testing.
+
+```go
+package main
+
+func setupRouter() *gin.Engine {
+ r := gin.Default()
+ r.GET("/ping", func(c *gin.Context) {
+ c.String(200, "pong")
+ })
+ return r
+}
+
+func main() {
+ r := setupRouter()
+ r.Run(":8080")
+}
+```
+
+Test for code example above:
+
+```go
+package main
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPingRoute(t *testing.T) {
+ router := setupRouter()
+
+ w := httptest.NewRecorder()
+ req, _ := http.NewRequest("GET", "/ping", nil)
+ router.ServeHTTP(w, req)
+
+ assert.Equal(t, 200, w.Code)
+ assert.Equal(t, "pong", w.Body.String())
+}
+```
## Users
-[Gin website](https://gin-gonic.com/docs/users/) lists some awesome projects made with Gin web framework.
-
-## Contributing
-
-Gin is the work of hundreds of contributors. We appreciate your help!
-
-Please see [CONTRIBUTING](CONTRIBUTING.md) for details on submitting patches and the contribution workflow.
+Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework.
+* [gorush](https://github.com/appleboy/gorush): A push notification server written in Go.
+* [fnproject](https://github.com/fnproject/fn): The container native, cloud agnostic serverless platform.
+* [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.
diff --git a/binding/binding_test.go b/binding/binding_test.go
index ee788225..6710e42b 100644
--- a/binding/binding_test.go
+++ b/binding/binding_test.go
@@ -24,6 +24,16 @@ import (
"github.com/ugorji/go/codec"
)
+type appkey struct {
+ Appkey string `json:"appkey" form:"appkey"`
+}
+
+type QueryTest struct {
+ Page int `json:"page" form:"page"`
+ Size int `json:"size" form:"size"`
+ appkey
+}
+
type FooStruct struct {
Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required"`
}
@@ -114,71 +124,6 @@ type FooStructForBoolType struct {
BoolFoo bool `form:"bool_foo"`
}
-type FooBarStructForIntType struct {
- IntFoo int `form:"int_foo"`
- IntBar int `form:"int_bar" binding:"required"`
-}
-
-type FooBarStructForInt8Type struct {
- Int8Foo int8 `form:"int8_foo"`
- Int8Bar int8 `form:"int8_bar" binding:"required"`
-}
-
-type FooBarStructForInt16Type struct {
- Int16Foo int16 `form:"int16_foo"`
- Int16Bar int16 `form:"int16_bar" binding:"required"`
-}
-
-type FooBarStructForInt32Type struct {
- Int32Foo int32 `form:"int32_foo"`
- Int32Bar int32 `form:"int32_bar" binding:"required"`
-}
-
-type FooBarStructForInt64Type struct {
- Int64Foo int64 `form:"int64_foo"`
- Int64Bar int64 `form:"int64_bar" binding:"required"`
-}
-
-type FooBarStructForUintType struct {
- UintFoo uint `form:"uint_foo"`
- UintBar uint `form:"uint_bar" binding:"required"`
-}
-
-type FooBarStructForUint8Type struct {
- Uint8Foo uint8 `form:"uint8_foo"`
- Uint8Bar uint8 `form:"uint8_bar" binding:"required"`
-}
-
-type FooBarStructForUint16Type struct {
- Uint16Foo uint16 `form:"uint16_foo"`
- Uint16Bar uint16 `form:"uint16_bar" binding:"required"`
-}
-
-type FooBarStructForUint32Type struct {
- Uint32Foo uint32 `form:"uint32_foo"`
- Uint32Bar uint32 `form:"uint32_bar" binding:"required"`
-}
-
-type FooBarStructForUint64Type struct {
- Uint64Foo uint64 `form:"uint64_foo"`
- Uint64Bar uint64 `form:"uint64_bar" binding:"required"`
-}
-
-type FooBarStructForBoolType struct {
- BoolFoo bool `form:"bool_foo"`
- BoolBar bool `form:"bool_bar" binding:"required"`
-}
-
-type FooBarStructForFloat32Type struct {
- Float32Foo float32 `form:"float32_foo"`
- Float32Bar float32 `form:"float32_bar" binding:"required"`
-}
-
-type FooBarStructForFloat64Type struct {
- Float64Foo float64 `form:"float64_foo"`
- Float64Bar float64 `form:"float64_bar" binding:"required"`
-}
-
type FooStructForStringPtrType struct {
PtrFoo *string `form:"ptr_foo"`
PtrBar *string `form:"ptr_bar" binding:"required"`
@@ -254,6 +199,18 @@ func TestBindingForm2(t *testing.T) {
"", "")
}
+func TestBindingFormEmbeddedStruct(t *testing.T) {
+ testFormBindingEmbeddedStruct(t, "POST",
+ "/", "/",
+ "page=1&size=2&appkey=test-appkey", "bar2=foo")
+}
+
+func TestBindingFormEmbeddedStruct2(t *testing.T) {
+ testFormBindingEmbeddedStruct(t, "GET",
+ "/?page=1&size=2&appkey=test-appkey", "/?bar2=foo",
+ "", "")
+}
+
func TestBindingFormDefaultValue(t *testing.T) {
testFormBindingDefaultValue(t, "POST",
"/", "/",
@@ -335,110 +292,6 @@ func TestBindingFormForType(t *testing.T) {
"/?slice_map_foo=1&slice_map_foo=2", "/?bar2=1&bar2=2",
"", "", "SliceMap")
- testFormBindingForType(t, "POST",
- "/", "/",
- "int_foo=&int_bar=-12", "bar2=-123", "Int")
-
- testFormBindingForType(t, "GET",
- "/?int_foo=&int_bar=-12", "/?bar2=-123",
- "", "", "Int")
-
- testFormBindingForType(t, "POST",
- "/", "/",
- "int8_foo=&int8_bar=-12", "bar2=-123", "Int8")
-
- testFormBindingForType(t, "GET",
- "/?int8_foo=&int8_bar=-12", "/?bar2=-123",
- "", "", "Int8")
-
- testFormBindingForType(t, "POST",
- "/", "/",
- "int16_foo=&int16_bar=-12", "bar2=-123", "Int16")
-
- testFormBindingForType(t, "GET",
- "/?int16_foo=&int16_bar=-12", "/?bar2=-123",
- "", "", "Int16")
-
- testFormBindingForType(t, "POST",
- "/", "/",
- "int32_foo=&int32_bar=-12", "bar2=-123", "Int32")
-
- testFormBindingForType(t, "GET",
- "/?int32_foo=&int32_bar=-12", "/?bar2=-123",
- "", "", "Int32")
-
- testFormBindingForType(t, "POST",
- "/", "/",
- "int64_foo=&int64_bar=-12", "bar2=-123", "Int64")
-
- testFormBindingForType(t, "GET",
- "/?int64_foo=&int64_bar=-12", "/?bar2=-123",
- "", "", "Int64")
-
- testFormBindingForType(t, "POST",
- "/", "/",
- "uint_foo=&uint_bar=12", "bar2=123", "Uint")
-
- testFormBindingForType(t, "GET",
- "/?uint_foo=&uint_bar=12", "/?bar2=123",
- "", "", "Uint")
-
- testFormBindingForType(t, "POST",
- "/", "/",
- "uint8_foo=&uint8_bar=12", "bar2=123", "Uint8")
-
- testFormBindingForType(t, "GET",
- "/?uint8_foo=&uint8_bar=12", "/?bar2=123",
- "", "", "Uint8")
-
- testFormBindingForType(t, "POST",
- "/", "/",
- "uint16_foo=&uint16_bar=12", "bar2=123", "Uint16")
-
- testFormBindingForType(t, "GET",
- "/?uint16_foo=&uint16_bar=12", "/?bar2=123",
- "", "", "Uint16")
-
- testFormBindingForType(t, "POST",
- "/", "/",
- "uint32_foo=&uint32_bar=12", "bar2=123", "Uint32")
-
- testFormBindingForType(t, "GET",
- "/?uint32_foo=&uint32_bar=12", "/?bar2=123",
- "", "", "Uint32")
-
- testFormBindingForType(t, "POST",
- "/", "/",
- "uint64_foo=&uint64_bar=12", "bar2=123", "Uint64")
-
- testFormBindingForType(t, "GET",
- "/?uint64_foo=&uint64_bar=12", "/?bar2=123",
- "", "", "Uint64")
-
- testFormBindingForType(t, "POST",
- "/", "/",
- "bool_foo=&bool_bar=true", "bar2=true", "Bool")
-
- testFormBindingForType(t, "GET",
- "/?bool_foo=&bool_bar=true", "/?bar2=true",
- "", "", "Bool")
-
- testFormBindingForType(t, "POST",
- "/", "/",
- "float32_foo=&float32_bar=-12.34", "bar2=12.3", "Float32")
-
- testFormBindingForType(t, "GET",
- "/?float32_foo=&float32_bar=-12.34", "/?bar2=12.3",
- "", "", "Float32")
-
- testFormBindingForType(t, "POST",
- "/", "/",
- "float64_foo=&float64_bar=-12.34", "bar2=12.3", "Float64")
-
- testFormBindingForType(t, "GET",
- "/?float64_foo=&float64_bar=-12.34", "/?bar2=12.3",
- "", "", "Float64")
-
testFormBindingForType(t, "POST",
"/", "/",
"ptr_bar=test", "bar2=test", "Ptr")
@@ -857,6 +710,23 @@ func TestUriInnerBinding(t *testing.T) {
assert.Equal(t, tag.S.Age, expectedAge)
}
+func testFormBindingEmbeddedStruct(t *testing.T, method, path, badPath, body, badBody string) {
+ b := Form
+ assert.Equal(t, "form", b.Name())
+
+ obj := QueryTest{}
+ req := requestWithBody(method, path, body)
+ if method == "POST" {
+ req.Header.Add("Content-Type", MIMEPOSTForm)
+ }
+ err := b.Bind(req, &obj)
+ assert.NoError(t, err)
+ assert.Equal(t, 1, obj.Page)
+ assert.Equal(t, 2, obj.Size)
+ assert.Equal(t, "test-appkey", obj.Appkey)
+
+}
+
func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) {
b := Form
assert.Equal(t, "form", b.Name())
@@ -1076,149 +946,6 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
req.Header.Add("Content-Type", MIMEPOSTForm)
}
switch typ {
- case "Int":
- obj := FooBarStructForIntType{}
- err := b.Bind(req, &obj)
- assert.NoError(t, err)
- assert.Equal(t, int(0), obj.IntFoo)
- assert.Equal(t, int(-12), obj.IntBar)
-
- obj = FooBarStructForIntType{}
- req = requestWithBody(method, badPath, badBody)
- err = JSON.Bind(req, &obj)
- assert.Error(t, err)
- case "Int8":
- obj := FooBarStructForInt8Type{}
- err := b.Bind(req, &obj)
- assert.NoError(t, err)
- assert.Equal(t, int8(0), obj.Int8Foo)
- assert.Equal(t, int8(-12), obj.Int8Bar)
-
- obj = FooBarStructForInt8Type{}
- req = requestWithBody(method, badPath, badBody)
- err = JSON.Bind(req, &obj)
- assert.Error(t, err)
- case "Int16":
- obj := FooBarStructForInt16Type{}
- err := b.Bind(req, &obj)
- assert.NoError(t, err)
- assert.Equal(t, int16(0), obj.Int16Foo)
- assert.Equal(t, int16(-12), obj.Int16Bar)
-
- obj = FooBarStructForInt16Type{}
- req = requestWithBody(method, badPath, badBody)
- err = JSON.Bind(req, &obj)
- assert.Error(t, err)
- case "Int32":
- obj := FooBarStructForInt32Type{}
- err := b.Bind(req, &obj)
- assert.NoError(t, err)
- assert.Equal(t, int32(0), obj.Int32Foo)
- assert.Equal(t, int32(-12), obj.Int32Bar)
-
- obj = FooBarStructForInt32Type{}
- req = requestWithBody(method, badPath, badBody)
- err = JSON.Bind(req, &obj)
- assert.Error(t, err)
- case "Int64":
- obj := FooBarStructForInt64Type{}
- err := b.Bind(req, &obj)
- assert.NoError(t, err)
- assert.Equal(t, int64(0), obj.Int64Foo)
- assert.Equal(t, int64(-12), obj.Int64Bar)
-
- obj = FooBarStructForInt64Type{}
- req = requestWithBody(method, badPath, badBody)
- err = JSON.Bind(req, &obj)
- assert.Error(t, err)
- case "Uint":
- obj := FooBarStructForUintType{}
- err := b.Bind(req, &obj)
- assert.NoError(t, err)
- assert.Equal(t, uint(0x0), obj.UintFoo)
- assert.Equal(t, uint(0xc), obj.UintBar)
-
- obj = FooBarStructForUintType{}
- req = requestWithBody(method, badPath, badBody)
- err = JSON.Bind(req, &obj)
- assert.Error(t, err)
- case "Uint8":
- obj := FooBarStructForUint8Type{}
- err := b.Bind(req, &obj)
- assert.NoError(t, err)
- assert.Equal(t, uint8(0x0), obj.Uint8Foo)
- assert.Equal(t, uint8(0xc), obj.Uint8Bar)
-
- obj = FooBarStructForUint8Type{}
- req = requestWithBody(method, badPath, badBody)
- err = JSON.Bind(req, &obj)
- assert.Error(t, err)
- case "Uint16":
- obj := FooBarStructForUint16Type{}
- err := b.Bind(req, &obj)
- assert.NoError(t, err)
- assert.Equal(t, uint16(0x0), obj.Uint16Foo)
- assert.Equal(t, uint16(0xc), obj.Uint16Bar)
-
- obj = FooBarStructForUint16Type{}
- req = requestWithBody(method, badPath, badBody)
- err = JSON.Bind(req, &obj)
- assert.Error(t, err)
- case "Uint32":
- obj := FooBarStructForUint32Type{}
- err := b.Bind(req, &obj)
- assert.NoError(t, err)
- assert.Equal(t, uint32(0x0), obj.Uint32Foo)
- assert.Equal(t, uint32(0xc), obj.Uint32Bar)
-
- obj = FooBarStructForUint32Type{}
- req = requestWithBody(method, badPath, badBody)
- err = JSON.Bind(req, &obj)
- assert.Error(t, err)
- case "Uint64":
- obj := FooBarStructForUint64Type{}
- err := b.Bind(req, &obj)
- assert.NoError(t, err)
- assert.Equal(t, uint64(0x0), obj.Uint64Foo)
- assert.Equal(t, uint64(0xc), obj.Uint64Bar)
-
- obj = FooBarStructForUint64Type{}
- req = requestWithBody(method, badPath, badBody)
- err = JSON.Bind(req, &obj)
- assert.Error(t, err)
- case "Float32":
- obj := FooBarStructForFloat32Type{}
- err := b.Bind(req, &obj)
- assert.NoError(t, err)
- assert.Equal(t, float32(0.0), obj.Float32Foo)
- assert.Equal(t, float32(-12.34), obj.Float32Bar)
-
- obj = FooBarStructForFloat32Type{}
- req = requestWithBody(method, badPath, badBody)
- err = JSON.Bind(req, &obj)
- assert.Error(t, err)
- case "Float64":
- obj := FooBarStructForFloat64Type{}
- err := b.Bind(req, &obj)
- assert.NoError(t, err)
- assert.Equal(t, float64(0.0), obj.Float64Foo)
- assert.Equal(t, float64(-12.34), obj.Float64Bar)
-
- obj = FooBarStructForFloat64Type{}
- req = requestWithBody(method, badPath, badBody)
- err = JSON.Bind(req, &obj)
- assert.Error(t, err)
- case "Bool":
- obj := FooBarStructForBoolType{}
- err := b.Bind(req, &obj)
- assert.NoError(t, err)
- assert.False(t, obj.BoolFoo)
- assert.True(t, obj.BoolBar)
-
- obj = FooBarStructForBoolType{}
- req = requestWithBody(method, badPath, badBody)
- err = JSON.Bind(req, &obj)
- assert.Error(t, err)
case "Slice":
obj := FooStructForSliceType{}
err := b.Bind(req, &obj)
@@ -1454,97 +1181,3 @@ func requestWithBody(method, path, body string) (req *http.Request) {
req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
return
}
-
-func TestCanSet(t *testing.T) {
- type CanSetStruct struct {
- lowerStart string `form:"lower"`
- }
-
- var c CanSetStruct
- assert.Nil(t, mapForm(&c, nil))
-}
-
-func formPostRequest(path, body string) *http.Request {
- req := requestWithBody("POST", path, body)
- req.Header.Add("Content-Type", MIMEPOSTForm)
- return req
-}
-
-func TestBindingSliceDefault(t *testing.T) {
- var s struct {
- Friends []string `form:"friends,default=mike"`
- }
- req := formPostRequest("", "")
- err := Form.Bind(req, &s)
- assert.NoError(t, err)
-
- assert.Len(t, s.Friends, 1)
- assert.Equal(t, "mike", s.Friends[0])
-}
-
-func TestBindingStructField(t *testing.T) {
- var s struct {
- Opts struct {
- Port int
- } `form:"opts"`
- }
- req := formPostRequest("", `opts={"Port": 8000}`)
- err := Form.Bind(req, &s)
- assert.NoError(t, err)
- assert.Equal(t, 8000, s.Opts.Port)
-}
-
-func TestBindingUnknownTypeChan(t *testing.T) {
- var s struct {
- Stop chan bool `form:"stop"`
- }
- req := formPostRequest("", "stop=true")
- err := Form.Bind(req, &s)
- assert.Error(t, err)
- assert.Equal(t, errUnknownType, err)
-}
-
-func TestBindingTimeDuration(t *testing.T) {
- var s struct {
- Timeout time.Duration `form:"timeout"`
- }
-
- // ok
- req := formPostRequest("", "timeout=5s")
- err := Form.Bind(req, &s)
- assert.NoError(t, err)
- assert.Equal(t, 5*time.Second, s.Timeout)
-
- // error
- req = formPostRequest("", "timeout=wrong")
- err = Form.Bind(req, &s)
- assert.Error(t, err)
-}
-
-func TestBindingArray(t *testing.T) {
- var s struct {
- Nums [2]int `form:"nums,default=4"`
- }
-
- // default
- req := formPostRequest("", "")
- err := Form.Bind(req, &s)
- assert.Error(t, err)
- assert.Equal(t, [2]int{0, 0}, s.Nums)
-
- // ok
- req = formPostRequest("", "nums=3&nums=8")
- err = Form.Bind(req, &s)
- assert.NoError(t, err)
- assert.Equal(t, [2]int{3, 8}, s.Nums)
-
- // not enough vals
- req = formPostRequest("", "nums=3")
- err = Form.Bind(req, &s)
- assert.Error(t, err)
-
- // error
- req = formPostRequest("", "nums=3&nums=wrong")
- err = Form.Bind(req, &s)
- assert.Error(t, err)
-}
diff --git a/binding/form.go b/binding/form.go
index f1f89195..0b28aa8a 100644
--- a/binding/form.go
+++ b/binding/form.go
@@ -4,7 +4,11 @@
package binding
-import "net/http"
+import (
+ "mime/multipart"
+ "net/http"
+ "reflect"
+)
const defaultMemory = 32 * 1024 * 1024
@@ -53,13 +57,33 @@ func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error {
if err := req.ParseMultipartForm(defaultMemory); err != nil {
return err
}
- if err := mapForm(obj, req.MultipartForm.Value); err != nil {
- return err
- }
-
- if err := mapFiles(obj, req); err != nil {
+ if err := mappingByPtr(obj, (*multipartRequest)(req), "form"); err != nil {
return err
}
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/form_mapping.go b/binding/form_mapping.go
index fc33b1df..ebf3b199 100644
--- a/binding/form_mapping.go
+++ b/binding/form_mapping.go
@@ -7,7 +7,6 @@ package binding
import (
"errors"
"fmt"
- "net/http"
"reflect"
"strconv"
"strings"
@@ -16,34 +15,6 @@ import (
"github.com/gin-gonic/gin/internal/json"
)
-func mapFiles(ptr interface{}, req *http.Request) error {
- typ := reflect.TypeOf(ptr).Elem()
- val := reflect.ValueOf(ptr).Elem()
- for i := 0; i < typ.NumField(); i++ {
- typeField := typ.Field(i)
- structField := val.Field(i)
-
- t := fmt.Sprintf("%s", typeField.Type)
- if string(t) != "*multipart.FileHeader" {
- continue
- }
-
- inputFieldName := typeField.Tag.Get("form")
- if inputFieldName == "" {
- inputFieldName = typeField.Name
- }
-
- _, fileHeader, err := req.FormFile(inputFieldName)
- if err != nil {
- return err
- }
-
- structField.Set(reflect.ValueOf(fileHeader))
-
- }
- return nil
-}
-
var errUnknownType = errors.New("Unknown type")
func mapUri(ptr interface{}, m map[string][]string) error {
@@ -57,11 +28,29 @@ func mapForm(ptr interface{}, form map[string][]string) error {
var emptyField = reflect.StructField{}
func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error {
- _, err := mapping(reflect.ValueOf(ptr), emptyField, form, tag)
+ return mappingByPtr(ptr, formSource(form), tag)
+}
+
+// setter tries to set value on a walking by fields of a struct
+type setter interface {
+ TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error)
+}
+
+type formSource map[string][]string
+
+var _ setter = formSource(nil)
+
+// TrySet tries to set a value by request's form source (like map[string][]string)
+func (form formSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSetted bool, err error) {
+ return setByForm(value, field, form, tagValue, opt)
+}
+
+func mappingByPtr(ptr interface{}, setter setter, tag string) error {
+ _, err := mapping(reflect.ValueOf(ptr), emptyField, setter, tag)
return err
}
-func mapping(value reflect.Value, field reflect.StructField, form map[string][]string, tag string) (bool, error) {
+func mapping(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {
var vKind = value.Kind()
if vKind == reflect.Ptr {
@@ -71,7 +60,7 @@ func mapping(value reflect.Value, field reflect.StructField, form map[string][]s
isNew = true
vPtr = reflect.New(value.Type().Elem())
}
- isSetted, err := mapping(vPtr.Elem(), field, form, tag)
+ isSetted, err := mapping(vPtr.Elem(), field, setter, tag)
if err != nil {
return false, err
}
@@ -81,12 +70,14 @@ func mapping(value reflect.Value, field reflect.StructField, form map[string][]s
return isSetted, nil
}
- ok, err := tryToSetValue(value, field, form, tag)
- if err != nil {
- return false, err
- }
- if ok {
- return true, nil
+ if vKind != reflect.Struct || !field.Anonymous {
+ ok, err := tryToSetValue(value, field, setter, tag)
+ if err != nil {
+ return false, err
+ }
+ if ok {
+ return true, nil
+ }
}
if vKind == reflect.Struct {
@@ -94,10 +85,11 @@ func mapping(value reflect.Value, field reflect.StructField, form map[string][]s
var isSetted bool
for i := 0; i < value.NumField(); i++ {
- if !value.Field(i).CanSet() {
+ sf := tValue.Field(i)
+ if sf.PkgPath != "" && !sf.Anonymous { // unexported
continue
}
- ok, err := mapping(value.Field(i), tValue.Field(i), form, tag)
+ ok, err := mapping(value.Field(i), tValue.Field(i), setter, tag)
if err != nil {
return false, err
}
@@ -108,9 +100,14 @@ func mapping(value reflect.Value, field reflect.StructField, form map[string][]s
return false, nil
}
-func tryToSetValue(value reflect.Value, field reflect.StructField, form map[string][]string, tag string) (bool, error) {
- var tagValue, defaultValue string
- var isDefaultExists bool
+type setOptions struct {
+ isDefaultExists bool
+ defaultValue string
+}
+
+func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {
+ var tagValue string
+ var setOpt setOptions
tagValue = field.Tag.Get(tag)
tagValue, opts := head(tagValue, ",")
@@ -129,28 +126,30 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, form map[stri
for len(opts) > 0 {
opt, opts = head(opts, ",")
- k, v := head(opt, "=")
- switch k {
- case "default":
- isDefaultExists = true
- defaultValue = v
+ if k, v := head(opt, "="); k == "default" {
+ setOpt.isDefaultExists = true
+ setOpt.defaultValue = v
}
}
+ return setter.TrySet(value, field, tagValue, setOpt)
+}
+
+func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSetted bool, err error) {
vs, ok := form[tagValue]
- if !ok && !isDefaultExists {
+ if !ok && !opt.isDefaultExists {
return false, nil
}
switch value.Kind() {
case reflect.Slice:
if !ok {
- vs = []string{defaultValue}
+ vs = []string{opt.defaultValue}
}
return true, setSlice(vs, value, field)
case reflect.Array:
if !ok {
- vs = []string{defaultValue}
+ vs = []string{opt.defaultValue}
}
if len(vs) != value.Len() {
return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String())
@@ -159,7 +158,7 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, form map[stri
default:
var val string
if !ok {
- val = defaultValue
+ val = opt.defaultValue
}
if len(vs) > 0 {
diff --git a/binding/form_mapping_test.go b/binding/form_mapping_test.go
new file mode 100644
index 00000000..c9d6111b
--- /dev/null
+++ b/binding/form_mapping_test.go
@@ -0,0 +1,271 @@
+// 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 (
+ "reflect"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestMappingBaseTypes(t *testing.T) {
+ intPtr := func(i int) *int {
+ return &i
+ }
+ for _, tt := range []struct {
+ name string
+ value interface{}
+ form string
+ expect interface{}
+ }{
+ {"base type", struct{ F int }{}, "9", int(9)},
+ {"base type", struct{ F int8 }{}, "9", int8(9)},
+ {"base type", struct{ F int16 }{}, "9", int16(9)},
+ {"base type", struct{ F int32 }{}, "9", int32(9)},
+ {"base type", struct{ F int64 }{}, "9", int64(9)},
+ {"base type", struct{ F uint }{}, "9", uint(9)},
+ {"base type", struct{ F uint8 }{}, "9", uint8(9)},
+ {"base type", struct{ F uint16 }{}, "9", uint16(9)},
+ {"base type", struct{ F uint32 }{}, "9", uint32(9)},
+ {"base type", struct{ F uint64 }{}, "9", uint64(9)},
+ {"base type", struct{ F bool }{}, "True", true},
+ {"base type", struct{ F float32 }{}, "9.1", float32(9.1)},
+ {"base type", struct{ F float64 }{}, "9.1", float64(9.1)},
+ {"base type", struct{ F string }{}, "test", string("test")},
+ {"base type", struct{ F *int }{}, "9", intPtr(9)},
+
+ // zero values
+ {"zero value", struct{ F int }{}, "", int(0)},
+ {"zero value", struct{ F uint }{}, "", uint(0)},
+ {"zero value", struct{ F bool }{}, "", false},
+ {"zero value", struct{ F float32 }{}, "", float32(0)},
+ } {
+ tp := reflect.TypeOf(tt.value)
+ testName := tt.name + ":" + tp.Field(0).Type.String()
+
+ val := reflect.New(reflect.TypeOf(tt.value))
+ val.Elem().Set(reflect.ValueOf(tt.value))
+
+ field := val.Elem().Type().Field(0)
+
+ _, err := mapping(val, emptyField, formSource{field.Name: {tt.form}}, "form")
+ assert.NoError(t, err, testName)
+
+ actual := val.Elem().Field(0).Interface()
+ assert.Equal(t, tt.expect, actual, testName)
+ }
+}
+
+func TestMappingDefault(t *testing.T) {
+ var s struct {
+ Int int `form:",default=9"`
+ Slice []int `form:",default=9"`
+ Array [1]int `form:",default=9"`
+ }
+ err := mappingByPtr(&s, formSource{}, "form")
+ assert.NoError(t, err)
+
+ assert.Equal(t, 9, s.Int)
+ assert.Equal(t, []int{9}, s.Slice)
+ assert.Equal(t, [1]int{9}, s.Array)
+}
+
+func TestMappingSkipField(t *testing.T) {
+ var s struct {
+ A int
+ }
+ err := mappingByPtr(&s, formSource{}, "form")
+ assert.NoError(t, err)
+
+ assert.Equal(t, 0, s.A)
+}
+
+func TestMappingIgnoreField(t *testing.T) {
+ var s struct {
+ A int `form:"A"`
+ B int `form:"-"`
+ }
+ err := mappingByPtr(&s, formSource{"A": {"9"}, "B": {"9"}}, "form")
+ assert.NoError(t, err)
+
+ assert.Equal(t, 9, s.A)
+ assert.Equal(t, 0, s.B)
+}
+
+func TestMappingUnexportedField(t *testing.T) {
+ var s struct {
+ A int `form:"a"`
+ b int `form:"b"`
+ }
+ err := mappingByPtr(&s, formSource{"a": {"9"}, "b": {"9"}}, "form")
+ assert.NoError(t, err)
+
+ assert.Equal(t, 9, s.A)
+ assert.Equal(t, 0, s.b)
+}
+
+func TestMappingPrivateField(t *testing.T) {
+ var s struct {
+ f int `form:"field"`
+ }
+ err := mappingByPtr(&s, formSource{"field": {"6"}}, "form")
+ assert.NoError(t, err)
+ assert.Equal(t, int(0), s.f)
+}
+
+func TestMappingUnknownFieldType(t *testing.T) {
+ var s struct {
+ U uintptr
+ }
+
+ err := mappingByPtr(&s, formSource{"U": {"unknown"}}, "form")
+ assert.Error(t, err)
+ assert.Equal(t, errUnknownType, err)
+}
+
+func TestMappingURI(t *testing.T) {
+ var s struct {
+ F int `uri:"field"`
+ }
+ err := mapUri(&s, map[string][]string{"field": {"6"}})
+ assert.NoError(t, err)
+ assert.Equal(t, int(6), s.F)
+}
+
+func TestMappingForm(t *testing.T) {
+ var s struct {
+ F int `form:"field"`
+ }
+ err := mapForm(&s, map[string][]string{"field": {"6"}})
+ assert.NoError(t, err)
+ assert.Equal(t, int(6), s.F)
+}
+
+func TestMappingTime(t *testing.T) {
+ var s struct {
+ Time time.Time
+ LocalTime time.Time `time_format:"2006-01-02"`
+ ZeroValue time.Time
+ CSTTime time.Time `time_format:"2006-01-02" time_location:"Asia/Shanghai"`
+ UTCTime time.Time `time_format:"2006-01-02" time_utc:"1"`
+ }
+
+ var err error
+ time.Local, err = time.LoadLocation("Europe/Berlin")
+ assert.NoError(t, err)
+
+ err = mapForm(&s, map[string][]string{
+ "Time": {"2019-01-20T16:02:58Z"},
+ "LocalTime": {"2019-01-20"},
+ "ZeroValue": {},
+ "CSTTime": {"2019-01-20"},
+ "UTCTime": {"2019-01-20"},
+ })
+ assert.NoError(t, err)
+
+ assert.Equal(t, "2019-01-20 16:02:58 +0000 UTC", s.Time.String())
+ assert.Equal(t, "2019-01-20 00:00:00 +0100 CET", s.LocalTime.String())
+ assert.Equal(t, "2019-01-19 23:00:00 +0000 UTC", s.LocalTime.UTC().String())
+ assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", s.ZeroValue.String())
+ assert.Equal(t, "2019-01-20 00:00:00 +0800 CST", s.CSTTime.String())
+ assert.Equal(t, "2019-01-19 16:00:00 +0000 UTC", s.CSTTime.UTC().String())
+ assert.Equal(t, "2019-01-20 00:00:00 +0000 UTC", s.UTCTime.String())
+
+ // wrong location
+ var wrongLoc struct {
+ Time time.Time `time_location:"wrong"`
+ }
+ err = mapForm(&wrongLoc, map[string][]string{"Time": {"2019-01-20T16:02:58Z"}})
+ assert.Error(t, err)
+
+ // wrong time value
+ var wrongTime struct {
+ Time time.Time
+ }
+ err = mapForm(&wrongTime, map[string][]string{"Time": {"wrong"}})
+ assert.Error(t, err)
+}
+
+func TestMapiingTimeDuration(t *testing.T) {
+ var s struct {
+ D time.Duration
+ }
+
+ // ok
+ err := mappingByPtr(&s, formSource{"D": {"5s"}}, "form")
+ assert.NoError(t, err)
+ assert.Equal(t, 5*time.Second, s.D)
+
+ // error
+ err = mappingByPtr(&s, formSource{"D": {"wrong"}}, "form")
+ assert.Error(t, err)
+}
+
+func TestMappingSlice(t *testing.T) {
+ var s struct {
+ Slice []int `form:"slice,default=9"`
+ }
+
+ // default value
+ err := mappingByPtr(&s, formSource{}, "form")
+ assert.NoError(t, err)
+ assert.Equal(t, []int{9}, s.Slice)
+
+ // ok
+ err = mappingByPtr(&s, formSource{"slice": {"3", "4"}}, "form")
+ assert.NoError(t, err)
+ assert.Equal(t, []int{3, 4}, s.Slice)
+
+ // error
+ err = mappingByPtr(&s, formSource{"slice": {"wrong"}}, "form")
+ assert.Error(t, err)
+}
+
+func TestMappingArray(t *testing.T) {
+ var s struct {
+ Array [2]int `form:"array,default=9"`
+ }
+
+ // wrong default
+ err := mappingByPtr(&s, formSource{}, "form")
+ assert.Error(t, err)
+
+ // ok
+ err = mappingByPtr(&s, formSource{"array": {"3", "4"}}, "form")
+ assert.NoError(t, err)
+ assert.Equal(t, [2]int{3, 4}, s.Array)
+
+ // error - not enough vals
+ err = mappingByPtr(&s, formSource{"array": {"3"}}, "form")
+ assert.Error(t, err)
+
+ // error - wrong value
+ err = mappingByPtr(&s, formSource{"array": {"wrong"}}, "form")
+ assert.Error(t, err)
+}
+
+func TestMappingStructField(t *testing.T) {
+ var s struct {
+ J struct {
+ I int
+ }
+ }
+
+ err := mappingByPtr(&s, formSource{"J": {`{"I": 9}`}}, "form")
+ assert.NoError(t, err)
+ assert.Equal(t, 9, s.J.I)
+}
+
+func TestMappingMapField(t *testing.T) {
+ var s struct {
+ M map[string]int
+ }
+
+ err := mappingByPtr(&s, formSource{"M": {`{"one": 1}`}}, "form")
+ assert.NoError(t, err)
+ assert.Equal(t, map[string]int{"one": 1}, s.M)
+}
diff --git a/context.go b/context.go
index df76c1cb..425b627f 100644
--- a/context.go
+++ b/context.go
@@ -450,11 +450,6 @@ func (c *Context) GetPostFormArray(key string) ([]string, bool) {
if values := req.PostForm[key]; len(values) > 0 {
return values, true
}
- if req.MultipartForm != nil && req.MultipartForm.File != nil {
- if values := req.MultipartForm.Value[key]; len(values) > 0 {
- return values, true
- }
- }
return []string{}, false
}
@@ -473,13 +468,7 @@ func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
debugPrint("error on parse multipart form map: %v", err)
}
}
- dicts, exist := c.get(req.PostForm, key)
-
- if !exist && req.MultipartForm != nil && req.MultipartForm.File != nil {
- dicts, exist = c.get(req.MultipartForm.Value, key)
- }
-
- return dicts, exist
+ return c.get(req.PostForm, key)
}
// get is an internal method and returns a map which satisfy conditions.
@@ -693,7 +682,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
@@ -839,6 +828,12 @@ func (c *Context) AsciiJSON(code int, obj interface{}) {
c.Render(code, render.AsciiJSON{Data: obj})
}
+// PureJSON serializes the given struct as JSON into the response body.
+// PureJSON, unlike JSON, does not replace special html characters with their unicode entities.
+func (c *Context) PureJSON(code int, obj interface{}) {
+ c.Render(code, render.PureJSON{Data: obj})
+}
+
// XML serializes the given struct as XML into the response body.
// It also sets the Content-Type as "application/xml".
func (c *Context) XML(code int, obj interface{}) {
diff --git a/context_17.go b/context_17.go
deleted file mode 100644
index 8e9f75ad..00000000
--- a/context_17.go
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright 2018 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.
-
-// +build go1.7
-
-package gin
-
-import (
- "github.com/gin-gonic/gin/render"
-)
-
-// PureJSON serializes the given struct as JSON into the response body.
-// PureJSON, unlike JSON, does not replace special html characters with their unicode entities.
-func (c *Context) PureJSON(code int, obj interface{}) {
- c.Render(code, render.PureJSON{Data: obj})
-}
diff --git a/context_17_test.go b/context_17_test.go
deleted file mode 100644
index 5b9ebcdc..00000000
--- a/context_17_test.go
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2018 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.
-
-// +build go1.7
-
-package gin
-
-import (
- "net/http"
- "net/http/httptest"
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-// Tests that the response is serialized as JSON
-// and Content-Type is set to application/json
-// and special HTML characters are preserved
-func TestContextRenderPureJSON(t *testing.T) {
- w := httptest.NewRecorder()
- c, _ := CreateTestContext(w)
- c.PureJSON(http.StatusCreated, H{"foo": "bar", "html": ""})
- assert.Equal(t, http.StatusCreated, w.Code)
- assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\"}\n", w.Body.String())
- assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
-}
diff --git a/context_test.go b/context_test.go
index 0da5fbe6..e8dcd3dc 100644
--- a/context_test.go
+++ b/context_test.go
@@ -622,8 +622,7 @@ func TestContextGetCookie(t *testing.T) {
}
func TestContextBodyAllowedForStatus(t *testing.T) {
- // todo(thinkerou): go1.6 not support StatusProcessing
- assert.False(t, false, bodyAllowedForStatus(102))
+ assert.False(t, false, bodyAllowedForStatus(http.StatusProcessing))
assert.False(t, false, bodyAllowedForStatus(http.StatusNoContent))
assert.False(t, false, bodyAllowedForStatus(http.StatusNotModified))
assert.True(t, true, bodyAllowedForStatus(http.StatusInternalServerError))
@@ -661,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"))
}
@@ -689,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"))
}
@@ -715,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"))
}
@@ -794,6 +793,18 @@ func TestContextRenderNoContentAsciiJSON(t *testing.T) {
assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
}
+// Tests that the response is serialized as JSON
+// and Content-Type is set to application/json
+// and special HTML characters are preserved
+func TestContextRenderPureJSON(t *testing.T) {
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+ c.PureJSON(http.StatusCreated, H{"foo": "bar", "html": ""})
+ assert.Equal(t, http.StatusCreated, w.Code)
+ assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\"}\n", w.Body.String())
+ assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
+}
+
// Tests that the response executes the templates
// and responds with Content-Type set to text/html
func TestContextRenderHTML(t *testing.T) {
@@ -1092,9 +1103,7 @@ func TestContextRenderRedirectAll(t *testing.T) {
assert.Panics(t, func() { c.Redirect(299, "/resource") })
assert.Panics(t, func() { c.Redirect(309, "/resource") })
assert.NotPanics(t, func() { c.Redirect(http.StatusMultipleChoices, "/resource") })
- // todo(thinkerou): go1.6 not support StatusPermanentRedirect(308)
- // when we upgrade go version we can use http.StatusPermanentRedirect
- assert.NotPanics(t, func() { c.Redirect(308, "/resource") })
+ assert.NotPanics(t, func() { c.Redirect(http.StatusPermanentRedirect, "/resource") })
}
func TestContextNegotiationWithJSON(t *testing.T) {
@@ -1108,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"))
}
@@ -1272,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/debug.go b/debug.go
index 98c67cf7..19e380fb 100644
--- a/debug.go
+++ b/debug.go
@@ -8,13 +8,12 @@ import (
"bytes"
"fmt"
"html/template"
- "os"
"runtime"
"strconv"
"strings"
)
-const ginSupportMinGoVer = 6
+const ginSupportMinGoVer = 8
// IsDebugging returns true if the framework is running in debug mode.
// Use SetMode(gin.ReleaseMode) to disable debug mode.
@@ -54,7 +53,7 @@ func debugPrint(format string, values ...interface{}) {
if !strings.HasSuffix(format, "\n") {
format += "\n"
}
- fmt.Fprintf(os.Stderr, "[GIN-debug] "+format, values...)
+ fmt.Fprintf(DefaultWriter, "[GIN-debug] "+format, values...)
}
}
@@ -69,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.6 or later and Go 1.7 will be required soon.
+ debugPrint(`[WARNING] Now Gin requires Go 1.8 or later and Go 1.9 will be required soon.
`)
}
@@ -98,6 +97,8 @@ at initialization. ie. before any route is registered or the router is listening
func debugPrintError(err error) {
if err != nil {
- debugPrint("[ERROR] %v\n", err)
+ if IsDebugging() {
+ fmt.Fprintf(DefaultErrorWriter, "[GIN-debug] [ERROR] %v\n", err)
+ }
}
}
diff --git a/debug_test.go b/debug_test.go
index d338f0a0..9ace2989 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.6 or later and Go 1.7 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.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)
} else {
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
}
@@ -111,15 +111,15 @@ func captureOutput(t *testing.T, f func()) string {
if err != nil {
panic(err)
}
- stdout := os.Stdout
- stderr := os.Stderr
+ defaultWriter := DefaultWriter
+ defaultErrorWriter := DefaultErrorWriter
defer func() {
- os.Stdout = stdout
- os.Stderr = stderr
+ DefaultWriter = defaultWriter
+ DefaultErrorWriter = defaultErrorWriter
log.SetOutput(os.Stderr)
}()
- os.Stdout = writer
- os.Stderr = writer
+ DefaultWriter = writer
+ DefaultErrorWriter = writer
log.SetOutput(writer)
out := make(chan string)
wg := new(sync.WaitGroup)
diff --git a/gin.go b/gin.go
index be08f010..220f0401 100644
--- a/gin.go
+++ b/gin.go
@@ -373,6 +373,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
rPath = c.Request.URL.RawPath
unescape = engine.UnescapePathValues
}
+ rPath = cleanPath(rPath)
// Find root of the tree for the given HTTP method
t := engine.trees
diff --git a/ginS/gins.go b/ginS/gins.go
index 3ce4a6f6..3080fd34 100644
--- a/ginS/gins.go
+++ b/ginS/gins.go
@@ -118,7 +118,7 @@ func StaticFS(relativePath string, fs http.FileSystem) gin.IRoutes {
return engine().StaticFS(relativePath, fs)
}
-// Use attachs a global middleware to the router. ie. the middlewares attached though Use() will be
+// Use attaches a global middleware to the router. ie. the middlewares attached though Use() will be
// included in the handlers chain for every single request. Even 404, 405, static files...
// For example, this is the right place for a logger or error management middleware.
func Use(middlewares ...gin.HandlerFunc) gin.IRoutes {
@@ -153,7 +153,7 @@ func RunUnix(file string) (err error) {
// RunFd attaches the router to a http.Server and starts listening and serving HTTP requests
// through the specified file descriptor.
-// Note: thie method will block the calling goroutine indefinitely unless on error happens.
+// Note: the method will block the calling goroutine indefinitely unless on error happens.
func RunFd(fd int) (err error) {
return engine().RunFd(fd)
}
diff --git a/gin_integration_test.go b/gin_integration_test.go
index b80cbb24..9beec14d 100644
--- a/gin_integration_test.go
+++ b/gin_integration_test.go
@@ -8,6 +8,7 @@ import (
"bufio"
"crypto/tls"
"fmt"
+ "html/template"
"io/ioutil"
"net"
"net/http"
@@ -69,6 +70,42 @@ func TestRunTLS(t *testing.T) {
testRequest(t, "https://localhost:8443/example")
}
+func TestPusher(t *testing.T) {
+ var html = template.Must(template.New("https").Parse(`
+
+
+ Https Test
+
+
+
+ Welcome, Ginner!
+
+
+`))
+
+ router := New()
+ router.Static("./assets", "./assets")
+ router.SetHTMLTemplate(html)
+
+ go func() {
+ router.GET("/pusher", func(c *Context) {
+ if pusher := c.Writer.Pusher(); pusher != nil {
+ pusher.Push("/assets/app.js", nil)
+ }
+ c.String(http.StatusOK, "it worked")
+ })
+
+ assert.NoError(t, router.RunTLS(":8449", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
+ }()
+
+ // have to wait for the goroutine to start and run the server
+ // otherwise the main thread will complete
+ time.Sleep(5 * time.Millisecond)
+
+ assert.Error(t, router.RunTLS(":8449", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
+ testRequest(t, "https://localhost:8449/pusher")
+}
+
func TestRunEmptyWithEnv(t *testing.T) {
os.Setenv("PORT", "3123")
router := New()
diff --git a/go.mod b/go.mod
index 01227574..1c5e995c 100644
--- a/go.mod
+++ b/go.mod
@@ -4,14 +4,14 @@ go 1.12
require (
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3
- github.com/golang/protobuf v1.3.0
- github.com/json-iterator/go v1.1.5
- github.com/mattn/go-isatty v0.0.6
+ 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/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/codec v0.0.0-20190204201341-e444a5086c43
- golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95
+ github.com/ugorji/go v1.1.4
+ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c
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 84cf8378..58104682 100644
--- a/go.sum
+++ b/go.sum
@@ -2,12 +2,12 @@ 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/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk=
-github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
-github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE=
-github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
-github.com/mattn/go-isatty v0.0.6 h1:SrwhHcpV4nWrMGdNcC2kXpMfcBVYGDuTArqyhocJgvA=
-github.com/mattn/go-isatty v0.0.6/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+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/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,18 +17,15 @@ 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.2 h1:JON3E2/GPW2iDNGoSAusl1KDf5TRQ8k8q7Tp097pZGs=
-github.com/ugorji/go v1.1.2/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
-github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43 h1:BasDe+IErOQKrMVXab7UayvSlIpiyGwRvuX3EKYY7UA=
-github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43/go.mod h1:iT03XoTwV7xq/+UGwKO3UbC1nNNlopQiY61beSdrtOA=
-golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95 h1:fY7Dsw114eJN4boqzVSbpVHO6rTdhq6/GnXeu+PKnzU=
-golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+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=
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=
-google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+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/logger.go b/logger.go
index 198a0192..5ab4639e 100644
--- a/logger.go
+++ b/logger.go
@@ -136,6 +136,10 @@ var defaultLogFormatter = func(param LogFormatterParams) string {
resetColor = param.ResetColor()
}
+ if param.Latency > time.Minute {
+ // Truncate in a golang < 1.8 safe way
+ param.Latency = param.Latency - param.Latency%time.Second
+ }
return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s",
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
statusColor, param.StatusCode, resetColor,
diff --git a/logger_test.go b/logger_test.go
index 11a859e6..9177e1d9 100644
--- a/logger_test.go
+++ b/logger_test.go
@@ -253,10 +253,34 @@ func TestDefaultLogFormatter(t *testing.T) {
ErrorMessage: "",
isTerm: true,
}
+ termTrueLongDurationParam := LogFormatterParams{
+ TimeStamp: timeStamp,
+ StatusCode: 200,
+ Latency: time.Millisecond * 9876543210,
+ ClientIP: "20.20.20.20",
+ Method: "GET",
+ Path: "/",
+ ErrorMessage: "",
+ isTerm: true,
+ }
+
+ termFalseLongDurationParam := LogFormatterParams{
+ TimeStamp: timeStamp,
+ StatusCode: 200,
+ Latency: time.Millisecond * 9876543210,
+ ClientIP: "20.20.20.20",
+ Method: "GET",
+ Path: "/",
+ ErrorMessage: "",
+ isTerm: false,
+ }
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET /\n", defaultLogFormatter(termFalseParam))
+ assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 2743h29m3s | 20.20.20.20 | GET /\n", defaultLogFormatter(termFalseLongDurationParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 5s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m /\n", defaultLogFormatter(termTrueParam))
+ assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 2743h29m3s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m /\n", defaultLogFormatter(termTrueLongDurationParam))
+
}
func TestColorForMethod(t *testing.T) {
@@ -345,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\n{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))
+ assert.Equal(t, strings.Replace("hola\n{\"foo\":\"bar\"}\n{\"foo\":\"bar\"}\nevent:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))
}
diff --git a/recovery.go b/recovery.go
index 9e893e1b..bc946c03 100644
--- a/recovery.go
+++ b/recovery.go
@@ -53,11 +53,18 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc {
if logger != nil {
stack := stack(3)
httpRequest, _ := httputil.DumpRequest(c.Request, false)
+ headers := strings.Split(string(httpRequest), "\r\n")
+ for idx, header := range headers {
+ current := strings.Split(header, ":")
+ if current[0] == "Authorization" {
+ headers[idx] = current[0] + ": *"
+ }
+ }
if brokenPipe {
logger.Printf("%s\n%s%s", err, string(httpRequest), reset)
} else if IsDebugging() {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
- timeFormat(time.Now()), string(httpRequest), err, stack, reset)
+ timeFormat(time.Now()), strings.Join(headers, "\r\n"), err, stack, reset)
} else {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
timeFormat(time.Now()), err, stack, reset)
diff --git a/recovery_test.go b/recovery_test.go
index 0a6d6271..21a0a480 100644
--- a/recovery_test.go
+++ b/recovery_test.go
@@ -2,12 +2,11 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
-// +build go1.7
-
package gin
import (
"bytes"
+ "fmt"
"net"
"net/http"
"os"
@@ -18,6 +17,37 @@ import (
"github.com/stretchr/testify/assert"
)
+func TestPanicClean(t *testing.T) {
+ buffer := new(bytes.Buffer)
+ router := New()
+ password := "my-super-secret-password"
+ router.Use(RecoveryWithWriter(buffer))
+ router.GET("/recovery", func(c *Context) {
+ c.AbortWithStatus(http.StatusBadRequest)
+ panic("Oupps, Houston, we have a problem")
+ })
+ // RUN
+ w := performRequest(router, "GET", "/recovery",
+ header{
+ Key: "Host",
+ Value: "www.google.com",
+ },
+ header{
+ Key: "Authorization",
+ Value: fmt.Sprintf("Bearer %s", password),
+ },
+ header{
+ Key: "Content-Type",
+ Value: "application/json",
+ },
+ )
+ // TEST
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+
+ // Check the buffer does not have the secret key
+ assert.NotContains(t, buffer.String(), password)
+}
+
// TestPanicInHandler assert that panic has been recovered.
func TestPanicInHandler(t *testing.T) {
buffer := new(bytes.Buffer)
diff --git a/render/json.go b/render/json.go
index c7cf330e..2b07cba0 100644
--- a/render/json.go
+++ b/render/json.go
@@ -43,6 +43,11 @@ type AsciiJSON struct {
// SecureJSONPrefix is a string which represents SecureJSON prefix.
type SecureJSONPrefix string
+// PureJSON contains the given interface object.
+type PureJSON struct {
+ Data interface{}
+}
+
var jsonContentType = []string{"application/json; charset=utf-8"}
var jsonpContentType = []string{"application/javascript; charset=utf-8"}
var jsonAsciiContentType = []string{"application/json"}
@@ -63,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
}
@@ -174,3 +176,16 @@ func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
func (r AsciiJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonAsciiContentType)
}
+
+// Render (PureJSON) writes custom ContentType and encodes the given interface object.
+func (r PureJSON) Render(w http.ResponseWriter) error {
+ r.WriteContentType(w)
+ encoder := json.NewEncoder(w)
+ encoder.SetEscapeHTML(false)
+ return encoder.Encode(r.Data)
+}
+
+// WriteContentType (PureJSON) writes custom ContentType.
+func (r PureJSON) WriteContentType(w http.ResponseWriter) {
+ writeContentType(w, jsonContentType)
+}
diff --git a/render/json_17.go b/render/json_17.go
deleted file mode 100644
index 208193c7..00000000
--- a/render/json_17.go
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright 2018 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.
-
-// +build go1.7
-
-package render
-
-import (
- "net/http"
-
- "github.com/gin-gonic/gin/internal/json"
-)
-
-// PureJSON contains the given interface object.
-type PureJSON struct {
- Data interface{}
-}
-
-// Render (PureJSON) writes custom ContentType and encodes the given interface object.
-func (r PureJSON) Render(w http.ResponseWriter) error {
- r.WriteContentType(w)
- encoder := json.NewEncoder(w)
- encoder.SetEscapeHTML(false)
- return encoder.Encode(r.Data)
-}
-
-// WriteContentType (PureJSON) writes custom ContentType.
-func (r PureJSON) WriteContentType(w http.ResponseWriter) {
- writeContentType(w, jsonContentType)
-}
diff --git a/render/redirect.go b/render/redirect.go
index 9c145fe2..c006691c 100644
--- a/render/redirect.go
+++ b/render/redirect.go
@@ -18,9 +18,7 @@ type Redirect struct {
// Render (Redirect) redirects the http request to new location and writes redirect response.
func (r Redirect) Render(w http.ResponseWriter) error {
- // todo(thinkerou): go1.6 not support StatusPermanentRedirect(308)
- // when we upgrade go version we can use http.StatusPermanentRedirect
- if (r.Code < 300 || r.Code > 308) && r.Code != 201 {
+ if (r.Code < http.StatusMultipleChoices || r.Code > http.StatusPermanentRedirect) && r.Code != http.StatusCreated {
panic(fmt.Sprintf("Cannot redirect with status code %d", r.Code))
}
http.Redirect(w, r.Request, r.Location, r.Code)
diff --git a/render/render_17_test.go b/render/render_17_test.go
deleted file mode 100644
index 68330090..00000000
--- a/render/render_17_test.go
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright 2018 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.
-
-// +build go1.7
-
-package render
-
-import (
- "net/http/httptest"
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestRenderPureJSON(t *testing.T) {
- w := httptest.NewRecorder()
- data := map[string]interface{}{
- "foo": "bar",
- "html": "",
- }
- err := (PureJSON{data}).Render(w)
- assert.NoError(t, err)
- assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\"}\n", w.Body.String())
- assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
-}
diff --git a/render/render_test.go b/render/render_test.go
index 76e29eeb..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"))
}
@@ -215,6 +215,18 @@ func TestRenderAsciiJSONFail(t *testing.T) {
assert.Error(t, (AsciiJSON{data}).Render(w))
}
+func TestRenderPureJSON(t *testing.T) {
+ w := httptest.NewRecorder()
+ data := map[string]interface{}{
+ "foo": "bar",
+ "html": "",
+ }
+ err := (PureJSON{data}).Render(w)
+ assert.NoError(t, err)
+ assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\"}\n", w.Body.String())
+ assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
+}
+
type xmlmap map[string]interface{}
// Allows type H to be used with xml.Marshal
diff --git a/response_writer.go b/response_writer.go
index 923b53f8..26826689 100644
--- a/response_writer.go
+++ b/response_writer.go
@@ -16,7 +16,8 @@ const (
defaultStatus = http.StatusOK
)
-type responseWriterBase interface {
+// ResponseWriter ...
+type ResponseWriter interface {
http.ResponseWriter
http.Hijacker
http.Flusher
@@ -37,6 +38,9 @@ type responseWriterBase interface {
// Forces to write the http header (status code + headers).
WriteHeaderNow()
+
+ // get the http.Pusher for server push
+ Pusher() http.Pusher
}
type responseWriter struct {
@@ -113,3 +117,10 @@ func (w *responseWriter) Flush() {
w.WriteHeaderNow()
w.ResponseWriter.(http.Flusher).Flush()
}
+
+func (w *responseWriter) Pusher() (pusher http.Pusher) {
+ if pusher, ok := w.ResponseWriter.(http.Pusher); ok {
+ return pusher
+ }
+ return nil
+}
diff --git a/response_writer_1.7.go b/response_writer_1.7.go
deleted file mode 100644
index 801d196b..00000000
--- a/response_writer_1.7.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// +build !go1.8
-
-// Copyright 2018 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 gin
-
-// ResponseWriter ...
-type ResponseWriter interface {
- responseWriterBase
-}
diff --git a/response_writer_1.8.go b/response_writer_1.8.go
deleted file mode 100644
index 527c0038..00000000
--- a/response_writer_1.8.go
+++ /dev/null
@@ -1,25 +0,0 @@
-// +build go1.8
-
-// Copyright 2018 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 gin
-
-import (
- "net/http"
-)
-
-// ResponseWriter ...
-type ResponseWriter interface {
- responseWriterBase
- // get the http.Pusher for server push
- Pusher() http.Pusher
-}
-
-func (w *responseWriter) Pusher() (pusher http.Pusher) {
- if pusher, ok := w.ResponseWriter.(http.Pusher); ok {
- return pusher
- }
- return nil
-}
diff --git a/routes_test.go b/routes_test.go
index f0e259a5..457c923e 100644
--- a/routes_test.go
+++ b/routes_test.go
@@ -22,7 +22,7 @@ type header struct {
}
func performRequest(r http.Handler, method, path string, headers ...header) *httptest.ResponseRecorder {
- req, _ := http.NewRequest(method, path, nil)
+ req := httptest.NewRequest(method, path, nil)
for _, h := range headers {
req.Header.Add(h.Key, h.Value)
}
@@ -257,6 +257,39 @@ func TestRouteParamsByName(t *testing.T) {
assert.Equal(t, "/is/super/great", wild)
}
+// TestContextParamsGet tests that a parameter can be parsed from the URL even with extra slashes.
+func TestRouteParamsByNameWithExtraSlash(t *testing.T) {
+ name := ""
+ lastName := ""
+ wild := ""
+ router := New()
+ router.GET("/test/:name/:last_name/*wild", func(c *Context) {
+ name = c.Params.ByName("name")
+ lastName = c.Params.ByName("last_name")
+ var ok bool
+ wild, ok = c.Params.Get("wild")
+
+ assert.True(t, ok)
+ assert.Equal(t, name, c.Param("name"))
+ assert.Equal(t, name, c.Param("name"))
+ assert.Equal(t, lastName, c.Param("last_name"))
+
+ assert.Empty(t, c.Param("wtf"))
+ assert.Empty(t, c.Params.ByName("wtf"))
+
+ wtf, ok := c.Params.Get("wtf")
+ assert.Empty(t, wtf)
+ assert.False(t, ok)
+ })
+
+ w := performRequest(router, "GET", "//test//john//smith//is//super//great")
+
+ assert.Equal(t, http.StatusOK, w.Code)
+ assert.Equal(t, "john", name)
+ assert.Equal(t, "smith", lastName)
+ assert.Equal(t, "/is/super/great", wild)
+}
+
// TestHandleStaticFile - ensure the static file handles properly
func TestRouteStaticFile(t *testing.T) {
// SETUP file
@@ -386,15 +419,14 @@ func TestRouterNotFound(t *testing.T) {
code int
location string
}{
- {"/path/", http.StatusMovedPermanently, "/path"}, // TSR -/
- {"/dir", http.StatusMovedPermanently, "/dir/"}, // TSR +/
- {"", http.StatusMovedPermanently, "/"}, // TSR +/
- {"/PATH", http.StatusMovedPermanently, "/path"}, // Fixed Case
- {"/DIR/", http.StatusMovedPermanently, "/dir/"}, // Fixed Case
- {"/PATH/", http.StatusMovedPermanently, "/path"}, // Fixed Case -/
- {"/DIR", http.StatusMovedPermanently, "/dir/"}, // Fixed Case +/
- {"/../path", http.StatusMovedPermanently, "/path"}, // CleanPath
- {"/nope", http.StatusNotFound, ""}, // NotFound
+ {"/path/", http.StatusMovedPermanently, "/path"}, // TSR -/
+ {"/dir", http.StatusMovedPermanently, "/dir/"}, // TSR +/
+ {"/PATH", http.StatusMovedPermanently, "/path"}, // Fixed Case
+ {"/DIR/", http.StatusMovedPermanently, "/dir/"}, // Fixed Case
+ {"/PATH/", http.StatusMovedPermanently, "/path"}, // Fixed Case -/
+ {"/DIR", http.StatusMovedPermanently, "/dir/"}, // Fixed Case +/
+ {"/../path", http.StatusOK, ""}, // CleanPath
+ {"/nope", http.StatusNotFound, ""}, // NotFound
}
for _, tr := range testRoutes {
w := performRequest(router, "GET", tr.route)
diff --git a/tree.go b/tree.go
index b9e272f9..9a789f2f 100644
--- a/tree.go
+++ b/tree.go
@@ -532,7 +532,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...)
@@ -636,7 +636,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
}
diff --git a/vendor/vendor.json b/vendor/vendor.json
index 6050e8f6..4de0bfd1 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -1,5 +1,5 @@
{
- "comment": "v1.3.0",
+ "comment": "v1.4.0",
"ignore": "test",
"package": [
{
@@ -13,32 +13,44 @@
{
"checksumSHA1": "QeKwBtN2df+j+4stw3bQJ6yO4EY=",
"path": "github.com/gin-contrib/sse",
- "revision": "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae",
- "revisionTime": "2017-01-09T09:34:21Z"
+ "revision": "5545eab6dad3bbbd6c5ae9186383c2a9d23c0dae",
+ "revisionTime": "2019-03-01T06:25:29Z"
},
{
- "checksumSHA1": "mE9XW26JSpe4meBObM6J/Oeq0eg=",
+ "checksumSHA1": "Y2MOwzNZfl4NRNDbLCZa6sgx7O0=",
"path": "github.com/golang/protobuf/proto",
- "revision": "aa810b61a9c79d51363740d207bb46cf8e620ed5",
- "revisionTime": "2018-08-14T21:14:27Z",
- "version": "v1.2",
- "versionExact": "v1.2.0"
+ "revision": "c823c79ea1570fb5ff454033735a8e68575d1d0f",
+ "revisionTime": "2019-02-05T22:20:52Z",
+ "version": "v1.3",
+ "versionExact": "v1.3.0"
},
{
- "checksumSHA1": "WqeEgS7pqqkwK8mlrAZmDgtWJMY=",
+ "checksumSHA1": "TB2vxux9xQbvsTHOVt4aRTuvSn4=",
"path": "github.com/json-iterator/go",
- "revision": "1624edc4454b8682399def8740d46db5e4362ba4",
- "revisionTime": "2018-08-06T06:07:27Z",
+ "revision": "0ff49de124c6f76f8494e194af75bde0f1a49a29",
+ "revisionTime": "2019-03-06T14:29:09Z",
"version": "v1.1",
- "versionExact": "v1.1.5"
+ "versionExact": "v1.1.6"
},
{
- "checksumSHA1": "w5RcOnfv5YDr3j2bd1YydkPiZx4=",
+ "checksumSHA1": "Ya+baVBU/RkXXUWD3LGFmGJiiIg=",
"path": "github.com/mattn/go-isatty",
- "revision": "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c",
- "revisionTime": "2017-11-07T05:05:31Z",
+ "revision": "c2a7a6ca930a4cd0bc33a3f298eb71960732a3a7",
+ "revisionTime": "2019-03-12T13:58:54Z",
"version": "v0.0",
- "versionExact": "v0.0.4"
+ "versionExact": "v0.0.7"
+ },
+ {
+ "checksumSHA1": "ZTcgWKWHsrX0RXYVXn5Xeb8Q0go=",
+ "path": "github.com/modern-go/concurrent",
+ "revision": "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94",
+ "revisionTime": "2018-03-06T01:26:44Z"
+ },
+ {
+ "checksumSHA1": "qvH48wzTIV3QKSDqI0dLFtVjaDI=",
+ "path": "github.com/modern-go/reflect2",
+ "revision": "94122c33edd36123c84d5368cfb2b69df93a0ec8",
+ "revisionTime": "2018-07-18T01:23:57Z"
},
{
"checksumSHA1": "LuFv4/jlrmFNnDb/5SCSEPAM9vU=",
@@ -46,6 +58,20 @@
"revision": "5d4384ee4fb2527b0a1256a821ebfc92f91efefc",
"revisionTime": "2018-12-26T10:54:42Z"
},
+ {
+ "checksumSHA1": "cpNsoLqBprpKh+VZTBOZNVXzBEk=",
+ "path": "github.com/stretchr/objx",
+ "revision": "c61a9dfcced1815e7d40e214d00d1a8669a9f58c",
+ "revisionTime": "2019-02-11T16:23:28Z"
+ },
+ {
+ "checksumSHA1": "DBdcVxnvaINHhWyyGgih/Mel6gE=",
+ "path": "github.com/stretchr/testify",
+ "revision": "ffdc059bfe9ce6a4e144ba849dbedead332c6053",
+ "revisionTime": "2018-12-05T02:12:43Z",
+ "version": "v1.3",
+ "versionExact": "v1.3.0"
+ },
{
"checksumSHA1": "c6pbpF7eowwO59phRTpF8cQ80Z0=",
"path": "github.com/stretchr/testify/assert",
@@ -55,24 +81,24 @@
"versionExact": "v1.2.2"
},
{
- "checksumSHA1": "5Bd8RPhhaKcEXkagzPqymP4Gx5E=",
+ "checksumSHA1": "csplo594qomjp2IZj82y7mTueOw=",
"path": "github.com/ugorji/go/codec",
- "revision": "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab",
- "revisionTime": "2018-04-07T10:07:33Z",
+ "revision": "2adff0894ba3bc2eeb9f9aea45fefd49802e1a13",
+ "revisionTime": "2019-04-08T19:08:48Z",
"version": "v1.1",
- "versionExact": "v1.1.1"
+ "versionExact": "v1.1.4"
},
{
"checksumSHA1": "GtamqiJoL7PGHsN454AoffBFMa8=",
"path": "golang.org/x/net/context",
- "revision": "49bb7cea24b1df9410e1712aa6433dae904ff66a",
- "revisionTime": "2018-10-11T05:27:23Z"
+ "revision": "f4e77d36d62c17c2336347bb2670ddbd02d092b7",
+ "revisionTime": "2019-05-02T22:26:14Z"
},
{
- "checksumSHA1": "SiJNkx+YGtq3Gtr6Ldu6OW83O+U=",
+ "checksumSHA1": "2gaep1KNRDNyDA3O+KgPTQsGWvs=",
"path": "golang.org/x/sys/unix",
- "revision": "fa43e7bc11baaae89f3f902b2b4d832b68234844",
- "revisionTime": "2018-10-11T14:35:51Z"
+ "revision": "a43fa875dd822b81eb6d2ad538bc1f4caba169bd",
+ "revisionTime": "2019-05-02T15:41:39Z"
},
{
"checksumSHA1": "P/k5ZGf0lEBgpKgkwy++F7K1PSg=",
@@ -83,13 +109,13 @@
"versionExact": "v8.18.2"
},
{
- "checksumSHA1": "ZSWoOPUNRr5+3dhkLK3C4cZAQPk=",
+ "checksumSHA1": "QqDq2x8XOU7IoOR98Cx1eiV5QY8=",
"path": "gopkg.in/yaml.v2",
- "revision": "5420a8b6744d3b0345ab293f6fcba19c978f1183",
- "revisionTime": "2018-03-28T19:50:20Z",
+ "revision": "51d6538a90f86fe93ac480b35f37b2be17fef232",
+ "revisionTime": "2018-11-15T11:05:04Z",
"version": "v2.2",
- "versionExact": "v2.2.1"
+ "versionExact": "v2.2.2"
}
],
"rootPath": "github.com/gin-gonic/gin"
-}
+}
\ No newline at end of file