diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..9d49aa41 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,13 @@ +- With issues: + - Use the search tool before opening a new issue. + - Please provide source code and commit sha if you found a bug. + - Review existing issues and provide feedback or react to them. + +- go version: +- gin version (or commit ref): +- operating system: + +## Description + +## Screenshots + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..8630bc35 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,7 @@ +- With pull requests: + - Open your pull request against `master` + - Your pull request should have no more than two commits, if not you should squash them. + - It should pass all tests in the available continuous integrations systems such as TravisCI. + - You should add/modify tests to cover your proposed code changes. + - If your pull request contains a new feature, please document it on the README. + diff --git a/.travis.yml b/.travis.yml index e9101568..2eeb0b3d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,13 +6,25 @@ go: - 1.8.x - 1.9.x - 1.10.x + - 1.11.x - master +matrix: + fast_finish: true + include: + - go: 1.11.x + env: GO111MODULE=on + git: depth: 10 +before_install: + - if [[ "${GO111MODULE}" = "on" ]]; then mkdir "${HOME}/go"; export GOPATH="${HOME}/go"; fi + install: - - make install + - if [[ "${GO111MODULE}" = "on" ]]; then go mod download; else make install; fi + - if [[ "${GO111MODULE}" = "on" ]]; then export PATH="${GOPATH}/bin:${GOROOT}/bin:${PATH}"; fi + - if [[ "${GO111MODULE}" = "on" ]]; then make tools; fi go_import_path: github.com/gin-gonic/gin diff --git a/AUTHORS.md b/AUTHORS.md index 7ab7213d..dda19bcf 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -1,8 +1,12 @@ List of all the awesome people working to make Gin the best Web Framework in Go. +## gin 1.x series authors + +**Gin Core Team:** Bo-Yi Wu (@appleboy), 田欧 (@thinkerou), Javier Provecho (@javierprovecho) + ## gin 0.x series authors -**Maintainer:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho) +**Maintainers:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho) People and companies, who have contributed, in alphabetical order. diff --git a/CHANGELOG.md b/CHANGELOG.md index ee485ec3..e6a108ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,28 @@ # CHANGELOG -### Gin 1.2 +### Gin 1.3.0 + +- [NEW] Add [`func (*Context) QueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.QueryMap), [`func (*Context) GetQueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetQueryMap), [`func (*Context) PostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.PostFormMap) and [`func (*Context) GetPostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetPostFormMap) to support `type map[string]string` as query string or form parameters, see [#1383](https://github.com/gin-gonic/gin/pull/1383) +- [NEW] Add [`func (*Context) AsciiJSON`](https://godoc.org/github.com/gin-gonic/gin#Context.AsciiJSON), see [#1358](https://github.com/gin-gonic/gin/pull/1358) +- [NEW] Add `Pusher()` in [`type ResponseWriter`](https://godoc.org/github.com/gin-gonic/gin#ResponseWriter) for supporting http2 push, see [#1273](https://github.com/gin-gonic/gin/pull/1273) +- [NEW] Add [`func (*Context) DataFromReader`](https://godoc.org/github.com/gin-gonic/gin#Context.DataFromReader) for serving dynamic data, see [#1304](https://github.com/gin-gonic/gin/pull/1304) +- [NEW] Add [`func (*Context) ShouldBindBodyWith`](https://godoc.org/github.com/gin-gonic/gin#Context.ShouldBindBodyWith) allowing to call binding multiple times, see [#1341](https://github.com/gin-gonic/gin/pull/1341) +- [NEW] Support pointers in form binding, see [#1336](https://github.com/gin-gonic/gin/pull/1336) +- [NEW] Add [`func (*Context) JSONP`](https://godoc.org/github.com/gin-gonic/gin#Context.JSONP), see [#1333](https://github.com/gin-gonic/gin/pull/1333) +- [NEW] Support default value in form binding, see [#1138](https://github.com/gin-gonic/gin/pull/1138) +- [NEW] Expose validator engine in [`type StructValidator`](https://godoc.org/github.com/gin-gonic/gin/binding#StructValidator), see [#1277](https://github.com/gin-gonic/gin/pull/1277) +- [NEW] Add [`func (*Context) ShouldBind`](https://godoc.org/github.com/gin-gonic/gin#Context.ShouldBind), [`func (*Context) ShouldBindQuery`](https://godoc.org/github.com/gin-gonic/gin#Context.ShouldBindQuery) and [`func (*Context) ShouldBindJSON`](https://godoc.org/github.com/gin-gonic/gin#Context.ShouldBindJSON), see [#1047](https://github.com/gin-gonic/gin/pull/1047) +- [NEW] Add support for `time.Time` location in form binding, see [#1117](https://github.com/gin-gonic/gin/pull/1117) +- [NEW] Add [`func (*Context) BindQuery`](https://godoc.org/github.com/gin-gonic/gin#Context.BindQuery), see [#1029](https://github.com/gin-gonic/gin/pull/1029) +- [NEW] Make [jsonite](https://github.com/json-iterator/go) optional with build tags, see [#1026](https://github.com/gin-gonic/gin/pull/1026) +- [NEW] Show query string in logger, see [#999](https://github.com/gin-gonic/gin/pull/999) +- [NEW] Add [`func (*Context) SecureJSON`](https://godoc.org/github.com/gin-gonic/gin#Context.SecureJSON), see [#987](https://github.com/gin-gonic/gin/pull/987) and [#993](https://github.com/gin-gonic/gin/pull/993) +- [DEPRECATE] `func (*Context) GetCookie` for [`func (*Context) Cookie`](https://godoc.org/github.com/gin-gonic/gin#Context.Cookie) +- [FIX] Don't display color tags if [`func DisableConsoleColor`](https://godoc.org/github.com/gin-gonic/gin#DisableConsoleColor) called, see [#1072](https://github.com/gin-gonic/gin/pull/1072) +- [FIX] Gin Mode `""` when calling [`func Mode`](https://godoc.org/github.com/gin-gonic/gin#Mode) now returns `const DebugMode`, see [#1250](https://github.com/gin-gonic/gin/pull/1250) +- [FIX] `Flush()` now doesn't overwrite `responseWriter` status code, see [#1460](https://github.com/gin-gonic/gin/pull/1460) + +### Gin 1.2.0 - [NEW] Switch from godeps to govendor - [NEW] Add support for Let's Encrypt via gin-gonic/autotls diff --git a/Makefile b/Makefile index 5468563a..b698ac09 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,9 @@ +GO ?= go GOFMT ?= gofmt "-s" -PACKAGES ?= $(shell go list ./... | grep -v /vendor/) +PACKAGES ?= $(shell $(GO) list ./... | grep -v /vendor/) +VETPACKAGES ?= $(shell $(GO) list ./... | grep -v /vendor/ | grep -v /examples/) GOFILES := $(shell find . -name "*.go" -type f -not -path "./vendor/*") +TESTFOLDER := $(shell $(GO) list ./... | grep -E 'gin$$|binding$$|render$$' | grep -v examples) all: install @@ -9,7 +12,14 @@ install: deps .PHONY: test test: - sh coverage.sh + echo "mode: count" > coverage.out + for d in $(TESTFOLDER); do \ + $(GO) test -v -covermode=count -coverprofile=profile.out $$d; \ + if [ -f profile.out ]; then \ + cat profile.out | grep -v "mode:" >> coverage.out; \ + rm profile.out; \ + fi; \ + done .PHONY: fmt fmt: @@ -17,7 +27,6 @@ fmt: .PHONY: fmt-check fmt-check: - # get all go files and run go fmt on them @diff=$$($(GOFMT) -d $(GOFILES)); \ if [ -n "$$diff" ]; then \ echo "Please run 'make fmt' and commit the result:"; \ @@ -26,14 +35,14 @@ fmt-check: fi; vet: - go vet $(PACKAGES) + $(GO) vet $(VETPACKAGES) deps: @hash govendor > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ - go get -u github.com/kardianos/govendor; \ + $(GO) get -u github.com/kardianos/govendor; \ fi @hash embedmd > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ - go get -u github.com/campoy/embedmd; \ + $(GO) get -u github.com/campoy/embedmd; \ fi embedmd: @@ -42,20 +51,26 @@ embedmd: .PHONY: lint lint: @hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ - go get -u github.com/golang/lint/golint; \ + $(GO) get -u golang.org/x/lint/golint; \ fi for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done; .PHONY: misspell-check misspell-check: @hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ - go get -u github.com/client9/misspell/cmd/misspell; \ + $(GO) get -u github.com/client9/misspell/cmd/misspell; \ fi misspell -error $(GOFILES) .PHONY: misspell misspell: @hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ - go get -u github.com/client9/misspell/cmd/misspell; \ + $(GO) get -u github.com/client9/misspell/cmd/misspell; \ fi misspell -w $(GOFILES) + +.PHONY: tools +tools: + go install golang.org/x/lint/golint; \ + go install github.com/client9/misspell/cmd/misspell; \ + go install github.com/campoy/embedmd; diff --git a/README.md b/README.md index fd5c1755..3f0c9170 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,13 @@ [![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin) - [![codecov](https://codecov.io/gh/gin-gonic/gin/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-gonic/gin) - [![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/gin)](https://goreportcard.com/report/github.com/gin-gonic/gin) - [![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin) - [![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![codecov](https://codecov.io/gh/gin-gonic/gin/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-gonic/gin) +[![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/gin)](https://goreportcard.com/report/github.com/gin-gonic/gin) +[![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin) +[![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Sourcegraph](https://sourcegraph.com/github.com/gin-gonic/gin/-/badge.svg)](https://sourcegraph.com/github.com/gin-gonic/gin?badge) [![Open Source Helpers](https://www.codetriage.com/gin-gonic/gin/badges/users.svg)](https://www.codetriage.com/gin-gonic/gin) +[![Release](https://img.shields.io/github/release/gin-gonic/gin.svg?style=flat-square)](https://github.com/gin-gonic/gin/releases) 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. @@ -15,10 +17,11 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi ## Contents +- [Installation](#installation) +- [Prerequisite](#prerequisite) - [Quick start](#quick-start) - [Benchmarks](#benchmarks) - [Gin v1.stable](#gin-v1-stable) -- [Start using it](#start-using-it) - [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) @@ -26,6 +29,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi - [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) @@ -35,9 +39,10 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi - [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 and YAML rendering](#xml-json-and-yaml-rendering) + - [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) @@ -54,8 +59,69 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi - [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--) +- [Users](#users) + +## Installation + +To install Gin package, you need to install Go and set your Go workspace first. + +1. Download and install it: + +```sh +$ go get -u github.com/gin-gonic/gin +``` + +2. Import it in your code: + +```go +import "github.com/gin-gonic/gin" +``` + +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/gin/master/examples/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 @@ -134,61 +200,9 @@ BenchmarkVulcan_GithubAll | 5000 | 394253 | 19894 - [x] Battle tested - [x] API frozen, new releases will not break your code. -## Start using it - -1. Download and install it: - -```sh -$ go get github.com/gin-gonic/gin -``` - -2. Import it in your code: - -```go -import "github.com/gin-gonic/gin" -``` - -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.2 -``` - -4. Copy a starting template inside your project - -```sh -$ curl https://raw.githubusercontent.com/gin-gonic/gin/master/examples/basic/main.go > main.go -``` - -5. Run your project - -```sh -$ go run main.go -``` - ## Build with [jsoniter](https://github.com/json-iterator/go) -Gin use `encoding/json` as default json package but you can change to [jsoniter](https://github.com/json-iterator/go) by build from other tags. +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. ```sh $ go build -tags=jsoniter . @@ -228,7 +242,7 @@ func main() { func main() { router := gin.Default() - // This handler will match /user/john but will not match neither /user/ or /user + // 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) @@ -315,6 +329,34 @@ func main() { 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 @@ -488,7 +530,7 @@ func main() { ### Model binding and validation -To bind a request body into a type, use model binding. We currently support binding of JSON, XML and standard form values (foo=bar&boo=baz). +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). @@ -496,10 +538,10 @@ Note that you need to set the corresponding binding tag on all fields you want t Also, Gin provides two sets of methods for binding: - **Type** - Must bind - - **Methods** - `Bind`, `BindJSON`, `BindQuery` + - **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`, `ShouldBindQuery` + - **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`. @@ -509,8 +551,8 @@ You can also specify that specific fields are required. If a field is decorated ```go // Binding from JSON type Login struct { - User string `form:"user" json:"user" binding:"required"` - Password string `form:"password" json:"password" binding:"required"` + User string `form:"user" json:"user" xml:"user" binding:"required"` + Password string `form:"password" json:"password" xml:"password" binding:"required"` } func main() { @@ -519,30 +561,55 @@ func main() { // 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 { - if json.User == "manu" && json.Password == "123" { - c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) - } else { - c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) - } - } else { + 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 { - if form.User == "manu" && form.Password == "123" { - c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) - } else { - c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) - } - } else { + 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 @@ -572,6 +639,10 @@ $ curl -v -X POST \ {"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](examples/custom-validation/server.go). @@ -590,6 +661,7 @@ import ( "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"` @@ -637,7 +709,7 @@ $ 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 registed this way. +[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](examples/struct-lvl-validations) to learn more. ### Only Bind Query String @@ -683,9 +755,12 @@ See the [detail information](https://github.com/gin-gonic/gin/issues/742#issueco ```go package main -import "log" -import "github.com/gin-gonic/gin" -import "time" +import ( + "log" + "time" + + "github.com/gin-gonic/gin" +) type Person struct { Name string `form:"name"` @@ -719,6 +794,40 @@ Test it with: $ 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) @@ -750,12 +859,12 @@ form.html

Check some colors

