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 b46c5637..3e817a78 100644
--- a/README.md
+++ b/README.md
@@ -13,35 +13,122 @@
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
+```
+
+## Prerequisite
+
+Now Gin requires Go 1.6 or later and Go 1.7 will be required soon.
+
+## Quick start
+
+```sh
+# assume the following codes in example.go file
+$ cat example.go
+```
```go
package main
@@ -59,8 +146,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 +153,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,30 +192,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)
-All documentation is available on the Gin website.
+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.
-- [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/)
+```sh
+$ go build -tags=jsoniter .
+```
-## Examples
+## API 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.
+You can find a number of ready-to-run examples at [Gin examples repository](https://github.com/gin-gonic/examples).
+
+### 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..73bb7700 100644
--- a/binding/binding_test.go
+++ b/binding/binding_test.go
@@ -114,71 +114,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"`
@@ -335,110 +270,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")
@@ -1076,149 +907,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 +1142,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..aaacf6c5 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,7 +70,7 @@ func mapping(value reflect.Value, field reflect.StructField, form map[string][]s
return isSetted, nil
}
- ok, err := tryToSetValue(value, field, form, tag)
+ ok, err := tryToSetValue(value, field, setter, tag)
if err != nil {
return false, err
}
@@ -97,7 +86,7 @@ func mapping(value reflect.Value, field reflect.StructField, form map[string][]s
if !value.Field(i).CanSet() {
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 +97,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, ",")
@@ -132,25 +126,29 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, form map[stri
k, v := head(opt, "=")
switch k {
case "default":
- isDefaultExists = true
- defaultValue = v
+ 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 +157,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 5dc7f8a0..af747a1e 100644
--- a/context.go
+++ b/context.go
@@ -439,11 +439,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
}
@@ -462,13 +457,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.
@@ -828,6 +817,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..490e4490 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))
@@ -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) {
diff --git a/debug.go b/debug.go
index 98c67cf7..6d40a5da 100644
--- a/debug.go
+++ b/debug.go
@@ -14,7 +14,7 @@ import (
"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.
@@ -69,7 +69,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.
`)
}
diff --git a/debug_test.go b/debug_test.go
index d338f0a0..86a67773 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)
}
diff --git a/doc.go b/doc.go
index 01ac4a90..1bd03864 100644
--- a/doc.go
+++ b/doc.go
@@ -1,6 +1,6 @@
/*
Package gin implements a HTTP web framework called gin.
-See https://gin-gonic.github.io/gin/ for more information about gin.
+See https://gin-gonic.com/ for more information about gin.
*/
package gin // import "github.com/gin-gonic/gin"
diff --git a/ginS/gins.go b/ginS/gins.go
index 0f08645a..3080fd34 100644
--- a/ginS/gins.go
+++ b/ginS/gins.go
@@ -118,30 +118,42 @@ 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 {
return engine().Use(middlewares...)
}
-// Run : The router is attached to a http.Server and starts listening and serving HTTP requests.
+// Routes returns a slice of registered routes.
+func Routes() gin.RoutesInfo {
+ return engine().Routes()
+}
+
+// Run attaches to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func Run(addr ...string) (err error) {
return engine().Run(addr...)
}
-// RunTLS : The router is attached to a http.Server and starts listening and serving HTTPS requests.
+// RunTLS attaches to a http.Server and starts listening and serving HTTPS requests.
// It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func RunTLS(addr, certFile, keyFile string) (err error) {
return engine().RunTLS(addr, certFile, keyFile)
}
-// RunUnix : The router is attached to a http.Server and starts listening and serving HTTP requests
+// RunUnix attaches to a http.Server and starts listening and serving HTTP requests
// through the specified unix socket (ie. a file)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func RunUnix(file string) (err error) {
return engine().RunUnix(file)
}
+
+// RunFd attaches the router to a http.Server and starts listening and serving HTTP requests
+// through the specified file descriptor.
+// 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/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..56bb3a00 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) {
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..18f27fa9 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"}
@@ -174,3 +179,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..3aa5dbcc 100644
--- a/render/render_test.go
+++ b/render/render_test.go
@@ -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/testdata/assets/console.png b/testdata/assets/console.png
deleted file mode 100644
index 7a695718..00000000
Binary files a/testdata/assets/console.png and /dev/null differ
diff --git a/vendor/vendor.json b/vendor/vendor.json
index 6050e8f6..fc7bb11d 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,16 +13,16 @@
{
"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=",
@@ -33,12 +33,24 @@
"versionExact": "v1.1.5"
},
{
- "checksumSHA1": "w5RcOnfv5YDr3j2bd1YydkPiZx4=",
+ "checksumSHA1": "rrXDDvz+nQ2KRLQk6nxWaE5Zj1U=",
"path": "github.com/mattn/go-isatty",
- "revision": "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c",
- "revisionTime": "2017-11-07T05:05:31Z",
+ "revision": "369ecd8cea9851e459abb67eb171853e3986591e",
+ "revisionTime": "2019-02-25T17:38:24Z",
"version": "v0.0",
- "versionExact": "v0.0.4"
+ "versionExact": "v0.0.6"
+ },
+ {
+ "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,12 +81,26 @@
"versionExact": "v1.2.2"
},
{
- "checksumSHA1": "5Bd8RPhhaKcEXkagzPqymP4Gx5E=",
+ "checksumSHA1": "fg3TzS9/QK3wZbzei3Z6O8XPLHg=",
+ "path": "github.com/stretchr/testify/http",
+ "revision": "ffdc059bfe9ce6a4e144ba849dbedead332c6053",
+ "revisionTime": "2018-12-05T02:12:43Z",
+ "version": "v1.3",
+ "versionExact": "v1.3.0"
+ },
+ {
+ "checksumSHA1": "lsdl3fgOiM4Iuy7xjTQxiBtAwB0=",
+ "path": "github.com/stretchr/testify/mock",
+ "revision": "ffdc059bfe9ce6a4e144ba849dbedead332c6053",
+ "revisionTime": "2018-12-05T02:12:43Z",
+ "version": "v1.3",
+ "versionExact": "v1.3.0"
+ },
+ {
+ "checksumSHA1": "WIhpR3EKGueRSJsYOZ6PIsfL4SI=",
"path": "github.com/ugorji/go/codec",
- "revision": "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab",
- "revisionTime": "2018-04-07T10:07:33Z",
- "version": "v1.1",
- "versionExact": "v1.1.1"
+ "revision": "e444a5086c436778cf9281a7059a3d58b9e17935",
+ "revisionTime": "2019-02-04T20:13:41Z"
},
{
"checksumSHA1": "GtamqiJoL7PGHsN454AoffBFMa8=",
@@ -83,13 +123,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
diff --git a/version.go b/version.go
index 028caebe..07e7859f 100644
--- a/version.go
+++ b/version.go
@@ -5,4 +5,4 @@
package gin
// Version is the current gin framework's version.
-const Version = "v1.4.0-dev"
+const Version = "v1.4.0"