- + - + - - + +
``` @@ -804,7 +913,7 @@ Test it with: $ curl -v --form user=user --form password=password http://localhost:8080/login ``` -### XML, JSON and YAML rendering +### XML, JSON, YAML and ProtoBuf rendering ```go func main() { @@ -838,6 +947,19 @@ func main() { 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") } @@ -888,6 +1010,57 @@ func main() { } ``` +#### 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 @@ -1022,7 +1195,7 @@ You may use custom delims ```go r := gin.Default() r.Delims("{[{", "}]}") - r.LoadHTMLGlob("/path/to/templates")) + r.LoadHTMLGlob("/path/to/templates") ``` #### Custom Template Funcs @@ -1052,7 +1225,7 @@ func main() { router.SetFuncMap(template.FuncMap{ "formatAsDate": formatAsDate, }) - router.LoadHTMLFiles("./fixtures/basic/raw.tmpl") + router.LoadHTMLFiles("./testdata/template/raw.tmpl") router.GET("/raw", func(c *gin.Context) { c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{ @@ -1584,11 +1757,11 @@ type StructX struct { } type StructY struct { - Y StructX `form:"name_y"` // HERE hava form + Y StructX `form:"name_y"` // HERE have form } type StructZ struct { - Z *StructZ `form:"name_z"` // HERE hava form + Z *StructZ `form:"name_z"` // HERE have form } ``` @@ -1652,6 +1825,127 @@ enough to call binding at once. 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. + +[embedmd]:# (examples/http-pusher/main.go go) +```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. @@ -1698,9 +1992,11 @@ func TestPingRoute(t *testing.T) { } ``` -## Users [![Sourcegraph](https://sourcegraph.com/github.com/gin-gonic/gin/-/badge.svg)](https://sourcegraph.com/github.com/gin-gonic/gin?badge) +## Users Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework. -* [drone](https://github.com/drone/drone): Drone is a Continuous Delivery platform built on Docker, written in Go +* [drone](https://github.com/drone/drone): Drone is a Continuous Delivery platform built on Docker, written in Go. * [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. diff --git a/README_ZH.md b/README_ZH.md new file mode 100644 index 00000000..c689ba28 --- /dev/null +++ b/README_ZH.md @@ -0,0 +1,1940 @@ +# Gin Web 框架中文文档 + + + +[![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin) +[![codecov](https://codecov.io/gh/gin-gonic/gin/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-gonic/gin) +[![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/gin)](https://goreportcard.com/report/github.com/gin-gonic/gin) +[![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin) +[![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Sourcegraph](https://sourcegraph.com/github.com/gin-gonic/gin/-/badge.svg)](https://sourcegraph.com/github.com/gin-gonic/gin?badge) +[![Open Source Helpers](https://www.codetriage.com/gin-gonic/gin/badges/users.svg)](https://www.codetriage.com/gin-gonic/gin) +[![Release](https://img.shields.io/github/release/gin-gonic/gin.svg?style=flat-square)](https://github.com/gin-gonic/gin/releases) + +Gin 是一个 Go (Golang) 语言框架。 它是一个拥有更好性能的 martini-like API 框架, 比 [httprouter](https://github.com/julienschmidt/httprouter) 的速度快了40倍. 如果你是性能和高效的追求者, 那么你会爱上 Gin. + +![Gin console logger](https://gin-gonic.github.io/gin/other/console.png) + +## Contents + +- [安装](#安装) +- [前提条件](#前提条件) +- [快速启动](#快速启动) +- [性能测试](#性能测试) +- [Gin v1 稳定版](#gin-v1-稳定版) +- [使用 jsoniter 构建](#使用-jsoniter-构建) +- [API 示例](#api-示例) + - [GET,POST,PUT,PATCH,DELETE,OPTIONS 使用](#get-post-put-patch-delete-options-使用) + - [路由参数](#路由参数) + - [查询字符串参数](#查询字符串参数) + - [Multipart Urlencoded 表单](#multipart-urlencoded-表单) + - [另一个实列 query + post form](#另一个实列:-query-+-post-form) + - [映射参数 表单参数](#映射参数-表单参数) + - [上传文件](#上传文件) + - [路由组](#路由组) + - [默认初始化 Gin](#默认初始化-gin) + - [中间件使用](#中间件使用) + - [如何记录日志](#如何记录日志) + - [模型绑定和验证](#模型绑定和验证) + - [自定义验证器](#自定义验证器) + - [只绑定查询字符串](#只绑定查询字符串) + - [绑定查询字符串或发布数据](#绑定查询字符串或发布数据) + - [绑定 HTML 复选框](#绑定-html-复选框) + - [Multipart Urlencoded 绑定](#multipart-urlencoded-绑定) + - [XML JSON YAML ProtoBuf 渲染](#xml-json-yaml-protobuf-渲染) + - [SecureJSON](#SecureJSON) + - [静态文件服务](#静态文件服务) + - [从读者服务数据](#从读者服务数据) + - [HTML 渲染](#html-渲染) + - [多模板](#多模板) + - [重定向](#重定向) + - [自定义中间件](#自定义中间件) + - [使用 BasicAuth() 中间件](#使用-basicauth()-中间件) + - [Goroutines](#goroutines) + - [自定义 HTTP 配置](#自定义-http-配置) + - [Let's Encrypt 支持](#lets-encrypt-支持) + - [使用 Gin 运行多个服务](使用-gin-运行多个服务) + - [优雅重启或停止](#优雅重启或停止) + - [使用模板构建单个二进制文件](#使用模板构建单个二进制文件) + - [使用自定义结构绑定表单数据请求](#使用自定义结构绑定表单数据请求) + - [尝试将body绑定到不同的结构中](#尝试将-body-绑定到不同的结构中) + - [http2 server 推送](#http2-server-推送) + - [定义路由日志的格式](#定义路由日志的格式) +- [测试](#测试) +- [用户](#用户) + +## 安装 + +要安装 Gin 软件包,需要先安装 Go 并设置 Go 工作区。 + +1. 下载并安装 gin: + +```sh +$ go get -u github.com/gin-gonic/gin +``` + +2. 将 gin 引入到代码中: + +```go +import "github.com/gin-gonic/gin" +``` + +3. (可选)如果使用诸如`http.StatusOK`之类的常量,则需要引入 `net/http` 包。 + +```go +import "net/http" +``` + +### 使用 [Govendor](https://github.com/kardianos/govendor) 工具创建项目 + +1. `go get` govendor + +```sh +$ go get github.com/kardianos/govendor +``` +2.创建项目并且 `cd` 到项目目录中 + +```sh +$ mkdir -p $GOPATH/src/github.com/myusername/project && cd "$_" +``` + +3. 使用 govendor 初始化项目,并且引入gin + +```sh +$ govendor init +$ govendor fetch github.com/gin-gonic/gin@v1.3 +``` + +4. 复制一个启动文件模板到项目目录中 + +```sh +$ curl https://raw.githubusercontent.com/gin-gonic/gin/master/examples/basic/main.go > main.go +``` + +5.启动项目 + +```sh +$ go run main.go +``` + +## 前提条件 + +新版本的 Gin 需要 Go 1.6 或者更高版本并且很快就会升级到 Go 1.7. + +## 快速启动 + +```sh +# assume the following codes in example.go file +$ cat example.go +``` + +```go +package main + +import "github.com/gin-gonic/gin" + +func main() { + r := gin.Default() + r.GET("/ping", func(c *gin.Context) { + c.JSON(200, gin.H{ + "message": "pong", + }) + }) + r.Run() // listen and serve on 0.0.0.0:8080 +} +``` + +``` +# run example.go and visit 0.0.0.0:8080/ping on browser +$ go run example.go +``` + +## 性能测试 + +Gin 使用自定义版本的 [HttpRouter](https://github.com/julienschmidt/httprouter) + +[所有性能测试](/BENCHMARKS.md) + +Benchmark name | (1) | (2) | (3) | (4) +--------------------------------------------|-----------:|------------:|-----------:|---------: +**BenchmarkGin_GithubAll** | **30000** | **48375** | **0** | **0** +BenchmarkAce_GithubAll | 10000 | 134059 | 13792 | 167 +BenchmarkBear_GithubAll | 5000 | 534445 | 86448 | 943 +BenchmarkBeego_GithubAll | 3000 | 592444 | 74705 | 812 +BenchmarkBone_GithubAll | 200 | 6957308 | 698784 | 8453 +BenchmarkDenco_GithubAll | 10000 | 158819 | 20224 | 167 +BenchmarkEcho_GithubAll | 10000 | 154700 | 6496 | 203 +BenchmarkGocraftWeb_GithubAll | 3000 | 570806 | 131656 | 1686 +BenchmarkGoji_GithubAll | 2000 | 818034 | 56112 | 334 +BenchmarkGojiv2_GithubAll | 2000 | 1213973 | 274768 | 3712 +BenchmarkGoJsonRest_GithubAll | 2000 | 785796 | 134371 | 2737 +BenchmarkGoRestful_GithubAll | 300 | 5238188 | 689672 | 4519 +BenchmarkGorillaMux_GithubAll | 100 | 10257726 | 211840 | 2272 +BenchmarkHttpRouter_GithubAll | 20000 | 105414 | 13792 | 167 +BenchmarkHttpTreeMux_GithubAll | 10000 | 319934 | 65856 | 671 +BenchmarkKocha_GithubAll | 10000 | 209442 | 23304 | 843 +BenchmarkLARS_GithubAll | 20000 | 62565 | 0 | 0 +BenchmarkMacaron_GithubAll | 2000 | 1161270 | 204194 | 2000 +BenchmarkMartini_GithubAll | 200 | 9991713 | 226549 | 2325 +BenchmarkPat_GithubAll | 200 | 5590793 | 1499568 | 27435 +BenchmarkPossum_GithubAll | 10000 | 319768 | 84448 | 609 +BenchmarkR2router_GithubAll | 10000 | 305134 | 77328 | 979 +BenchmarkRivet_GithubAll | 10000 | 132134 | 16272 | 167 +BenchmarkTango_GithubAll | 3000 | 552754 | 63826 | 1618 +BenchmarkTigerTonic_GithubAll | 1000 | 1439483 | 239104 | 5374 +BenchmarkTraffic_GithubAll | 100 | 11383067 | 2659329 | 21848 +BenchmarkVulcan_GithubAll | 5000 | 394253 | 19894 | 609 + +- (1): 在不断的时间内实现总重复,更高意味着更自信的结果 +- (2): 单次重复持续时间(ns / op),越低越好 +- (3): 堆内存(B / op),越低越好 +- (4): 每次重复的平均分配(allocs / op)越低越好 + +## Gin v1 稳定版 + +- [x] 零分配路由器。 +- [x] 仍然是最快的http路由器和框架。 +- [x] 完整的单元测试套件 +- [x] 对战测试 +- [x] API冻结,新版本不会破坏您的代码。 + +## 使用 [jsoniter](https://github.com/json-iterator/go) 构建 + +Gin使用`encoding/json`作为默认的json包,但您可以通过其他标签的构建更改为[jsoniter](https://github.com/json-iterator/go)。 + +```sh +$ go build -tags=jsoniter . +``` + +## API 示例 + +### GET, POST, PUT, PATCH, DELETE , OPTIONS 使用 + +```go +func main() { + // Disable Console Color + // gin.DisableConsoleColor() + + // 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 +} +``` + +### 路由参数 + +```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") +} +``` + +### 查询字符串参数 + +```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 表单 + +```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") +} +``` + +### 另一个实列 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 +``` + +### 映射参数 表单参数 + +``` +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] +``` + +### 上传文件 + +#### 单个文件上传 + +参考问题[#774](https://github.com/gin-gonic/gin/issues/774)和详细[示例代码](examples/upload-file/single)。 + +```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") +} +``` + +如何 `curl`: + +```bash +curl -X POST http://localhost:8080/upload \ + -F "file=@/Users/appleboy/test.zip" \ + -H "Content-Type: multipart/form-data" +``` + +#### 多文件上传 + +查看详细信息[示例代码](examples/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") +} +``` + +如何 `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" +``` + +### 路由组 + +```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") +} +``` + +### 默认初始化 Gin + +用 + +```go +r := gin.New() +``` + +代替 + +```go +// Default With the Logger and Recovery middleware already attached +r := gin.Default() +``` + + +### 中间件使用 +```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") +} +``` + +### 如何记录日志 +```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") +} +``` + +### 模型绑定和验证 + +要将请求主体绑定到类型中,请使用模型绑定。我们目前支持JSON,XML和标准表单值的绑定(foo = bar&boo = baz)。 + +Gin使用[** go-playground/validator.v8 **](https://github.com/go-playground/validator)进行验证。检查有关标签用法的完整文档[此处](http://godoc.org/gopkg.in/go-playground/validator.v8#hdr-Baked_In_Validators_and_Tags)。 + +请注意,您需要在要绑定的所有字段上设置相应的绑定标记。例如,从JSON绑定时,设置`json:“fieldname”`。 + +此外,Gin提供了两组绑定方法: +- **类型** - 必须绑定 +   - **方法** - `Bind`,`BindJSON`,`BindXML`,`BindQuery` +   - **行为** - 这些方法在引擎盖下使用`MustBindWith`。如果存在绑定错误,则使用`c.AbortWithError(400,err).SetType(ErrorTypeBind)`中止请求。这将响应状态代码设置为400,并将`Content-Type`标头设置为`text/plain;字符集= UTF-8`。请注意,如果您在此之后尝试设置响应代码,则会发出警告“[GIN-debug] [警告]标题已经写入。想用422`覆盖状态代码400。如果您希望更好地控制行为,请考虑使用`ShouldBind`等效方法。 +- **类型** - 应该绑定 +   - **方法** - `ShouldBind`,`ShouldBindJSON`,`ShouldBindXML`,`ShouldBindQuery` +   - **行为** - 这些方法在引擎盖下使用`ShouldBindWith`。如果存在绑定错误,则返回错误,开发人员有责任正确处理请求和错误。 + +使用Bind方法时,Gin会尝试根据Content-Type标头推断出绑定器。如果你确定你绑定了什么,你可以使用 `MustBindWith` 或 `ShouldBindWith`。 + +您还可以指定需要特定字段。如果字段用 `binding:“必需”` 来装饰,并且在绑定时具有空值,则会返回错误。 + +```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") +} +``` + +**简单请求** +```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"} +``` + +**跳过验证** + +使用上面 的`curl` 命令运行上面的例子时,它返回错误。 因为这个例子使用 `binding:'需要``````。 如果使用 `binding:“ - ``````,那么在再次运行上面的例子时它不会返回错误。 + +### 自定义验证器 + +也可以注册自定义验证器。 请参阅[示例代码](examples/custom-validation/server.go)。 + +[embedmd]:#(examples/custom-validation/server.go 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"} +``` + +[结构级验证](https://github.com/go-playground/validator/releases/tag/v8.7)也可以这种方式注册。 +请参阅[struct-lvl-validation示例](examples/struct-lvl-validations)以了解更多信息。 + +### 只绑定查询字符串 + +`ShouldBindQuery` 函数只绑定查询参数而不是后期数据。 请参阅[详细信息](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") +} + +``` + +### 绑定查询字符串或发布数据 + +请参阅[详细信息](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") +} +``` + +测试: +```sh +$ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15" +``` + +### 绑定 HTML 复选框 + +参见[详细信息](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 +
+

Check some colors

+ + + + + + + +
+``` + +result: + +``` +{"color":["red","green","blue"]} +``` + +### Multipart Urlencoded 绑定 + +```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 ProtoBuf 渲染 + +```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 + +使用 SecureJSON 来防止 json 劫持。 如果给定的结构是数组值,则默认预 置`“while(1),”` 到响应体。 + +```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 + +使用 JSONP 从不同域中的服务器请求数据。 如果查询参数回调存在,则将回调添加到响应正文。 + +```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 + +使用 Ascii JSON 生成具有转义的非 ASCII 字符的仅 ASCII JSON。 + +```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 + +通常,JSON 用其 unicod e实体替换特殊 HTML 字符,例如 `<` 变为 `\ u003c`。 如果要按字面意思对这些字符进行编码,则可以使用 PureJSON。 +Go 1.6 及更低版本无法使用此功能。 + +```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) +} +``` + +### 静态文件服务 + +```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") +} +``` + +### 从 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 渲染 + +使用LoadHTMLGlob()或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 }} +

+ +``` + +在不同目录中使用具有相同名称的模板 + +```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 }} +``` + +#### 自定义模板渲染器 + +您还可以使用自己的 html 模板渲染 + +```go +import "html/template" + +func main() { + router := gin.Default() + html := template.Must(template.ParseFiles("file1", "file2")) + router.SetHTMLTemplate(html) + router.Run(":8080") +} +``` + +#### 自定义分隔符 + +您可以使用自定义分隔 + +```go + r := gin.Default() + r.Delims("{[{", "}]}") + r.LoadHTMLGlob("/path/to/templates") +``` + +#### 自定义模板功能 + +查看详细信息[示例代码](examples/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 +日期: {[{.now | formatAsDate}]} +``` + +结果: +``` +Date: 2017/07/01 +``` + +### 多模板 + +Gin 允许默认只使用一个 html 模板。 检查[多模板渲染](https://github.com/gin-contrib/multitemplate)以使用 go 1.6 `block template` 等功能。 + +### 重定向 + +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/") +}) +``` + + +发出路由器重定向,使用如下的“HandleContext”。 + +``` 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"}) +}) +``` + + +### 自定义中间件 + +```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") +} +``` + +### BasicAuth() 中间件 + +```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 + +当在中间件或处理程序中启动新的 Goroutines 时,你不应该**使用其中的原始上下文,你必须使用只读副本。 + +```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") +} +``` + +### 自定义 HTTP 配置 + +直接使用`http.ListenAndServe()`,如下所示: + +```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() +} +``` + +### Let's Encrypt 支持 + +单行 LetsEncrypt HTTPS 服务器的示例。 + +[embedmd]:# (examples/auto-tls/example1/main.go go) +```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")) +} +``` + +自定义autocert管理器的示例。 + +[embedmd]:# (examples/auto-tls/example2/main.go go) +```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)) +} +``` + +### 使用 Gin 运行多个服务 + +请参阅[问题](https://github.com/gin-gonic/gin/issues/346)并尝试以下示例: + +[embedmd]:# (examples/multiple-service/main.go go) +```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) + } +} +``` + +### 优雅重启或停止 + +您想要优雅地重启或停止您的Web服务器吗? +有一些方法可以做到这一点。 + +我们可以使用[fvbock/endless](https://github.com/fvbock/endless)来替换默认的`ListenAndServe`。 有关更多详细信息,请参阅问题[#296](https://github.com/gin-gonic/gin/issues/296)。 + +```go +router := gin.Default() +router.GET("/", handler) +// [...] +endless.ListenAndServe(":4242", router) +``` + +另一种替代方案: + +* [manners](https://github.com/braintree/manners):礼貌的Go HTTP服务器,可以正常关闭。 +* [graceful](https://github.com/tylerb/graceful):Graceful是一个Go包,可以正常关闭http.Handler服务器。 +* [grace](https://github.com/facebookgo/grace):Go服务器的平滑重启和零停机时间部署。 + +如果您使用的是Go 1.8,则可能不需要使用此库! 考虑使用http.Server的内置[Shutdown()](https://golang.org/pkg/net/http/#Server.Shutdown)方法进行正常关机。 请参阅gin的完整[graceful-shutdown](./examples /graceful-shutdown)示例。 + +[embedmd]:# (examples/graceful-shutdown/graceful-shutdown/server.go go) +```go +// +build go1.8 + +package main + +import ( + "context" + "log" + "net/http" + "os" + "os/signal" + "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) + signal.Notify(quit, os.Interrupt) + <-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) + } + log.Println("Server exiting") +} +``` + +### 使用模板构建单个二进制文件 + +您可以使用[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 +} +``` + +请参阅`examples/assets-in-binary`目录中的完整示例。 + +### Bind form-data request with 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() +} +``` + +使用命令`curl`命令结果: + +``` +$ 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"}} +``` + +**注意**:不支持以下样式结构: + +```go +type StructX struct { + X struct {} `form:"name_x"` // HERE have form +} + +type StructY struct { + Y StructX `form:"name_y"` // HERE have form +} + +type StructZ struct { + Z *StructZ `form:"name_z"` // HERE have form +} +``` + +总之,只支持现在没有`form`的嵌套自定义结构。 + +### 尝试将 body 绑定到不同的结构中 + +绑定请求体的常规方法使用`c.Request.Body`和它们 +不能多次调用。 + +```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 { + ... + } +} +``` + +为此,您可以使用`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 { + ... + } +} +``` + + +482/5000 +*`c.ShouldBindBodyWith`在绑定之前将body存储到上下文中。 这有 +对性能有轻微影响,所以如果你这样做,你不应该使用这种方法 +足以立刻调用绑定。 +*只有某些格式需要此功能 - “JSON”,“XML”,“MsgPack”, +`ProtoBuf`。 对于其他格式,`Query`,`Form`,`FormPost`,`FormMultipart`, +可以被`c.ShouldBind()`多次调用而不会造成任何损害 +表现(见[#1341](https://github.com/gin-gonic/gin/pull/1341)。 + +### http2 server 推送 + + +http.Pusher仅支持** go1.8 + **。 有关详细信息,请参阅[golang blog](https://blog.golang.org/h2push)。 + +[embedmd]:# (examples/http-pusher/main.go go) +```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") +} +``` + +### 定义路由日志的格式 + +路由的默认日志是: +``` +[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) +``` + +如果要以给定格式记录此信息(例如JSON,键值或其他内容),则可以使用`gin.DebugPrintRouteFunc`定义此格式。 +在下面的示例中,我们使用标准日志包记录所有路由,但您可以使用其他适合您需求的日志工具。 +```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() +} +``` + + +## 测试 + +`net/http/httptest`包是HTTP测试的首选方式。 + +```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") +} +``` + +测试上面的代码示例: + +```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()) +} +``` + +## 用户 + + +使用[Gin](https://github.com/gin-gonic/gin)Web框架的令人尊敬的项目列表。 + +* [drone](https://github.com/drone/drone):drone,用Go编写。 +* [gorush](https://github.com/appleboy/gorush):用Go编写的推送通知服务器。 +* [fnproject](https://github.com/fnproject/fn):容器本机,云无关的无服务器平台。 diff --git a/auth.go b/auth.go index c2143091..9ed81b5d 100644 --- a/auth.go +++ b/auth.go @@ -7,6 +7,7 @@ package gin import ( "crypto/subtle" "encoding/base64" + "net/http" "strconv" ) @@ -51,7 +52,7 @@ func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc { if !found { // Credentials doesn't match, we return 401 and abort handlers chain. c.Header("WWW-Authenticate", realm) - c.AbortWithStatus(401) + c.AbortWithStatus(http.StatusUnauthorized) return } diff --git a/auth_test.go b/auth_test.go index dc8523b0..197e9208 100644 --- a/auth_test.go +++ b/auth_test.go @@ -93,7 +93,7 @@ func TestBasicAuthSucceed(t *testing.T) { router := New() router.Use(BasicAuth(accounts)) router.GET("/login", func(c *Context) { - c.String(200, c.MustGet(AuthUserKey).(string)) + c.String(http.StatusOK, c.MustGet(AuthUserKey).(string)) }) w := httptest.NewRecorder() @@ -101,7 +101,7 @@ func TestBasicAuthSucceed(t *testing.T) { req.Header.Set("Authorization", authorizationHeader("admin", "password")) router.ServeHTTP(w, req) - assert.Equal(t, 200, w.Code) + assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, "admin", w.Body.String()) } @@ -112,7 +112,7 @@ func TestBasicAuth401(t *testing.T) { router.Use(BasicAuth(accounts)) router.GET("/login", func(c *Context) { called = true - c.String(200, c.MustGet(AuthUserKey).(string)) + c.String(http.StatusOK, c.MustGet(AuthUserKey).(string)) }) w := httptest.NewRecorder() @@ -121,8 +121,8 @@ func TestBasicAuth401(t *testing.T) { router.ServeHTTP(w, req) assert.False(t, called) - assert.Equal(t, 401, w.Code) - assert.Equal(t, "Basic realm=\"Authorization Required\"", w.HeaderMap.Get("WWW-Authenticate")) + assert.Equal(t, http.StatusUnauthorized, w.Code) + assert.Equal(t, "Basic realm=\"Authorization Required\"", w.Header().Get("WWW-Authenticate")) } func TestBasicAuth401WithCustomRealm(t *testing.T) { @@ -132,7 +132,7 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) { router.Use(BasicAuthForRealm(accounts, "My Custom \"Realm\"")) router.GET("/login", func(c *Context) { called = true - c.String(200, c.MustGet(AuthUserKey).(string)) + c.String(http.StatusOK, c.MustGet(AuthUserKey).(string)) }) w := httptest.NewRecorder() @@ -141,6 +141,6 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) { router.ServeHTTP(w, req) assert.False(t, called) - assert.Equal(t, 401, w.Code) - assert.Equal(t, "Basic realm=\"My Custom \\\"Realm\\\"\"", w.HeaderMap.Get("WWW-Authenticate")) + assert.Equal(t, http.StatusUnauthorized, w.Code) + assert.Equal(t, "Basic realm=\"My Custom \\\"Realm\\\"\"", w.Header().Get("WWW-Authenticate")) } diff --git a/benchmarks_test.go b/benchmarks_test.go index e7970034..0b3f82df 100644 --- a/benchmarks_test.go +++ b/benchmarks_test.go @@ -54,7 +54,7 @@ func BenchmarkOneRouteJSON(B *testing.B) { Status string `json:"status"` }{"ok"} router.GET("/json", func(c *Context) { - c.JSON(200, data) + c.JSON(http.StatusOK, data) }) runRequest(B, router, "GET", "/json") } @@ -66,7 +66,7 @@ func BenchmarkOneRouteHTML(B *testing.B) { router.SetHTMLTemplate(t) router.GET("/html", func(c *Context) { - c.HTML(200, "index", "hola") + c.HTML(http.StatusOK, "index", "hola") }) runRequest(B, router, "GET", "/html") } @@ -82,7 +82,7 @@ func BenchmarkOneRouteSet(B *testing.B) { func BenchmarkOneRouteString(B *testing.B) { router := New() router.GET("/text", func(c *Context) { - c.String(200, "this is a plain text") + c.String(http.StatusOK, "this is a plain text") }) runRequest(B, router, "GET", "/text") } diff --git a/binding/binding.go b/binding/binding.go index 1a984777..26d71c9f 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -4,10 +4,9 @@ package binding -import ( - "net/http" -) +import "net/http" +// Content-Type MIME of the most common data formats. const ( MIMEJSON = "application/json" MIMEHTML = "text/html" @@ -19,6 +18,7 @@ const ( MIMEPROTOBUF = "application/x-protobuf" MIMEMSGPACK = "application/x-msgpack" MIMEMSGPACK2 = "application/msgpack" + MIMEYAML = "application/x-yaml" ) // Binding describes the interface which needs to be implemented for binding the @@ -36,9 +36,16 @@ type BindingBody interface { BindBody([]byte, interface{}) error } +// BindingUri adds BindUri method to Binding. BindUri is similar with Bind, +// but it read the Params. +type BindingUri interface { + Name() string + BindUri(map[string][]string, interface{}) error +} + // StructValidator is the minimal interface which needs to be implemented in // order for it to be used as the validator engine for ensuring the correctness -// of the reqest. Gin provides a default implementation for this using +// of the request. Gin provides a default implementation for this using // https://github.com/go-playground/validator/tree/v8.18.2. type StructValidator interface { // ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right. @@ -69,6 +76,8 @@ var ( FormMultipart = formMultipartBinding{} ProtoBuf = protobufBinding{} MsgPack = msgpackBinding{} + YAML = yamlBinding{} + Uri = uriBinding{} ) // Default returns the appropriate Binding instance based on the HTTP method @@ -87,6 +96,8 @@ func Default(method, contentType string) Binding { return ProtoBuf case MIMEMSGPACK, MIMEMSGPACK2: return MsgPack + case MIMEYAML: + return YAML default: //case MIMEPOSTForm, MIMEMultipartPOSTForm: return Form } diff --git a/binding/binding_body_test.go b/binding/binding_body_test.go index c41d9f86..901d429c 100644 --- a/binding/binding_body_test.go +++ b/binding/binding_body_test.go @@ -5,7 +5,7 @@ import ( "io/ioutil" "testing" - "github.com/gin-gonic/gin/binding/example" + "github.com/gin-gonic/gin/testdata/protoexample" "github.com/golang/protobuf/proto" "github.com/stretchr/testify/assert" "github.com/ugorji/go/codec" @@ -19,12 +19,12 @@ func TestBindingBody(t *testing.T) { want string }{ { - name: "JSON bidning", + name: "JSON binding", binding: JSON, body: `{"foo":"FOO"}`, }, { - name: "XML bidning", + name: "XML binding", binding: XML, body: ` @@ -36,6 +36,11 @@ func TestBindingBody(t *testing.T) { binding: MsgPack, body: msgPackBody(t), }, + { + name: "YAML binding", + binding: YAML, + body: `foo: FOO`, + }, } { t.Logf("testing: %s", tt.name) req := requestWithBody("POST", "/", tt.body) @@ -55,12 +60,12 @@ func msgPackBody(t *testing.T) string { } func TestBindingBodyProto(t *testing.T) { - test := example.Test{ + test := protoexample.Test{ Label: proto.String("FOO"), } data, _ := proto.Marshal(&test) req := requestWithBody("POST", "/", string(data)) - form := example.Test{} + form := protoexample.Test{} body, _ := ioutil.ReadAll(req.Body) assert.NoError(t, ProtoBuf.BindBody(body, &form)) assert.Equal(t, test, form) diff --git a/binding/binding_test.go b/binding/binding_test.go index 69a5f632..058c1c90 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -14,7 +14,7 @@ import ( "testing" "time" - "github.com/gin-gonic/gin/binding/example" + "github.com/gin-gonic/gin/testdata/protoexample" "github.com/golang/protobuf/proto" "github.com/stretchr/testify/assert" "github.com/ugorji/go/codec" @@ -189,6 +189,16 @@ func TestBindingDefault(t *testing.T) { assert.Equal(t, MsgPack, Default("POST", MIMEMSGPACK)) assert.Equal(t, MsgPack, Default("PUT", MIMEMSGPACK2)) + + assert.Equal(t, YAML, Default("POST", MIMEYAML)) + assert.Equal(t, YAML, Default("PUT", MIMEYAML)) +} + +func TestBindingJSONNilBody(t *testing.T) { + var obj FooStruct + req, _ := http.NewRequest(http.MethodPost, "/", nil) + err := JSON.Bind(req, &obj) + assert.Error(t, err) } func TestBindingJSON(t *testing.T) { @@ -472,6 +482,20 @@ func TestBindingXMLFail(t *testing.T) { "bar", "foo") } +func TestBindingYAML(t *testing.T) { + testBodyBinding(t, + YAML, "yaml", + "/", "/", + `foo: bar`, `bar: foo`) +} + +func TestBindingYAMLFail(t *testing.T) { + testBodyBindingFail(t, + YAML, "yaml", + "/", "/", + `foo:\nbar`, `bar: foo`) +} + func createFormPostRequest() *http.Request { req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar&bar=foo")) req.Header.Set("Content-Type", MIMEPOSTForm) @@ -598,7 +622,7 @@ func TestBindingFormMultipartForMapFail(t *testing.T) { } func TestBindingProtoBuf(t *testing.T) { - test := &example.Test{ + test := &protoexample.Test{ Label: proto.String("yes"), } data, _ := proto.Marshal(test) @@ -610,7 +634,7 @@ func TestBindingProtoBuf(t *testing.T) { } func TestBindingProtoBufFail(t *testing.T) { - test := &example.Test{ + test := &protoexample.Test{ Label: proto.String("yes"), } data, _ := proto.Marshal(test) @@ -681,6 +705,27 @@ func TestExistsFails(t *testing.T) { assert.Error(t, err) } +func TestUriBinding(t *testing.T) { + b := Uri + assert.Equal(t, "uri", b.Name()) + + type Tag struct { + Name string `uri:"name"` + } + var tag Tag + m := make(map[string][]string) + m["name"] = []string{"thinkerou"} + assert.NoError(t, b.BindUri(m, &tag)) + assert.Equal(t, "thinkerou", tag.Name) + + type NotSupportStruct struct { + Name map[string]interface{} `uri:"name"` + } + var not NotSupportStruct + assert.Error(t, b.BindUri(m, ¬)) + assert.Equal(t, "", not.Name) +} + func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) { b := Form assert.Equal(t, "form", b.Name()) @@ -1193,14 +1238,14 @@ func testBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, bad func testProtoBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) { assert.Equal(t, name, b.Name()) - obj := example.Test{} + obj := protoexample.Test{} req := requestWithBody("POST", path, body) req.Header.Add("Content-Type", MIMEPROTOBUF) err := b.Bind(req, &obj) assert.NoError(t, err) assert.Equal(t, "yes", *obj.Label) - obj = example.Test{} + obj = protoexample.Test{} req = requestWithBody("POST", badPath, badBody) req.Header.Add("Content-Type", MIMEPROTOBUF) err = ProtoBuf.Bind(req, &obj) @@ -1216,7 +1261,7 @@ func (h hook) Read([]byte) (int, error) { func testProtoBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, badBody string) { assert.Equal(t, name, b.Name()) - obj := example.Test{} + obj := protoexample.Test{} req := requestWithBody("POST", path, body) req.Body = ioutil.NopCloser(&hook{}) @@ -1224,7 +1269,7 @@ func testProtoBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body err := b.Bind(req, &obj) assert.Error(t, err) - obj = example.Test{} + obj = protoexample.Test{} req = requestWithBody("POST", badPath, badBody) req.Header.Add("Content-Type", MIMEPROTOBUF) err = ProtoBuf.Bind(req, &obj) @@ -1252,3 +1297,12 @@ 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)) +} diff --git a/binding/default_validator.go b/binding/default_validator.go index c67aa8a3..e7a302de 100644 --- a/binding/default_validator.go +++ b/binding/default_validator.go @@ -18,11 +18,17 @@ type defaultValidator struct { var _ StructValidator = &defaultValidator{} +// ValidateStruct receives any kind of type, but only performed struct or pointer to struct type. func (v *defaultValidator) ValidateStruct(obj interface{}) error { - if kindOfData(obj) == reflect.Struct { + value := reflect.ValueOf(obj) + valueType := value.Kind() + if valueType == reflect.Ptr { + valueType = value.Elem().Kind() + } + if valueType == reflect.Struct { v.lazyinit() if err := v.validate.Struct(obj); err != nil { - return error(err) + return err } } return nil @@ -43,12 +49,3 @@ func (v *defaultValidator) lazyinit() { v.validate = validator.New(config) }) } - -func kindOfData(data interface{}) reflect.Kind { - value := reflect.ValueOf(data) - valueType := value.Kind() - if valueType == reflect.Ptr { - valueType = value.Elem().Kind() - } - return valueType -} diff --git a/binding/form_mapping.go b/binding/form_mapping.go index 5a01e41e..0ae600e6 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -13,7 +13,15 @@ import ( "time" ) +func mapUri(ptr interface{}, m map[string][]string) error { + return mapFormByTag(ptr, m, "uri") +} + func mapForm(ptr interface{}, form map[string][]string) error { + return mapFormByTag(ptr, form, "form") +} + +func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error { typ := reflect.TypeOf(ptr).Elem() val := reflect.ValueOf(ptr).Elem() for i := 0; i < typ.NumField(); i++ { @@ -24,7 +32,7 @@ func mapForm(ptr interface{}, form map[string][]string) error { } structFieldKind := structField.Kind() - inputFieldName := typeField.Tag.Get("form") + inputFieldName := typeField.Tag.Get(tag) inputFieldNameList := strings.Split(inputFieldName, ",") inputFieldName = inputFieldNameList[0] var defaultValue string @@ -99,6 +107,10 @@ func mapForm(ptr interface{}, form map[string][]string) error { if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil { return err } + continue + } + if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil { + return err } } return nil @@ -193,7 +205,7 @@ func setFloatField(val string, bitSize int, field reflect.Value) error { func setTimeField(val string, structField reflect.StructField, value reflect.Value) error { timeFormat := structField.Tag.Get("time_format") if timeFormat == "" { - return errors.New("Blank time format") + timeFormat = time.RFC3339 } if val == "" { diff --git a/binding/json.go b/binding/json.go index fea17bb2..f968161b 100644 --- a/binding/json.go +++ b/binding/json.go @@ -6,10 +6,11 @@ package binding import ( "bytes" + "fmt" "io" "net/http" - "github.com/gin-gonic/gin/json" + "github.com/gin-gonic/gin/internal/json" ) // EnableDecoderUseNumber is used to call the UseNumber method on the JSON @@ -24,6 +25,9 @@ func (jsonBinding) Name() string { } func (jsonBinding) Bind(req *http.Request, obj interface{}) error { + if req == nil || req.Body == nil { + return fmt.Errorf("invalid request") + } return decodeJSON(req.Body, obj) } diff --git a/binding/protobuf.go b/binding/protobuf.go index 540e9c1f..f9ece928 100644 --- a/binding/protobuf.go +++ b/binding/protobuf.go @@ -29,7 +29,7 @@ func (protobufBinding) BindBody(body []byte, obj interface{}) error { if err := proto.Unmarshal(body, obj.(proto.Message)); err != nil { return err } - // Here it's same to return validate(obj), but util now we cann't add + // Here it's same to return validate(obj), but util now we can't add // `binding:""` to the struct which automatically generate by gen-proto return nil // return validate(obj) diff --git a/binding/uri.go b/binding/uri.go new file mode 100644 index 00000000..f91ec381 --- /dev/null +++ b/binding/uri.go @@ -0,0 +1,18 @@ +// 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 binding + +type uriBinding struct{} + +func (uriBinding) Name() string { + return "uri" +} + +func (uriBinding) BindUri(m map[string][]string, obj interface{}) error { + if err := mapUri(obj, m); err != nil { + return err + } + return validate(obj) +} diff --git a/binding/yaml.go b/binding/yaml.go new file mode 100644 index 00000000..a2d36d6a --- /dev/null +++ b/binding/yaml.go @@ -0,0 +1,35 @@ +// 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 binding + +import ( + "bytes" + "io" + "net/http" + + "gopkg.in/yaml.v2" +) + +type yamlBinding struct{} + +func (yamlBinding) Name() string { + return "yaml" +} + +func (yamlBinding) Bind(req *http.Request, obj interface{}) error { + return decodeYAML(req.Body, obj) +} + +func (yamlBinding) BindBody(body []byte, obj interface{}) error { + return decodeYAML(bytes.NewReader(body), obj) +} + +func decodeYAML(r io.Reader, obj interface{}) error { + decoder := yaml.NewDecoder(r) + if err := decoder.Decode(obj); err != nil { + return err + } + return validate(obj) +} diff --git a/context.go b/context.go old mode 100755 new mode 100644 index 462357e1..478e8c09 --- a/context.go +++ b/context.go @@ -31,6 +31,7 @@ const ( MIMEPlain = binding.MIMEPlain MIMEPOSTForm = binding.MIMEPOSTForm MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm + MIMEYAML = binding.MIMEYAML BodyBytesKey = "_gin-gonic/gin/bodybyteskey" ) @@ -159,16 +160,15 @@ func (c *Context) Error(err error) *Error { if err == nil { panic("err is nil") } - var parsedError *Error - switch err.(type) { - case *Error: - parsedError = err.(*Error) - default: + + parsedError, ok := err.(*Error) + if !ok { parsedError = &Error{ Err: err, Type: ErrorTypePrivate, } } + c.Errors = append(c.Errors, parsedError) return parsedError } @@ -361,6 +361,18 @@ func (c *Context) GetQueryArray(key string) ([]string, bool) { return []string{}, false } +// QueryMap returns a map for a given query key. +func (c *Context) QueryMap(key string) map[string]string { + dicts, _ := c.GetQueryMap(key) + return dicts +} + +// GetQueryMap returns a map for a given query key, plus a boolean value +// whether at least one value exists for the given key. +func (c *Context) GetQueryMap(key string) (map[string]string, bool) { + return c.get(c.Request.URL.Query(), key) +} + // PostForm returns the specified key from a POST urlencoded form or multipart form // when it exists, otherwise it returns an empty string `("")`. func (c *Context) PostForm(key string) string { @@ -403,7 +415,6 @@ func (c *Context) PostFormArray(key string) []string { // a boolean value whether at least one value exists for the given key. func (c *Context) GetPostFormArray(key string) ([]string, bool) { req := c.Request - req.ParseForm() req.ParseMultipartForm(c.engine.MaxMultipartMemory) if values := req.PostForm[key]; len(values) > 0 { return values, true @@ -416,8 +427,48 @@ func (c *Context) GetPostFormArray(key string) ([]string, bool) { return []string{}, false } +// PostFormMap returns a map for a given form key. +func (c *Context) PostFormMap(key string) map[string]string { + dicts, _ := c.GetPostFormMap(key) + return dicts +} + +// GetPostFormMap returns a map for a given form key, plus a boolean value +// whether at least one value exists for the given key. +func (c *Context) GetPostFormMap(key string) (map[string]string, bool) { + req := c.Request + req.ParseMultipartForm(c.engine.MaxMultipartMemory) + 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 +} + +// get is an internal method and returns a map which satisfy conditions. +func (c *Context) get(m map[string][]string, key string) (map[string]string, bool) { + dicts := make(map[string]string) + exist := false + for k, v := range m { + if i := strings.IndexByte(k, '['); i >= 1 && k[0:i] == key { + if j := strings.IndexByte(k[i+1:], ']'); j >= 1 { + exist = true + dicts[k[i+1:][:j]] = v[0] + } + } + } + return dicts, exist +} + // FormFile returns the first file for the provided form key. func (c *Context) FormFile(name string) (*multipart.FileHeader, error) { + if c.Request.MultipartForm == nil { + if err := c.Request.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil { + return nil, err + } + } _, fh, err := c.Request.FormFile(name) return fh, err } @@ -464,17 +515,27 @@ func (c *Context) BindJSON(obj interface{}) error { return c.MustBindWith(obj, binding.JSON) } +// BindXML is a shortcut for c.MustBindWith(obj, binding.BindXML). +func (c *Context) BindXML(obj interface{}) error { + return c.MustBindWith(obj, binding.XML) +} + // BindQuery is a shortcut for c.MustBindWith(obj, binding.Query). func (c *Context) BindQuery(obj interface{}) error { return c.MustBindWith(obj, binding.Query) } +// BindYAML is a shortcut for c.MustBindWith(obj, binding.YAML). +func (c *Context) BindYAML(obj interface{}) error { + return c.MustBindWith(obj, binding.YAML) +} + // MustBindWith binds the passed struct pointer using the specified binding engine. -// It will abort the request with HTTP 400 if any error ocurrs. +// It will abort the request with HTTP 400 if any error occurs. // See the binding package. func (c *Context) MustBindWith(obj interface{}, b binding.Binding) (err error) { if err = c.ShouldBindWith(obj, b); err != nil { - c.AbortWithError(400, err).SetType(ErrorTypeBind) + c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) } return @@ -498,11 +559,30 @@ func (c *Context) ShouldBindJSON(obj interface{}) error { return c.ShouldBindWith(obj, binding.JSON) } +// ShouldBindXML is a shortcut for c.ShouldBindWith(obj, binding.XML). +func (c *Context) ShouldBindXML(obj interface{}) error { + return c.ShouldBindWith(obj, binding.XML) +} + // ShouldBindQuery is a shortcut for c.ShouldBindWith(obj, binding.Query). func (c *Context) ShouldBindQuery(obj interface{}) error { return c.ShouldBindWith(obj, binding.Query) } +// ShouldBindYAML is a shortcut for c.ShouldBindWith(obj, binding.YAML). +func (c *Context) ShouldBindYAML(obj interface{}) error { + return c.ShouldBindWith(obj, binding.YAML) +} + +// ShouldBindUri binds the passed struct pointer using the specified binding engine. +func (c *Context) ShouldBindUri(obj interface{}) error { + m := make(map[string][]string) + for _, v := range c.Params { + m[v.Key] = []string{v.Value} + } + return binding.Uri.BindUri(m, obj) +} + // ShouldBindWith binds the passed struct pointer using the specified binding engine. // See the binding package. func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error { @@ -514,9 +594,7 @@ func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error { // // NOTE: This method reads the body before binding. So you should use // ShouldBindWith for better performance if you need to call only once. -func (c *Context) ShouldBindBodyWith( - obj interface{}, bb binding.BindingBody, -) (err error) { +func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (err error) { var body []byte if cb, ok := c.Get(BodyBytesKey); ok { if cbb, ok := cb.([]byte); ok { @@ -539,14 +617,10 @@ func (c *Context) ShouldBindBodyWith( func (c *Context) ClientIP() string { if c.engine.ForwardedByClientIP { clientIP := c.requestHeader("X-Forwarded-For") - if index := strings.IndexByte(clientIP, ','); index >= 0 { - clientIP = clientIP[0:index] + clientIP = strings.TrimSpace(strings.Split(clientIP, ",")[0]) + if clientIP == "" { + clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip")) } - clientIP = strings.TrimSpace(clientIP) - if clientIP != "" { - return clientIP - } - clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip")) if clientIP != "" { return clientIP } @@ -593,9 +667,9 @@ func bodyAllowedForStatus(status int) bool { switch { case status >= 100 && status <= 199: return false - case status == 204: + case status == http.StatusNoContent: return false - case status == 304: + case status == http.StatusNotModified: return false } return true @@ -612,9 +686,9 @@ func (c *Context) Status(code int) { func (c *Context) Header(key, value string) { if value == "" { c.Writer.Header().Del(key) - } else { - c.Writer.Header().Set(key, value) + return } + c.Writer.Header().Set(key, value) } // GetHeader returns value from request headers. @@ -658,6 +732,7 @@ func (c *Context) Cookie(name string) (string, error) { return val, nil } +// Render writes the response headers and calls render.Render to render data. func (c *Context) Render(code int, r render.Render) { c.Status(code) @@ -699,7 +774,12 @@ func (c *Context) SecureJSON(code int, obj interface{}) { // It add padding to response body to request data from a server residing in a different domain than the client. // It also sets the Content-Type as "application/javascript". func (c *Context) JSONP(code int, obj interface{}) { - c.Render(code, render.JsonpJSON{Callback: c.DefaultQuery("callback", ""), Data: obj}) + callback := c.DefaultQuery("callback", "") + if callback == "" { + c.Render(code, render.JSON{Data: obj}) + return + } + c.Render(code, render.JsonpJSON{Callback: callback, Data: obj}) } // JSON serializes the given struct as JSON into the response body. @@ -708,6 +788,12 @@ func (c *Context) JSON(code int, obj interface{}) { c.Render(code, render.JSON{Data: obj}) } +// AsciiJSON serializes the given struct as JSON into the response body with unicode to ASCII string. +// It also sets the Content-Type as "application/json". +func (c *Context) AsciiJSON(code int, obj interface{}) { + c.Render(code, render.AsciiJSON{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{}) { @@ -719,6 +805,11 @@ func (c *Context) YAML(code int, obj interface{}) { c.Render(code, render.YAML{Data: obj}) } +// ProtoBuf serializes the given struct as ProtoBuf into the response body. +func (c *Context) ProtoBuf(code int, obj interface{}) { + c.Render(code, render.ProtoBuf{Data: obj}) +} + // String writes the given string into the response body. func (c *Context) String(code int, format string, values ...interface{}) { c.Render(code, render.String{Format: format, Data: values}) @@ -764,6 +855,7 @@ func (c *Context) SSEvent(name string, message interface{}) { }) } +// Stream sends a streaming response. func (c *Context) Stream(step func(w io.Writer) bool) { w := c.Writer clientGone := w.CloseNotify() @@ -785,6 +877,7 @@ func (c *Context) Stream(step func(w io.Writer) bool) { /******** CONTENT NEGOTIATION *******/ /************************************/ +// Negotiate contains all negotiations data. type Negotiate struct { Offered []string HTMLName string @@ -794,6 +887,7 @@ type Negotiate struct { Data interface{} } +// Negotiate calls different Render according acceptable Accept format. func (c *Context) Negotiate(code int, config Negotiate) { switch c.NegotiateFormat(config.Offered...) { case binding.MIMEJSON: @@ -813,6 +907,7 @@ func (c *Context) Negotiate(code int, config Negotiate) { } } +// NegotiateFormat returns an acceptable Accept format. func (c *Context) NegotiateFormat(offered ...string) string { assert1(len(offered) > 0, "you must provide at least one offer") @@ -832,6 +927,7 @@ func (c *Context) NegotiateFormat(offered ...string) string { return "" } +// SetAccepted sets Accept header data. func (c *Context) SetAccepted(formats ...string) { c.Accepted = formats } @@ -840,18 +936,33 @@ func (c *Context) SetAccepted(formats ...string) { /***** GOLANG.ORG/X/NET/CONTEXT *****/ /************************************/ +// Deadline returns the time when work done on behalf of this context +// should be canceled. Deadline returns ok==false when no deadline is +// set. Successive calls to Deadline return the same results. func (c *Context) Deadline() (deadline time.Time, ok bool) { return } +// Done returns a channel that's closed when work done on behalf of this +// context should be canceled. Done may return nil if this context can +// never be canceled. Successive calls to Done return the same value. func (c *Context) Done() <-chan struct{} { return nil } +// Err returns a non-nil error value after Done is closed, +// successive calls to Err return the same error. +// If Done is not yet closed, Err returns nil. +// If Done is closed, Err returns a non-nil error explaining why: +// Canceled if the context was canceled +// or DeadlineExceeded if the context's deadline passed. func (c *Context) Err() error { return nil } +// Value returns the value associated with this context for key, or nil +// if no value is associated with key. Successive calls to Value with +// the same key returns the same result. func (c *Context) Value(key interface{}) interface{} { if key == 0 { return c.Request diff --git a/context_17.go b/context_17.go new file mode 100644 index 00000000..8e9f75ad --- /dev/null +++ b/context_17.go @@ -0,0 +1,17 @@ +// 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 new file mode 100644 index 00000000..5b9ebcdc --- /dev/null +++ b/context_17_test.go @@ -0,0 +1,27 @@ +// 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 12e02fa0..dced73fd 100644 --- a/context_test.go +++ b/context_test.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "html/template" + "io" "mime/multipart" "net/http" "net/http/httptest" @@ -19,8 +20,11 @@ import ( "github.com/gin-contrib/sse" "github.com/gin-gonic/gin/binding" + "github.com/golang/protobuf/proto" "github.com/stretchr/testify/assert" "golang.org/x/net/context" + + testdata "github.com/gin-gonic/gin/testdata/protoexample" ) var _ context.Context = &Context{} @@ -47,6 +51,8 @@ func createMultipartRequest() *http.Request { must(mw.WriteField("time_local", "31/12/2016 14:55")) must(mw.WriteField("time_utc", "31/12/2016 14:55")) must(mw.WriteField("time_location", "31/12/2016 14:55")) + must(mw.WriteField("names[a]", "thinkerou")) + must(mw.WriteField("names[b]", "tianou")) req, err := http.NewRequest("POST", "/", body) must(err) req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary) @@ -78,6 +84,19 @@ func TestContextFormFile(t *testing.T) { assert.NoError(t, c.SaveUploadedFile(f, "test")) } +func TestContextFormFileFailed(t *testing.T) { + buf := new(bytes.Buffer) + mw := multipart.NewWriter(buf) + mw.Close() + c, _ := CreateTestContext(httptest.NewRecorder()) + c.Request, _ = http.NewRequest("POST", "/", nil) + c.Request.Header.Set("Content-Type", mw.FormDataContentType()) + c.engine.MaxMultipartMemory = 8 << 20 + f, err := c.FormFile("file") + assert.Error(t, err) + assert.Nil(t, f) +} + func TestContextMultipartForm(t *testing.T) { buf := new(bytes.Buffer) mw := multipart.NewWriter(buf) @@ -371,7 +390,8 @@ func TestContextQuery(t *testing.T) { func TestContextQueryAndPostForm(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) body := bytes.NewBufferString("foo=bar&page=11&both=&foo=second") - c.Request, _ = http.NewRequest("POST", "/?both=GET&id=main&id=omit&array[]=first&array[]=second", body) + c.Request, _ = http.NewRequest("POST", + "/?both=GET&id=main&id=omit&array[]=first&array[]=second&ids[a]=hi&ids[b]=3.14", body) c.Request.Header.Add("Content-Type", MIMEPOSTForm) assert.Equal(t, "bar", c.DefaultPostForm("foo", "none")) @@ -439,6 +459,30 @@ func TestContextQueryAndPostForm(t *testing.T) { values = c.QueryArray("both") assert.Equal(t, 1, len(values)) assert.Equal(t, "GET", values[0]) + + dicts, ok := c.GetQueryMap("ids") + assert.True(t, ok) + assert.Equal(t, "hi", dicts["a"]) + assert.Equal(t, "3.14", dicts["b"]) + + dicts, ok = c.GetQueryMap("nokey") + assert.False(t, ok) + assert.Equal(t, 0, len(dicts)) + + dicts, ok = c.GetQueryMap("both") + assert.False(t, ok) + assert.Equal(t, 0, len(dicts)) + + dicts, ok = c.GetQueryMap("array") + assert.False(t, ok) + assert.Equal(t, 0, len(dicts)) + + dicts = c.QueryMap("ids") + assert.Equal(t, "hi", dicts["a"]) + assert.Equal(t, "3.14", dicts["b"]) + + dicts = c.QueryMap("nokey") + assert.Equal(t, 0, len(dicts)) } func TestContextPostFormMultipart(t *testing.T) { @@ -515,6 +559,22 @@ func TestContextPostFormMultipart(t *testing.T) { values = c.PostFormArray("foo") assert.Equal(t, 1, len(values)) assert.Equal(t, "bar", values[0]) + + dicts, ok := c.GetPostFormMap("names") + assert.True(t, ok) + assert.Equal(t, "thinkerou", dicts["a"]) + assert.Equal(t, "tianou", dicts["b"]) + + dicts, ok = c.GetPostFormMap("nokey") + assert.False(t, ok) + assert.Equal(t, 0, len(dicts)) + + dicts = c.PostFormMap("names") + assert.Equal(t, "thinkerou", dicts["a"]) + assert.Equal(t, "tianou", dicts["b"]) + + dicts = c.PostFormMap("nokey") + assert.Equal(t, 0, len(dicts)) } func TestContextSetCookie(t *testing.T) { @@ -541,10 +601,11 @@ 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(204)) - assert.False(t, false, bodyAllowedForStatus(304)) - assert.True(t, true, bodyAllowedForStatus(500)) + assert.False(t, false, bodyAllowedForStatus(http.StatusNoContent)) + assert.False(t, false, bodyAllowedForStatus(http.StatusNotModified)) + assert.True(t, true, bodyAllowedForStatus(http.StatusInternalServerError)) } type TestPanicRender struct { @@ -571,15 +632,16 @@ func TestContextRenderPanicIfErr(t *testing.T) { // Tests that the response is serialized as JSON // and Content-Type is set to application/json +// and special HTML characters are escaped func TestContextRenderJSON(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) - c.JSON(201, H{"foo": "bar"}) + c.JSON(http.StatusCreated, H{"foo": "bar", "html": ""}) - assert.Equal(t, 201, w.Code) - assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String()) - assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, http.StatusCreated, w.Code) + assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String()) + assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) } // Tests that the response is serialized as JSONP @@ -589,11 +651,25 @@ func TestContextRenderJSONP(t *testing.T) { c, _ := CreateTestContext(w) c.Request, _ = http.NewRequest("GET", "http://example.com/?callback=x", nil) - c.JSONP(201, H{"foo": "bar"}) + c.JSONP(http.StatusCreated, H{"foo": "bar"}) - assert.Equal(t, 201, w.Code) + assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, "x({\"foo\":\"bar\"})", w.Body.String()) - assert.Equal(t, "application/javascript; charset=utf-8", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type")) +} + +// Tests that the response is serialized as JSONP +// and Content-Type is set to application/json +func TestContextRenderJSONPWithoutCallback(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.Request, _ = http.NewRequest("GET", "http://example.com", nil) + + c.JSONP(http.StatusCreated, H{"foo": "bar"}) + + assert.Equal(t, http.StatusCreated, w.Code) + assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String()) + assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) } // Tests that no JSON is rendered if code is 204 @@ -601,11 +677,11 @@ func TestContextRenderNoContentJSON(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) - c.JSON(204, H{"foo": "bar"}) + c.JSON(http.StatusNoContent, H{"foo": "bar"}) - assert.Equal(t, 204, w.Code) + assert.Equal(t, http.StatusNoContent, w.Code) assert.Empty(t, w.Body.String()) - assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) } // Tests that the response is serialized as JSON @@ -615,11 +691,11 @@ func TestContextRenderAPIJSON(t *testing.T) { c, _ := CreateTestContext(w) c.Header("Content-Type", "application/vnd.api+json") - c.JSON(201, H{"foo": "bar"}) + c.JSON(http.StatusCreated, H{"foo": "bar"}) - assert.Equal(t, 201, w.Code) + assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String()) - assert.Equal(t, "application/vnd.api+json", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, "application/vnd.api+json", w.Header().Get("Content-Type")) } // Tests that no Custom JSON is rendered if code is 204 @@ -628,11 +704,11 @@ func TestContextRenderNoContentAPIJSON(t *testing.T) { c, _ := CreateTestContext(w) c.Header("Content-Type", "application/vnd.api+json") - c.JSON(204, H{"foo": "bar"}) + c.JSON(http.StatusNoContent, H{"foo": "bar"}) - assert.Equal(t, 204, w.Code) + assert.Equal(t, http.StatusNoContent, w.Code) assert.Empty(t, w.Body.String()) - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/vnd.api+json") + assert.Equal(t, w.Header().Get("Content-Type"), "application/vnd.api+json") } // Tests that the response is serialized as JSON @@ -641,11 +717,11 @@ func TestContextRenderIndentedJSON(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) - c.IndentedJSON(201, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}}) + c.IndentedJSON(http.StatusCreated, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}}) - assert.Equal(t, 201, w.Code) + assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, "{\n \"bar\": \"foo\",\n \"foo\": \"bar\",\n \"nested\": {\n \"foo\": \"bar\"\n }\n}", w.Body.String()) - assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) } // Tests that no Custom JSON is rendered if code is 204 @@ -653,11 +729,11 @@ func TestContextRenderNoContentIndentedJSON(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) - c.IndentedJSON(204, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}}) + c.IndentedJSON(http.StatusNoContent, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}}) - assert.Equal(t, 204, w.Code) + assert.Equal(t, http.StatusNoContent, w.Code) assert.Empty(t, w.Body.String()) - assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) } // Tests that the response is serialized as Secure JSON @@ -667,11 +743,11 @@ func TestContextRenderSecureJSON(t *testing.T) { c, router := CreateTestContext(w) router.SecureJsonPrefix("&&&START&&&") - c.SecureJSON(201, []string{"foo", "bar"}) + c.SecureJSON(http.StatusCreated, []string{"foo", "bar"}) - assert.Equal(t, 201, w.Code) + assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, "&&&START&&&[\"foo\",\"bar\"]", w.Body.String()) - assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) } // Tests that no Custom JSON is rendered if code is 204 @@ -679,11 +755,22 @@ func TestContextRenderNoContentSecureJSON(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) - c.SecureJSON(204, []string{"foo", "bar"}) + c.SecureJSON(http.StatusNoContent, []string{"foo", "bar"}) - assert.Equal(t, 204, w.Code) + assert.Equal(t, http.StatusNoContent, w.Code) assert.Empty(t, w.Body.String()) - assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) +} + +func TestContextRenderNoContentAsciiJSON(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.AsciiJSON(http.StatusNoContent, []string{"lang", "Go语言"}) + + assert.Equal(t, http.StatusNoContent, w.Code) + assert.Empty(t, w.Body.String()) + assert.Equal(t, "application/json", w.Header().Get("Content-Type")) } // Tests that the response executes the templates @@ -695,11 +782,11 @@ func TestContextRenderHTML(t *testing.T) { templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) router.SetHTMLTemplate(templ) - c.HTML(201, "t", H{"name": "alexandernyquist"}) + c.HTML(http.StatusCreated, "t", H{"name": "alexandernyquist"}) - assert.Equal(t, 201, w.Code) + assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, "Hello alexandernyquist", w.Body.String()) - assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type")) } func TestContextRenderHTML2(t *testing.T) { @@ -710,20 +797,20 @@ func TestContextRenderHTML2(t *testing.T) { router.addRoute("GET", "/", HandlersChain{func(_ *Context) {}}) assert.Len(t, router.trees, 1) - var b bytes.Buffer - setup(&b) - defer teardown() - templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) - router.SetHTMLTemplate(templ) + re := captureOutput(func() { + SetMode(DebugMode) + router.SetHTMLTemplate(templ) + SetMode(TestMode) + }) - assert.Equal(t, "[GIN-debug] [WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called\nat initialization. ie. before any route is registered or the router is listening in a socket:\n\n\trouter := gin.Default()\n\trouter.SetHTMLTemplate(template) // << good place\n\n", b.String()) + assert.Equal(t, "[GIN-debug] [WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called\nat initialization. ie. before any route is registered or the router is listening in a socket:\n\n\trouter := gin.Default()\n\trouter.SetHTMLTemplate(template) // << good place\n\n", re) - c.HTML(201, "t", H{"name": "alexandernyquist"}) + c.HTML(http.StatusCreated, "t", H{"name": "alexandernyquist"}) - assert.Equal(t, 201, w.Code) + assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, "Hello alexandernyquist", w.Body.String()) - assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type")) } // Tests that no HTML is rendered if code is 204 @@ -733,11 +820,11 @@ func TestContextRenderNoContentHTML(t *testing.T) { templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) router.SetHTMLTemplate(templ) - c.HTML(204, "t", H{"name": "alexandernyquist"}) + c.HTML(http.StatusNoContent, "t", H{"name": "alexandernyquist"}) - assert.Equal(t, 204, w.Code) + assert.Equal(t, http.StatusNoContent, w.Code) assert.Empty(t, w.Body.String()) - assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type")) } // TestContextXML tests that the response is serialized as XML @@ -746,11 +833,11 @@ func TestContextRenderXML(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) - c.XML(201, H{"foo": "bar"}) + c.XML(http.StatusCreated, H{"foo": "bar"}) - assert.Equal(t, 201, w.Code) + assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, "bar", w.Body.String()) - assert.Equal(t, "application/xml; charset=utf-8", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type")) } // Tests that no XML is rendered if code is 204 @@ -758,11 +845,11 @@ func TestContextRenderNoContentXML(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) - c.XML(204, H{"foo": "bar"}) + c.XML(http.StatusNoContent, H{"foo": "bar"}) - assert.Equal(t, 204, w.Code) + assert.Equal(t, http.StatusNoContent, w.Code) assert.Empty(t, w.Body.String()) - assert.Equal(t, "application/xml; charset=utf-8", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type")) } // TestContextString tests that the response is returned @@ -771,11 +858,11 @@ func TestContextRenderString(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) - c.String(201, "test %s %d", "string", 2) + c.String(http.StatusCreated, "test %s %d", "string", 2) - assert.Equal(t, 201, w.Code) + assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, "test string 2", w.Body.String()) - assert.Equal(t, "text/plain; charset=utf-8", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type")) } // Tests that no String is rendered if code is 204 @@ -783,11 +870,11 @@ func TestContextRenderNoContentString(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) - c.String(204, "test %s %d", "string", 2) + c.String(http.StatusNoContent, "test %s %d", "string", 2) - assert.Equal(t, 204, w.Code) + assert.Equal(t, http.StatusNoContent, w.Code) assert.Empty(t, w.Body.String()) - assert.Equal(t, "text/plain; charset=utf-8", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type")) } // TestContextString tests that the response is returned @@ -797,11 +884,11 @@ func TestContextRenderHTMLString(t *testing.T) { c, _ := CreateTestContext(w) c.Header("Content-Type", "text/html; charset=utf-8") - c.String(201, "%s %d", "string", 3) + c.String(http.StatusCreated, "%s %d", "string", 3) - assert.Equal(t, 201, w.Code) + assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, "string 3", w.Body.String()) - assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type")) } // Tests that no HTML String is rendered if code is 204 @@ -810,11 +897,11 @@ func TestContextRenderNoContentHTMLString(t *testing.T) { c, _ := CreateTestContext(w) c.Header("Content-Type", "text/html; charset=utf-8") - c.String(204, "%s %d", "string", 3) + c.String(http.StatusNoContent, "%s %d", "string", 3) - assert.Equal(t, 204, w.Code) + assert.Equal(t, http.StatusNoContent, w.Code) assert.Empty(t, w.Body.String()) - assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type")) } // TestContextData tests that the response can be written from `bytesting` @@ -823,11 +910,11 @@ func TestContextRenderData(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) - c.Data(201, "text/csv", []byte(`foo,bar`)) + c.Data(http.StatusCreated, "text/csv", []byte(`foo,bar`)) - assert.Equal(t, 201, w.Code) + assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, "foo,bar", w.Body.String()) - assert.Equal(t, "text/csv", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, "text/csv", w.Header().Get("Content-Type")) } // Tests that no Custom Data is rendered if code is 204 @@ -835,11 +922,11 @@ func TestContextRenderNoContentData(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) - c.Data(204, "text/csv", []byte(`foo,bar`)) + c.Data(http.StatusNoContent, "text/csv", []byte(`foo,bar`)) - assert.Equal(t, 204, w.Code) + assert.Equal(t, http.StatusNoContent, w.Code) assert.Empty(t, w.Body.String()) - assert.Equal(t, "text/csv", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, "text/csv", w.Header().Get("Content-Type")) } func TestContextRenderSSE(t *testing.T) { @@ -866,9 +953,9 @@ func TestContextRenderFile(t *testing.T) { c.Request, _ = http.NewRequest("GET", "/", nil) c.File("./gin.go") - assert.Equal(t, 200, w.Code) + assert.Equal(t, http.StatusOK, w.Code) assert.Contains(t, w.Body.String(), "func New() *Engine {") - assert.Equal(t, "text/plain; charset=utf-8", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type")) } // TestContextRenderYAML tests that the response is serialized as YAML @@ -877,11 +964,35 @@ func TestContextRenderYAML(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) - c.YAML(201, H{"foo": "bar"}) + c.YAML(http.StatusCreated, H{"foo": "bar"}) - assert.Equal(t, 201, w.Code) + assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, "foo: bar\n", w.Body.String()) - assert.Equal(t, "application/x-yaml; charset=utf-8", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, "application/x-yaml; charset=utf-8", w.Header().Get("Content-Type")) +} + +// TestContextRenderProtoBuf tests that the response is serialized as ProtoBuf +// and Content-Type is set to application/x-protobuf +// and we just use the example protobuf to check if the response is correct +func TestContextRenderProtoBuf(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + reps := []int64{int64(1), int64(2)} + label := "test" + data := &testdata.Test{ + Label: &label, + Reps: reps, + } + + c.ProtoBuf(http.StatusCreated, data) + + protoData, err := proto.Marshal(data) + assert.NoError(t, err) + + assert.Equal(t, http.StatusCreated, w.Code) + assert.Equal(t, string(protoData), w.Body.String()) + assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type")) } func TestContextHeaders(t *testing.T) { @@ -909,9 +1020,9 @@ func TestContextRenderRedirectWithRelativePath(t *testing.T) { assert.Panics(t, func() { c.Redirect(299, "/new_path") }) assert.Panics(t, func() { c.Redirect(309, "/new_path") }) - c.Redirect(301, "/path") + c.Redirect(http.StatusMovedPermanently, "/path") c.Writer.WriteHeaderNow() - assert.Equal(t, 301, w.Code) + assert.Equal(t, http.StatusMovedPermanently, w.Code) assert.Equal(t, "/path", w.Header().Get("Location")) } @@ -920,10 +1031,10 @@ func TestContextRenderRedirectWithAbsolutePath(t *testing.T) { c, _ := CreateTestContext(w) c.Request, _ = http.NewRequest("POST", "http://example.com", nil) - c.Redirect(302, "http://google.com") + c.Redirect(http.StatusFound, "http://google.com") c.Writer.WriteHeaderNow() - assert.Equal(t, 302, w.Code) + assert.Equal(t, http.StatusFound, w.Code) assert.Equal(t, "http://google.com", w.Header().Get("Location")) } @@ -932,21 +1043,23 @@ func TestContextRenderRedirectWith201(t *testing.T) { c, _ := CreateTestContext(w) c.Request, _ = http.NewRequest("POST", "http://example.com", nil) - c.Redirect(201, "/resource") + c.Redirect(http.StatusCreated, "/resource") c.Writer.WriteHeaderNow() - assert.Equal(t, 201, w.Code) + assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, "/resource", w.Header().Get("Location")) } func TestContextRenderRedirectAll(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) c.Request, _ = http.NewRequest("POST", "http://example.com", nil) - assert.Panics(t, func() { c.Redirect(200, "/resource") }) - assert.Panics(t, func() { c.Redirect(202, "/resource") }) + assert.Panics(t, func() { c.Redirect(http.StatusOK, "/resource") }) + assert.Panics(t, func() { c.Redirect(http.StatusAccepted, "/resource") }) assert.Panics(t, func() { c.Redirect(299, "/resource") }) assert.Panics(t, func() { c.Redirect(309, "/resource") }) - assert.NotPanics(t, func() { c.Redirect(300, "/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") }) } @@ -955,14 +1068,14 @@ func TestContextNegotiationWithJSON(t *testing.T) { c, _ := CreateTestContext(w) c.Request, _ = http.NewRequest("POST", "", nil) - c.Negotiate(200, Negotiate{ + c.Negotiate(http.StatusOK, Negotiate{ Offered: []string{MIMEJSON, MIMEXML}, Data: H{"foo": "bar"}, }) - assert.Equal(t, 200, w.Code) + assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String()) - assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) } func TestContextNegotiationWithXML(t *testing.T) { @@ -970,14 +1083,14 @@ func TestContextNegotiationWithXML(t *testing.T) { c, _ := CreateTestContext(w) c.Request, _ = http.NewRequest("POST", "", nil) - c.Negotiate(200, Negotiate{ + c.Negotiate(http.StatusOK, Negotiate{ Offered: []string{MIMEXML, MIMEJSON}, Data: H{"foo": "bar"}, }) - assert.Equal(t, 200, w.Code) + assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, "bar", w.Body.String()) - assert.Equal(t, "application/xml; charset=utf-8", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type")) } func TestContextNegotiationWithHTML(t *testing.T) { @@ -987,15 +1100,15 @@ func TestContextNegotiationWithHTML(t *testing.T) { templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) router.SetHTMLTemplate(templ) - c.Negotiate(200, Negotiate{ + c.Negotiate(http.StatusOK, Negotiate{ Offered: []string{MIMEHTML}, Data: H{"name": "gin"}, HTMLName: "t", }) - assert.Equal(t, 200, w.Code) + assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, "Hello gin", w.Body.String()) - assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type")) } func TestContextNegotiationNotSupport(t *testing.T) { @@ -1003,11 +1116,11 @@ func TestContextNegotiationNotSupport(t *testing.T) { c, _ := CreateTestContext(w) c.Request, _ = http.NewRequest("POST", "", nil) - c.Negotiate(200, Negotiate{ + c.Negotiate(http.StatusOK, Negotiate{ Offered: []string{MIMEPOSTForm}, }) - assert.Equal(t, 406, w.Code) + assert.Equal(t, http.StatusNotAcceptable, w.Code) assert.Equal(t, c.index, abortIndex) assert.True(t, c.IsAborted()) } @@ -1031,7 +1144,7 @@ func TestContextNegotiationFormatWithAccept(t *testing.T) { assert.Empty(t, c.NegotiateFormat(MIMEJSON)) } -func TestContextNegotiationFormatCustum(t *testing.T) { +func TestContextNegotiationFormatCustom(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) c.Request, _ = http.NewRequest("POST", "/", nil) c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") @@ -1065,11 +1178,11 @@ func TestContextAbortWithStatus(t *testing.T) { c, _ := CreateTestContext(w) c.index = 4 - c.AbortWithStatus(401) + c.AbortWithStatus(http.StatusUnauthorized) assert.Equal(t, abortIndex, c.index) - assert.Equal(t, 401, c.Writer.Status()) - assert.Equal(t, 401, w.Code) + assert.Equal(t, http.StatusUnauthorized, c.Writer.Status()) + assert.Equal(t, http.StatusUnauthorized, w.Code) assert.True(t, c.IsAborted()) } @@ -1087,11 +1200,11 @@ func TestContextAbortWithStatusJSON(t *testing.T) { in.Bar = "barValue" in.Foo = "fooValue" - c.AbortWithStatusJSON(415, in) + c.AbortWithStatusJSON(http.StatusUnsupportedMediaType, in) assert.Equal(t, abortIndex, c.index) - assert.Equal(t, 415, c.Writer.Status()) - assert.Equal(t, 415, w.Code) + assert.Equal(t, http.StatusUnsupportedMediaType, c.Writer.Status()) + assert.Equal(t, http.StatusUnsupportedMediaType, w.Code) assert.True(t, c.IsAborted()) contentType := w.Header().Get("Content-Type") @@ -1154,9 +1267,9 @@ func TestContextAbortWithError(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) - c.AbortWithError(401, errors.New("bad input")).SetMeta("some input") + c.AbortWithError(http.StatusUnauthorized, errors.New("bad input")).SetMeta("some input") - assert.Equal(t, 401, w.Code) + assert.Equal(t, http.StatusUnauthorized, w.Code) assert.Equal(t, abortIndex, c.index) assert.True(t, c.IsAborted()) } @@ -1230,6 +1343,26 @@ func TestContextBindWithJSON(t *testing.T) { assert.Equal(t, "bar", obj.Foo) assert.Equal(t, 0, w.Body.Len()) } +func TestContextBindWithXML(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString(` + + FOO + BAR + `)) + c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type + + var obj struct { + Foo string `xml:"foo"` + Bar string `xml:"bar"` + } + assert.NoError(t, c.BindXML(&obj)) + assert.Equal(t, "FOO", obj.Foo) + assert.Equal(t, "BAR", obj.Bar) + assert.Equal(t, 0, w.Body.Len()) +} func TestContextBindWithQuery(t *testing.T) { w := httptest.NewRecorder() @@ -1247,6 +1380,23 @@ func TestContextBindWithQuery(t *testing.T) { assert.Equal(t, 0, w.Body.Len()) } +func TestContextBindWithYAML(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("foo: bar\nbar: foo")) + c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type + + var obj struct { + Foo string `yaml:"foo"` + Bar string `yaml:"bar"` + } + assert.NoError(t, c.BindYAML(&obj)) + assert.Equal(t, "foo", obj.Bar) + assert.Equal(t, "bar", obj.Foo) + assert.Equal(t, 0, w.Body.Len()) +} + func TestContextBadAutoBind(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) @@ -1264,7 +1414,7 @@ func TestContextBadAutoBind(t *testing.T) { assert.Empty(t, obj.Bar) assert.Empty(t, obj.Foo) - assert.Equal(t, 400, w.Code) + assert.Equal(t, http.StatusBadRequest, w.Code) assert.True(t, c.IsAborted()) } @@ -1300,6 +1450,27 @@ func TestContextShouldBindWithJSON(t *testing.T) { assert.Equal(t, 0, w.Body.Len()) } +func TestContextShouldBindWithXML(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString(` + + FOO + BAR + `)) + c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type + + var obj struct { + Foo string `xml:"foo"` + Bar string `xml:"bar"` + } + assert.NoError(t, c.ShouldBindXML(&obj)) + assert.Equal(t, "FOO", obj.Foo) + assert.Equal(t, "BAR", obj.Bar) + assert.Equal(t, 0, w.Body.Len()) +} + func TestContextShouldBindWithQuery(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) @@ -1316,6 +1487,23 @@ func TestContextShouldBindWithQuery(t *testing.T) { assert.Equal(t, 0, w.Body.Len()) } +func TestContextShouldBindWithYAML(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("foo: bar\nbar: foo")) + c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type + + var obj struct { + Foo string `yaml:"foo"` + Bar string `yaml:"bar"` + } + assert.NoError(t, c.ShouldBindYAML(&obj)) + assert.Equal(t, "foo", obj.Bar) + assert.Equal(t, "bar", obj.Foo) + assert.Equal(t, 0, w.Body.Len()) +} + func TestContextBadAutoShouldBind(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) @@ -1486,7 +1674,62 @@ func TestContextRenderDataFromReader(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, body, w.Body.String()) - assert.Equal(t, contentType, w.HeaderMap.Get("Content-Type")) - assert.Equal(t, fmt.Sprintf("%d", contentLength), w.HeaderMap.Get("Content-Length")) - assert.Equal(t, extraHeaders["Content-Disposition"], w.HeaderMap.Get("Content-Disposition")) + assert.Equal(t, contentType, w.Header().Get("Content-Type")) + assert.Equal(t, fmt.Sprintf("%d", contentLength), w.Header().Get("Content-Length")) + assert.Equal(t, extraHeaders["Content-Disposition"], w.Header().Get("Content-Disposition")) +} + +type TestResponseRecorder struct { + *httptest.ResponseRecorder + closeChannel chan bool +} + +func (r *TestResponseRecorder) CloseNotify() <-chan bool { + return r.closeChannel +} + +func (r *TestResponseRecorder) closeClient() { + r.closeChannel <- true +} + +func CreateTestResponseRecorder() *TestResponseRecorder { + return &TestResponseRecorder{ + httptest.NewRecorder(), + make(chan bool, 1), + } +} + +func TestContextStream(t *testing.T) { + w := CreateTestResponseRecorder() + c, _ := CreateTestContext(w) + + stopStream := true + c.Stream(func(w io.Writer) bool { + defer func() { + stopStream = false + }() + + w.Write([]byte("test")) + + return stopStream + }) + + assert.Equal(t, "testtest", w.Body.String()) +} + +func TestContextStreamWithClientGone(t *testing.T) { + w := CreateTestResponseRecorder() + c, _ := CreateTestContext(w) + + c.Stream(func(writer io.Writer) bool { + defer func() { + w.closeClient() + }() + + writer.Write([]byte("test")) + + return true + }) + + assert.Equal(t, "test", w.Body.String()) } diff --git a/coverage.sh b/coverage.sh deleted file mode 100644 index 81437f91..00000000 --- a/coverage.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -set -e - -echo "mode: count" > coverage.out - -for d in $(go list ./... | grep -E 'gin$|binding$|render$'); do - go test -v -covermode=count -coverprofile=profile.out $d - if [ -f profile.out ]; then - cat profile.out | grep -v "mode:" >> coverage.out - rm profile.out - fi -done diff --git a/debug.go b/debug.go index 897c4943..98c67cf7 100644 --- a/debug.go +++ b/debug.go @@ -6,13 +6,15 @@ package gin import ( "bytes" + "fmt" "html/template" - "log" + "os" + "runtime" + "strconv" + "strings" ) -func init() { - log.SetFlags(0) -} +const ginSupportMinGoVer = 6 // IsDebugging returns true if the framework is running in debug mode. // Use SetMode(gin.ReleaseMode) to disable debug mode. @@ -20,11 +22,18 @@ func IsDebugging() bool { return ginMode == debugCode } +// DebugPrintRouteFunc indicates debug log output format. +var DebugPrintRouteFunc func(httpMethod, absolutePath, handlerName string, nuHandlers int) + func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) { if IsDebugging() { nuHandlers := len(handlers) handlerName := nameOfFunction(handlers.Last()) - debugPrint("%-6s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers) + if DebugPrintRouteFunc == nil { + debugPrint("%-6s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers) + } else { + DebugPrintRouteFunc(httpMethod, absolutePath, handlerName, nuHandlers) + } } } @@ -42,11 +51,28 @@ func debugPrintLoadTemplate(tmpl *template.Template) { func debugPrint(format string, values ...interface{}) { if IsDebugging() { - log.Printf("[GIN-debug] "+format, values...) + if !strings.HasSuffix(format, "\n") { + format += "\n" + } + fmt.Fprintf(os.Stderr, "[GIN-debug] "+format, values...) } } +func getMinVer(v string) (uint64, error) { + first := strings.IndexByte(v, '.') + last := strings.LastIndexByte(v, '.') + if first == last { + return strconv.ParseUint(v[first+1:], 10, 64) + } + return strconv.ParseUint(v[first+1:last], 10, 64) +} + 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] Creating an Engine instance with the Logger and Recovery middleware already attached. `) diff --git a/debug_test.go b/debug_test.go index dfd54c82..97ff166b 100644 --- a/debug_test.go +++ b/debug_test.go @@ -11,6 +11,8 @@ import ( "io" "log" "os" + "runtime" + "sync" "testing" "github.com/stretchr/testify/assert" @@ -30,86 +32,121 @@ func TestIsDebugging(t *testing.T) { } func TestDebugPrint(t *testing.T) { - var w bytes.Buffer - setup(&w) - defer teardown() - - SetMode(ReleaseMode) - debugPrint("DEBUG this!") - SetMode(TestMode) - debugPrint("DEBUG this!") - assert.Empty(t, w.String()) - - SetMode(DebugMode) - debugPrint("these are %d %s\n", 2, "error messages") - assert.Equal(t, "[GIN-debug] these are 2 error messages\n", w.String()) + re := captureOutput(func() { + SetMode(DebugMode) + SetMode(ReleaseMode) + debugPrint("DEBUG this!") + SetMode(TestMode) + debugPrint("DEBUG this!") + SetMode(DebugMode) + debugPrint("these are %d %s\n", 2, "error messages") + SetMode(TestMode) + }) + assert.Equal(t, "[GIN-debug] these are 2 error messages\n", re) } func TestDebugPrintError(t *testing.T) { - var w bytes.Buffer - setup(&w) - defer teardown() - - SetMode(DebugMode) - debugPrintError(nil) - assert.Empty(t, w.String()) - - debugPrintError(errors.New("this is an error")) - assert.Equal(t, "[GIN-debug] [ERROR] this is an error\n", w.String()) + re := captureOutput(func() { + SetMode(DebugMode) + debugPrintError(nil) + debugPrintError(errors.New("this is an error")) + SetMode(TestMode) + }) + assert.Equal(t, "[GIN-debug] [ERROR] this is an error\n", re) } func TestDebugPrintRoutes(t *testing.T) { - var w bytes.Buffer - setup(&w) - defer teardown() - - debugPrintRoute("GET", "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest}) - assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, w.String()) + re := captureOutput(func() { + SetMode(DebugMode) + debugPrintRoute("GET", "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest}) + SetMode(TestMode) + }) + assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re) } func TestDebugPrintLoadTemplate(t *testing.T) { - var w bytes.Buffer - setup(&w) - defer teardown() - - templ := template.Must(template.New("").Delims("{[{", "}]}").ParseGlob("./fixtures/basic/hello.tmpl")) - debugPrintLoadTemplate(templ) - assert.Regexp(t, `^\[GIN-debug\] Loaded HTML Templates \(2\): \n(\t- \n|\t- hello\.tmpl\n){2}\n`, w.String()) + re := captureOutput(func() { + SetMode(DebugMode) + templ := template.Must(template.New("").Delims("{[{", "}]}").ParseGlob("./testdata/template/hello.tmpl")) + debugPrintLoadTemplate(templ) + SetMode(TestMode) + }) + assert.Regexp(t, `^\[GIN-debug\] Loaded HTML Templates \(2\): \n(\t- \n|\t- hello\.tmpl\n){2}\n`, re) } func TestDebugPrintWARNINGSetHTMLTemplate(t *testing.T) { - var w bytes.Buffer - setup(&w) - defer teardown() - - debugPrintWARNINGSetHTMLTemplate() - assert.Equal(t, "[GIN-debug] [WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called\nat initialization. ie. before any route is registered or the router is listening in a socket:\n\n\trouter := gin.Default()\n\trouter.SetHTMLTemplate(template) // << good place\n\n", w.String()) + re := captureOutput(func() { + SetMode(DebugMode) + debugPrintWARNINGSetHTMLTemplate() + SetMode(TestMode) + }) + assert.Equal(t, "[GIN-debug] [WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called\nat initialization. ie. before any route is registered or the router is listening in a socket:\n\n\trouter := gin.Default()\n\trouter.SetHTMLTemplate(template) // << good place\n\n", re) } func TestDebugPrintWARNINGDefault(t *testing.T) { - var w bytes.Buffer - setup(&w) - defer teardown() - - debugPrintWARNINGDefault() - assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", w.String()) + re := captureOutput(func() { + SetMode(DebugMode) + debugPrintWARNINGDefault() + SetMode(TestMode) + }) + 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) + } else { + assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re) + } } func TestDebugPrintWARNINGNew(t *testing.T) { - var w bytes.Buffer - setup(&w) - defer teardown() - - debugPrintWARNINGNew() - assert.Equal(t, "[GIN-debug] [WARNING] Running in \"debug\" mode. Switch to \"release\" mode in production.\n - using env:\texport GIN_MODE=release\n - using code:\tgin.SetMode(gin.ReleaseMode)\n\n", w.String()) + re := captureOutput(func() { + SetMode(DebugMode) + debugPrintWARNINGNew() + SetMode(TestMode) + }) + assert.Equal(t, "[GIN-debug] [WARNING] Running in \"debug\" mode. Switch to \"release\" mode in production.\n - using env:\texport GIN_MODE=release\n - using code:\tgin.SetMode(gin.ReleaseMode)\n\n", re) } -func setup(w io.Writer) { - SetMode(DebugMode) - log.SetOutput(w) +func captureOutput(f func()) string { + reader, writer, err := os.Pipe() + if err != nil { + panic(err) + } + stdout := os.Stdout + stderr := os.Stderr + defer func() { + os.Stdout = stdout + os.Stderr = stderr + log.SetOutput(os.Stderr) + }() + os.Stdout = writer + os.Stderr = writer + log.SetOutput(writer) + out := make(chan string) + wg := new(sync.WaitGroup) + wg.Add(1) + go func() { + var buf bytes.Buffer + wg.Done() + io.Copy(&buf, reader) + out <- buf.String() + }() + wg.Wait() + f() + writer.Close() + return <-out } -func teardown() { - SetMode(TestMode) - log.SetOutput(os.Stdout) +func TestGetMinVer(t *testing.T) { + var m uint64 + var e error + _, e = getMinVer("go1") + assert.NotNil(t, e) + m, e = getMinVer("go1.1") + assert.Equal(t, uint64(1), m) + assert.Nil(t, e) + m, e = getMinVer("go1.1.1") + assert.Nil(t, e) + assert.Equal(t, uint64(1), m) + _, e = getMinVer("go1.1.1.1") + assert.NotNil(t, e) } diff --git a/docs/how-to-build-an-effective-middleware.md b/docs/how-to-build-an-effective-middleware.md new file mode 100644 index 00000000..568d5720 --- /dev/null +++ b/docs/how-to-build-an-effective-middleware.md @@ -0,0 +1,137 @@ +# How to build one effective middleware? + +## Consitituent part + +The middleware has two parts: + + - part one is what is executed once, when you initialize your middleware. That's where you set up all the global objects, logicals etc. Everything that happens one per application lifetime. + + - part two is what executes on every request. For example, a database middleware you simply inject your "global" database object into the context. Once it's inside the context, you can retrieve it from within other middlewares and your handler function. + +```go +func funcName(params string) gin.HandlerFunc { + // <--- + // This is part one + // ---> + // The follow code is an example + if err := check(params); err != nil { + panic(err) + } + + return func(c *gin.Context) { + // <--- + // This is part two + // ---> + // The follow code is an example + c.Set("TestVar", params) + c.Next() + } +} +``` + +## Execution process + +Firstly, we have the follow example code: + +```go +func main() { + router := gin.Default() + + router.Use(globalMiddleware()) + + router.GET("/rest/n/api/*some", mid1(), mid2(), handler) + + router.Run() +} + +func globalMiddleware() gin.HandlerFunc { + fmt.Println("globalMiddleware...1") + + return func(c *gin.Context) { + fmt.Println("globalMiddleware...2") + c.Next() + fmt.Println("globalMiddleware...3") + } +} + +func handler(c *gin.Context) { + fmt.Println("exec handler.") +} + +func mid1() gin.HandlerFunc { + fmt.Println("mid1...1") + + return func(c *gin.Context) { + + fmt.Println("mid1...2") + c.Next() + fmt.Println("mid1...3") + } +} + +func mid2() gin.HandlerFunc { + fmt.Println("mid2...1") + + return func(c *gin.Context) { + fmt.Println("mid2...2") + c.Next() + fmt.Println("mid2...3") + } +} +``` + +According to [Consitituent part](#consitituent-part) said, when we run the gin process, **part one** will execute firstly and will print the follow information: + +```go +globalMiddleware...1 +mid1...1 +mid2...1 +``` + +And init order are: + +```go +globalMiddleware...1 + | + v +mid1...1 + | + v +mid2...1 +``` + +When we curl one request `curl -v localhost:8080/rest/n/api/some`, **part two** will execute their middleware and output the following information: + +```go +globalMiddleware...2 +mid1...2 +mid2...2 +exec handler. +mid2...3 +mid1...3 +globalMiddleware...3 +``` + +In other words, run order are: + +```go +globalMiddleware...2 + | + v +mid1...2 + | + v +mid2...2 + | + v +exec handler. + | + v +mid2...3 + | + v +mid1...3 + | + v +globalMiddleware...3 +``` diff --git a/errors.go b/errors.go index dbfccd85..ab13ca61 100644 --- a/errors.go +++ b/errors.go @@ -9,21 +9,28 @@ import ( "fmt" "reflect" - "github.com/gin-gonic/gin/json" + "github.com/gin-gonic/gin/internal/json" ) +// ErrorType is an unsigned 64-bit error code as defined in the gin spec. type ErrorType uint64 const ( - ErrorTypeBind ErrorType = 1 << 63 // used when c.Bind() fails - ErrorTypeRender ErrorType = 1 << 62 // used when c.Render() fails + // ErrorTypeBind is used when Context.Bind() fails. + ErrorTypeBind ErrorType = 1 << 63 + // ErrorTypeRender is used when Context.Render() fails. + ErrorTypeRender ErrorType = 1 << 62 + // ErrorTypePrivate indicates a private error. ErrorTypePrivate ErrorType = 1 << 0 - ErrorTypePublic ErrorType = 1 << 1 - + // ErrorTypePublic indicates a public error. + ErrorTypePublic ErrorType = 1 << 1 + // ErrorTypeAny indicates any other error. ErrorTypeAny ErrorType = 1<<64 - 1 - ErrorTypeNu = 2 + // ErrorTypeNu indicates any other error. + ErrorTypeNu = 2 ) +// Error represents a error's specification. type Error struct { Err error Type ErrorType @@ -34,16 +41,19 @@ type errorMsgs []*Error var _ error = &Error{} +// SetType sets the error's type. func (msg *Error) SetType(flags ErrorType) *Error { msg.Type = flags return msg } +// SetMeta sets the error's meta data. func (msg *Error) SetMeta(data interface{}) *Error { msg.Meta = data return msg } +// JSON creates a properly formated JSON func (msg *Error) JSON() interface{} { json := H{} if msg.Meta != nil { @@ -70,11 +80,12 @@ func (msg *Error) MarshalJSON() ([]byte, error) { return json.Marshal(msg.JSON()) } -// Error implements the error interface +// Error implements the error interface. func (msg Error) Error() string { return msg.Err.Error() } +// IsType judges one error. func (msg *Error) IsType(flags ErrorType) bool { return (msg.Type & flags) > 0 } @@ -138,6 +149,7 @@ func (a errorMsgs) JSON() interface{} { } } +// MarshalJSON implements the json.Marshaller interface. func (a errorMsgs) MarshalJSON() ([]byte, error) { return json.Marshal(a.JSON()) } diff --git a/errors_test.go b/errors_test.go index a666d7c1..9351b578 100644 --- a/errors_test.go +++ b/errors_test.go @@ -8,7 +8,7 @@ import ( "errors" "testing" - "github.com/gin-gonic/gin/json" + "github.com/gin-gonic/gin/internal/json" "github.com/stretchr/testify/assert" ) @@ -19,17 +19,17 @@ func TestError(t *testing.T) { Type: ErrorTypePrivate, } assert.Equal(t, err.Error(), baseError.Error()) - assert.Equal(t, err.JSON(), H{"error": baseError.Error()}) + assert.Equal(t, H{"error": baseError.Error()}, err.JSON()) assert.Equal(t, err.SetType(ErrorTypePublic), err) - assert.Equal(t, err.Type, ErrorTypePublic) + assert.Equal(t, ErrorTypePublic, err.Type) assert.Equal(t, err.SetMeta("some data"), err) - assert.Equal(t, err.Meta, "some data") - assert.Equal(t, err.JSON(), H{ + assert.Equal(t, "some data", err.Meta) + assert.Equal(t, H{ "error": baseError.Error(), "meta": "some data", - }) + }, err.JSON()) jsonBytes, _ := json.Marshal(err) assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes)) @@ -38,22 +38,22 @@ func TestError(t *testing.T) { "status": "200", "data": "some data", }) - assert.Equal(t, err.JSON(), H{ + assert.Equal(t, H{ "error": baseError.Error(), "status": "200", "data": "some data", - }) + }, err.JSON()) err.SetMeta(H{ "error": "custom error", "status": "200", "data": "some data", }) - assert.Equal(t, err.JSON(), H{ + assert.Equal(t, H{ "error": "custom error", "status": "200", "data": "some data", - }) + }, err.JSON()) type customError struct { status string diff --git a/examples/basic/main.go b/examples/basic/main.go index 473c6a09..1c9e0ac4 100644 --- a/examples/basic/main.go +++ b/examples/basic/main.go @@ -1,10 +1,12 @@ package main import ( + "net/http" + "github.com/gin-gonic/gin" ) -var DB = make(map[string]string) +var db = make(map[string]string) func setupRouter() *gin.Engine { // Disable Console Color @@ -13,17 +15,17 @@ func setupRouter() *gin.Engine { // Ping test r.GET("/ping", func(c *gin.Context) { - c.String(200, "pong") + c.String(http.StatusOK, "pong") }) // Get user value r.GET("/user/:name", func(c *gin.Context) { user := c.Params.ByName("name") - value, ok := DB[user] + value, ok := db[user] if ok { - c.JSON(200, gin.H{"user": user, "value": value}) + c.JSON(http.StatusOK, gin.H{"user": user, "value": value}) } else { - c.JSON(200, gin.H{"user": user, "status": "no value"}) + c.JSON(http.StatusOK, gin.H{"user": user, "status": "no value"}) } }) @@ -48,8 +50,8 @@ func setupRouter() *gin.Engine { } if c.Bind(&json) == nil { - DB[user] = json.Value - c.JSON(200, gin.H{"status": "ok"}) + db[user] = json.Value + c.JSON(http.StatusOK, gin.H{"status": "ok"}) } }) diff --git a/examples/basic/main_test.go b/examples/basic/main_test.go index 61203d66..5eb85240 100644 --- a/examples/basic/main_test.go +++ b/examples/basic/main_test.go @@ -15,6 +15,6 @@ func TestPingRoute(t *testing.T) { req, _ := http.NewRequest("GET", "/ping", nil) router.ServeHTTP(w, req) - assert.Equal(t, 200, w.Code) + assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, "pong", w.Body.String()) } diff --git a/examples/custom-validation/server.go b/examples/custom-validation/server.go index dea0c302..9238200b 100644 --- a/examples/custom-validation/server.go +++ b/examples/custom-validation/server.go @@ -10,6 +10,7 @@ import ( "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"` diff --git a/examples/favicon/main.go b/examples/favicon/main.go index 5ad39331..d32ca098 100644 --- a/examples/favicon/main.go +++ b/examples/favicon/main.go @@ -1,6 +1,8 @@ package main import ( + "net/http" + "github.com/gin-gonic/gin" "github.com/thinkerou/favicon" ) @@ -9,7 +11,7 @@ func main() { app := gin.Default() app.Use(favicon.New("./favicon.ico")) app.GET("/ping", func(c *gin.Context) { - c.String(200, "Hello favicon.") + c.String(http.StatusOK, "Hello favicon.") }) app.Run(":8080") } diff --git a/examples/grpc/README.md b/examples/grpc/README.md new file mode 100644 index 00000000..a96d3c1c --- /dev/null +++ b/examples/grpc/README.md @@ -0,0 +1,19 @@ +## How to run this example + +1. run grpc server + +```sh +$ go run grpc/server.go +``` + +2. run gin server + +```sh +$ go run gin/main.go +``` + +3. use curl command to test it + +```sh +$ curl -v 'http://localhost:8052/rest/n/thinkerou' +``` diff --git a/examples/grpc/gin/main.go b/examples/grpc/gin/main.go new file mode 100644 index 00000000..820e65a3 --- /dev/null +++ b/examples/grpc/gin/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "fmt" + "log" + "net/http" + + "github.com/gin-gonic/gin" + pb "github.com/gin-gonic/gin/examples/grpc/pb" + "google.golang.org/grpc" +) + +func main() { + // Set up a connection to the server. + conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure()) + if err != nil { + log.Fatalf("did not connect: %v", err) + } + defer conn.Close() + client := pb.NewGreeterClient(conn) + + // Set up a http server. + r := gin.Default() + r.GET("/rest/n/:name", func(c *gin.Context) { + name := c.Param("name") + + // Contact the server and print out its response. + req := &pb.HelloRequest{Name: name} + res, err := client.SayHello(c, req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "result": fmt.Sprint(res.Message), + }) + }) + + // Run http server + if err := r.Run(":8052"); err != nil { + log.Fatalf("could not run server: %v", err) + } +} diff --git a/examples/grpc/grpc/server.go b/examples/grpc/grpc/server.go new file mode 100644 index 00000000..d9bf9fc5 --- /dev/null +++ b/examples/grpc/grpc/server.go @@ -0,0 +1,34 @@ +package main + +import ( + "log" + "net" + + pb "github.com/gin-gonic/gin/examples/grpc/pb" + "golang.org/x/net/context" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +// server is used to implement helloworld.GreeterServer. +type server struct{} + +// SayHello implements helloworld.GreeterServer +func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { + return &pb.HelloReply{Message: "Hello " + in.Name}, nil +} + +func main() { + lis, err := net.Listen("tcp", ":50051") + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + s := grpc.NewServer() + pb.RegisterGreeterServer(s, &server{}) + + // Register reflection service on gRPC server. + reflection.Register(s) + if err := s.Serve(lis); err != nil { + log.Fatalf("failed to serve: %v", err) + } +} diff --git a/examples/grpc/pb/helloworld.pb.go b/examples/grpc/pb/helloworld.pb.go new file mode 100644 index 00000000..c8c8942a --- /dev/null +++ b/examples/grpc/pb/helloworld.pb.go @@ -0,0 +1,151 @@ +// Code generated by protoc-gen-go. +// source: helloworld.proto +// DO NOT EDIT! + +/* +Package helloworld is a generated protocol buffer package. + +It is generated from these files: + helloworld.proto + +It has these top-level messages: + HelloRequest + HelloReply +*/ +package helloworld + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +import ( + context "golang.org/x/net/context" + grpc "google.golang.org/grpc" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +// The request message containing the user's name. +type HelloRequest struct { + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` +} + +func (m *HelloRequest) Reset() { *m = HelloRequest{} } +func (m *HelloRequest) String() string { return proto.CompactTextString(m) } +func (*HelloRequest) ProtoMessage() {} +func (*HelloRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } + +// The response message containing the greetings +type HelloReply struct { + Message string `protobuf:"bytes,1,opt,name=message" json:"message,omitempty"` +} + +func (m *HelloReply) Reset() { *m = HelloReply{} } +func (m *HelloReply) String() string { return proto.CompactTextString(m) } +func (*HelloReply) ProtoMessage() {} +func (*HelloReply) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } + +func init() { + proto.RegisterType((*HelloRequest)(nil), "helloworld.HelloRequest") + proto.RegisterType((*HelloReply)(nil), "helloworld.HelloReply") +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// Client API for Greeter service + +type GreeterClient interface { + // Sends a greeting + SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) +} + +type greeterClient struct { + cc *grpc.ClientConn +} + +func NewGreeterClient(cc *grpc.ClientConn) GreeterClient { + return &greeterClient{cc} +} + +func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) { + out := new(HelloReply) + err := grpc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// Server API for Greeter service + +type GreeterServer interface { + // Sends a greeting + SayHello(context.Context, *HelloRequest) (*HelloReply, error) +} + +func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) { + s.RegisterService(&_Greeter_serviceDesc, srv) +} + +func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HelloRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GreeterServer).SayHello(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/helloworld.Greeter/SayHello", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _Greeter_serviceDesc = grpc.ServiceDesc{ + ServiceName: "helloworld.Greeter", + HandlerType: (*GreeterServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SayHello", + Handler: _Greeter_SayHello_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "helloworld.proto", +} + +func init() { proto.RegisterFile("helloworld.proto", fileDescriptor0) } + +var fileDescriptor0 = []byte{ + // 174 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0xc8, 0x48, 0xcd, 0xc9, + 0xc9, 0x2f, 0xcf, 0x2f, 0xca, 0x49, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x42, 0x88, + 0x28, 0x29, 0x71, 0xf1, 0x78, 0x80, 0x78, 0x41, 0xa9, 0x85, 0xa5, 0xa9, 0xc5, 0x25, 0x42, 0x42, + 0x5c, 0x2c, 0x79, 0x89, 0xb9, 0xa9, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x60, 0xb6, 0x92, + 0x1a, 0x17, 0x17, 0x54, 0x4d, 0x41, 0x4e, 0xa5, 0x90, 0x04, 0x17, 0x7b, 0x6e, 0x6a, 0x71, 0x71, + 0x62, 0x3a, 0x4c, 0x11, 0x8c, 0x6b, 0xe4, 0xc9, 0xc5, 0xee, 0x5e, 0x94, 0x9a, 0x5a, 0x92, 0x5a, + 0x24, 0x64, 0xc7, 0xc5, 0x11, 0x9c, 0x58, 0x09, 0xd6, 0x25, 0x24, 0xa1, 0x87, 0xe4, 0x02, 0x64, + 0xcb, 0xa4, 0xc4, 0xb0, 0xc8, 0x00, 0xad, 0x50, 0x62, 0x70, 0x32, 0xe0, 0x92, 0xce, 0xcc, 0xd7, + 0x4b, 0x2f, 0x2a, 0x48, 0xd6, 0x4b, 0xad, 0x48, 0xcc, 0x2d, 0xc8, 0x49, 0x2d, 0x46, 0x52, 0xeb, + 0xc4, 0x0f, 0x56, 0x1c, 0x0e, 0x62, 0x07, 0x80, 0xbc, 0x14, 0xc0, 0x98, 0xc4, 0x06, 0xf6, 0x9b, + 0x31, 0x20, 0x00, 0x00, 0xff, 0xff, 0x0f, 0xb7, 0xcd, 0xf2, 0xef, 0x00, 0x00, 0x00, +} diff --git a/examples/grpc/pb/helloworld.proto b/examples/grpc/pb/helloworld.proto new file mode 100644 index 00000000..d79a6a0d --- /dev/null +++ b/examples/grpc/pb/helloworld.proto @@ -0,0 +1,37 @@ +// Copyright 2015 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "io.grpc.examples.helloworld"; +option java_outer_classname = "HelloWorldProto"; + +package helloworld; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} diff --git a/examples/http-pusher/assets/app.js b/examples/http-pusher/assets/app.js new file mode 100644 index 00000000..05271b67 --- /dev/null +++ b/examples/http-pusher/assets/app.js @@ -0,0 +1 @@ +console.log("http2 pusher"); diff --git a/examples/http-pusher/main.go b/examples/http-pusher/main.go new file mode 100644 index 00000000..d4f33aa0 --- /dev/null +++ b/examples/http-pusher/main.go @@ -0,0 +1,41 @@ +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") +} diff --git a/examples/http-pusher/testdata/ca.pem b/examples/http-pusher/testdata/ca.pem new file mode 100644 index 00000000..6c8511a7 --- /dev/null +++ b/examples/http-pusher/testdata/ca.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICSjCCAbOgAwIBAgIJAJHGGR4dGioHMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQxDzANBgNVBAMTBnRlc3RjYTAeFw0xNDExMTEyMjMxMjla +Fw0yNDExMDgyMjMxMjlaMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0 +YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMT +BnRlc3RjYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwEDfBV5MYdlHVHJ7 ++L4nxrZy7mBfAVXpOc5vMYztssUI7mL2/iYujiIXM+weZYNTEpLdjyJdu7R5gGUu +g1jSVK/EPHfc74O7AyZU34PNIP4Sh33N+/A5YexrNgJlPY+E3GdVYi4ldWJjgkAd +Qah2PH5ACLrIIC6tRka9hcaBlIECAwEAAaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADgYEAHzC7jdYlzAVmddi/gdAeKPau +sPBG/C2HCWqHzpCUHcKuvMzDVkY/MP2o6JIW2DBbY64bO/FceExhjcykgaYtCH/m +oIU63+CFOTtR7otyQAWHqXa7q4SbCDlG7DyRFxqG0txPtGvy12lgldA2+RgcigQG +Dfcog5wrJytaQ6UA0wE= +-----END CERTIFICATE----- diff --git a/examples/http-pusher/testdata/server.key b/examples/http-pusher/testdata/server.key new file mode 100644 index 00000000..143a5b87 --- /dev/null +++ b/examples/http-pusher/testdata/server.key @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAOHDFScoLCVJpYDD +M4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1BgzkWF+slf +3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd9N8YwbBY +AckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAECgYAn7qGnM2vbjJNBm0VZCkOkTIWm +V10okw7EPJrdL2mkre9NasghNXbE1y5zDshx5Nt3KsazKOxTT8d0Jwh/3KbaN+YY +tTCbKGW0pXDRBhwUHRcuRzScjli8Rih5UOCiZkhefUTcRb6xIhZJuQy71tjaSy0p +dHZRmYyBYO2YEQ8xoQJBAPrJPhMBkzmEYFtyIEqAxQ/o/A6E+E4w8i+KM7nQCK7q +K4JXzyXVAjLfyBZWHGM2uro/fjqPggGD6QH1qXCkI4MCQQDmdKeb2TrKRh5BY1LR +81aJGKcJ2XbcDu6wMZK4oqWbTX2KiYn9GB0woM6nSr/Y6iy1u145YzYxEV/iMwff +DJULAkB8B2MnyzOg0pNFJqBJuH29bKCcHa8gHJzqXhNO5lAlEbMK95p/P2Wi+4Hd +aiEIAF1BF326QJcvYKmwSmrORp85AkAlSNxRJ50OWrfMZnBgzVjDx3xG6KsFQVk2 +ol6VhqL6dFgKUORFUWBvnKSyhjJxurlPEahV6oo6+A+mPhFY8eUvAkAZQyTdupP3 +XEFQKctGz+9+gKkemDp7LBBMEMBXrGTLPhpEfcjv/7KPdnFHYmhYeBTBnuVmTVWe +F98XJ7tIFfJq +-----END PRIVATE KEY----- diff --git a/examples/http-pusher/testdata/server.pem b/examples/http-pusher/testdata/server.pem new file mode 100644 index 00000000..f3d43fcc --- /dev/null +++ b/examples/http-pusher/testdata/server.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICnDCCAgWgAwIBAgIBBzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJBVTET +MBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ +dHkgTHRkMQ8wDQYDVQQDEwZ0ZXN0Y2EwHhcNMTUxMTA0MDIyMDI0WhcNMjUxMTAx +MDIyMDI0WjBlMQswCQYDVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNV +BAcTB0NoaWNhZ28xFTATBgNVBAoTDEV4YW1wbGUsIENvLjEaMBgGA1UEAxQRKi50 +ZXN0Lmdvb2dsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOHDFSco +LCVJpYDDM4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1Bg +zkWF+slf3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd +9N8YwbBYAckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAGjazBpMAkGA1UdEwQCMAAw +CwYDVR0PBAQDAgXgME8GA1UdEQRIMEaCECoudGVzdC5nb29nbGUuZnKCGHdhdGVy +em9vaS50ZXN0Lmdvb2dsZS5iZYISKi50ZXN0LnlvdXR1YmUuY29thwTAqAEDMA0G +CSqGSIb3DQEBCwUAA4GBAJFXVifQNub1LUP4JlnX5lXNlo8FxZ2a12AFQs+bzoJ6 +hM044EDjqyxUqSbVePK0ni3w1fHQB5rY9yYC5f8G7aqqTY1QOhoUk8ZTSTRpnkTh +y4jjdvTZeLDVBlueZUTDRmy2feY5aZIU18vFDK08dTG0A87pppuv1LNIR3loveU8 +-----END CERTIFICATE----- diff --git a/examples/http2/main.go b/examples/http2/main.go index 07df01e2..6598a4c9 100644 --- a/examples/http2/main.go +++ b/examples/http2/main.go @@ -3,6 +3,7 @@ package main import ( "html/template" "log" + "net/http" "os" "github.com/gin-gonic/gin" @@ -27,7 +28,7 @@ func main() { r.SetHTMLTemplate(html) r.GET("/welcome", func(c *gin.Context) { - c.HTML(200, "https", gin.H{ + c.HTML(http.StatusOK, "https", gin.H{ "status": "success", }) }) diff --git a/examples/realtime-advanced/main.go b/examples/realtime-advanced/main.go index 1f3c8585..f3ead476 100644 --- a/examples/realtime-advanced/main.go +++ b/examples/realtime-advanced/main.go @@ -13,16 +13,19 @@ func main() { StartGin() } +// ConfigRuntime sets the number of operating system threads. func ConfigRuntime() { nuCPU := runtime.NumCPU() runtime.GOMAXPROCS(nuCPU) fmt.Printf("Running with %d CPUs\n", nuCPU) } +// StartWorkers start starsWorker by goroutine. func StartWorkers() { go statsWorker() } +// StartGin starts gin web server with setting router. func StartGin() { gin.SetMode(gin.ReleaseMode) diff --git a/examples/realtime-advanced/resources/room_login.templ.html b/examples/realtime-advanced/resources/room_login.templ.html index 27dac387..905c012f 100644 --- a/examples/realtime-advanced/resources/room_login.templ.html +++ b/examples/realtime-advanced/resources/room_login.templ.html @@ -1,4 +1,4 @@ - + @@ -20,9 +20,9 @@ - + -