From 6ab50f944ca52bdd4d982d0bec454d94bf7b802a Mon Sep 17 00:00:00 2001 From: andriikushch Date: Fri, 12 Oct 2018 01:31:31 +0200 Subject: [PATCH 001/111] replace deprecated HeaderMap with Header() (#1585) --- auth_test.go | 4 +-- context_17_test.go | 2 +- context_test.go | 64 +++++++++++++++++++++---------------------- render/render_test.go | 6 ++-- routes_test.go | 12 ++++---- 5 files changed, 44 insertions(+), 44 deletions(-) diff --git a/auth_test.go b/auth_test.go index ab7e94be..197e9208 100644 --- a/auth_test.go +++ b/auth_test.go @@ -122,7 +122,7 @@ func TestBasicAuth401(t *testing.T) { assert.False(t, called) assert.Equal(t, http.StatusUnauthorized, w.Code) - assert.Equal(t, "Basic realm=\"Authorization Required\"", w.HeaderMap.Get("WWW-Authenticate")) + assert.Equal(t, "Basic realm=\"Authorization Required\"", w.Header().Get("WWW-Authenticate")) } func TestBasicAuth401WithCustomRealm(t *testing.T) { @@ -142,5 +142,5 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) { assert.False(t, called) assert.Equal(t, http.StatusUnauthorized, w.Code) - assert.Equal(t, "Basic realm=\"My Custom \\\"Realm\\\"\"", w.HeaderMap.Get("WWW-Authenticate")) + assert.Equal(t, "Basic realm=\"My Custom \\\"Realm\\\"\"", w.Header().Get("WWW-Authenticate")) } diff --git a/context_17_test.go b/context_17_test.go index d2251904..5b9ebcdc 100644 --- a/context_17_test.go +++ b/context_17_test.go @@ -23,5 +23,5 @@ func TestContextRenderPureJSON(t *testing.T) { 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.HeaderMap.Get("Content-Type")) + assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) } diff --git a/context_test.go b/context_test.go index 5a5bb6e1..2e972f18 100644 --- a/context_test.go +++ b/context_test.go @@ -628,7 +628,7 @@ func TestContextRenderJSON(t *testing.T) { 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.HeaderMap.Get("Content-Type")) + assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) } // Tests that the response is serialized as JSONP @@ -642,7 +642,7 @@ func TestContextRenderJSONP(t *testing.T) { 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 @@ -656,7 +656,7 @@ func TestContextRenderJSONPWithoutCallback(t *testing.T) { assert.Equal(t, http.StatusCreated, 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")) } // Tests that no JSON is rendered if code is 204 @@ -668,7 +668,7 @@ func TestContextRenderNoContentJSON(t *testing.T) { 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 @@ -682,7 +682,7 @@ func TestContextRenderAPIJSON(t *testing.T) { 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 @@ -695,7 +695,7 @@ func TestContextRenderNoContentAPIJSON(t *testing.T) { 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 @@ -708,7 +708,7 @@ func TestContextRenderIndentedJSON(t *testing.T) { 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 @@ -720,7 +720,7 @@ func TestContextRenderNoContentIndentedJSON(t *testing.T) { 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 @@ -734,7 +734,7 @@ func TestContextRenderSecureJSON(t *testing.T) { 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 @@ -746,7 +746,7 @@ func TestContextRenderNoContentSecureJSON(t *testing.T) { 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) { @@ -757,7 +757,7 @@ func TestContextRenderNoContentAsciiJSON(t *testing.T) { assert.Equal(t, http.StatusNoContent, w.Code) assert.Empty(t, w.Body.String()) - assert.Equal(t, "application/json", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, "application/json", w.Header().Get("Content-Type")) } // Tests that the response executes the templates @@ -773,7 +773,7 @@ func TestContextRenderHTML(t *testing.T) { 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) { @@ -797,7 +797,7 @@ func TestContextRenderHTML2(t *testing.T) { 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 @@ -811,7 +811,7 @@ func TestContextRenderNoContentHTML(t *testing.T) { 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 @@ -824,7 +824,7 @@ func TestContextRenderXML(t *testing.T) { 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 @@ -836,7 +836,7 @@ func TestContextRenderNoContentXML(t *testing.T) { 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 @@ -849,7 +849,7 @@ func TestContextRenderString(t *testing.T) { 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 @@ -861,7 +861,7 @@ func TestContextRenderNoContentString(t *testing.T) { 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 @@ -875,7 +875,7 @@ func TestContextRenderHTMLString(t *testing.T) { 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 @@ -888,7 +888,7 @@ func TestContextRenderNoContentHTMLString(t *testing.T) { 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` @@ -901,7 +901,7 @@ func TestContextRenderData(t *testing.T) { 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 @@ -913,7 +913,7 @@ func TestContextRenderNoContentData(t *testing.T) { 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) { @@ -942,7 +942,7 @@ func TestContextRenderFile(t *testing.T) { 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 @@ -955,7 +955,7 @@ func TestContextRenderYAML(t *testing.T) { 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 @@ -979,7 +979,7 @@ func TestContextRenderProtoBuf(t *testing.T) { assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, string(protoData), w.Body.String()) - assert.Equal(t, "application/x-protobuf", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type")) } func TestContextHeaders(t *testing.T) { @@ -1062,7 +1062,7 @@ func TestContextNegotiationWithJSON(t *testing.T) { 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) { @@ -1077,7 +1077,7 @@ func TestContextNegotiationWithXML(t *testing.T) { 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) { @@ -1095,7 +1095,7 @@ func TestContextNegotiationWithHTML(t *testing.T) { 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) { @@ -1131,7 +1131,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") @@ -1627,9 +1627,9 @@ 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 { diff --git a/render/render_test.go b/render/render_test.go index 09ccc658..4c9b180d 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -480,7 +480,7 @@ func TestRenderReader(t *testing.T) { assert.NoError(t, err) assert.Equal(t, body, w.Body.String()) - assert.Equal(t, "image/png", w.HeaderMap.Get("Content-Type")) - assert.Equal(t, strconv.Itoa(len(body)), w.HeaderMap.Get("Content-Length")) - assert.Equal(t, headers["Content-Disposition"], w.HeaderMap.Get("Content-Disposition")) + assert.Equal(t, "image/png", w.Header().Get("Content-Type")) + assert.Equal(t, strconv.Itoa(len(body)), w.Header().Get("Content-Length")) + assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition")) } diff --git a/routes_test.go b/routes_test.go index 23e749e2..60f1c81b 100644 --- a/routes_test.go +++ b/routes_test.go @@ -267,7 +267,7 @@ func TestRouteStaticFile(t *testing.T) { assert.Equal(t, w, w2) assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, "Gin Web Framework", 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")) w3 := performRequest(router, "HEAD", "/using_static/"+filename) w4 := performRequest(router, "HEAD", "/result") @@ -285,7 +285,7 @@ func TestRouteStaticListingDir(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) assert.Contains(t, w.Body.String(), "gin.go") - 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")) } // TestHandleHeadToDir - ensure the root/sub dir handles properly @@ -312,10 +312,10 @@ func TestRouterMiddlewareAndStatic(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) assert.Contains(t, w.Body.String(), "package gin") - assert.Equal(t, "text/plain; charset=utf-8", w.HeaderMap.Get("Content-Type")) - assert.NotEqual(t, w.HeaderMap.Get("Last-Modified"), "Mon, 02 Jan 2006 15:04:05 MST") - assert.Equal(t, "Mon, 02 Jan 2006 15:04:05 MST", w.HeaderMap.Get("Expires")) - assert.Equal(t, "Gin Framework", w.HeaderMap.Get("x-GIN")) + assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type")) + assert.NotEqual(t, w.Header().Get("Last-Modified"), "Mon, 02 Jan 2006 15:04:05 MST") + assert.Equal(t, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Expires")) + assert.Equal(t, "Gin Framework", w.Header().Get("x-GIN")) } func TestRouteNotAllowedEnabled(t *testing.T) { From 268e30710b77e1cd48b25df235cf702dc8e942a2 Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Sun, 14 Oct 2018 11:05:24 +0800 Subject: [PATCH 002/111] fix(Makefile): golint to new URL (#1592) as title. Just update the golint to new URL. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 51b9969f..ea7b4f7c 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,7 @@ 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; From 01ca2530d4b6c44c718b00c06f0e1d092572d49e Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Sun, 14 Oct 2018 12:39:16 +0800 Subject: [PATCH 003/111] refactor(Makefile): allow overriding default go program (#1593) --- Makefile | 28 ++++++++++++++++++---------- coverage.sh | 13 ------------- 2 files changed, 18 insertions(+), 23 deletions(-) delete mode 100644 coverage.sh diff --git a/Makefile b/Makefile index ea7b4f7c..c20429a1 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,9 @@ +GO ?= go GOFMT ?= gofmt "-s" -PACKAGES ?= $(shell go list ./... | grep -v /vendor/) -VETPACKAGES ?= $(shell go list ./... | grep -v /vendor/ | grep -v /examples/) +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 @@ -10,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: @@ -18,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:"; \ @@ -27,14 +35,14 @@ fmt-check: fi; vet: - go vet $(VETPACKAGES) + $(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: @@ -43,20 +51,20 @@ embedmd: .PHONY: lint lint: @hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ - go get -u golang.org/x/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) diff --git a/coverage.sh b/coverage.sh deleted file mode 100644 index 4d1ee036..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$' | grep -v 'examples'); 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 From 523435e5245faade0a43a33bd3faab32456bab32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Mon, 15 Oct 2018 12:52:51 +0800 Subject: [PATCH 004/111] attempt to support go module (#1569) * support go module * update golint package url * update golint --- .travis.yml | 10 +++++++++- Makefile | 6 ++++++ go.mod | 13 ++++++++----- go.sum | 26 ++++++++++++++++---------- tools.go | 25 +++++++++++++++++++++++++ 5 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 tools.go diff --git a/.travis.yml b/.travis.yml index a765f37f..2eeb0b3d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,12 +11,20 @@ go: 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/Makefile b/Makefile index c20429a1..b698ac09 100644 --- a/Makefile +++ b/Makefile @@ -68,3 +68,9 @@ 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/go.mod b/go.mod index bd4ad975..0797b934 100644 --- a/go.mod +++ b/go.mod @@ -1,25 +1,28 @@ module github.com/gin-gonic/gin require ( - github.com/davecgh/go-spew v0.0.0-20180221232628-8991bc29aa16 // indirect + github.com/campoy/embedmd v0.0.0-20171205015432-c59ce00e0296 + github.com/client9/misspell v0.3.4 + github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-broadcast v0.0.0-20171205050544-f664265f5a66 github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 github.com/gin-gonic/autotls v0.0.0-20180426091246-be87bd5ef97b github.com/golang/protobuf v1.2.0 github.com/jessevdk/go-assets v0.0.0-20160921144138-4f4301a06e15 - github.com/json-iterator/go v0.0.0-20180806060727-1624edc4454b + github.com/json-iterator/go v1.1.5 github.com/manucorporat/stats v0.0.0-20180402194714-3ba42d56d227 github.com/mattn/go-isatty v0.0.3 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/testify v1.2.2 - github.com/thinkerou/favicon v0.0.0-20170710140520-94a442a49da6 + github.com/thinkerou/favicon v0.1.0 github.com/ugorji/go v1.1.1 - golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b + golang.org/x/crypto v0.0.0-20180927165925-5295e8364332 + golang.org/x/lint v0.0.0-20180702182130-06c8688daad7 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f - golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e // indirect + golang.org/x/sys v0.0.0-20180928133829-e4b3c5e90611 // indirect google.golang.org/grpc v1.15.0 gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/go-playground/validator.v8 v8.18.2 diff --git a/go.sum b/go.sum index 3382beed..2d307e7e 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,10 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/campoy/embedmd v0.0.0-20171205015432-c59ce00e0296 h1:tRsilif6pbtt+PX6uRoyGd+qR+4ZPucFZLHlc3Ak6z8= +github.com/campoy/embedmd v0.0.0-20171205015432-c59ce00e0296/go.mod h1:/dBk8ICkslPCmyRdn4azP+QvBxL6Eg3EYxUGI9xMMFw= +github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/davecgh/go-spew v0.0.0-20180221232628-8991bc29aa16 h1:HqkufMBR7waVfFFRABWqHa1WgTvjtVDJTLJe3CR576A= -github.com/davecgh/go-spew v0.0.0-20180221232628-8991bc29aa16/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-broadcast v0.0.0-20171205050544-f664265f5a66 h1:QnnoVdChKs+GeTvN4rPYTW6b5U6M3HMEvQ/+x4IGtfY= github.com/dustin/go-broadcast v0.0.0-20171205050544-f664265f5a66/go.mod h1:kTEh6M2J/mh7nsskr28alwLCXm/DSG5OSA/o31yy2XU= github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY= @@ -10,14 +13,15 @@ github.com/gin-gonic/autotls v0.0.0-20180426091246-be87bd5ef97b h1:dm/NYytoj7p8J github.com/gin-gonic/autotls v0.0.0-20180426091246-be87bd5ef97b/go.mod h1:vwfeXwKgEIWq63oVfwaBjoByS4dZzYbHHROHjV4IjNY= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7 h1:2hRPrmiwPrp3fQX967rNJIhQPtiGXdlQWAxKbKw3VHA= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/jessevdk/go-assets v0.0.0-20160921144138-4f4301a06e15 h1:cW/amwGEJK5MSKntPXRjX4dxs/nGxGT8gXKIsKFmHGc= github.com/jessevdk/go-assets v0.0.0-20160921144138-4f4301a06e15/go.mod h1:Fdm/oWRW+CH8PRbLntksCNtmcCBximKPkVQYvmMl80k= -github.com/json-iterator/go v0.0.0-20180806060727-1624edc4454b h1:X61dhFTE1Au92SvyF8HyAwdjWqiSdfBgFR7wTxC0+uU= -github.com/json-iterator/go v0.0.0-20180806060727-1624edc4454b/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/manucorporat/stats v0.0.0-20180402194714-3ba42d56d227 h1:KIaAZ/V+/0/6BOULrmBQ9T1ed8BkKqGIjIKW923nJuo= github.com/manucorporat/stats v0.0.0-20180402194714-3ba42d56d227/go.mod h1:ruMr5t05gVho4tuDv0PbI0Bb8nOxc/5Y6JzRHe/yfA0= @@ -31,12 +35,13 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/thinkerou/favicon v0.0.0-20170710140520-94a442a49da6 h1:d/LEgOfWe+AlOCz/kzmkvlO+gq9LRGhjSHqt2nx8Muc= -github.com/thinkerou/favicon v0.0.0-20170710140520-94a442a49da6/go.mod h1:HL7Pap5kOluZv1ku34pZo/AJ44GaxMEPFZ3pmuexV2s= +github.com/thinkerou/favicon v0.1.0 h1:eWMISKTpHq2G8HOuKn7ydD55j5DDehx94b0C2y8ABMs= +github.com/thinkerou/favicon v0.1.0/go.mod h1:HL7Pap5kOluZv1ku34pZo/AJ44GaxMEPFZ3pmuexV2s= github.com/ugorji/go v1.1.1 h1:gmervu+jDMvXTbcHQ0pd2wee85nEoE0BsVyEuzkfK8w= github.com/ugorji/go v1.1.1/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= -golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b h1:2b9XGzhjiYsYPnKXoEfL7klWZQIt8IfyRCz62gCqqlQ= -golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180927165925-5295e8364332 h1:hvQVdF6P9DX4OiKA5tpehlG6JsgzmyQiThG7q5Bn3UQ= +golang.org/x/crypto v0.0.0-20180927165925-5295e8364332/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7 h1:00BeQWmeaGazuOrq8Q5K5d3/cHaGuFrZzpaHBXfrsUA= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d h1:g9qWBGx4puODJTMVyoPrpoxPFgVGd+z1DZwjfRu4d0I= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -44,10 +49,11 @@ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAG golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180928133829-e4b3c5e90611 h1:O33LKL7WyJgjN9CvxfTIomjIClbd/Kq86/iipowHQU0= +golang.org/x/sys v0.0.0-20180928133829-e4b3c5e90611/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52 h1:JG/0uqcGdTNgq7FdU+61l5Pdmb8putNZlXb65bJBROs= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= diff --git a/tools.go b/tools.go new file mode 100644 index 00000000..9f96406a --- /dev/null +++ b/tools.go @@ -0,0 +1,25 @@ +// 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 tools + +// This file exists to cause `go mod` and `go get` to believe these tools +// are dependencies, even though they are not runtime dependencies of any +// gin package. This means they will appear in `go.mod` file, but will not +// be a part of the build. + +package gin + +import ( + _ "github.com/campoy/embedmd" + _ "github.com/client9/misspell/cmd/misspell" + _ "github.com/dustin/go-broadcast" + _ "github.com/gin-gonic/autotls" + _ "github.com/jessevdk/go-assets" + _ "github.com/manucorporat/stats" + _ "github.com/thinkerou/favicon" + _ "golang.org/x/crypto/acme/autocert" + _ "golang.org/x/lint/golint" + _ "google.golang.org/grpc" +) From 98082fd590798eda8bdada82fd1550dfe6941964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Mon, 15 Oct 2018 13:01:44 +0800 Subject: [PATCH 005/111] document: add docs dir and middleware document (#1521) * init docs dir * add middleware document * fix indent * update docs --- docs/how-to-build-an-effective-middleware.md | 137 +++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 docs/how-to-build-an-effective-middleware.md 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..db04428c --- /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 initalize 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 furnction. + +```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 +``` From 524757b81c99e51a64e6ecc7ee0c181abc39bd79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Mon, 15 Oct 2018 20:24:32 +0800 Subject: [PATCH 006/111] vendor: upgrade some dependency package version (#1596) ref https://github.com/gin-gonic/gin/pull/1569#issuecomment-429731722 --- go.mod | 10 +++++----- go.sum | 19 ++++++++++--------- vendor/vendor.json | 43 +++++++++++++++++++++++++------------------ 3 files changed, 40 insertions(+), 32 deletions(-) diff --git a/go.mod b/go.mod index 0797b934..ef4103fd 100644 --- a/go.mod +++ b/go.mod @@ -11,18 +11,18 @@ require ( github.com/jessevdk/go-assets v0.0.0-20160921144138-4f4301a06e15 github.com/json-iterator/go v1.1.5 github.com/manucorporat/stats v0.0.0-20180402194714-3ba42d56d227 - github.com/mattn/go-isatty v0.0.3 + github.com/mattn/go-isatty v0.0.4 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/testify v1.2.2 github.com/thinkerou/favicon v0.1.0 github.com/ugorji/go v1.1.1 - golang.org/x/crypto v0.0.0-20180927165925-5295e8364332 - golang.org/x/lint v0.0.0-20180702182130-06c8688daad7 - golang.org/x/net v0.0.0-20180826012351-8a410e7b638d + golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e + golang.org/x/lint v0.0.0-20181011164241-5906bd5c48cd + golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f - golang.org/x/sys v0.0.0-20180928133829-e4b3c5e90611 // indirect + golang.org/x/sys v0.0.0-20181011152604-fa43e7bc11ba // indirect google.golang.org/grpc v1.15.0 gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/go-playground/validator.v8 v8.18.2 diff --git a/go.sum b/go.sum index 2d307e7e..2ef7f13b 100644 --- a/go.sum +++ b/go.sum @@ -13,7 +13,6 @@ github.com/gin-gonic/autotls v0.0.0-20180426091246-be87bd5ef97b h1:dm/NYytoj7p8J github.com/gin-gonic/autotls v0.0.0-20180426091246-be87bd5ef97b/go.mod h1:vwfeXwKgEIWq63oVfwaBjoByS4dZzYbHHROHjV4IjNY= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/lint v0.0.0-20180702182130-06c8688daad7 h1:2hRPrmiwPrp3fQX967rNJIhQPtiGXdlQWAxKbKw3VHA= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= @@ -25,8 +24,8 @@ github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/manucorporat/stats v0.0.0-20180402194714-3ba42d56d227 h1:KIaAZ/V+/0/6BOULrmBQ9T1ed8BkKqGIjIKW923nJuo= github.com/manucorporat/stats v0.0.0-20180402194714-3ba42d56d227/go.mod h1:ruMr5t05gVho4tuDv0PbI0Bb8nOxc/5Y6JzRHe/yfA0= -github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= @@ -39,18 +38,20 @@ github.com/thinkerou/favicon v0.1.0 h1:eWMISKTpHq2G8HOuKn7ydD55j5DDehx94b0C2y8AB github.com/thinkerou/favicon v0.1.0/go.mod h1:HL7Pap5kOluZv1ku34pZo/AJ44GaxMEPFZ3pmuexV2s= github.com/ugorji/go v1.1.1 h1:gmervu+jDMvXTbcHQ0pd2wee85nEoE0BsVyEuzkfK8w= github.com/ugorji/go v1.1.1/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= -golang.org/x/crypto v0.0.0-20180927165925-5295e8364332 h1:hvQVdF6P9DX4OiKA5tpehlG6JsgzmyQiThG7q5Bn3UQ= -golang.org/x/crypto v0.0.0-20180927165925-5295e8364332/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/lint v0.0.0-20180702182130-06c8688daad7 h1:00BeQWmeaGazuOrq8Q5K5d3/cHaGuFrZzpaHBXfrsUA= +golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e h1:IzypfodbhbnViNUO/MEh0FzCUooG97cIGfdggUrUSyU= +golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d h1:g9qWBGx4puODJTMVyoPrpoxPFgVGd+z1DZwjfRu4d0I= +golang.org/x/lint v0.0.0-20181011164241-5906bd5c48cd h1:cgsAvzdqkDKdI02tIvDjO225vDPHMDCgfKqx5KEVI7U= +golang.org/x/lint v0.0.0-20181011164241-5906bd5c48cd/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1 h1:Y/KGZSOdz/2r0WJ9Mkmz6NJBusp0kiNx1Cn82lzJQ6w= +golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180928133829-e4b3c5e90611 h1:O33LKL7WyJgjN9CvxfTIomjIClbd/Kq86/iipowHQU0= -golang.org/x/sys v0.0.0-20180928133829-e4b3c5e90611/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181011152604-fa43e7bc11ba h1:nZJIJPGow0Kf9bU9QTc1U6OXbs/7Hu4e+cNv+hxH+Zc= +golang.org/x/sys v0.0.0-20181011152604-fa43e7bc11ba/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52 h1:JG/0uqcGdTNgq7FdU+61l5Pdmb8putNZlXb65bJBROs= diff --git a/vendor/vendor.json b/vendor/vendor.json index 86df11be..af1a0148 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -6,7 +6,9 @@ "checksumSHA1": "CSPbwbyzqA6sfORicn4HFtIhF/c=", "path": "github.com/davecgh/go-spew/spew", "revision": "8991bc29aa16c548c550c7ff78260e27b9ab7c73", - "revisionTime": "2018-02-21T22:46:20Z" + "revisionTime": "2018-02-21T22:46:20Z", + "version": "v1.1", + "versionExact": "v1.1.1" }, { "checksumSHA1": "QeKwBtN2df+j+4stw3bQJ6yO4EY=", @@ -15,12 +17,12 @@ "revisionTime": "2017-01-09T09:34:21Z" }, { - "checksumSHA1": "Pyou8mceOASSFxc7GeXZuVdSMi0=", + "checksumSHA1": "mE9XW26JSpe4meBObM6J/Oeq0eg=", "path": "github.com/golang/protobuf/proto", - "revision": "b4deda0973fb4c70b50d226b1af49f3da59f5265", - "revisionTime": "2018-04-30T18:52:41Z", - "version": "v1.1.0", - "versionExact": "v1.1.0" + "revision": "aa810b61a9c79d51363740d207bb46cf8e620ed5", + "revisionTime": "2018-08-14T21:14:27Z", + "version": "v1.2", + "versionExact": "v1.2.0" }, { "checksumSHA1": "WqeEgS7pqqkwK8mlrAZmDgtWJMY=", @@ -31,19 +33,19 @@ "versionExact": "v1.1.5" }, { - "checksumSHA1": "y/A5iuvwjytQE2CqVuphQRXR2nI=", + "checksumSHA1": "w5RcOnfv5YDr3j2bd1YydkPiZx4=", "path": "github.com/mattn/go-isatty", - "revision": "0360b2af4f38e8d38c7fce2a9f4e702702d73a39", - "revisionTime": "2017-09-25T05:34:41Z", - "version": "v0.0.3", - "versionExact": "v0.0.3" + "revision": "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c", + "revisionTime": "2017-11-07T05:05:31Z", + "version": "v0.0", + "versionExact": "v0.0.4" }, { "checksumSHA1": "c6pbpF7eowwO59phRTpF8cQ80Z0=", "path": "github.com/stretchr/testify/assert", "revision": "f35b8ab0b5a2cef36673838d662e249dd9c94686", "revisionTime": "2018-05-06T18:05:49Z", - "version": "v1.2.2", + "version": "v1.2", "versionExact": "v1.2.2" }, { @@ -51,15 +53,20 @@ "path": "github.com/ugorji/go/codec", "revision": "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab", "revisionTime": "2018-04-07T10:07:33Z", - "version": "v1.1.1", + "version": "v1.1", "versionExact": "v1.1.1" }, { - "checksumSHA1": "9jjO5GjLa0XF/nfWihF02RoH4qc=", - "comment": "release-branch.go1.7", + "checksumSHA1": "GtamqiJoL7PGHsN454AoffBFMa8=", "path": "golang.org/x/net/context", - "revision": "d4c55e66d8c3a2f3382d264b08e3e3454a66355a", - "revisionTime": "2016-10-18T08:54:36Z" + "revision": "49bb7cea24b1df9410e1712aa6433dae904ff66a", + "revisionTime": "2018-10-11T05:27:23Z" + }, + { + "checksumSHA1": "SiJNkx+YGtq3Gtr6Ldu6OW83O+U=", + "path": "golang.org/x/sys/unix", + "revision": "fa43e7bc11baaae89f3f902b2b4d832b68234844", + "revisionTime": "2018-10-11T14:35:51Z" }, { "checksumSHA1": "P/k5ZGf0lEBgpKgkwy++F7K1PSg=", @@ -74,7 +81,7 @@ "path": "gopkg.in/yaml.v2", "revision": "5420a8b6744d3b0345ab293f6fcba19c978f1183", "revisionTime": "2018-03-28T19:50:20Z", - "version": "v2.2.1", + "version": "v2.2", "versionExact": "v2.2.1" } ], From cfa092f4f045d004b4c03d5b69af8bab7ed5fc1b Mon Sep 17 00:00:00 2001 From: Sergey Ponomarev Date: Tue, 16 Oct 2018 03:48:41 +0300 Subject: [PATCH 007/111] Fix LoadHTML* tests (#1559) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Digging into the test code base I've found out that some of the tests for `LoadHTML*` methods are not reliable and efficient. They use timeouts to be sure that goroutine with the server has started. And even more, in old implementation, the server started only once – all the new instances silently failed due to the occupied network port. Here is a short overview of the proposed changes: - it's not necessary to rely on timeouts, the server starts listening synchronously and returns control when it is ready - once the server is run, it's stopped after a test passes - dry out http server setup - magic with empty closure return is eliminated - preserve router.RunTLS coverage with integration tests --- gin_integration_test.go | 26 ++++- gin_test.go | 252 ++++++++++++++++++++++------------------ 2 files changed, 165 insertions(+), 113 deletions(-) diff --git a/gin_integration_test.go b/gin_integration_test.go index 12a943b0..038c8b7c 100644 --- a/gin_integration_test.go +++ b/gin_integration_test.go @@ -6,6 +6,7 @@ package gin import ( "bufio" + "crypto/tls" "fmt" "io/ioutil" "net" @@ -20,7 +21,14 @@ import ( ) func testRequest(t *testing.T, url string) { - resp, err := http.Get(url) + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + client := &http.Client{Transport: tr} + + resp, err := client.Get(url) assert.NoError(t, err) defer resp.Body.Close() @@ -45,6 +53,22 @@ func TestRunEmpty(t *testing.T) { testRequest(t, "http://localhost:8080/example") } +func TestRunTLS(t *testing.T) { + router := New() + go func() { + router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) + + assert.NoError(t, router.RunTLS(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem")) + }() + + // have to wait for the goroutine to start and run the server + // otherwise the main thread will complete + time.Sleep(5 * time.Millisecond) + + assert.Error(t, router.RunTLS(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem")) + testRequest(t, "https://localhost:8443/example") +} + func TestRunEmptyWithEnv(t *testing.T) { os.Setenv("PORT", "3123") router := New() diff --git a/gin_test.go b/gin_test.go index 95b9cc16..353c9be1 100644 --- a/gin_test.go +++ b/gin_test.go @@ -10,6 +10,7 @@ import ( "html/template" "io/ioutil" "net/http" + "net/http/httptest" "reflect" "testing" "time" @@ -22,105 +23,105 @@ func formatAsDate(t time.Time) string { return fmt.Sprintf("%d/%02d/%02d", year, month, day) } -func setupHTMLFiles(t *testing.T, mode string, tls bool) func() { - go func() { - SetMode(mode) - router := New() - router.Delims("{[{", "}]}") - router.SetFuncMap(template.FuncMap{ - "formatAsDate": formatAsDate, +func setupHTMLFiles(t *testing.T, mode string, tls bool, loadMethod func(*Engine)) *httptest.Server { + SetMode(mode) + router := New() + router.Delims("{[{", "}]}") + router.SetFuncMap(template.FuncMap{ + "formatAsDate": formatAsDate, + }) + loadMethod(router) + router.GET("/test", func(c *Context) { + c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"}) + }) + router.GET("/raw", func(c *Context) { + c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{ + "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), }) - router.LoadHTMLFiles("./testdata/template/hello.tmpl", "./testdata/template/raw.tmpl") - router.GET("/test", func(c *Context) { - c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"}) - }) - router.GET("/raw", func(c *Context) { - c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{ - "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), - }) - }) - if tls { - // these files generated by `go run $GOROOT/src/crypto/tls/generate_cert.go --host 127.0.0.1` - router.RunTLS(":9999", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem") - } else { - router.Run(":8888") - } - }() - t.Log("waiting 1 second for server startup") - time.Sleep(1 * time.Second) - return func() {} + }) + + var ts *httptest.Server + + if tls { + ts = httptest.NewTLSServer(router) + } else { + ts = httptest.NewServer(router) + } + + return ts } -func setupHTMLGlob(t *testing.T, mode string, tls bool) func() { - go func() { - SetMode(mode) - router := New() - router.Delims("{[{", "}]}") - router.SetFuncMap(template.FuncMap{ - "formatAsDate": formatAsDate, - }) - router.LoadHTMLGlob("./testdata/template/*") - router.GET("/test", func(c *Context) { - c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"}) - }) - router.GET("/raw", func(c *Context) { - c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{ - "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), - }) - }) - if tls { - // these files generated by `go run $GOROOT/src/crypto/tls/generate_cert.go --host 127.0.0.1` - router.RunTLS(":9999", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem") - } else { - router.Run(":8888") - } - }() - t.Log("waiting 1 second for server startup") - time.Sleep(1 * time.Second) - return func() {} -} +func TestLoadHTMLGlobDebugMode(t *testing.T) { + ts := setupHTMLFiles( + t, + DebugMode, + false, + func(router *Engine) { + router.LoadHTMLGlob("./testdata/template/*") + }, + ) + defer ts.Close() -func TestLoadHTMLGlob(t *testing.T) { - td := setupHTMLGlob(t, DebugMode, false) - res, err := http.Get("http://127.0.0.1:8888/test") + res, err := http.Get(fmt.Sprintf("%s/test", ts.URL)) if err != nil { fmt.Println(err) } resp, _ := ioutil.ReadAll(res.Body) assert.Equal(t, "

Hello world

", string(resp)) - - td() } -func TestLoadHTMLGlob2(t *testing.T) { - td := setupHTMLGlob(t, TestMode, false) - res, err := http.Get("http://127.0.0.1:8888/test") +func TestLoadHTMLGlobTestMode(t *testing.T) { + ts := setupHTMLFiles( + t, + TestMode, + false, + func(router *Engine) { + router.LoadHTMLGlob("./testdata/template/*") + }, + ) + defer ts.Close() + + res, err := http.Get(fmt.Sprintf("%s/test", ts.URL)) if err != nil { fmt.Println(err) } resp, _ := ioutil.ReadAll(res.Body) assert.Equal(t, "

Hello world

", string(resp)) - - td() } -func TestLoadHTMLGlob3(t *testing.T) { - td := setupHTMLGlob(t, ReleaseMode, false) - res, err := http.Get("http://127.0.0.1:8888/test") +func TestLoadHTMLGlobReleaseMode(t *testing.T) { + ts := setupHTMLFiles( + t, + ReleaseMode, + false, + func(router *Engine) { + router.LoadHTMLGlob("./testdata/template/*") + }, + ) + defer ts.Close() + + res, err := http.Get(fmt.Sprintf("%s/test", ts.URL)) if err != nil { fmt.Println(err) } resp, _ := ioutil.ReadAll(res.Body) assert.Equal(t, "

Hello world

", string(resp)) - - td() } func TestLoadHTMLGlobUsingTLS(t *testing.T) { - td := setupHTMLGlob(t, DebugMode, true) + ts := setupHTMLFiles( + t, + DebugMode, + true, + func(router *Engine) { + router.LoadHTMLGlob("./testdata/template/*") + }, + ) + defer ts.Close() + // Use InsecureSkipVerify for avoiding `x509: certificate signed by unknown authority` error tr := &http.Transport{ TLSClientConfig: &tls.Config{ @@ -128,29 +129,33 @@ func TestLoadHTMLGlobUsingTLS(t *testing.T) { }, } client := &http.Client{Transport: tr} - res, err := client.Get("https://127.0.0.1:9999/test") + res, err := client.Get(fmt.Sprintf("%s/test", ts.URL)) if err != nil { fmt.Println(err) } resp, _ := ioutil.ReadAll(res.Body) assert.Equal(t, "

Hello world

", string(resp)) - - td() } func TestLoadHTMLGlobFromFuncMap(t *testing.T) { - time.Now() - td := setupHTMLGlob(t, DebugMode, false) - res, err := http.Get("http://127.0.0.1:8888/raw") + ts := setupHTMLFiles( + t, + DebugMode, + false, + func(router *Engine) { + router.LoadHTMLGlob("./testdata/template/*") + }, + ) + defer ts.Close() + + res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL)) if err != nil { fmt.Println(err) } resp, _ := ioutil.ReadAll(res.Body) assert.Equal(t, "Date: 2017/07/01\n", string(resp)) - - td() } func init() { @@ -164,59 +169,77 @@ func TestCreateEngine(t *testing.T) { assert.Empty(t, router.Handlers) } -// func TestLoadHTMLDebugMode(t *testing.T) { -// router := New() -// SetMode(DebugMode) -// router.LoadHTMLGlob("*.testtmpl") -// r := router.HTMLRender.(render.HTMLDebug) -// assert.Empty(t, r.Files) -// assert.Equal(t, "*.testtmpl", r.Glob) -// -// router.LoadHTMLFiles("index.html.testtmpl", "login.html.testtmpl") -// r = router.HTMLRender.(render.HTMLDebug) -// assert.Empty(t, r.Glob) -// assert.Equal(t, []string{"index.html", "login.html"}, r.Files) -// SetMode(TestMode) -// } +func TestLoadHTMLFilesTestMode(t *testing.T) { + ts := setupHTMLFiles( + t, + TestMode, + false, + func(router *Engine) { + router.LoadHTMLFiles("./testdata/template/hello.tmpl", "./testdata/template/raw.tmpl") + }, + ) + defer ts.Close() -func TestLoadHTMLFiles(t *testing.T) { - td := setupHTMLFiles(t, TestMode, false) - res, err := http.Get("http://127.0.0.1:8888/test") + res, err := http.Get(fmt.Sprintf("%s/test", ts.URL)) if err != nil { fmt.Println(err) } resp, _ := ioutil.ReadAll(res.Body) assert.Equal(t, "

Hello world

", string(resp)) - td() } -func TestLoadHTMLFiles2(t *testing.T) { - td := setupHTMLFiles(t, DebugMode, false) - res, err := http.Get("http://127.0.0.1:8888/test") +func TestLoadHTMLFilesDebugMode(t *testing.T) { + ts := setupHTMLFiles( + t, + DebugMode, + false, + func(router *Engine) { + router.LoadHTMLFiles("./testdata/template/hello.tmpl", "./testdata/template/raw.tmpl") + }, + ) + defer ts.Close() + + res, err := http.Get(fmt.Sprintf("%s/test", ts.URL)) if err != nil { fmt.Println(err) } resp, _ := ioutil.ReadAll(res.Body) assert.Equal(t, "

Hello world

", string(resp)) - td() } -func TestLoadHTMLFiles3(t *testing.T) { - td := setupHTMLFiles(t, ReleaseMode, false) - res, err := http.Get("http://127.0.0.1:8888/test") +func TestLoadHTMLFilesReleaseMode(t *testing.T) { + ts := setupHTMLFiles( + t, + ReleaseMode, + false, + func(router *Engine) { + router.LoadHTMLFiles("./testdata/template/hello.tmpl", "./testdata/template/raw.tmpl") + }, + ) + defer ts.Close() + + res, err := http.Get(fmt.Sprintf("%s/test", ts.URL)) if err != nil { fmt.Println(err) } resp, _ := ioutil.ReadAll(res.Body) assert.Equal(t, "

Hello world

", string(resp)) - td() } func TestLoadHTMLFilesUsingTLS(t *testing.T) { - td := setupHTMLFiles(t, TestMode, true) + ts := setupHTMLFiles( + t, + TestMode, + true, + func(router *Engine) { + router.LoadHTMLFiles("./testdata/template/hello.tmpl", "./testdata/template/raw.tmpl") + }, + ) + defer ts.Close() + // Use InsecureSkipVerify for avoiding `x509: certificate signed by unknown authority` error tr := &http.Transport{ TLSClientConfig: &tls.Config{ @@ -224,28 +247,33 @@ func TestLoadHTMLFilesUsingTLS(t *testing.T) { }, } client := &http.Client{Transport: tr} - res, err := client.Get("https://127.0.0.1:9999/test") + res, err := client.Get(fmt.Sprintf("%s/test", ts.URL)) if err != nil { fmt.Println(err) } resp, _ := ioutil.ReadAll(res.Body) assert.Equal(t, "

Hello world

", string(resp)) - td() } func TestLoadHTMLFilesFuncMap(t *testing.T) { - time.Now() - td := setupHTMLFiles(t, TestMode, false) - res, err := http.Get("http://127.0.0.1:8888/raw") + ts := setupHTMLFiles( + t, + TestMode, + false, + func(router *Engine) { + router.LoadHTMLFiles("./testdata/template/hello.tmpl", "./testdata/template/raw.tmpl") + }, + ) + defer ts.Close() + + res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL)) if err != nil { fmt.Println(err) } resp, _ := ioutil.ReadAll(res.Body) assert.Equal(t, "Date: 2017/07/01\n", string(resp)) - - td() } func TestAddRoute(t *testing.T) { From 333bac5f94a1bd3237996fbcae6e78fee8dbc5c6 Mon Sep 17 00:00:00 2001 From: "A. F" Date: Wed, 17 Oct 2018 09:40:57 +0200 Subject: [PATCH 008/111] add example to set and get cookies (#1599) --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/README.md b/README.md index 95bd3204..5c14c440 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi - [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) @@ -1880,6 +1881,35 @@ func main() { } ``` +### 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 From a1a32562de0fe61b17aab42cd4fa7847ae814cb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Fri, 19 Oct 2018 11:06:23 +0800 Subject: [PATCH 009/111] add gin user - photoprism (#1601) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5c14c440..1b5fb493 100644 --- a/README.md +++ b/README.md @@ -1964,3 +1964,4 @@ Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framewor * [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. From dbc330b804052d7db230729c15d041cbb775e7b6 Mon Sep 17 00:00:00 2001 From: Ismail Gjevori Date: Mon, 22 Oct 2018 17:01:14 +0200 Subject: [PATCH 010/111] Pass MaxMultipartMemory when FormFile is called (#1600) When `gin.Context.FormFile("...")` is called the `engine.MaxMultipartMemory` is never used. This PR makes sure that the `MaxMultipartMemory` is passed and removes 2 calls to `http.Request.ParseForm` since they are called from `http.Request.ParseMultipartForm` --- context.go | 7 +++++-- context_test.go | 13 +++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/context.go b/context.go index 809902a3..c8a59bb0 100644 --- a/context.go +++ b/context.go @@ -414,7 +414,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 @@ -437,7 +436,6 @@ func (c *Context) PostFormMap(key string) map[string]string { // whether at least one value exists for the given key. func (c *Context) GetPostFormMap(key string) (map[string]string, bool) { req := c.Request - req.ParseForm() req.ParseMultipartForm(c.engine.MaxMultipartMemory) dicts, exist := c.get(req.PostForm, key) @@ -465,6 +463,11 @@ func (c *Context) get(m map[string][]string, key string) (map[string]string, boo // 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 } diff --git a/context_test.go b/context_test.go index 2e972f18..fb492e02 100644 --- a/context_test.go +++ b/context_test.go @@ -84,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) From c65e5efc9a0ae6666819cda8e9a86db4d3d19833 Mon Sep 17 00:00:00 2001 From: Thomas Schaffer Date: Tue, 23 Oct 2018 04:56:33 +0200 Subject: [PATCH 011/111] Expose HandlerFunc in RouteInfos (#1272) --- gin.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/gin.go b/gin.go index 92c24ba2..440519f5 100644 --- a/gin.go +++ b/gin.go @@ -38,9 +38,10 @@ func (c HandlersChain) Last() HandlerFunc { // RouteInfo represents a request route's specification which contains method and path and its handler. type RouteInfo struct { - Method string - Path string - Handler string + Method string + Path string + Handler string + HandlerFunc HandlerFunc } // RoutesInfo defines a RouteInfo array. @@ -266,10 +267,12 @@ func (engine *Engine) Routes() (routes RoutesInfo) { func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo { path += root.path if len(root.handlers) > 0 { + handlerFunc := root.handlers.Last() routes = append(routes, RouteInfo{ - Method: method, - Path: path, - Handler: nameOfFunction(root.handlers.Last()), + Method: method, + Path: path, + Handler: nameOfFunction(handlerFunc), + HandlerFunc: handlerFunc, }) } for _, child := range root.children { From 8e9619767c12607afcb086a5e9c791eda2b9e116 Mon Sep 17 00:00:00 2001 From: forging2012 Date: Wed, 31 Oct 2018 20:19:58 +0800 Subject: [PATCH 012/111] FIX r.LoadHTMLGlob("/path/to/templates") (#1616) FIX r.LoadHTMLGlob("/path/to/templates")) to r.LoadHTMLGlob("/path/to/templates") --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1b5fb493..ce413325 100644 --- a/README.md +++ b/README.md @@ -1160,7 +1160,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 From 8fb21a8beff46febac15894200e381473889f0e6 Mon Sep 17 00:00:00 2001 From: "root@andrea:~#" Date: Thu, 1 Nov 2018 01:30:19 -0600 Subject: [PATCH 013/111] Added some comments to avoid having golint warnings (#1619) The following comments to vars, conts and method were added to pass `golinter` with 100%. ![captura de pantalla 2018-10-31 a la s 15 23 37](https://user-images.githubusercontent.com/10160626/47819725-faba3780-dd20-11e8-978c-1b3ab7de26ed.png) --- errors.go | 6 ++++-- mode.go | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/errors.go b/errors.go index 477b9d58..ab13ca61 100644 --- a/errors.go +++ b/errors.go @@ -24,9 +24,10 @@ const ( ErrorTypePrivate ErrorType = 1 << 0 // ErrorTypePublic indicates a public error. ErrorTypePublic ErrorType = 1 << 1 - // ErrorTypeAny indicates other any error. + // 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. @@ -52,6 +53,7 @@ func (msg *Error) SetMeta(data interface{}) *Error { return msg } +// JSON creates a properly formated JSON func (msg *Error) JSON() interface{} { json := H{} if msg.Meta != nil { diff --git a/mode.go b/mode.go index 7cb0143a..6a696329 100644 --- a/mode.go +++ b/mode.go @@ -28,7 +28,7 @@ const ( testCode ) -// DefaultWriter is the default io.Writer used the Gin for debug output and +// DefaultWriter is the default io.Writer used by Gin for debug output and // middleware output like Logger() or Recovery(). // Note that both Logger and Recovery provides custom ways to configure their // output io.Writer. @@ -36,6 +36,8 @@ const ( // import "github.com/mattn/go-colorable" // gin.DefaultWriter = colorable.NewColorableStdout() var DefaultWriter io.Writer = os.Stdout + +// DefaultErrorWriter is the default io.Writer used by Gin to debug errors var DefaultErrorWriter io.Writer = os.Stderr var ginMode = debugCode From 6f7fe487b38baec254343376df603adecd201f10 Mon Sep 17 00:00:00 2001 From: Barnabus Date: Thu, 1 Nov 2018 18:05:40 +1000 Subject: [PATCH 014/111] Change HTML input tags to use HTML5 syntax. (#1617) In XHTML, the tag must be properly closed, like this ``. In HTML5 the `` tag has no ending slash. https://www.w3schools.com/tags/tag_input.asp --- README.md | 8 ++++---- .../realtime-advanced/resources/room_login.templ.html | 8 ++++---- examples/realtime-chat/template.go | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index ce413325..8c2c2c5b 100644 --- a/README.md +++ b/README.md @@ -824,12 +824,12 @@ form.html

Check some colors

- + - + - - + +
``` diff --git a/examples/realtime-advanced/resources/room_login.templ.html b/examples/realtime-advanced/resources/room_login.templ.html index 27dac387..5fb2b334 100644 --- a/examples/realtime-advanced/resources/room_login.templ.html +++ b/examples/realtime-advanced/resources/room_login.templ.html @@ -79,19 +79,19 @@
{{.nick}}
- +
- + {{else}}
Join the SSE real-time chat
- +
- +
{{end}} diff --git a/examples/realtime-chat/template.go b/examples/realtime-chat/template.go index b9024de6..4190251d 100644 --- a/examples/realtime-chat/template.go +++ b/examples/realtime-chat/template.go @@ -35,9 +35,9 @@ var html = template.Must(template.New("chat_room").Parse(`

Welcome to {{.roomid}} room

- User: - Message: - + User: + Message: +
From 6be9b5437b0c1092a94800c449988699a24b3f51 Mon Sep 17 00:00:00 2001 From: Barnabus Date: Thu, 1 Nov 2018 23:48:26 +1000 Subject: [PATCH 015/111] Change HTML link tags to use HTML5 syntax. (#1621) The `` element is an empty element, it contains attributes only. In HTML5 the `` tag has no end tag. In XHTML the `` tag must be properly closed. --- examples/realtime-advanced/resources/room_login.templ.html | 2 +- examples/realtime-chat/template.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/realtime-advanced/resources/room_login.templ.html b/examples/realtime-advanced/resources/room_login.templ.html index 5fb2b334..d4991d8d 100644 --- a/examples/realtime-advanced/resources/room_login.templ.html +++ b/examples/realtime-advanced/resources/room_login.templ.html @@ -20,7 +20,7 @@ - + + + +

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):容器本机,云无关的无服务器平台。 From 1f576fb27c3f3ec00baa0d2868e41830e37d0b54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=B9=E5=AE=9D=E5=BC=BA?= Date: Fri, 23 Nov 2018 09:46:41 +0800 Subject: [PATCH 030/111] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E8=BF=9E=E6=8E=A5?= =?UTF-8?q?=E9=94=99=E8=AF=AF=EF=BC=8C=E4=BF=AE=E6=AD=A3=E4=B8=80=E4=BA=9B?= =?UTF-8?q?=E6=8A=A5=E5=BC=95=E7=94=A8=E9=94=99=E8=AF=AF=20(#1655)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复了全角括号导致超链接不能正常访问的错误。 修复了一些URL中的"/"被改成" / "的错误。 修复了一些包引用中"/"被改成" / "的错误。 修复有超链接被翻译成中文的错误。 --- README_ZH.md | 68 ++++++++++++++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/README_ZH.md b/README_ZH.md index d411f9eb..c689ba28 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -79,7 +79,7 @@ $ go get -u github.com/gin-gonic/gin import "github.com/gin-gonic/gin" ``` -3. (可选)如果使用诸如`http.StatusOK`之类的常量,则需要引入 `net / http` 包。 +3. (可选)如果使用诸如`http.StatusOK`之类的常量,则需要引入 `net/http` 包。 ```go import "net/http" @@ -200,7 +200,7 @@ BenchmarkVulcan_GithubAll | 5000 | 394253 | 19894 ## 使用 [jsoniter](https://github.com/json-iterator/go) 构建 -Gin使用`encoding / json`作为默认的json包,但您可以通过其他标签的构建更改为[jsoniter](https://github.com/json-iterator/go)。 +Gin使用`encoding/json`作为默认的json包,但您可以通过其他标签的构建更改为[jsoniter](https://github.com/json-iterator/go)。 ```sh $ go build -tags=jsoniter . @@ -359,7 +359,7 @@ 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)。 +参考问题[#774](https://github.com/gin-gonic/gin/issues/774)和详细[示例代码](examples/upload-file/single)。 ```go func main() { @@ -390,7 +390,7 @@ curl -X POST http://localhost:8080/upload \ #### 多文件上传 -查看详细信息[示例代码](examples / upload-file / multiple)。 +查看详细信息[示例代码](examples/upload-file/multiple)。 ```go func main() { @@ -530,14 +530,14 @@ func main() { 要将请求主体绑定到类型中,请使用模型绑定。我们目前支持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)。 +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`等效方法。 +   - **行为** - 这些方法在引擎盖下使用`MustBindWith`。如果存在绑定错误,则使用`c.AbortWithError(400,err).SetType(ErrorTypeBind)`中止请求。这将响应状态代码设置为400,并将`Content-Type`标头设置为`text/plain;字符集= UTF-8`。请注意,如果您在此之后尝试设置响应代码,则会发出警告“[GIN-debug] [警告]标题已经写入。想用422`覆盖状态代码400。如果您希望更好地控制行为,请考虑使用`ShouldBind`等效方法。 - **类型** - 应该绑定    - **方法** - `ShouldBind`,`ShouldBindJSON`,`ShouldBindXML`,`ShouldBindQuery`    - **行为** - 这些方法在引擎盖下使用`ShouldBindWith`。如果存在绑定错误,则返回错误,开发人员有责任正确处理请求和错误。 @@ -643,9 +643,9 @@ $ curl -v -X POST \ ### 自定义验证器 -也可以注册自定义验证器。 请参阅[示例代码](examples / custom-validation / server.go)。 +也可以注册自定义验证器。 请参阅[示例代码](examples/custom-validation/server.go)。 -[embedmd]:#(examples / custom-validation / server.go go) +[embedmd]:#(examples/custom-validation/server.go go) ```go package main @@ -707,12 +707,12 @@ $ 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)以了解更多信息。 +[结构级验证](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)。 +`ShouldBindQuery` 函数只绑定查询参数而不是后期数据。 请参阅[详细信息](https://github.com/gin-gonic/gin/issues/742#issuecomment-315953017)。 ```go package main @@ -748,7 +748,7 @@ func startPage(c *gin.Context) { ### 绑定查询字符串或发布数据 -请参阅[详细信息](https://github.com/gin-gonic/gin/issues/742#issuecomment-264681292)。 +请参阅[详细信息](https://github.com/gin-gonic/gin/issues/742#issuecomment-264681292)。 ```go package main @@ -794,7 +794,7 @@ $ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03 ### 绑定 HTML 复选框 -参见[详细信息](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092) +参见[详细信息](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092) main.go @@ -931,7 +931,7 @@ func main() { #### SecureJSON -使用 SecureJSON 来防止 json 劫持。 如果给定的结构是数组值,则默认预 置`“while(1),”` 到响应体。 +使用 SecureJSON 来防止 json 劫持。 如果给定的结构是数组值,则默认预 置`“while(1),”` 到响应体。 ```go func main() { @@ -1067,7 +1067,7 @@ func main() { ### HTML 渲染 -使用LoadHTMLGlob()或LoadHTMLFiles() +使用LoadHTMLGlob()或LoadHTMLFiles() ```go func main() { @@ -1159,12 +1159,12 @@ func main() { ```go r := gin.Default() r.Delims("{[{", "}]}") - r.LoadHTMLGlob("/path/to/templates")) + r.LoadHTMLGlob("/path/to/templates") ``` #### 自定义模板功能 -查看详细信息[示例代码](示例/模板)。 +查看详细信息[示例代码](examples/template)。 main.go @@ -1215,7 +1215,7 @@ Date: 2017/07/01 ### 多模板 -Gin 允许默认只使用一个 html 模板。 检查[多模板渲染](https://github.com/gin-contrib/multitemplate)以使用 go 1.6 `block template` 等功能。 +Gin 允许默认只使用一个 html 模板。 检查[多模板渲染](https://github.com/gin-contrib/multitemplate)以使用 go 1.6 `block template` 等功能。 ### 重定向 @@ -1355,7 +1355,7 @@ func main() { ### 自定义 HTTP 配置 -直接使用`http.ListenAndServe()`,如下所示: +直接使用`http.ListenAndServe()`,如下所示: ```go func main() { @@ -1441,7 +1441,7 @@ func main() { ### 使用 Gin 运行多个服务 -请参阅[问题](https://github.com/gin-gonic/gin/issues/346)并尝试以下示例: +请参阅[问题](https://github.com/gin-gonic/gin/issues/346)并尝试以下示例: [embedmd]:# (examples/multiple-service/main.go go) ```go @@ -1526,7 +1526,7 @@ func main() { 您想要优雅地重启或停止您的Web服务器吗? 有一些方法可以做到这一点。 -我们可以使用[fvbock / endless](https://github.com/fvbock/endless)来替换默认的`ListenAndServe`。 有关更多详细信息,请参阅问题[#296](https://github.com/gin-gonic/gin/issues/296)。 +我们可以使用[fvbock/endless](https://github.com/fvbock/endless)来替换默认的`ListenAndServe`。 有关更多详细信息,请参阅问题[#296](https://github.com/gin-gonic/gin/issues/296)。 ```go router := gin.Default() @@ -1537,11 +1537,11 @@ 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服务器的平滑重启和零停机时间部署。 +* [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)示例。 +如果您使用的是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 @@ -1637,7 +1637,7 @@ func loadTemplate() (*template.Template, error) { } ``` -请参阅`examples / assets-in-binary`目录中的完整示例。 +请参阅`examples/assets-in-binary`目录中的完整示例。 ### Bind form-data request with custom struct @@ -1788,13 +1788,13 @@ func SomeHandler(c *gin.Context) { 足以立刻调用绑定。 *只有某些格式需要此功能 - “JSON”,“XML”,“MsgPack”, `ProtoBuf`。 对于其他格式,`Query`,`Form`,`FormPost`,`FormMultipart`, -可以被`c.ShouldBind()`多次调用而不会造成任何损害 -表现(见[#1341](https://github.com/gin-gonic/gin/pull/1341))。 +可以被`c.ShouldBind()`多次调用而不会造成任何损害 +表现(见[#1341](https://github.com/gin-gonic/gin/pull/1341)。 ### http2 server 推送 -http.Pusher仅支持** go1.8 + **。 有关详细信息,请参阅[golang blog](https://blog.golang.org/h2push)。 +http.Pusher仅支持** go1.8 + **。 有关详细信息,请参阅[golang blog](https://blog.golang.org/h2push)。 [embedmd]:# (examples/http-pusher/main.go go) ```go @@ -1886,7 +1886,7 @@ func main() { ## 测试 -`net / http / httptest`包是HTTP测试的首选方式。 +`net/http/httptest`包是HTTP测试的首选方式。 ```go package main @@ -1933,8 +1933,8 @@ func TestPingRoute(t *testing.T) { ## 用户 -使用[Gin](https://github.com/gin-gonic/gin)Web框架的令人敬畏的项目列表。 +使用[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):容器本机,云无关的无服务器平台。 +* [drone](https://github.com/drone/drone):drone,用Go编写。 +* [gorush](https://github.com/appleboy/gorush):用Go编写的推送通知服务器。 +* [fnproject](https://github.com/fnproject/fn):容器本机,云无关的无服务器平台。 From f52bea87f60248c00e55f7599d9584f9bc8e8978 Mon Sep 17 00:00:00 2001 From: weibaohui Date: Sat, 24 Nov 2018 19:15:19 +0800 Subject: [PATCH 031/111] =?UTF-8?q?=E8=B0=83=E6=95=B4=E6=8F=8F=E8=BF=B0?= =?UTF-8?q?=E8=AF=AD=E5=8F=A5=20(#1657)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 调整描述语句 --- README_ZH.md | 95 +++++++++++++++++++++++++--------------------------- 1 file changed, 46 insertions(+), 49 deletions(-) diff --git a/README_ZH.md b/README_ZH.md index c689ba28..7867c1f7 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -25,20 +25,20 @@ Gin 是一个 Go (Golang) 语言框架。 它是一个拥有更好性能的 mart - [使用 jsoniter 构建](#使用-jsoniter-构建) - [API 示例](#api-示例) - [GET,POST,PUT,PATCH,DELETE,OPTIONS 使用](#get-post-put-patch-delete-options-使用) - - [路由参数](#路由参数) - - [查询字符串参数](#查询字符串参数) + - [获取路由参数](#获取路由参数) + - [获取url查询参数](#获取url查询参数) - [Multipart Urlencoded 表单](#multipart-urlencoded-表单) - - [另一个实列 query + post form](#另一个实列:-query-+-post-form) + - [获取post表单数据(url带查询参数)](#获取post表单数据(url带查询参数)) - [映射参数 表单参数](#映射参数-表单参数) - [上传文件](#上传文件) - [路由组](#路由组) - [默认初始化 Gin](#默认初始化-gin) - - [中间件使用](#中间件使用) + - [使用中间件](#使用中间件) - [如何记录日志](#如何记录日志) - [模型绑定和验证](#模型绑定和验证) - [自定义验证器](#自定义验证器) - - [只绑定查询字符串](#只绑定查询字符串) - - [绑定查询字符串或发布数据](#绑定查询字符串或发布数据) + - [只绑定url查询参数](#只绑定url查询参数) + - [url查询参数绑定到struct(或POST表单数据)](#url查询参数绑定到struct(或POST表单数据)) - [绑定 HTML 复选框](#绑定-html-复选框) - [Multipart Urlencoded 绑定](#multipart-urlencoded-绑定) - [XML JSON YAML ProtoBuf 渲染](#xml-json-yaml-protobuf-渲染) @@ -56,8 +56,8 @@ Gin 是一个 Go (Golang) 语言框架。 它是一个拥有更好性能的 mart - [使用 Gin 运行多个服务](使用-gin-运行多个服务) - [优雅重启或停止](#优雅重启或停止) - [使用模板构建单个二进制文件](#使用模板构建单个二进制文件) - - [使用自定义结构绑定表单数据请求](#使用自定义结构绑定表单数据请求) - - [尝试将body绑定到不同的结构中](#尝试将-body-绑定到不同的结构中) + - [表单数据绑定到自定义结构体](#表单数据绑定到自定义结构体) + - [将request body绑定到不同的结构体中](#将request body绑定到不同的结构体中) - [http2 server 推送](#http2-server-推送) - [定义路由日志的格式](#定义路由日志的格式) - [测试](#测试) @@ -105,7 +105,7 @@ $ govendor init $ govendor fetch github.com/gin-gonic/gin@v1.3 ``` -4. 复制一个启动文件模板到项目目录中 +4. 复制启动文件模板到项目目录中 ```sh $ curl https://raw.githubusercontent.com/gin-gonic/gin/master/examples/basic/main.go > main.go @@ -119,7 +119,7 @@ $ go run main.go ## 前提条件 -新版本的 Gin 需要 Go 1.6 或者更高版本并且很快就会升级到 Go 1.7. +新版本的 Gin 需要 Go 1.6 或者更高版本,并且很快就会升级到 Go 1.7. ## 快速启动 @@ -234,7 +234,7 @@ func main() { } ``` -### 路由参数 +### 获取路由参数 ```go func main() { @@ -259,7 +259,7 @@ func main() { } ``` -### 查询字符串参数 +### 获取url查询参数 ```go func main() { @@ -297,7 +297,7 @@ func main() { } ``` -### 另一个实列 query + post form +### 获取post表单数据(url带查询参数) ``` POST /post?id=1234&page=1 HTTP/1.1 @@ -380,7 +380,7 @@ func main() { } ``` -如何 `curl`: +`curl`示例: ```bash curl -X POST http://localhost:8080/upload \ @@ -414,7 +414,7 @@ func main() { } ``` -如何 `curl`: +`curl`示例: ```bash curl -X POST http://localhost:8080/upload \ @@ -465,7 +465,7 @@ r := gin.Default() ``` -### 中间件使用 +### 使用中间件 ```go func main() { // Creates a router without any middleware by default @@ -710,9 +710,9 @@ $ curl "localhost:8085/bookable?check_in=2018-03-08&check_out=2018-03-09" [结构级验证](https://github.com/go-playground/validator/releases/tag/v8.7)也可以这种方式注册。 请参阅[struct-lvl-validation示例](examples/struct-lvl-validations)以了解更多信息。 -### 只绑定查询字符串 +### 只绑定url查询参数 -`ShouldBindQuery` 函数只绑定查询参数而不是后期数据。 请参阅[详细信息](https://github.com/gin-gonic/gin/issues/742#issuecomment-315953017)。 +`ShouldBindQuery` 函数只绑定url查询参数而不是post字段。 请参阅[详细信息](https://github.com/gin-gonic/gin/issues/742#issuecomment-315953017)。 ```go package main @@ -746,7 +746,7 @@ func startPage(c *gin.Context) { ``` -### 绑定查询字符串或发布数据 +### url查询参数绑定到struct(或POST表单数据) 请参阅[详细信息](https://github.com/gin-gonic/gin/issues/742#issuecomment-264681292)。 @@ -872,7 +872,7 @@ func main() { } ``` -Test it with: +测试: ```sh $ curl -v --form user=user --form password=password http://localhost:8080/login ``` @@ -999,8 +999,8 @@ func main() { #### PureJSON -通常,JSON 用其 unicod e实体替换特殊 HTML 字符,例如 `<` 变为 `\ u003c`。 如果要按字面意思对这些字符进行编码,则可以使用 PureJSON。 -Go 1.6 及更低版本无法使用此功能。 +通常,JSON使用unicode替换特殊HTML字符,例如 `<` 变为 `\ u003c`。 如果要按字面意思对这些字符进行编码,则可以使用 PureJSON。 +Go 1.6及更低版本无法使用此功能。 ```go func main() { @@ -1382,7 +1382,7 @@ func main() { ### Let's Encrypt 支持 -单行 LetsEncrypt HTTPS 服务器的示例。 +一行代码支持 LetsEncrypt HTTPS示例。 [embedmd]:# (examples/auto-tls/example1/main.go go) ```go @@ -1407,7 +1407,7 @@ func main() { } ``` -自定义autocert管理器的示例。 +autocert使用示例。 [embedmd]:# (examples/auto-tls/example2/main.go go) ```go @@ -1535,13 +1535,13 @@ 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)示例。 +如果您使用的是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 @@ -1597,7 +1597,7 @@ func main() { ### 使用模板构建单个二进制文件 -您可以使用[go-assets] []将服务器构建到包含模板的单个二进制文件中。 +您可以使用[go-assets]将服务器打包为包含模板的单个二进制可执行文件。 [go-assets]:https://github.com/jessevdk/go-assets @@ -1639,7 +1639,7 @@ func loadTemplate() (*template.Template, error) { 请参阅`examples/assets-in-binary`目录中的完整示例。 -### Bind form-data request with custom struct +### 表单数据绑定到自定义结构体 以下示例使用自定义结构: @@ -1731,10 +1731,9 @@ type StructZ struct { 总之,只支持现在没有`form`的嵌套自定义结构。 -### 尝试将 body 绑定到不同的结构中 +### 将request body绑定到不同的结构体中 -绑定请求体的常规方法使用`c.Request.Body`和它们 -不能多次调用。 +一般通过调用`c.Request.Body`方法绑定数据,但不能多次调用这个方法。 ```go type formA struct { @@ -1760,7 +1759,7 @@ func SomeHandler(c *gin.Context) { } ``` -为此,您可以使用`c.ShouldBindBodyWith`. +为此,要想多次绑定,需要使用`c.ShouldBindBodyWith`. ```go func SomeHandler(c *gin.Context) { @@ -1782,14 +1781,11 @@ func SomeHandler(c *gin.Context) { ``` -482/5000 -*`c.ShouldBindBodyWith`在绑定之前将body存储到上下文中。 这有 -对性能有轻微影响,所以如果你这样做,你不应该使用这种方法 -足以立刻调用绑定。 -*只有某些格式需要此功能 - “JSON”,“XML”,“MsgPack”, -`ProtoBuf`。 对于其他格式,`Query`,`Form`,`FormPost`,`FormMultipart`, -可以被`c.ShouldBind()`多次调用而不会造成任何损害 -表现(见[#1341](https://github.com/gin-gonic/gin/pull/1341)。 +*`c.ShouldBindBodyWith`会在绑定之前将body存储到上下文中。 这会 +对性能造成轻微影响,如果调用一次就能完成绑定的话,那就不要用这个方法。 +*只有某些格式需要此功能 ,如“JSON”,“XML”,“MsgPack”, +`ProtoBuf`。 对于其他格式,如`Query`,`Form`,`FormPost`,`FormMultipart`, +可以多次调用`c.ShouldBind()`而不会造成任任何性能损失(见[#1341](https://github.com/gin-gonic/gin/pull/1341))。 ### http2 server 推送 @@ -1843,15 +1839,15 @@ func main() { ### 定义路由日志的格式 -路由的默认日志是: +默认的路由日志格式: ``` [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`定义此格式。 -在下面的示例中,我们使用标准日志包记录所有路由,但您可以使用其他适合您需求的日志工具。 +如果要以指的格式(例如JSON,Key Values或其他格式)记录信息,则可以使用`gin.DebugPrintRouteFunc`指定格式。 +在下面的示例中,我们使用标准日志包记录所有路由,但您可以使用其他满足需求的日志工具。 ```go import ( "log" @@ -1886,7 +1882,7 @@ func main() { ## 测试 -`net/http/httptest`包是HTTP测试的首选方式。 +HTTP测试首选`net/http/httptest`包。 ```go package main @@ -1905,7 +1901,7 @@ func main() { } ``` -测试上面的代码示例: +上面这段代码的测试用例: ```go package main @@ -1933,8 +1929,9 @@ func TestPingRoute(t *testing.T) { ## 用户 -使用[Gin](https://github.com/gin-gonic/gin)Web框架的令人尊敬的项目列表。 +使用[Gin](https://github.com/gin-gonic/gin)框架的著名项目。 -* [drone](https://github.com/drone/drone):drone,用Go编写。 -* [gorush](https://github.com/appleboy/gorush):用Go编写的推送通知服务器。 -* [fnproject](https://github.com/fnproject/fn):容器本机,云无关的无服务器平台。 +* [drone](https://github.com/drone/drone):用Go编写的基于docker的持续集成平台。 +* [gorush](https://github.com/appleboy/gorush):用Go编写的推送通知服务。 +* [fnproject](https://github.com/fnproject/fn):容器驱动、云无关的无服务器平台。 +* [photoprism](https://github.com/photoprism/photoprism): 用Go编写的基于TensorFlow的个人相册管理系统. From 331af2219c9e6376e39221c6e85255fbfc64dcc8 Mon Sep 17 00:00:00 2001 From: thinkerou Date: Sat, 24 Nov 2018 20:49:26 +0800 Subject: [PATCH 032/111] add krakend to gin user list (#1658) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3f0c9170..0f74d30a 100644 --- a/README.md +++ b/README.md @@ -2000,3 +2000,4 @@ Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framewor * [gorush](https://github.com/appleboy/gorush): A push notification server written in Go. * [fnproject](https://github.com/fnproject/fn): The container native, cloud agnostic serverless platform. * [photoprism](https://github.com/photoprism/photoprism): Personal photo management powered by Go and Google TensorFlow. +* [krakend](https://github.com/devopsfaith/krakend): Ultra performant API Gateway with middlewares. From 687d8b9ac6178c31b0b1119cc669e6062a59bc93 Mon Sep 17 00:00:00 2001 From: thinkerou Date: Sun, 25 Nov 2018 20:52:46 +0800 Subject: [PATCH 033/111] add picfit to gin user list (#1661) agreed with the project's author. cc @thoas thanks! --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 0f74d30a..e7b92b2d 100644 --- a/README.md +++ b/README.md @@ -2001,3 +2001,4 @@ Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framewor * [fnproject](https://github.com/fnproject/fn): The container native, cloud agnostic serverless platform. * [photoprism](https://github.com/photoprism/photoprism): Personal photo management powered by Go and Google TensorFlow. * [krakend](https://github.com/devopsfaith/krakend): Ultra performant API Gateway with middlewares. +* [picfit](https://github.com/thoas/picfit): An image resizing server written in Go. From 465ead47d0f40a24b067798f5bab1c263f09660d Mon Sep 17 00:00:00 2001 From: weibaohui Date: Sun, 25 Nov 2018 21:18:00 +0800 Subject: [PATCH 034/111] doc: update README_ZH.md (#1659) --- README_ZH.md | 166 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 111 insertions(+), 55 deletions(-) diff --git a/README_ZH.md b/README_ZH.md index 7867c1f7..8c9f8abb 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -19,16 +19,16 @@ Gin 是一个 Go (Golang) 语言框架。 它是一个拥有更好性能的 mart - [安装](#安装) - [前提条件](#前提条件) -- [快速启动](#快速启动) +- [快速开始](#快速开始) - [性能测试](#性能测试) - [Gin v1 稳定版](#gin-v1-稳定版) -- [使用 jsoniter 构建](#使用-jsoniter-构建) +- [使用 jsoniter ](#使用-jsoniter) - [API 示例](#api-示例) - [GET,POST,PUT,PATCH,DELETE,OPTIONS 使用](#get-post-put-patch-delete-options-使用) - [获取路由参数](#获取路由参数) - [获取url查询参数](#获取url查询参数) - [Multipart Urlencoded 表单](#multipart-urlencoded-表单) - - [获取post表单数据(url带查询参数)](#获取post表单数据(url带查询参数)) + - [获取post表单数据(url带查询参数)](#获取post表单数据url带查询参数) - [映射参数 表单参数](#映射参数-表单参数) - [上传文件](#上传文件) - [路由组](#路由组) @@ -38,28 +38,30 @@ Gin 是一个 Go (Golang) 语言框架。 它是一个拥有更好性能的 mart - [模型绑定和验证](#模型绑定和验证) - [自定义验证器](#自定义验证器) - [只绑定url查询参数](#只绑定url查询参数) - - [url查询参数绑定到struct(或POST表单数据)](#url查询参数绑定到struct(或POST表单数据)) + - [url查询参数或表单数据绑定到结构体](#url查询参数或表单数据绑定到结构体) + - [url路径参数绑定](#url路径参数绑定) - [绑定 HTML 复选框](#绑定-html-复选框) - [Multipart Urlencoded 绑定](#multipart-urlencoded-绑定) - [XML JSON YAML ProtoBuf 渲染](#xml-json-yaml-protobuf-渲染) - [SecureJSON](#SecureJSON) - [静态文件服务](#静态文件服务) - - [从读者服务数据](#从读者服务数据) + - [从reader 读取数据](#从-reader-读取数据) - [HTML 渲染](#html-渲染) - [多模板](#多模板) - [重定向](#重定向) - [自定义中间件](#自定义中间件) - [使用 BasicAuth() 中间件](#使用-basicauth()-中间件) - - [Goroutines](#goroutines) + - [在中间件中使用Goroutines](#在中间件中使用Goroutines) - [自定义 HTTP 配置](#自定义-http-配置) - [Let's Encrypt 支持](#lets-encrypt-支持) - [使用 Gin 运行多个服务](使用-gin-运行多个服务) - [优雅重启或停止](#优雅重启或停止) - - [使用模板构建单个二进制文件](#使用模板构建单个二进制文件) + - [静态资源嵌入](#静态资源嵌入) - [表单数据绑定到自定义结构体](#表单数据绑定到自定义结构体) - - [将request body绑定到不同的结构体中](#将request body绑定到不同的结构体中) + - [将request body绑定到不同的结构体中](#将request-body绑定到不同的结构体中) - [http2 server 推送](#http2-server-推送) - [定义路由日志的格式](#定义路由日志的格式) + - [如何使用Cookie](#如何使用Cookie) - [测试](#测试) - [用户](#用户) @@ -119,9 +121,9 @@ $ go run main.go ## 前提条件 -新版本的 Gin 需要 Go 1.6 或者更高版本,并且很快就会升级到 Go 1.7. +新版本的 Gin 需要 Go 1.6 或者更高版本,并且很快就会要求升级到 Go 1.7. -## 快速启动 +## 快速开始 ```sh # assume the following codes in example.go file @@ -192,15 +194,15 @@ BenchmarkVulcan_GithubAll | 5000 | 394253 | 19894 ## Gin v1 稳定版 -- [x] 零分配路由器。 +- [x] 零分配路由。 - [x] 仍然是最快的http路由器和框架。 -- [x] 完整的单元测试套件 +- [x] 完整的单元测试支持 - [x] 对战测试 -- [x] API冻结,新版本不会破坏您的代码。 +- [x] API冻结,使用新版本不需要修改原有代码。 -## 使用 [jsoniter](https://github.com/json-iterator/go) 构建 +## 使用 [jsoniter](https://github.com/json-iterator/go) -Gin使用`encoding/json`作为默认的json包,但您可以通过其他标签的构建更改为[jsoniter](https://github.com/json-iterator/go)。 +Gin默认使用`encoding/json`解析json数据,但您可以通过`go build -tags=`更改为使用[jsoniter](https://github.com/json-iterator/go)。 ```sh $ go build -tags=jsoniter . @@ -208,7 +210,7 @@ $ go build -tags=jsoniter . ## API 示例 -### GET, POST, PUT, PATCH, DELETE , OPTIONS 使用 +### 使用GET, POST, PUT, PATCH, DELETE , OPTIONS ```go func main() { @@ -297,7 +299,7 @@ func main() { } ``` -### 获取post表单数据(url带查询参数) +### 获取post表单数据(url带查询参数) ``` POST /post?id=1234&page=1 HTTP/1.1 @@ -528,23 +530,23 @@ func main() { ### 模型绑定和验证 -要将请求主体绑定到类型中,请使用模型绑定。我们目前支持JSON,XML和标准表单值的绑定(foo = bar&boo = baz)。 +要将请求主体绑定到结构体中,请使用模型绑定。Gin目前支持JSON、XML、YAML和标准表单值的绑定(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)。 +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”`。 +使用时,需要在要绑定的所有字段上,设置相应的tag。例如,使用JSON绑定时,字段tag设置为`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`等效方法。 -- **类型** - 应该绑定 +Gin提供了两类绑定方法: +- **MustBind** - + - **方法** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`, `BindYAML` + - **说明** - 这些方法属于`MustBindWith`的具体调用。如果发生绑定错误,则请求终止,并触发`c.AbortWithError(400,err).SetType(ErrorTypeBind)`。响应状态码被设置为400,`Content-Type`被设置为`text/plain;charset= UTF-8`。如果您在此之后尝试设置响应状态码,Gin会输出日志“ `[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422`。如果您希望更好地把控绑定,请考虑使用`ShouldBind`等效方法。 +- **ShouldBind** -    - **方法** - `ShouldBind`,`ShouldBindJSON`,`ShouldBindXML`,`ShouldBindQuery` -   - **行为** - 这些方法在引擎盖下使用`ShouldBindWith`。如果存在绑定错误,则返回错误,开发人员有责任正确处理请求和错误。 + - **说明** - 这些方法属于`ShouldBindWith`的具体调用。如果发生绑定错误,Gin会返回错误。由您处理错误以及请求。 -使用Bind方法时,Gin会尝试根据Content-Type标头推断出绑定器。如果你确定你绑定了什么,你可以使用 `MustBindWith` 或 `ShouldBindWith`。 +使用Bind方法时,Gin会根据Content-Type尝试推断如何绑定,如果您明确知道,您可以使用 `MustBindWith` 或 `ShouldBindWith`。 -您还可以指定需要特定字段。如果字段用 `binding:“必需”` 来装饰,并且在绑定时具有空值,则会返回错误。 +指定必须绑定的字段,在该字段Tag上加上`binding:"required"` ,如果绑定时是空值,Gin会报错。 ```go // Binding from JSON @@ -615,7 +617,7 @@ func main() { } ``` -**简单请求** +**测试** ```shell $ curl -v -X POST \ http://localhost:8080/loginJSON \ @@ -637,15 +639,14 @@ $ curl -v -X POST \ {"error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'required' tag"} ``` -**跳过验证** +**忽略验证** -使用上面 的`curl` 命令运行上面的例子时,它返回错误。 因为这个例子使用 `binding:'需要``````。 如果使用 `binding:“ - ``````,那么在再次运行上面的例子时它不会返回错误。 +使用`curl` 命令运行上面的例子时,它返回错误,因为这个例子使用`binding:"required"` 。 如果使用 `binding:"-"` ,那么再次运行上面的例子时它不会返回错误。 ### 自定义验证器 -也可以注册自定义验证器。 请参阅[示例代码](examples/custom-validation/server.go)。 +注册自定义验证器, 请参阅[示例代码](examples/custom-validation/server.go)。 -[embedmd]:#(examples/custom-validation/server.go go) ```go package main @@ -707,7 +708,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"} ``` -[结构级验证](https://github.com/go-playground/validator/releases/tag/v8.7)也可以这种方式注册。 +结构体验证也可以参考[这种方法](https://github.com/go-playground/validator/releases/tag/v8.7)注册。 请参阅[struct-lvl-validation示例](examples/struct-lvl-validations)以了解更多信息。 ### 只绑定url查询参数 @@ -746,7 +747,7 @@ func startPage(c *gin.Context) { ``` -### url查询参数绑定到struct(或POST表单数据) +### url查询参数或表单数据绑定到结构体 请参阅[详细信息](https://github.com/gin-gonic/gin/issues/742#issuecomment-264681292)。 @@ -791,6 +792,39 @@ func startPage(c *gin.Context) { ```sh $ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15" ``` +### url路径参数绑定 + +查看[详细信息](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") +} +``` + +测试: +```sh +$ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3 +$ curl -v localhost:8088/thinkerou/not-uuid +``` ### 绑定 HTML 复选框 @@ -1320,9 +1354,9 @@ func main() { } ``` -### Goroutines +### 在中间件中使用Goroutines -当在中间件或处理程序中启动新的 Goroutines 时,你不应该**使用其中的原始上下文,你必须使用只读副本。 +当在中间件或handler中启动新的Goroutines时,不能使用原始上下文,必须使用只读副本。 ```go func main() { @@ -1541,7 +1575,8 @@ endless.ListenAndServe(":4242", router) * [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)示例。 +如果您使用的是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 @@ -1595,11 +1630,9 @@ func main() { } ``` -### 使用模板构建单个二进制文件 +### 静态资源嵌入 -您可以使用[go-assets]将服务器打包为包含模板的单个二进制可执行文件。 - -[go-assets]:https://github.com/jessevdk/go-assets +使用[go-assets](https://github.com/jessevdk/go-assets)将静态资源打包到可执行文件中。 ```go func main() { @@ -1641,7 +1674,7 @@ func loadTemplate() (*template.Template, error) { ### 表单数据绑定到自定义结构体 -以下示例使用自定义结构: +以下示例使用自定义结构体: ```go type StructA struct { @@ -1702,7 +1735,7 @@ func main() { } ``` -使用命令`curl`命令结果: +`curl`命令示例: ``` $ curl "http://localhost:8080/getb?field_a=hello&field_b=world" @@ -1713,7 +1746,7 @@ $ curl "http://localhost:8080/getd?field_x=hello&field_d=world" {"d":"world","x":{"FieldX":"hello"}} ``` -**注意**:不支持以下样式结构: +**注意**:不支持以下格式结构体: ```go type StructX struct { @@ -1729,7 +1762,7 @@ type StructZ struct { } ``` -总之,只支持现在没有`form`的嵌套自定义结构。 +一句话,现在只支持没有`form`的嵌套结构体。 ### 将request body绑定到不同的结构体中 @@ -1781,16 +1814,13 @@ func SomeHandler(c *gin.Context) { ``` -*`c.ShouldBindBodyWith`会在绑定之前将body存储到上下文中。 这会 -对性能造成轻微影响,如果调用一次就能完成绑定的话,那就不要用这个方法。 -*只有某些格式需要此功能 ,如“JSON”,“XML”,“MsgPack”, -`ProtoBuf`。 对于其他格式,如`Query`,`Form`,`FormPost`,`FormMultipart`, -可以多次调用`c.ShouldBind()`而不会造成任任何性能损失(见[#1341](https://github.com/gin-gonic/gin/pull/1341))。 +* `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)。 +http.Pusher仅支持go1.8 +。 有关详细信息,请参阅[golang blog](https://blog.golang.org/h2push)。 [embedmd]:# (examples/http-pusher/main.go go) ```go @@ -1878,7 +1908,34 @@ func main() { r.Run() } ``` +### 如何使用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() +} +``` ## 测试 @@ -1927,11 +1984,10 @@ func TestPingRoute(t *testing.T) { ``` ## 用户 - - -使用[Gin](https://github.com/gin-gonic/gin)框架的著名项目。 +使用[Gin](https://github.com/gin-gonic/gin)框架的著名项目: * [drone](https://github.com/drone/drone):用Go编写的基于docker的持续集成平台。 * [gorush](https://github.com/appleboy/gorush):用Go编写的推送通知服务。 * [fnproject](https://github.com/fnproject/fn):容器驱动、云无关的无服务器平台。 * [photoprism](https://github.com/photoprism/photoprism): 用Go编写的基于TensorFlow的个人相册管理系统. +* [krakend](https://github.com/devopsfaith/krakend): 表现优异的API网关中间件。 From 149ef75cdd1848d76b0bc2a205898a5eb53cb059 Mon Sep 17 00:00:00 2001 From: thinkerou Date: Mon, 26 Nov 2018 21:05:54 +0800 Subject: [PATCH 035/111] doc: remove README_ZH.md (#1667) --- README_ZH.md | 1993 -------------------------------------------------- 1 file changed, 1993 deletions(-) delete mode 100644 README_ZH.md diff --git a/README_ZH.md b/README_ZH.md deleted file mode 100644 index 8c9f8abb..00000000 --- a/README_ZH.md +++ /dev/null @@ -1,1993 +0,0 @@ -# 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-使用) - - [获取路由参数](#获取路由参数) - - [获取url查询参数](#获取url查询参数) - - [Multipart Urlencoded 表单](#multipart-urlencoded-表单) - - [获取post表单数据(url带查询参数)](#获取post表单数据url带查询参数) - - [映射参数 表单参数](#映射参数-表单参数) - - [上传文件](#上传文件) - - [路由组](#路由组) - - [默认初始化 Gin](#默认初始化-gin) - - [使用中间件](#使用中间件) - - [如何记录日志](#如何记录日志) - - [模型绑定和验证](#模型绑定和验证) - - [自定义验证器](#自定义验证器) - - [只绑定url查询参数](#只绑定url查询参数) - - [url查询参数或表单数据绑定到结构体](#url查询参数或表单数据绑定到结构体) - - [url路径参数绑定](#url路径参数绑定) - - [绑定 HTML 复选框](#绑定-html-复选框) - - [Multipart Urlencoded 绑定](#multipart-urlencoded-绑定) - - [XML JSON YAML ProtoBuf 渲染](#xml-json-yaml-protobuf-渲染) - - [SecureJSON](#SecureJSON) - - [静态文件服务](#静态文件服务) - - [从reader 读取数据](#从-reader-读取数据) - - [HTML 渲染](#html-渲染) - - [多模板](#多模板) - - [重定向](#重定向) - - [自定义中间件](#自定义中间件) - - [使用 BasicAuth() 中间件](#使用-basicauth()-中间件) - - [在中间件中使用Goroutines](#在中间件中使用Goroutines) - - [自定义 HTTP 配置](#自定义-http-配置) - - [Let's Encrypt 支持](#lets-encrypt-支持) - - [使用 Gin 运行多个服务](使用-gin-运行多个服务) - - [优雅重启或停止](#优雅重启或停止) - - [静态资源嵌入](#静态资源嵌入) - - [表单数据绑定到自定义结构体](#表单数据绑定到自定义结构体) - - [将request body绑定到不同的结构体中](#将request-body绑定到不同的结构体中) - - [http2 server 推送](#http2-server-推送) - - [定义路由日志的格式](#定义路由日志的格式) - - [如何使用Cookie](#如何使用Cookie) -- [测试](#测试) -- [用户](#用户) - -## 安装 - -要安装 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数据,但您可以通过`go build -tags=`更改为使用[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") -} -``` - -### 获取url查询参数 - -```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") -} -``` - -### 获取post表单数据(url带查询参数) - -``` -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") -} -``` - -### 模型绑定和验证 - -要将请求主体绑定到结构体中,请使用模型绑定。Gin目前支持JSON、XML、YAML和标准表单值的绑定(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)。 - -使用时,需要在要绑定的所有字段上,设置相应的tag。例如,使用JSON绑定时,字段tag设置为`json:"fieldname"`。 - -Gin提供了两类绑定方法: -- **MustBind** - - - **方法** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`, `BindYAML` - - **说明** - 这些方法属于`MustBindWith`的具体调用。如果发生绑定错误,则请求终止,并触发`c.AbortWithError(400,err).SetType(ErrorTypeBind)`。响应状态码被设置为400,`Content-Type`被设置为`text/plain;charset= UTF-8`。如果您在此之后尝试设置响应状态码,Gin会输出日志“ `[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422`。如果您希望更好地把控绑定,请考虑使用`ShouldBind`等效方法。 -- **ShouldBind** - -   - **方法** - `ShouldBind`,`ShouldBindJSON`,`ShouldBindXML`,`ShouldBindQuery` - - **说明** - 这些方法属于`ShouldBindWith`的具体调用。如果发生绑定错误,Gin会返回错误。由您处理错误以及请求。 - -使用Bind方法时,Gin会根据Content-Type尝试推断如何绑定,如果您明确知道,您可以使用 `MustBindWith` 或 `ShouldBindWith`。 - -指定必须绑定的字段,在该字段Tag上加上`binding:"required"` ,如果绑定时是空值,Gin会报错。 - -```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:"required"` 。 如果使用 `binding:"-"` ,那么再次运行上面的例子时它不会返回错误。 - -### 自定义验证器 - -注册自定义验证器, 请参阅[示例代码](examples/custom-validation/server.go)。 - -```go -package main - -import ( - "net/http" - "reflect" - "time" - - "github.com/gin-gonic/gin" - "github.com/gin-gonic/gin/binding" - "gopkg.in/go-playground/validator.v8" -) - -// Booking contains binded and validated data. -type Booking struct { - CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"` - CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"` -} - -func bookableDate( - v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value, - field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string, -) bool { - if date, ok := field.Interface().(time.Time); ok { - today := time.Now() - if today.Year() > date.Year() || today.YearDay() > date.YearDay() { - return false - } - } - return true -} - -func main() { - route := gin.Default() - - if v, ok := binding.Validator.Engine().(*validator.Validate); ok { - v.RegisterValidation("bookabledate", bookableDate) - } - - route.GET("/bookable", getBookable) - route.Run(":8085") -} - -func getBookable(c *gin.Context) { - var b Booking - if err := c.ShouldBindWith(&b, binding.Query); err == nil { - c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"}) - } else { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - } -} -``` - -```console -$ curl "localhost:8085/bookable?check_in=2018-04-16&check_out=2018-04-17" -{"message":"Booking dates are valid!"} - -$ curl "localhost:8085/bookable?check_in=2018-03-08&check_out=2018-03-09" -{"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag"} -``` - -结构体验证也可以参考[这种方法](https://github.com/go-playground/validator/releases/tag/v8.7)注册。 -请参阅[struct-lvl-validation示例](examples/struct-lvl-validations)以了解更多信息。 - -### 只绑定url查询参数 - -`ShouldBindQuery` 函数只绑定url查询参数而不是post字段。 请参阅[详细信息](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") -} - -``` - -### url查询参数或表单数据绑定到结构体 - -请参阅[详细信息](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" -``` -### url路径参数绑定 - -查看[详细信息](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") -} -``` - -测试: -```sh -$ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3 -$ curl -v localhost:8088/thinkerou/not-uuid -``` - -### 绑定 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") -} -``` - -测试: -```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使用unicode替换特殊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 - -当在中间件或handler中启动新的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](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`目录中的完整示例。 - -### 表单数据绑定到自定义结构体 - -以下示例使用自定义结构体: - -```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`的嵌套结构体。 - -### 将request 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 { - ... - } -} -``` - - -* `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,Key Values或其他格式)记录信息,则可以使用`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() -} -``` -### 如何使用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() -} -``` - -## 测试 - -HTTP测试首选`net/http/httptest`包。 - -```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)框架的著名项目: - -* [drone](https://github.com/drone/drone):用Go编写的基于docker的持续集成平台。 -* [gorush](https://github.com/appleboy/gorush):用Go编写的推送通知服务。 -* [fnproject](https://github.com/fnproject/fn):容器驱动、云无关的无服务器平台。 -* [photoprism](https://github.com/photoprism/photoprism): 用Go编写的基于TensorFlow的个人相册管理系统. -* [krakend](https://github.com/devopsfaith/krakend): 表现优异的API网关中间件。 From b97ccf3a43d2901d295870b5876277bf478a4909 Mon Sep 17 00:00:00 2001 From: MetalBreaker Date: Mon, 26 Nov 2018 16:01:51 +0100 Subject: [PATCH 036/111] Router: Route StaticFS() not found to Router's NoRoute() (#1663) Closes #1220 --- routergroup.go | 12 ++++++++++-- routes_test.go | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/routergroup.go b/routergroup.go index 9cb0b989..5615a50c 100644 --- a/routergroup.go +++ b/routergroup.go @@ -185,11 +185,19 @@ func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRou func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem) HandlerFunc { absolutePath := group.calculateAbsolutePath(relativePath) fileServer := http.StripPrefix(absolutePath, http.FileServer(fs)) - _, nolisting := fs.(*onlyfilesFS) + return func(c *Context) { - if nolisting { + file := c.Param("filepath") + + // Check if file exists and/or if we have permission to access it + if _, err := fs.Open(file); err != nil { c.Writer.WriteHeader(http.StatusNotFound) + c.handlers = group.engine.allNoRoute + // Reset index + c.index = -1 + return } + fileServer.ServeHTTP(c.Writer, c.Request) } } diff --git a/routes_test.go b/routes_test.go index 60f1c81b..c4d59725 100644 --- a/routes_test.go +++ b/routes_test.go @@ -411,6 +411,21 @@ func TestRouterNotFound(t *testing.T) { assert.Equal(t, http.StatusNotFound, w.Code) } +func TestRouterStaticFSNotFound(t *testing.T) { + router := New() + + router.StaticFS("/", http.FileSystem(http.Dir("/thisreallydoesntexist/"))) + router.NoRoute(func(c *Context) { + c.String(404, "non existent") + }) + + w := performRequest(router, "GET", "/nonexistent") + assert.Equal(t, "non existent", w.Body.String()) + + w = performRequest(router, "HEAD", "/nonexistent") + assert.Equal(t, "non existent", w.Body.String()) +} + func TestRouteRawPath(t *testing.T) { route := New() route.UseRawPath = true From 54e9610400af963d321f8bdd91ce1507f7f8af9f Mon Sep 17 00:00:00 2001 From: thinkerou Date: Wed, 5 Dec 2018 02:02:03 +0800 Subject: [PATCH 037/111] chore: remove wercker yml file (#1676) Now the `wercker.yml` have no longer used. --- wercker.yml | 1 - 1 file changed, 1 deletion(-) delete mode 100644 wercker.yml diff --git a/wercker.yml b/wercker.yml deleted file mode 100644 index 3ab8084c..00000000 --- a/wercker.yml +++ /dev/null @@ -1 +0,0 @@ -box: wercker/default \ No newline at end of file From f463d847c23c85070653fe725cf27172633f57a0 Mon Sep 17 00:00:00 2001 From: thinkerou Date: Wed, 5 Dec 2018 05:58:35 +0800 Subject: [PATCH 038/111] chore: fix test fail (#1669) * chore: fix test fail * fix binduri test fail --- binding/binding_test.go | 2 +- recovery_test.go | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/binding/binding_test.go b/binding/binding_test.go index 5b311764..c0204d7f 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -687,7 +687,7 @@ func TestUriBinding(t *testing.T) { } var not NotSupportStruct assert.Error(t, b.BindUri(m, ¬)) - assert.Equal(t, "", not.Name) + assert.Equal(t, map[string]interface{}(nil), not.Name) } func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) { diff --git a/recovery_test.go b/recovery_test.go index c9fb29ce..e886eaac 100644 --- a/recovery_test.go +++ b/recovery_test.go @@ -11,6 +11,7 @@ import ( "net" "net/http" "os" + "strings" "syscall" "testing" @@ -84,7 +85,7 @@ func TestPanicWithBrokenPipe(t *testing.T) { const expectCode = 204 expectMsgs := map[syscall.Errno]string{ - syscall.EPIPE: "Broken pipe", + syscall.EPIPE: "broken pipe", syscall.ECONNRESET: "connection reset by peer", } @@ -108,7 +109,7 @@ func TestPanicWithBrokenPipe(t *testing.T) { w := performRequest(router, "GET", "/recovery") // TEST assert.Equal(t, expectCode, w.Code) - assert.Contains(t, buf.String(), expectMsg) + assert.Contains(t, strings.ToLower(buf.String()), expectMsg) }) } } From 98c7ac7202ffc7cfb60706bb48e0ef10f737abb1 Mon Sep 17 00:00:00 2001 From: thinkerou Date: Wed, 5 Dec 2018 13:36:49 +0800 Subject: [PATCH 039/111] fix bug (#1682) --- routergroup.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/routergroup.go b/routergroup.go index 5615a50c..2b41dfda 100644 --- a/routergroup.go +++ b/routergroup.go @@ -187,8 +187,11 @@ func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileS fileServer := http.StripPrefix(absolutePath, http.FileServer(fs)) return func(c *Context) { - file := c.Param("filepath") + if _, nolisting := fs.(*onlyfilesFS); nolisting { + c.Writer.WriteHeader(http.StatusNotFound) + } + file := c.Param("filepath") // Check if file exists and/or if we have permission to access it if _, err := fs.Open(file); err != nil { c.Writer.WriteHeader(http.StatusNotFound) From cce49582d617333c72f4d86abd8ef0e62eb778a0 Mon Sep 17 00:00:00 2001 From: thinkerou Date: Wed, 5 Dec 2018 13:49:03 +0800 Subject: [PATCH 040/111] ci: break when test fail (#1671) --- .gitignore | 2 ++ Makefile | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 14dc8f20..bdd50c95 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ vendor/* coverage.out count.out test +profile.out +tmp.out diff --git a/Makefile b/Makefile index b698ac09..b0d2e24a 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,12 @@ install: deps test: echo "mode: count" > coverage.out for d in $(TESTFOLDER); do \ - $(GO) test -v -covermode=count -coverprofile=profile.out $$d; \ + $(GO) test -v -covermode=count -coverprofile=profile.out $$d > tmp.out; \ + cat tmp.out; \ + if grep -q "^--- FAIL" tmp.out; then \ + rm tmp.out; \ + exit 1;\ + fi; \ if [ -f profile.out ]; then \ cat profile.out | grep -v "mode:" >> coverage.out; \ rm profile.out; \ From f76ccb25f1eee8684e545ec00f8f2934fec98f09 Mon Sep 17 00:00:00 2001 From: Sai Date: Wed, 12 Dec 2018 10:05:16 +0900 Subject: [PATCH 041/111] Add LoggerWithFormatter method (#1677) * Add LoggerWithFormatter * Add tests for LoggerWithFormatter & LoggerWithConfig * Add note for README * Add tests for DefaultLogFormatter * Add comment * Change DefaultLogFormatter to a private method --- README.md | 38 ++++++++++ logger.go | 116 +++++++++++++++++++++++------ logger_test.go | 194 ++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 324 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index e7b92b2d..c1f902a9 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi - [Blank Gin without middleware by default](#blank-gin-without-middleware-by-default) - [Using middleware](#using-middleware) - [How to write log file](#how-to-write-log-file) + - [Custom Log Format](#custom-log-format) - [Model binding and validation](#model-binding-and-validation) - [Custom Validators](#custom-validators) - [Only Bind Query String](#only-bind-query-string) @@ -528,6 +529,43 @@ func main() { } ``` +### Custom Log Format +```go +func main() { + router := gin.New() + + // LoggerWithFormatter middleware will write the logs to gin.DefaultWriter + // By default gin.DefaultWriter = os.Stdout + router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { + + // your custom format + return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n", + param.ClientIP, + param.TimeStamp.Format(time.RFC1123), + param.Method, + param.Path, + param.Request.Proto, + param.StatusCode, + param.Latency, + param.Request.UserAgent(), + param.ErrorMessage, + ) + })) + router.Use(gin.Recovery()) + + router.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + + router.Run(":8080") +} +``` + +**Sample Output** +``` +::1 - [Fri, 07 Dec 2018 17:04:38 JST] "GET /ping HTTP/1.1 200 122.767µs "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36" " +``` + ### Model binding and validation To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML and standard form values (foo=bar&boo=baz). diff --git a/logger.go b/logger.go index 74dd2e6f..a64af697 100644 --- a/logger.go +++ b/logger.go @@ -26,6 +26,56 @@ var ( disableColor = false ) +// LoggerConfig defines the config for Logger middleware. +type LoggerConfig struct { + // Optional. Default value is gin.defaultLogFormatter + Formatter LogFormatter + + // Output is a writer where logs are written. + // Optional. Default value is gin.DefaultWriter. + Output io.Writer + + // SkipPathes is a url path array which logs are not written. + // Optional. + SkipPathes []string +} + +// LogFormatter gives the signature of the formatter function passed to LoggerWithFormatter +type LogFormatter func(params LogFormatterParams) string + +// LogFormatterParams is the structure any formatter will be handed when time to log comes +type LogFormatterParams struct { + Request *http.Request + TimeStamp time.Time + StatusCode int + Latency time.Duration + ClientIP string + Method string + Path string + ErrorMessage string + IsTerm bool +} + +// defaultLogFormatter is the default log format function Logger middleware uses. +var defaultLogFormatter = func(param LogFormatterParams) string { + var statusColor, methodColor, resetColor string + if param.IsTerm { + statusColor = colorForStatus(param.StatusCode) + methodColor = colorForMethod(param.Method) + resetColor = reset + } + + return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s", + param.TimeStamp.Format("2006/01/02 - 15:04:05"), + statusColor, param.StatusCode, resetColor, + param.Latency, + param.ClientIP, + methodColor, param.Method, resetColor, + param.Path, + param.ErrorMessage, + ) +} + // DisableConsoleColor disables color output in the console. func DisableConsoleColor() { disableColor = true @@ -50,12 +100,39 @@ func ErrorLoggerT(typ ErrorType) HandlerFunc { // Logger instances a Logger middleware that will write the logs to gin.DefaultWriter. // By default gin.DefaultWriter = os.Stdout. func Logger() HandlerFunc { - return LoggerWithWriter(DefaultWriter) + return LoggerWithConfig(LoggerConfig{}) +} + +// LoggerWithFormatter instance a Logger middleware with the specified log format function. +func LoggerWithFormatter(f LogFormatter) HandlerFunc { + return LoggerWithConfig(LoggerConfig{ + Formatter: f, + }) } // LoggerWithWriter instance a Logger middleware with the specified writer buffer. // Example: os.Stdout, a file opened in write mode, a socket... func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc { + return LoggerWithConfig(LoggerConfig{ + Output: out, + SkipPathes: notlogged, + }) +} + +// LoggerWithConfig instance a Logger middleware with config. +func LoggerWithConfig(conf LoggerConfig) HandlerFunc { + formatter := conf.Formatter + if formatter == nil { + formatter = defaultLogFormatter + } + + out := conf.Output + if out == nil { + out = DefaultWriter + } + + notlogged := conf.SkipPathes + isTerm := true if w, ok := out.(*os.File); !ok || @@ -85,34 +162,27 @@ func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc { // Log only when path is not being skipped if _, ok := skip[path]; !ok { - // Stop timer - end := time.Now() - latency := end.Sub(start) - - clientIP := c.ClientIP() - method := c.Request.Method - statusCode := c.Writer.Status() - var statusColor, methodColor, resetColor string - if isTerm { - statusColor = colorForStatus(statusCode) - methodColor = colorForMethod(method) - resetColor = reset + param := LogFormatterParams{ + Request: c.Request, + IsTerm: isTerm, } - comment := c.Errors.ByType(ErrorTypePrivate).String() + + // Stop timer + param.TimeStamp = time.Now() + param.Latency = param.TimeStamp.Sub(start) + + param.ClientIP = c.ClientIP() + param.Method = c.Request.Method + param.StatusCode = c.Writer.Status() + param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String() if raw != "" { path = path + "?" + raw } - fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s", - end.Format("2006/01/02 - 15:04:05"), - statusColor, statusCode, resetColor, - latency, - clientIP, - methodColor, method, resetColor, - path, - comment, - ) + param.Path = path + + fmt.Fprintf(out, formatter(param)) } } } diff --git a/logger_test.go b/logger_test.go index 6118cb04..909ddd39 100644 --- a/logger_test.go +++ b/logger_test.go @@ -7,8 +7,10 @@ package gin import ( "bytes" "errors" + "fmt" "net/http" "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -79,7 +81,179 @@ func TestLogger(t *testing.T) { assert.Contains(t, buffer.String(), "404") assert.Contains(t, buffer.String(), "GET") assert.Contains(t, buffer.String(), "/notfound") +} +func TestLoggerWithConfig(t *testing.T) { + buffer := new(bytes.Buffer) + router := New() + router.Use(LoggerWithConfig(LoggerConfig{Output: buffer})) + router.GET("/example", func(c *Context) {}) + router.POST("/example", func(c *Context) {}) + router.PUT("/example", func(c *Context) {}) + router.DELETE("/example", func(c *Context) {}) + router.PATCH("/example", func(c *Context) {}) + router.HEAD("/example", func(c *Context) {}) + router.OPTIONS("/example", func(c *Context) {}) + + performRequest(router, "GET", "/example?a=100") + assert.Contains(t, buffer.String(), "200") + assert.Contains(t, buffer.String(), "GET") + assert.Contains(t, buffer.String(), "/example") + assert.Contains(t, buffer.String(), "a=100") + + // I wrote these first (extending the above) but then realized they are more + // like integration tests because they test the whole logging process rather + // than individual functions. Im not sure where these should go. + buffer.Reset() + performRequest(router, "POST", "/example") + assert.Contains(t, buffer.String(), "200") + assert.Contains(t, buffer.String(), "POST") + assert.Contains(t, buffer.String(), "/example") + + buffer.Reset() + performRequest(router, "PUT", "/example") + assert.Contains(t, buffer.String(), "200") + assert.Contains(t, buffer.String(), "PUT") + assert.Contains(t, buffer.String(), "/example") + + buffer.Reset() + performRequest(router, "DELETE", "/example") + assert.Contains(t, buffer.String(), "200") + assert.Contains(t, buffer.String(), "DELETE") + assert.Contains(t, buffer.String(), "/example") + + buffer.Reset() + performRequest(router, "PATCH", "/example") + assert.Contains(t, buffer.String(), "200") + assert.Contains(t, buffer.String(), "PATCH") + assert.Contains(t, buffer.String(), "/example") + + buffer.Reset() + performRequest(router, "HEAD", "/example") + assert.Contains(t, buffer.String(), "200") + assert.Contains(t, buffer.String(), "HEAD") + assert.Contains(t, buffer.String(), "/example") + + buffer.Reset() + performRequest(router, "OPTIONS", "/example") + assert.Contains(t, buffer.String(), "200") + assert.Contains(t, buffer.String(), "OPTIONS") + assert.Contains(t, buffer.String(), "/example") + + buffer.Reset() + performRequest(router, "GET", "/notfound") + assert.Contains(t, buffer.String(), "404") + assert.Contains(t, buffer.String(), "GET") + assert.Contains(t, buffer.String(), "/notfound") +} + +func TestLoggerWithFormatter(t *testing.T) { + buffer := new(bytes.Buffer) + + d := DefaultWriter + DefaultWriter = buffer + defer func() { + DefaultWriter = d + }() + + router := New() + router.Use(LoggerWithFormatter(func(param LogFormatterParams) string { + return fmt.Sprintf("[FORMATTER TEST] %v | %3d | %13v | %15s | %-7s %s\n%s", + param.TimeStamp.Format("2006/01/02 - 15:04:05"), + param.StatusCode, + param.Latency, + param.ClientIP, + param.Method, + param.Path, + param.ErrorMessage, + ) + })) + router.GET("/example", func(c *Context) {}) + performRequest(router, "GET", "/example?a=100") + + // output test + assert.Contains(t, buffer.String(), "[FORMATTER TEST]") + assert.Contains(t, buffer.String(), "200") + assert.Contains(t, buffer.String(), "GET") + assert.Contains(t, buffer.String(), "/example") + assert.Contains(t, buffer.String(), "a=100") +} + +func TestLoggerWithConfigFormatting(t *testing.T) { + var gotParam LogFormatterParams + buffer := new(bytes.Buffer) + + router := New() + router.Use(LoggerWithConfig(LoggerConfig{ + Output: buffer, + Formatter: func(param LogFormatterParams) string { + // for assert test + gotParam = param + + return fmt.Sprintf("[FORMATTER TEST] %v | %3d | %13v | %15s | %-7s %s\n%s", + param.TimeStamp.Format("2006/01/02 - 15:04:05"), + param.StatusCode, + param.Latency, + param.ClientIP, + param.Method, + param.Path, + param.ErrorMessage, + ) + }, + })) + router.GET("/example", func(c *Context) { + // set dummy ClientIP + c.Request.Header.Set("X-Forwarded-For", "20.20.20.20") + }) + performRequest(router, "GET", "/example?a=100") + + // output test + assert.Contains(t, buffer.String(), "[FORMATTER TEST]") + assert.Contains(t, buffer.String(), "200") + assert.Contains(t, buffer.String(), "GET") + assert.Contains(t, buffer.String(), "/example") + assert.Contains(t, buffer.String(), "a=100") + + // LogFormatterParams test + assert.NotNil(t, gotParam.Request) + assert.NotEmpty(t, gotParam.TimeStamp) + assert.Equal(t, 200, gotParam.StatusCode) + assert.NotEmpty(t, gotParam.Latency) + assert.Equal(t, "20.20.20.20", gotParam.ClientIP) + assert.Equal(t, "GET", gotParam.Method) + assert.Equal(t, "/example?a=100", gotParam.Path) + assert.Empty(t, gotParam.ErrorMessage) + +} + +func TestDefaultLogFormatter(t *testing.T) { + timeStamp := time.Unix(1544173902, 0).UTC() + + termFalseParam := LogFormatterParams{ + TimeStamp: timeStamp, + StatusCode: 200, + Latency: time.Second * 5, + ClientIP: "20.20.20.20", + Method: "GET", + Path: "/", + ErrorMessage: "", + IsTerm: false, + } + + termTrueParam := LogFormatterParams{ + TimeStamp: timeStamp, + StatusCode: 200, + Latency: time.Second * 5, + ClientIP: "20.20.20.20", + Method: "GET", + Path: "/", + ErrorMessage: "", + IsTerm: true, + } + + assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET /\n", defaultLogFormatter(termFalseParam)) + + assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 5s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m /\n", defaultLogFormatter(termTrueParam)) } func TestColorForMethod(t *testing.T) { @@ -127,7 +301,7 @@ func TestErrorLogger(t *testing.T) { assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String()) } -func TestSkippingPaths(t *testing.T) { +func TestLoggerWithWriterSkippingPaths(t *testing.T) { buffer := new(bytes.Buffer) router := New() router.Use(LoggerWithWriter(buffer, "/skipped")) @@ -142,6 +316,24 @@ func TestSkippingPaths(t *testing.T) { assert.Contains(t, buffer.String(), "") } +func TestLoggerWithConfigSkippingPaths(t *testing.T) { + buffer := new(bytes.Buffer) + router := New() + router.Use(LoggerWithConfig(LoggerConfig{ + Output: buffer, + SkipPathes: []string{"/skipped"}, + })) + router.GET("/logged", func(c *Context) {}) + router.GET("/skipped", func(c *Context) {}) + + performRequest(router, "GET", "/logged") + assert.Contains(t, buffer.String(), "200") + + buffer.Reset() + performRequest(router, "GET", "/skipped") + assert.Contains(t, buffer.String(), "") +} + func TestDisableConsoleColor(t *testing.T) { New() assert.False(t, disableColor) From 59695e7ba86dec1a8d847c3329a3e7c9f7705125 Mon Sep 17 00:00:00 2001 From: thinkerou Date: Wed, 12 Dec 2018 23:40:29 +0800 Subject: [PATCH 042/111] Add BindUri (#1694) * add BindUri * fix bug * fix code style --- context.go | 18 ++++++++++++++---- githubapi_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/context.go b/context.go index 478e8c09..c94926e1 100644 --- a/context.go +++ b/context.go @@ -530,15 +530,25 @@ func (c *Context) BindYAML(obj interface{}) error { return c.MustBindWith(obj, binding.YAML) } +// BindUri binds the passed struct pointer using binding.Uri. +// It will abort the request with HTTP 400 if any error occurs. +func (c *Context) BindUri(obj interface{}) error { + if err := c.ShouldBindUri(obj); err != nil { + c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) + return err + } + return nil +} + // MustBindWith binds the passed struct pointer using the specified binding engine. // 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 { +func (c *Context) MustBindWith(obj interface{}, b binding.Binding) error { + if err := c.ShouldBindWith(obj, b); err != nil { c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) + return err } - - return + return nil } // ShouldBind checks the Content-Type to select a binding engine automatically, diff --git a/githubapi_test.go b/githubapi_test.go index 6b56a2b7..5253425a 100644 --- a/githubapi_test.go +++ b/githubapi_test.go @@ -290,8 +290,8 @@ func TestShouldBindUri(t *testing.T) { router := Default() type Person struct { - Name string `uri:"name"` - Id string `uri:"id"` + Name string `uri:"name" binding:"required"` + Id string `uri:"id" binding:"required"` } router.Handle("GET", "/rest/:name/:id", func(c *Context) { var person Person @@ -304,6 +304,46 @@ func TestShouldBindUri(t *testing.T) { path, _ := exampleFromPath("/rest/:name/:id") w := performRequest(router, "GET", path) assert.Equal(t, "ShouldBindUri test OK", w.Body.String()) + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestBindUri(t *testing.T) { + DefaultWriter = os.Stdout + router := Default() + + type Person struct { + Name string `uri:"name" binding:"required"` + Id string `uri:"id" binding:"required"` + } + router.Handle("GET", "/rest/:name/:id", func(c *Context) { + var person Person + assert.NoError(t, c.BindUri(&person)) + assert.True(t, "" != person.Name) + assert.True(t, "" != person.Id) + c.String(http.StatusOK, "BindUri test OK") + }) + + path, _ := exampleFromPath("/rest/:name/:id") + w := performRequest(router, "GET", path) + assert.Equal(t, "BindUri test OK", w.Body.String()) + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestBindUriError(t *testing.T) { + DefaultWriter = os.Stdout + router := Default() + + type Member struct { + Number string `uri:"num" binding:"required,uuid"` + } + router.Handle("GET", "/new/rest/:num", func(c *Context) { + var m Member + c.BindUri(&m) + }) + + path1, _ := exampleFromPath("/new/rest/:num") + w1 := performRequest(router, "GET", path1) + assert.Equal(t, http.StatusBadRequest, w1.Code) } func githubConfigRouter(router *Engine) { From f67d7a90c4d2e5bdf310a78d7e6a04e3d9aee851 Mon Sep 17 00:00:00 2001 From: Romain Beuque Date: Thu, 13 Dec 2018 12:20:17 +0900 Subject: [PATCH 043/111] context: inherits context cancelation and deadline from http.Request context for Go>=1.7 (#1690) *gin.Context implements standard context.Context methods, but always returns data as context is still valid. Since Go 1.7, http.Request now contains a context.Context object, which can be controlled by the http.Server to indicates that the context is now closed, and persue of request should be canceled. This implements the propagation of http.Request context methods inside gin.Context to have HTTP context cancelation information at gin.Context level. Signed-off-by: Romain Beuque --- context.go | 28 -------------------------- context_17.go | 30 ++++++++++++++++++++++++++++ context_17_test.go | 49 ++++++++++++++++++++++++++++++++++++++++++++++ context_pre17.go | 39 ++++++++++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 28 deletions(-) create mode 100644 context_pre17.go diff --git a/context.go b/context.go index c94926e1..c38b2b87 100644 --- a/context.go +++ b/context.go @@ -942,34 +942,6 @@ func (c *Context) SetAccepted(formats ...string) { c.Accepted = formats } -/************************************/ -/***** 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. diff --git a/context_17.go b/context_17.go index 8e9f75ad..024dcb70 100644 --- a/context_17.go +++ b/context_17.go @@ -7,6 +7,8 @@ package gin import ( + "time" + "github.com/gin-gonic/gin/render" ) @@ -15,3 +17,31 @@ import ( func (c *Context) PureJSON(code int, obj interface{}) { c.Render(code, render.PureJSON{Data: obj}) } + +/************************************/ +/***** 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() (time.Time, bool) { + return c.Request.Context().Deadline() +} + +// 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 c.Request.Context().Done() +} + +// 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 c.Request.Context().Err() +} diff --git a/context_17_test.go b/context_17_test.go index 5b9ebcdc..f2a2f184 100644 --- a/context_17_test.go +++ b/context_17_test.go @@ -7,9 +7,12 @@ package gin import ( + "bytes" + "context" "net/http" "net/http/httptest" "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -25,3 +28,49 @@ func TestContextRenderPureJSON(t *testing.T) { assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\"}\n", w.Body.String()) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) } + +func TestContextHTTPContext(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + req, _ := http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() + c.Request = req.WithContext(ctx) + + assert.NoError(t, c.Err()) + assert.NotNil(t, c.Done()) + select { + case <-c.Done(): + assert.Fail(t, "context should not be canceled") + default: + } + + ti, ok := c.Deadline() + assert.Equal(t, ti, time.Time{}) + assert.False(t, ok) + assert.Equal(t, c.Value(0), c.Request) + + cancelFunc() + assert.NotNil(t, c.Done()) + select { + case <-c.Done(): + default: + assert.Fail(t, "context should be canceled") + } +} + +func TestContextHTTPContextWithDeadline(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + req, _ := http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) + location, _ := time.LoadLocation("Europe/Paris") + assert.NotNil(t, location) + date := time.Date(2031, 12, 27, 16, 00, 00, 00, location) + ctx, cancelFunc := context.WithDeadline(context.Background(), date) + defer cancelFunc() + c.Request = req.WithContext(ctx) + + assert.NoError(t, c.Err()) + + ti, ok := c.Deadline() + assert.Equal(t, ti, date) + assert.True(t, ok) +} diff --git a/context_pre17.go b/context_pre17.go new file mode 100644 index 00000000..2008d3c6 --- /dev/null +++ b/context_pre17.go @@ -0,0 +1,39 @@ +// 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 ( + "time" +) + +/************************************/ +/***** 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 +} From 1542eff27f7d67ef56543358e2f623eb4ea8adf9 Mon Sep 17 00:00:00 2001 From: Ganlv Date: Mon, 17 Dec 2018 08:13:07 +0800 Subject: [PATCH 044/111] Fix #1693: file.Filename should not be trusted (#1699) --- README.md | 4 ++++ examples/upload-file/multiple/main.go | 4 +++- examples/upload-file/single/main.go | 4 +++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c1f902a9..2dc9e5ff 100644 --- a/README.md +++ b/README.md @@ -364,6 +364,10 @@ ids: map[b:hello a:1234], names: map[second:tianou first:thinkerou] References issue [#774](https://github.com/gin-gonic/gin/issues/774) and detail [example code](examples/upload-file/single). +`file.Filename` **SHOULD NOT** be trusted. See [`Content-Disposition` on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#Directives) and [#1693](https://github.com/gin-gonic/gin/issues/1693) + +> The filename is always optional and must not be used blindly by the application: path information should be stripped, and conversion to the server file system rules should be done. + ```go func main() { router := gin.Default() diff --git a/examples/upload-file/multiple/main.go b/examples/upload-file/multiple/main.go index a55325ed..2b9d6d91 100644 --- a/examples/upload-file/multiple/main.go +++ b/examples/upload-file/multiple/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "net/http" + "path/filepath" "github.com/gin-gonic/gin" ) @@ -25,7 +26,8 @@ func main() { files := form.File["files"] for _, file := range files { - if err := c.SaveUploadedFile(file, file.Filename); err != nil { + filename := filepath.Base(file.Filename) + if err := c.SaveUploadedFile(file, filename); err != nil { c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error())) return } diff --git a/examples/upload-file/single/main.go b/examples/upload-file/single/main.go index 5d438651..ba289f54 100644 --- a/examples/upload-file/single/main.go +++ b/examples/upload-file/single/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "net/http" + "path/filepath" "github.com/gin-gonic/gin" ) @@ -23,7 +24,8 @@ func main() { return } - if err := c.SaveUploadedFile(file, file.Filename); err != nil { + filename := filepath.Base(file.Filename) + if err := c.SaveUploadedFile(file, filename); err != nil { c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error())) return } From 678e09c736505225e28d8c585087b84faaf4bb80 Mon Sep 17 00:00:00 2001 From: Sai Date: Thu, 20 Dec 2018 18:54:08 +0900 Subject: [PATCH 045/111] Plural is "Paths", not "Pathes" (#1706) --- logger.go | 10 +++++----- logger_test.go | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/logger.go b/logger.go index a64af697..b9c63c73 100644 --- a/logger.go +++ b/logger.go @@ -35,9 +35,9 @@ type LoggerConfig struct { // Optional. Default value is gin.DefaultWriter. Output io.Writer - // SkipPathes is a url path array which logs are not written. + // SkipPaths is a url path array which logs are not written. // Optional. - SkipPathes []string + SkipPaths []string } // LogFormatter gives the signature of the formatter function passed to LoggerWithFormatter @@ -114,8 +114,8 @@ func LoggerWithFormatter(f LogFormatter) HandlerFunc { // Example: os.Stdout, a file opened in write mode, a socket... func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc { return LoggerWithConfig(LoggerConfig{ - Output: out, - SkipPathes: notlogged, + Output: out, + SkipPaths: notlogged, }) } @@ -131,7 +131,7 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc { out = DefaultWriter } - notlogged := conf.SkipPathes + notlogged := conf.SkipPaths isTerm := true diff --git a/logger_test.go b/logger_test.go index 909ddd39..350599d4 100644 --- a/logger_test.go +++ b/logger_test.go @@ -320,8 +320,8 @@ func TestLoggerWithConfigSkippingPaths(t *testing.T) { buffer := new(bytes.Buffer) router := New() router.Use(LoggerWithConfig(LoggerConfig{ - Output: buffer, - SkipPathes: []string{"/skipped"}, + Output: buffer, + SkipPaths: []string{"/skipped"}, })) router.GET("/logged", func(c *Context) {}) router.GET("/skipped", func(c *Context) {}) From 2d33c82028b4085827137c5a72cc0a076d8e2b08 Mon Sep 17 00:00:00 2001 From: Sai Date: Wed, 26 Dec 2018 00:27:24 +0900 Subject: [PATCH 046/111] Add comment to LogFormatterParams struct's fields (#1711) By https://github.com/gin-gonic/gin/issues/1701, I thought it's necessary. --- logger.go | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/logger.go b/logger.go index b9c63c73..a55d26e0 100644 --- a/logger.go +++ b/logger.go @@ -45,15 +45,24 @@ type LogFormatter func(params LogFormatterParams) string // LogFormatterParams is the structure any formatter will be handed when time to log comes type LogFormatterParams struct { - Request *http.Request - TimeStamp time.Time - StatusCode int - Latency time.Duration - ClientIP string - Method string - Path string + Request *http.Request + + // TimeStamp shows the time after the server returns a response. + TimeStamp time.Time + // StatusCode is HTTP response code. + StatusCode int + // Latency is how much time the server cost to process a certain request. + Latency time.Duration + // ClientIP equals Context's ClientIP method. + ClientIP string + // Method is the HTTP method given to the request. + Method string + // Path is a path the client requests. + Path string + // ErrorMessage is set if error has occurred in processing the request. ErrorMessage string - IsTerm bool + // IsTerm shows whether does gin's output descriptor refers to a terminal. + IsTerm bool } // defaultLogFormatter is the default log format function Logger middleware uses. From 1b34e8e8de41654004dfabf20fbadf43f619d41b Mon Sep 17 00:00:00 2001 From: thinkerou Date: Tue, 25 Dec 2018 23:40:11 +0800 Subject: [PATCH 047/111] chore: attemp to fix #1700 (#1707) --- tools.go => tools/tools.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename tools.go => tools/tools.go (88%) diff --git a/tools.go b/tools/tools.go similarity index 88% rename from tools.go rename to tools/tools.go index 9f96406a..7113e71e 100644 --- a/tools.go +++ b/tools/tools.go @@ -4,12 +4,12 @@ // +build tools -// This file exists to cause `go mod` and `go get` to believe these tools +// This package exists to cause `go mod` and `go get` to believe these tools // are dependencies, even though they are not runtime dependencies of any // gin package. This means they will appear in `go.mod` file, but will not // be a part of the build. -package gin +package tools import ( _ "github.com/campoy/embedmd" From 0bfc9cbcdbaa13e5fd633f77a32f22c30f1e2553 Mon Sep 17 00:00:00 2001 From: thinkerou Date: Wed, 26 Dec 2018 00:27:46 +0800 Subject: [PATCH 048/111] ci: exit 1 when build fail (#1695) Like this: ``` FAIL github.com/gin-gonic/gin [build failed] ``` --- Makefile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b0d2e24a..7211144a 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,10 @@ test: cat tmp.out; \ if grep -q "^--- FAIL" tmp.out; then \ rm tmp.out; \ - exit 1;\ + exit 1; \ + elif grep -q "build failed" tmp.out; then \ + rm tmp.out; \ + exit; \ fi; \ if [ -f profile.out ]; then \ cat profile.out | grep -v "mode:" >> coverage.out; \ From 49e4b0c60cb533e943c34a8d637944f25fa47ee6 Mon Sep 17 00:00:00 2001 From: Dmitry Kutakov Date: Fri, 28 Dec 2018 04:57:09 +0300 Subject: [PATCH 049/111] fix mapping inner structs with correct tag (#1718) --- binding/binding_test.go | 23 +++++++++++++++++++++++ binding/form_mapping.go | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/binding/binding_test.go b/binding/binding_test.go index c0204d7f..740749be 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -11,6 +11,7 @@ import ( "io/ioutil" "mime/multipart" "net/http" + "strconv" "testing" "time" @@ -690,6 +691,28 @@ func TestUriBinding(t *testing.T) { assert.Equal(t, map[string]interface{}(nil), not.Name) } +func TestUriInnerBinding(t *testing.T) { + type Tag struct { + Name string `uri:"name"` + S struct { + Age int `uri:"age"` + } + } + + expectedName := "mike" + expectedAge := 25 + + m := map[string][]string{ + "name": {expectedName}, + "age": {strconv.Itoa(expectedAge)}, + } + + var tag Tag + assert.NoError(t, Uri.BindUri(m, &tag)) + assert.Equal(t, tag.Name, expectedName) + assert.Equal(t, tag.S.Age, expectedAge) +} + func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) { b := Form assert.Equal(t, "form", b.Name()) diff --git a/binding/form_mapping.go b/binding/form_mapping.go index d893c21c..8900ab70 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -55,7 +55,7 @@ func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error { structFieldKind = structField.Kind() } if structFieldKind == reflect.Struct { - err := mapForm(structField.Addr().Interface(), form) + err := mapFormByTag(structField.Addr().Interface(), form, tag) if err != nil { return err } From 807368579f8939cedfa59ec689708754920f93e4 Mon Sep 17 00:00:00 2001 From: Dmitry Kutakov Date: Fri, 28 Dec 2018 05:26:29 +0300 Subject: [PATCH 050/111] fix test - auto choose port number (#1719) --- gin_integration_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gin_integration_test.go b/gin_integration_test.go index e14a688c..01d5cf5e 100644 --- a/gin_integration_test.go +++ b/gin_integration_test.go @@ -137,7 +137,7 @@ func TestBadUnixSocket(t *testing.T) { func TestFileDescriptor(t *testing.T) { router := New() - addr, err := net.ResolveTCPAddr("tcp", ":8000") + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") assert.NoError(t, err) listener, err := net.ListenTCP("tcp", addr) assert.NoError(t, err) @@ -152,7 +152,7 @@ func TestFileDescriptor(t *testing.T) { // otherwise the main thread will complete time.Sleep(5 * time.Millisecond) - c, err := net.Dial("tcp", "localhost:8000") + c, err := net.Dial("tcp", listener.Addr().String()) assert.NoError(t, err) fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n") From 85b92cdf4bc9bf33fd6f199ff866a1eed3511c80 Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Sat, 29 Dec 2018 11:46:26 +0800 Subject: [PATCH 051/111] chore(testing): case sensitive for query string (#1720) fix #1692 --- context_test.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/context_test.go b/context_test.go index dced73fd..836b3ecb 100644 --- a/context_test.go +++ b/context_test.go @@ -1457,7 +1457,7 @@ func TestContextShouldBindWithXML(t *testing.T) { c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString(` FOO - BAR + BAR `)) c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type @@ -1475,15 +1475,19 @@ func TestContextShouldBindWithQuery(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) - c.Request, _ = http.NewRequest("POST", "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused")) + c.Request, _ = http.NewRequest("POST", "/?foo=bar&bar=foo&Foo=bar1&Bar=foo1", bytes.NewBufferString("foo=unused")) var obj struct { - Foo string `form:"foo"` - Bar string `form:"bar"` + Foo string `form:"foo"` + Bar string `form:"bar"` + Foo1 string `form:"Foo"` + Bar1 string `form:"Bar"` } assert.NoError(t, c.ShouldBindQuery(&obj)) assert.Equal(t, "foo", obj.Bar) assert.Equal(t, "bar", obj.Foo) + assert.Equal(t, "foo1", obj.Bar1) + assert.Equal(t, "bar1", obj.Foo1) assert.Equal(t, 0, w.Body.Len()) } From d8fb18c33b1657271b6302e0899d033902012f49 Mon Sep 17 00:00:00 2001 From: John Bampton Date: Mon, 31 Dec 2018 11:02:53 +1000 Subject: [PATCH 052/111] Fix case of GitHub (#1726) --- routergroup.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routergroup.go b/routergroup.go index 2b41dfda..297d3574 100644 --- a/routergroup.go +++ b/routergroup.go @@ -47,7 +47,7 @@ type RouterGroup struct { var _ IRouter = &RouterGroup{} -// Use adds middleware to the group, see example code in github. +// Use adds middleware to the group, see example code in GitHub. func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes { group.Handlers = append(group.Handlers, middleware...) return group.returnObj() @@ -78,7 +78,7 @@ func (group *RouterGroup) handle(httpMethod, relativePath string, handlers Handl // Handle registers a new request handle and middleware with the given path and method. // The last handler should be the real handler, the other ones should be middleware that can and should be shared among different routes. -// See the example code in github. +// See the example code in GitHub. // // For GET, POST, PUT, PATCH and DELETE requests the respective shortcut // functions can be used. From 29a145c85dc0fafc3dd0ada62d856c4d95240010 Mon Sep 17 00:00:00 2001 From: thinkerou Date: Wed, 9 Jan 2019 09:32:44 +0800 Subject: [PATCH 053/111] Revert "context: inherits context cancelation and deadline from http.Request context for Go>=1.7 (#1690)" (#1736) This reverts commit f67d7a90c4d2e5bdf310a78d7e6a04e3d9aee851. --- context.go | 28 ++++++++++++++++++++++++++ context_17.go | 30 ---------------------------- context_17_test.go | 49 ---------------------------------------------- context_pre17.go | 39 ------------------------------------ 4 files changed, 28 insertions(+), 118 deletions(-) delete mode 100644 context_pre17.go diff --git a/context.go b/context.go index c38b2b87..c94926e1 100644 --- a/context.go +++ b/context.go @@ -942,6 +942,34 @@ func (c *Context) SetAccepted(formats ...string) { c.Accepted = formats } +/************************************/ +/***** 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. diff --git a/context_17.go b/context_17.go index 024dcb70..8e9f75ad 100644 --- a/context_17.go +++ b/context_17.go @@ -7,8 +7,6 @@ package gin import ( - "time" - "github.com/gin-gonic/gin/render" ) @@ -17,31 +15,3 @@ import ( func (c *Context) PureJSON(code int, obj interface{}) { c.Render(code, render.PureJSON{Data: obj}) } - -/************************************/ -/***** 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() (time.Time, bool) { - return c.Request.Context().Deadline() -} - -// 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 c.Request.Context().Done() -} - -// 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 c.Request.Context().Err() -} diff --git a/context_17_test.go b/context_17_test.go index f2a2f184..5b9ebcdc 100644 --- a/context_17_test.go +++ b/context_17_test.go @@ -7,12 +7,9 @@ package gin import ( - "bytes" - "context" "net/http" "net/http/httptest" "testing" - "time" "github.com/stretchr/testify/assert" ) @@ -28,49 +25,3 @@ func TestContextRenderPureJSON(t *testing.T) { assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\"}\n", w.Body.String()) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) } - -func TestContextHTTPContext(t *testing.T) { - c, _ := CreateTestContext(httptest.NewRecorder()) - req, _ := http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) - ctx, cancelFunc := context.WithCancel(context.Background()) - defer cancelFunc() - c.Request = req.WithContext(ctx) - - assert.NoError(t, c.Err()) - assert.NotNil(t, c.Done()) - select { - case <-c.Done(): - assert.Fail(t, "context should not be canceled") - default: - } - - ti, ok := c.Deadline() - assert.Equal(t, ti, time.Time{}) - assert.False(t, ok) - assert.Equal(t, c.Value(0), c.Request) - - cancelFunc() - assert.NotNil(t, c.Done()) - select { - case <-c.Done(): - default: - assert.Fail(t, "context should be canceled") - } -} - -func TestContextHTTPContextWithDeadline(t *testing.T) { - c, _ := CreateTestContext(httptest.NewRecorder()) - req, _ := http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) - location, _ := time.LoadLocation("Europe/Paris") - assert.NotNil(t, location) - date := time.Date(2031, 12, 27, 16, 00, 00, 00, location) - ctx, cancelFunc := context.WithDeadline(context.Background(), date) - defer cancelFunc() - c.Request = req.WithContext(ctx) - - assert.NoError(t, c.Err()) - - ti, ok := c.Deadline() - assert.Equal(t, ti, date) - assert.True(t, ok) -} diff --git a/context_pre17.go b/context_pre17.go deleted file mode 100644 index 2008d3c6..00000000 --- a/context_pre17.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2018 Gin Core Team. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -// +build !go1.7 - -package gin - -import ( - "time" -) - -/************************************/ -/***** 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 -} From b056a34bdc2aa45256c4f5bdad306c35ec70c37e Mon Sep 17 00:00:00 2001 From: Dmitry Kutakov Date: Fri, 18 Jan 2019 04:32:53 +0300 Subject: [PATCH 054/111] fix errcheck warnings (#1739) --- binding/binding_test.go | 24 +++++++++++----------- binding/form.go | 6 +++++- context.go | 22 +++++++++++++------- context_test.go | 36 +++++++++++++++++++-------------- debug_test.go | 19 ++++++++--------- errors_test.go | 6 +++--- gin.go | 5 ++++- gin_integration_test.go | 2 +- githubapi_test.go | 2 +- logger_test.go | 6 +++--- middleware_test.go | 2 +- recovery.go | 2 +- render/json.go | 45 +++++++++++++++++++++++++++-------------- render/protobuf.go | 4 ++-- render/render_test.go | 4 ++-- render/text.go | 10 ++++----- render/yaml.go | 4 ++-- response_writer_test.go | 3 ++- routes_test.go | 3 ++- 19 files changed, 122 insertions(+), 83 deletions(-) diff --git a/binding/binding_test.go b/binding/binding_test.go index 740749be..1044e6c2 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -516,28 +516,28 @@ func createFormPostRequestFail() *http.Request { return req } -func createFormMultipartRequest() *http.Request { +func createFormMultipartRequest(t *testing.T) *http.Request { boundary := "--testboundary" body := new(bytes.Buffer) mw := multipart.NewWriter(body) defer mw.Close() - mw.SetBoundary(boundary) - mw.WriteField("foo", "bar") - mw.WriteField("bar", "foo") + assert.NoError(t, mw.SetBoundary(boundary)) + assert.NoError(t, mw.WriteField("foo", "bar")) + assert.NoError(t, mw.WriteField("bar", "foo")) req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body) req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary) return req } -func createFormMultipartRequestFail() *http.Request { +func createFormMultipartRequestFail(t *testing.T) *http.Request { boundary := "--testboundary" body := new(bytes.Buffer) mw := multipart.NewWriter(body) defer mw.Close() - mw.SetBoundary(boundary) - mw.WriteField("map_foo", "bar") + assert.NoError(t, mw.SetBoundary(boundary)) + assert.NoError(t, mw.WriteField("map_foo", "bar")) req, _ := http.NewRequest("POST", "/?map_foo=getfoo", body) req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary) return req @@ -546,7 +546,7 @@ func createFormMultipartRequestFail() *http.Request { func TestBindingFormPost(t *testing.T) { req := createFormPostRequest() var obj FooBarStruct - FormPost.Bind(req, &obj) + assert.NoError(t, FormPost.Bind(req, &obj)) assert.Equal(t, "form-urlencoded", FormPost.Name()) assert.Equal(t, "bar", obj.Foo) @@ -556,7 +556,7 @@ func TestBindingFormPost(t *testing.T) { func TestBindingDefaultValueFormPost(t *testing.T) { req := createDefaultFormPostRequest() var obj FooDefaultBarStruct - FormPost.Bind(req, &obj) + assert.NoError(t, FormPost.Bind(req, &obj)) assert.Equal(t, "bar", obj.Foo) assert.Equal(t, "hello", obj.Bar) @@ -570,9 +570,9 @@ func TestBindingFormPostFail(t *testing.T) { } func TestBindingFormMultipart(t *testing.T) { - req := createFormMultipartRequest() + req := createFormMultipartRequest(t) var obj FooBarStruct - FormMultipart.Bind(req, &obj) + assert.NoError(t, FormMultipart.Bind(req, &obj)) assert.Equal(t, "multipart/form-data", FormMultipart.Name()) assert.Equal(t, "bar", obj.Foo) @@ -580,7 +580,7 @@ func TestBindingFormMultipart(t *testing.T) { } func TestBindingFormMultipartFail(t *testing.T) { - req := createFormMultipartRequestFail() + req := createFormMultipartRequestFail(t) var obj FooStructForMapType err := FormMultipart.Bind(req, &obj) assert.Error(t, err) diff --git a/binding/form.go b/binding/form.go index 0be59660..8955c95b 100644 --- a/binding/form.go +++ b/binding/form.go @@ -20,7 +20,11 @@ func (formBinding) Bind(req *http.Request, obj interface{}) error { if err := req.ParseForm(); err != nil { return err } - req.ParseMultipartForm(defaultMemory) + if err := req.ParseMultipartForm(defaultMemory); err != nil { + if err != http.ErrNotMultipart { + return err + } + } if err := mapForm(obj, req.Form); err != nil { return err } diff --git a/context.go b/context.go index c94926e1..4dc94ea9 100644 --- a/context.go +++ b/context.go @@ -415,7 +415,11 @@ 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.ParseMultipartForm(c.engine.MaxMultipartMemory) + if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil { + if err != http.ErrNotMultipart { + debugPrint("error on parse multipart form array: %v", err) + } + } if values := req.PostForm[key]; len(values) > 0 { return values, true } @@ -437,7 +441,11 @@ func (c *Context) PostFormMap(key string) map[string]string { // 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) + if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil { + if err != http.ErrNotMultipart { + debugPrint("error on parse multipart form map: %v", err) + } + } dicts, exist := c.get(req.PostForm, key) if !exist && req.MultipartForm != nil && req.MultipartForm.File != nil { @@ -493,8 +501,8 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error } defer out.Close() - io.Copy(out, src) - return nil + _, err = io.Copy(out, src) + return err } // Bind checks the Content-Type to select a binding engine automatically, @@ -534,7 +542,7 @@ func (c *Context) BindYAML(obj interface{}) error { // It will abort the request with HTTP 400 if any error occurs. func (c *Context) BindUri(obj interface{}) error { if err := c.ShouldBindUri(obj); err != nil { - c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) + c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // nolint: errcheck return err } return nil @@ -545,7 +553,7 @@ func (c *Context) BindUri(obj interface{}) error { // See the binding package. func (c *Context) MustBindWith(obj interface{}, b binding.Binding) error { if err := c.ShouldBindWith(obj, b); err != nil { - c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) + c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // nolint: errcheck return err } return nil @@ -913,7 +921,7 @@ func (c *Context) Negotiate(code int, config Negotiate) { c.XML(code, data) default: - c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) + c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) // nolint: errcheck } } diff --git a/context_test.go b/context_test.go index 836b3ecb..29ec1a24 100644 --- a/context_test.go +++ b/context_test.go @@ -70,7 +70,8 @@ func TestContextFormFile(t *testing.T) { mw := multipart.NewWriter(buf) w, err := mw.CreateFormFile("file", "test") if assert.NoError(t, err) { - w.Write([]byte("test")) + _, err = w.Write([]byte("test")) + assert.NoError(t, err) } mw.Close() c, _ := CreateTestContext(httptest.NewRecorder()) @@ -100,10 +101,11 @@ func TestContextFormFileFailed(t *testing.T) { func TestContextMultipartForm(t *testing.T) { buf := new(bytes.Buffer) mw := multipart.NewWriter(buf) - mw.WriteField("foo", "bar") + assert.NoError(t, mw.WriteField("foo", "bar")) w, err := mw.CreateFormFile("file", "test") if assert.NoError(t, err) { - w.Write([]byte("test")) + _, err = w.Write([]byte("test")) + assert.NoError(t, err) } mw.Close() c, _ := CreateTestContext(httptest.NewRecorder()) @@ -137,7 +139,8 @@ func TestSaveUploadedCreateFailed(t *testing.T) { mw := multipart.NewWriter(buf) w, err := mw.CreateFormFile("file", "test") if assert.NoError(t, err) { - w.Write([]byte("test")) + _, err = w.Write([]byte("test")) + assert.NoError(t, err) } mw.Close() c, _ := CreateTestContext(httptest.NewRecorder()) @@ -159,7 +162,7 @@ func TestContextReset(t *testing.T) { c.index = 2 c.Writer = &responseWriter{ResponseWriter: httptest.NewRecorder()} c.Params = Params{Param{}} - c.Error(errors.New("test")) + c.Error(errors.New("test")) // nolint: errcheck c.Set("foo", "bar") c.reset() @@ -798,7 +801,7 @@ func TestContextRenderHTML2(t *testing.T) { assert.Len(t, router.trees, 1) templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) - re := captureOutput(func() { + re := captureOutput(t, func() { SetMode(DebugMode) router.SetHTMLTemplate(templ) SetMode(TestMode) @@ -1211,7 +1214,8 @@ func TestContextAbortWithStatusJSON(t *testing.T) { assert.Equal(t, "application/json; charset=utf-8", contentType) buf := new(bytes.Buffer) - buf.ReadFrom(w.Body) + _, err := buf.ReadFrom(w.Body) + assert.NoError(t, err) jsonStringBody := buf.String() assert.Equal(t, fmt.Sprint(`{"foo":"fooValue","bar":"barValue"}`), jsonStringBody) } @@ -1220,11 +1224,11 @@ func TestContextError(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) assert.Empty(t, c.Errors) - c.Error(errors.New("first error")) + c.Error(errors.New("first error")) // nolint: errcheck assert.Len(t, c.Errors, 1) assert.Equal(t, "Error #01: first error\n", c.Errors.String()) - c.Error(&Error{ + c.Error(&Error{ // nolint: errcheck Err: errors.New("second error"), Meta: "some data 2", Type: ErrorTypePublic, @@ -1246,13 +1250,13 @@ func TestContextError(t *testing.T) { t.Error("didn't panic") } }() - c.Error(nil) + c.Error(nil) // nolint: errcheck } func TestContextTypedError(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) - c.Error(errors.New("externo 0")).SetType(ErrorTypePublic) - c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate) + c.Error(errors.New("externo 0")).SetType(ErrorTypePublic) // nolint: errcheck + c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate) // nolint: errcheck for _, err := range c.Errors.ByType(ErrorTypePublic) { assert.Equal(t, ErrorTypePublic, err.Type) @@ -1267,7 +1271,7 @@ func TestContextAbortWithError(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) - c.AbortWithError(http.StatusUnauthorized, errors.New("bad input")).SetMeta("some input") + c.AbortWithError(http.StatusUnauthorized, errors.New("bad input")).SetMeta("some input") // nolint: errcheck assert.Equal(t, http.StatusUnauthorized, w.Code) assert.Equal(t, abortIndex, c.index) @@ -1713,7 +1717,8 @@ func TestContextStream(t *testing.T) { stopStream = false }() - w.Write([]byte("test")) + _, err := w.Write([]byte("test")) + assert.NoError(t, err) return stopStream }) @@ -1730,7 +1735,8 @@ func TestContextStreamWithClientGone(t *testing.T) { w.closeClient() }() - writer.Write([]byte("test")) + _, err := writer.Write([]byte("test")) + assert.NoError(t, err) return true }) diff --git a/debug_test.go b/debug_test.go index 97ff166b..b3485d70 100644 --- a/debug_test.go +++ b/debug_test.go @@ -32,7 +32,7 @@ func TestIsDebugging(t *testing.T) { } func TestDebugPrint(t *testing.T) { - re := captureOutput(func() { + re := captureOutput(t, func() { SetMode(DebugMode) SetMode(ReleaseMode) debugPrint("DEBUG this!") @@ -46,7 +46,7 @@ func TestDebugPrint(t *testing.T) { } func TestDebugPrintError(t *testing.T) { - re := captureOutput(func() { + re := captureOutput(t, func() { SetMode(DebugMode) debugPrintError(nil) debugPrintError(errors.New("this is an error")) @@ -56,7 +56,7 @@ func TestDebugPrintError(t *testing.T) { } func TestDebugPrintRoutes(t *testing.T) { - re := captureOutput(func() { + re := captureOutput(t, func() { SetMode(DebugMode) debugPrintRoute("GET", "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest}) SetMode(TestMode) @@ -65,7 +65,7 @@ func TestDebugPrintRoutes(t *testing.T) { } func TestDebugPrintLoadTemplate(t *testing.T) { - re := captureOutput(func() { + re := captureOutput(t, func() { SetMode(DebugMode) templ := template.Must(template.New("").Delims("{[{", "}]}").ParseGlob("./testdata/template/hello.tmpl")) debugPrintLoadTemplate(templ) @@ -75,7 +75,7 @@ func TestDebugPrintLoadTemplate(t *testing.T) { } func TestDebugPrintWARNINGSetHTMLTemplate(t *testing.T) { - re := captureOutput(func() { + re := captureOutput(t, func() { SetMode(DebugMode) debugPrintWARNINGSetHTMLTemplate() SetMode(TestMode) @@ -84,7 +84,7 @@ func TestDebugPrintWARNINGSetHTMLTemplate(t *testing.T) { } func TestDebugPrintWARNINGDefault(t *testing.T) { - re := captureOutput(func() { + re := captureOutput(t, func() { SetMode(DebugMode) debugPrintWARNINGDefault() SetMode(TestMode) @@ -98,7 +98,7 @@ func TestDebugPrintWARNINGDefault(t *testing.T) { } func TestDebugPrintWARNINGNew(t *testing.T) { - re := captureOutput(func() { + re := captureOutput(t, func() { SetMode(DebugMode) debugPrintWARNINGNew() SetMode(TestMode) @@ -106,7 +106,7 @@ func TestDebugPrintWARNINGNew(t *testing.T) { 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 captureOutput(f func()) string { +func captureOutput(t *testing.T, f func()) string { reader, writer, err := os.Pipe() if err != nil { panic(err) @@ -127,7 +127,8 @@ func captureOutput(f func()) string { go func() { var buf bytes.Buffer wg.Done() - io.Copy(&buf, reader) + _, err := io.Copy(&buf, reader) + assert.NoError(t, err) out <- buf.String() }() wg.Wait() diff --git a/errors_test.go b/errors_test.go index 9351b578..6aae1c10 100644 --- a/errors_test.go +++ b/errors_test.go @@ -34,7 +34,7 @@ func TestError(t *testing.T) { jsonBytes, _ := json.Marshal(err) assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes)) - err.SetMeta(H{ + err.SetMeta(H{ // nolint: errcheck "status": "200", "data": "some data", }) @@ -44,7 +44,7 @@ func TestError(t *testing.T) { "data": "some data", }, err.JSON()) - err.SetMeta(H{ + err.SetMeta(H{ // nolint: errcheck "error": "custom error", "status": "200", "data": "some data", @@ -59,7 +59,7 @@ func TestError(t *testing.T) { status string data string } - err.SetMeta(customError{status: "200", data: "other data"}) + err.SetMeta(customError{status: "200", data: "other data"}) // nolint: errcheck assert.Equal(t, customError{status: "200", data: "other data"}, err.JSON()) } diff --git a/gin.go b/gin.go index b7c77e1f..cd47200f 100644 --- a/gin.go +++ b/gin.go @@ -422,7 +422,10 @@ func serveError(c *Context, code int, defaultMessage []byte) { } if c.writermem.Status() == code { c.writermem.Header()["Content-Type"] = mimePlain - c.Writer.Write(defaultMessage) + _, err := c.Writer.Write(defaultMessage) + if err != nil { + debugPrint("cannot write message to writer during serve error: %v", err) + } return } c.writermem.WriteHeaderNow() diff --git a/gin_integration_test.go b/gin_integration_test.go index 01d5cf5e..3ce6d80a 100644 --- a/gin_integration_test.go +++ b/gin_integration_test.go @@ -87,7 +87,7 @@ func TestRunEmptyWithEnv(t *testing.T) { func TestRunTooMuchParams(t *testing.T) { router := New() assert.Panics(t, func() { - router.Run("2", "2") + assert.NoError(t, router.Run("2", "2")) }) } diff --git a/githubapi_test.go b/githubapi_test.go index 5253425a..50faca09 100644 --- a/githubapi_test.go +++ b/githubapi_test.go @@ -338,7 +338,7 @@ func TestBindUriError(t *testing.T) { } router.Handle("GET", "/new/rest/:num", func(c *Context) { var m Member - c.BindUri(&m) + assert.Error(t, c.BindUri(&m)) }) path1, _ := exampleFromPath("/new/rest/:num") diff --git a/logger_test.go b/logger_test.go index 350599d4..d0169251 100644 --- a/logger_test.go +++ b/logger_test.go @@ -278,13 +278,13 @@ func TestErrorLogger(t *testing.T) { router := New() router.Use(ErrorLogger()) router.GET("/error", func(c *Context) { - c.Error(errors.New("this is an error")) + c.Error(errors.New("this is an error")) // nolint: errcheck }) router.GET("/abort", func(c *Context) { - c.AbortWithError(http.StatusUnauthorized, errors.New("no authorized")) + c.AbortWithError(http.StatusUnauthorized, errors.New("no authorized")) // nolint: errcheck }) router.GET("/print", func(c *Context) { - c.Error(errors.New("this is an error")) + c.Error(errors.New("this is an error")) // nolint: errcheck c.String(http.StatusInternalServerError, "hola!") }) diff --git a/middleware_test.go b/middleware_test.go index 983ad933..fca1c530 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -208,7 +208,7 @@ func TestMiddlewareFailHandlersChain(t *testing.T) { router := New() router.Use(func(context *Context) { signature += "A" - context.AbortWithError(http.StatusInternalServerError, errors.New("foo")) + context.AbortWithError(http.StatusInternalServerError, errors.New("foo")) // nolint: errcheck }) router.Use(func(context *Context) { signature += "B" diff --git a/recovery.go b/recovery.go index f06ad56b..0e35968f 100644 --- a/recovery.go +++ b/recovery.go @@ -66,7 +66,7 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc { // If the connection is dead, we can't write a status to it. if brokenPipe { - c.Error(err.(error)) + c.Error(err.(error)) // nolint: errcheck c.Abort() } else { c.AbortWithStatus(http.StatusInternalServerError) diff --git a/render/json.go b/render/json.go index 32d0fc42..c7cf330e 100644 --- a/render/json.go +++ b/render/json.go @@ -67,8 +67,8 @@ func WriteJSON(w http.ResponseWriter, obj interface{}) error { if err != nil { return err } - w.Write(jsonBytes) - return nil + _, err = w.Write(jsonBytes) + return err } // Render (IndentedJSON) marshals the given interface object and writes it with custom ContentType. @@ -78,8 +78,8 @@ func (r IndentedJSON) Render(w http.ResponseWriter) error { if err != nil { return err } - w.Write(jsonBytes) - return nil + _, err = w.Write(jsonBytes) + return err } // WriteContentType (IndentedJSON) writes JSON ContentType. @@ -96,10 +96,13 @@ func (r SecureJSON) Render(w http.ResponseWriter) error { } // if the jsonBytes is array values if bytes.HasPrefix(jsonBytes, []byte("[")) && bytes.HasSuffix(jsonBytes, []byte("]")) { - w.Write([]byte(r.Prefix)) + _, err = w.Write([]byte(r.Prefix)) + if err != nil { + return err + } } - w.Write(jsonBytes) - return nil + _, err = w.Write(jsonBytes) + return err } // WriteContentType (SecureJSON) writes JSON ContentType. @@ -116,15 +119,27 @@ func (r JsonpJSON) Render(w http.ResponseWriter) (err error) { } if r.Callback == "" { - w.Write(ret) - return nil + _, err = w.Write(ret) + return err } callback := template.JSEscapeString(r.Callback) - w.Write([]byte(callback)) - w.Write([]byte("(")) - w.Write(ret) - w.Write([]byte(")")) + _, err = w.Write([]byte(callback)) + if err != nil { + return err + } + _, err = w.Write([]byte("(")) + if err != nil { + return err + } + _, err = w.Write(ret) + if err != nil { + return err + } + _, err = w.Write([]byte(")")) + if err != nil { + return err + } return nil } @@ -151,8 +166,8 @@ func (r AsciiJSON) Render(w http.ResponseWriter) (err error) { buffer.WriteString(cvt) } - w.Write(buffer.Bytes()) - return nil + _, err = w.Write(buffer.Bytes()) + return err } // WriteContentType (AsciiJSON) writes JSON ContentType. diff --git a/render/protobuf.go b/render/protobuf.go index 47895072..15aca995 100644 --- a/render/protobuf.go +++ b/render/protobuf.go @@ -26,8 +26,8 @@ func (r ProtoBuf) Render(w http.ResponseWriter) error { return err } - w.Write(bytes) - return nil + _, err = w.Write(bytes) + return err } // WriteContentType (ProtoBuf) writes ProtoBuf ContentType. diff --git a/render/render_test.go b/render/render_test.go index 4c9b180d..3df04a17 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -71,7 +71,7 @@ func TestRenderJSONPanics(t *testing.T) { data := make(chan int) // json: unsupported type: chan int - assert.Panics(t, func() { (JSON{data}).Render(w) }) + assert.Panics(t, func() { assert.NoError(t, (JSON{data}).Render(w)) }) } func TestRenderIndentedJSON(t *testing.T) { @@ -335,7 +335,7 @@ func TestRenderRedirect(t *testing.T) { } w = httptest.NewRecorder() - assert.Panics(t, func() { data2.Render(w) }) + assert.Panics(t, func() { assert.NoError(t, data2.Render(w)) }) // only improve coverage data2.WriteContentType(w) diff --git a/render/text.go b/render/text.go index 2ea7343c..4e52d4c5 100644 --- a/render/text.go +++ b/render/text.go @@ -20,8 +20,7 @@ var plainContentType = []string{"text/plain; charset=utf-8"} // Render (String) writes data with custom ContentType. func (r String) Render(w http.ResponseWriter) error { - WriteString(w, r.Format, r.Data) - return nil + return WriteString(w, r.Format, r.Data) } // WriteContentType (String) writes Plain ContentType. @@ -30,11 +29,12 @@ func (r String) WriteContentType(w http.ResponseWriter) { } // WriteString writes data according to its format and write custom ContentType. -func WriteString(w http.ResponseWriter, format string, data []interface{}) { +func WriteString(w http.ResponseWriter, format string, data []interface{}) (err error) { writeContentType(w, plainContentType) if len(data) > 0 { - fmt.Fprintf(w, format, data...) + _, err = fmt.Fprintf(w, format, data...) return } - io.WriteString(w, format) + _, err = io.WriteString(w, format) + return } diff --git a/render/yaml.go b/render/yaml.go index 33bc3254..0df78360 100644 --- a/render/yaml.go +++ b/render/yaml.go @@ -26,8 +26,8 @@ func (r YAML) Render(w http.ResponseWriter) error { return err } - w.Write(bytes) - return nil + _, err = w.Write(bytes) + return err } // WriteContentType (YAML) writes YAML ContentType for response. diff --git a/response_writer_test.go b/response_writer_test.go index cc5a89dc..a5e111e5 100644 --- a/response_writer_test.go +++ b/response_writer_test.go @@ -103,7 +103,8 @@ func TestResponseWriterHijack(t *testing.T) { w := ResponseWriter(writer) assert.Panics(t, func() { - w.Hijack() + _, _, err := w.Hijack() + assert.NoError(t, err) }) assert.True(t, w.Written()) diff --git a/routes_test.go b/routes_test.go index c4d59725..af4caf7f 100644 --- a/routes_test.go +++ b/routes_test.go @@ -251,7 +251,8 @@ func TestRouteStaticFile(t *testing.T) { t.Error(err) } defer os.Remove(f.Name()) - f.WriteString("Gin Web Framework") + _, err = f.WriteString("Gin Web Framework") + assert.NoError(t, err) f.Close() dir, filename := filepath.Split(f.Name()) From 4867ff9634d1889156587d5add70d2b29c447542 Mon Sep 17 00:00:00 2001 From: Dmitry Kutakov Date: Fri, 18 Jan 2019 04:57:06 +0300 Subject: [PATCH 055/111] fix Context.Next() - recheck len of handlers every iteration (#1745) * fix Context.Next() - recheck len of handlers every iteration * add tests when Context.reset() can be called inside of handler TestEngineHandleContext TestContextResetInHandler TestRouterStaticFSFileNotFound * Context.Next() - format to while style --- context.go | 3 ++- context_test.go | 12 ++++++++++++ gin_test.go | 17 +++++++++++++++++ routes_test.go | 10 ++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/context.go b/context.go index 4dc94ea9..26badfc3 100644 --- a/context.go +++ b/context.go @@ -105,8 +105,9 @@ func (c *Context) Handler() HandlerFunc { // See example in GitHub. func (c *Context) Next() { c.index++ - for s := int8(len(c.handlers)); c.index < s; c.index++ { + for c.index < int8(len(c.handlers)) { c.handlers[c.index](c) + c.index++ } } diff --git a/context_test.go b/context_test.go index 29ec1a24..ea936b85 100644 --- a/context_test.go +++ b/context_test.go @@ -1743,3 +1743,15 @@ func TestContextStreamWithClientGone(t *testing.T) { assert.Equal(t, "test", w.Body.String()) } + +func TestContextResetInHandler(t *testing.T) { + w := CreateTestResponseRecorder() + c, _ := CreateTestContext(w) + + c.handlers = []HandlerFunc{ + func(c *Context) { c.reset() }, + } + assert.NotPanics(t, func() { + c.Next() + }) +} diff --git a/gin_test.go b/gin_test.go index 353c9be1..e013df09 100644 --- a/gin_test.go +++ b/gin_test.go @@ -471,6 +471,23 @@ func TestListOfRoutes(t *testing.T) { }) } +func TestEngineHandleContext(t *testing.T) { + r := New() + r.GET("/", func(c *Context) { + c.Request.URL.Path = "/v2" + r.HandleContext(c) + }) + v2 := r.Group("/v2") + { + v2.GET("/", func(c *Context) {}) + } + + assert.NotPanics(t, func() { + w := performRequest(r, "GET", "/") + assert.Equal(t, 301, w.Code) + }) +} + func assertRoutePresent(t *testing.T, gotRoutes RoutesInfo, wantRoute RouteInfo) { for _, gotRoute := range gotRoutes { if gotRoute.Path == wantRoute.Path && gotRoute.Method == wantRoute.Method { diff --git a/routes_test.go b/routes_test.go index af4caf7f..8d50292d 100644 --- a/routes_test.go +++ b/routes_test.go @@ -427,6 +427,16 @@ func TestRouterStaticFSNotFound(t *testing.T) { assert.Equal(t, "non existent", w.Body.String()) } +func TestRouterStaticFSFileNotFound(t *testing.T) { + router := New() + + router.StaticFS("/", http.FileSystem(http.Dir("."))) + + assert.NotPanics(t, func() { + performRequest(router, "GET", "/nonexistent") + }) +} + func TestRouteRawPath(t *testing.T) { route := New() route.UseRawPath = true From b4f51559825a055dcec93fa282c20018d073aaaa Mon Sep 17 00:00:00 2001 From: Sai Date: Sun, 20 Jan 2019 09:39:09 +0900 Subject: [PATCH 056/111] Fix not to pass formatted string to Fprintf's format specifier parameter (#1747) --- logger.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logger.go b/logger.go index a55d26e0..d2869f51 100644 --- a/logger.go +++ b/logger.go @@ -191,7 +191,7 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc { param.Path = path - fmt.Fprintf(out, formatter(param)) + fmt.Fprint(out, formatter(param)) } } } From f38a3fe65f102dd765e097166e6a41f9e99777f6 Mon Sep 17 00:00:00 2001 From: Ryan <46182144+ryanker@users.noreply.github.com> Date: Sun, 20 Jan 2019 18:27:04 +0800 Subject: [PATCH 057/111] fix password error (#1728) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2dc9e5ff..0dcaa8e9 100644 --- a/README.md +++ b/README.md @@ -620,7 +620,7 @@ func main() { // // // user - // 123 + // 123 // ) router.POST("/loginXML", func(c *gin.Context) { var xml Login From d27685e714cb0b9375e0c9d3ca3df5a6a4945a6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Sat, 26 Jan 2019 02:28:39 +0800 Subject: [PATCH 058/111] chore: attempt to fix some gomod issue (#1751) #1604 #1566 #1700 #1737 because some dependencies only are used on example i.e. grpc. Or migrate `examples` to gin-gonic/examples`? --- go.mod | 39 +++++++++++++++--------------- go.sum | 64 ++++++++++++++++++-------------------------------- tools/tools.go | 25 -------------------- 3 files changed, 43 insertions(+), 85 deletions(-) delete mode 100644 tools/tools.go diff --git a/go.mod b/go.mod index ef4103fd..54573a8b 100644 --- a/go.mod +++ b/go.mod @@ -1,30 +1,31 @@ module github.com/gin-gonic/gin require ( - github.com/campoy/embedmd v0.0.0-20171205015432-c59ce00e0296 - github.com/client9/misspell v0.3.4 - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dustin/go-broadcast v0.0.0-20171205050544-f664265f5a66 - github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 - github.com/gin-gonic/autotls v0.0.0-20180426091246-be87bd5ef97b + github.com/gin-contrib/sse v0.0.0-20190124093953-61b50c2ef482 github.com/golang/protobuf v1.2.0 - github.com/jessevdk/go-assets v0.0.0-20160921144138-4f4301a06e15 github.com/json-iterator/go v1.1.5 - github.com/manucorporat/stats v0.0.0-20180402194714-3ba42d56d227 github.com/mattn/go-isatty v0.0.4 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/testify v1.2.2 - github.com/thinkerou/favicon v0.1.0 - github.com/ugorji/go v1.1.1 - golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e - golang.org/x/lint v0.0.0-20181011164241-5906bd5c48cd - golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1 - golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f - golang.org/x/sys v0.0.0-20181011152604-fa43e7bc11ba // indirect - google.golang.org/grpc v1.15.0 + github.com/stretchr/testify v1.3.0 + github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 + golang.org/x/net v0.0.0-20190119204137-ed066c81e75e + golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 + golang.org/x/sys v0.0.0-20190124100055-b90733256f2e // indirect gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/go-playground/validator.v8 v8.18.2 - gopkg.in/yaml.v2 v2.2.1 + gopkg.in/yaml.v2 v2.2.2 +) + +exclude ( + github.com/campoy/embedmd v0.0.0-20181127031020-97c13d6e4160 + github.com/client9/misspell v0.3.4 + github.com/dustin/go-broadcast v0.0.0-20171205050544-f664265f5a66 + github.com/gin-gonic/autotls v0.0.0-20190119125636-0b5f4fc15768 + github.com/jessevdk/go-assets v0.0.0-20160921144138-4f4301a06e15 + github.com/manucorporat/stats v0.0.0-20180402194714-3ba42d56d227 + github.com/thinkerou/favicon v0.1.0 + golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b + golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1 + google.golang.org/grpc v1.18.0 ) diff --git a/go.sum b/go.sum index 2ef7f13b..95e2b4f6 100644 --- a/go.sum +++ b/go.sum @@ -1,72 +1,54 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/campoy/embedmd v0.0.0-20171205015432-c59ce00e0296 h1:tRsilif6pbtt+PX6uRoyGd+qR+4ZPucFZLHlc3Ak6z8= -github.com/campoy/embedmd v0.0.0-20171205015432-c59ce00e0296/go.mod h1:/dBk8ICkslPCmyRdn4azP+QvBxL6Eg3EYxUGI9xMMFw= +github.com/campoy/embedmd v0.0.0-20181127031020-97c13d6e4160 h1:HJpuhXOHC4EkXDARsLjmXAV9FhlY6qFDnKI/MJM6eoE= +github.com/campoy/embedmd v0.0.0-20181127031020-97c13d6e4160/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dustin/go-broadcast v0.0.0-20171205050544-f664265f5a66 h1:QnnoVdChKs+GeTvN4rPYTW6b5U6M3HMEvQ/+x4IGtfY= -github.com/dustin/go-broadcast v0.0.0-20171205050544-f664265f5a66/go.mod h1:kTEh6M2J/mh7nsskr28alwLCXm/DSG5OSA/o31yy2XU= -github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY= -github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= -github.com/gin-gonic/autotls v0.0.0-20180426091246-be87bd5ef97b h1:dm/NYytoj7p8Jc6zMvyRz3PCQrTTCXnVRvEzyBcM890= -github.com/gin-gonic/autotls v0.0.0-20180426091246-be87bd5ef97b/go.mod h1:vwfeXwKgEIWq63oVfwaBjoByS4dZzYbHHROHjV4IjNY= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gin-contrib/sse v0.0.0-20190124093953-61b50c2ef482 h1:iOz5sIQUvuOlpiC7Q6+MmJQpWnlneYX98QIGf+2m50Y= +github.com/gin-contrib/sse v0.0.0-20190124093953-61b50c2ef482/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/jessevdk/go-assets v0.0.0-20160921144138-4f4301a06e15 h1:cW/amwGEJK5MSKntPXRjX4dxs/nGxGT8gXKIsKFmHGc= -github.com/jessevdk/go-assets v0.0.0-20160921144138-4f4301a06e15/go.mod h1:Fdm/oWRW+CH8PRbLntksCNtmcCBximKPkVQYvmMl80k= -github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE= github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/manucorporat/stats v0.0.0-20180402194714-3ba42d56d227 h1:KIaAZ/V+/0/6BOULrmBQ9T1ed8BkKqGIjIKW923nJuo= -github.com/manucorporat/stats v0.0.0-20180402194714-3ba42d56d227/go.mod h1:ruMr5t05gVho4tuDv0PbI0Bb8nOxc/5Y6JzRHe/yfA0= github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/thinkerou/favicon v0.1.0 h1:eWMISKTpHq2G8HOuKn7ydD55j5DDehx94b0C2y8ABMs= -github.com/thinkerou/favicon v0.1.0/go.mod h1:HL7Pap5kOluZv1ku34pZo/AJ44GaxMEPFZ3pmuexV2s= -github.com/ugorji/go v1.1.1 h1:gmervu+jDMvXTbcHQ0pd2wee85nEoE0BsVyEuzkfK8w= -github.com/ugorji/go v1.1.1/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= -golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e h1:IzypfodbhbnViNUO/MEh0FzCUooG97cIGfdggUrUSyU= -golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20181011164241-5906bd5c48cd h1:cgsAvzdqkDKdI02tIvDjO225vDPHMDCgfKqx5KEVI7U= -golang.org/x/lint v0.0.0-20181011164241-5906bd5c48cd/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 h1:EICbibRW4JNKMcY+LsWmuwob+CRS1BmdRdjphAm9mH4= +github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3 h1:x/bBzNauLQAlE3fLku/xy92Y8QwKX5HZymrMz2IiKFc= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1 h1:Y/KGZSOdz/2r0WJ9Mkmz6NJBusp0kiNx1Cn82lzJQ6w= -golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190119204137-ed066c81e75e h1:MDa3fSUp6MdYHouVmCCNz/zaH2a6CRcxY3VhT/K3C5Q= +golang.org/x/net v0.0.0-20190119204137-ed066c81e75e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181011152604-fa43e7bc11ba h1:nZJIJPGow0Kf9bU9QTc1U6OXbs/7Hu4e+cNv+hxH+Zc= -golang.org/x/sys v0.0.0-20181011152604-fa43e7bc11ba/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190124100055-b90733256f2e h1:3GIlrlVLfkoipSReOMNAgApI0ajnalyLa/EZHHca/XI= +golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52 h1:JG/0uqcGdTNgq7FdU+61l5Pdmb8putNZlXb65bJBROs= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/grpc v1.15.0 h1:Az/KuahOM4NAidTEuJCv/RonAA7rYsTPkqXVjr+8OOw= -google.golang.org/grpc v1.15.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.18.0 h1:IZl7mfBGfbhYx2p2rKRtYgDFw6SBz+kclmxYrCksPPA= +google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= -gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/tools/tools.go b/tools/tools.go deleted file mode 100644 index 7113e71e..00000000 --- a/tools/tools.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2018 Gin Core Team. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -// +build tools - -// This package exists to cause `go mod` and `go get` to believe these tools -// are dependencies, even though they are not runtime dependencies of any -// gin package. This means they will appear in `go.mod` file, but will not -// be a part of the build. - -package tools - -import ( - _ "github.com/campoy/embedmd" - _ "github.com/client9/misspell/cmd/misspell" - _ "github.com/dustin/go-broadcast" - _ "github.com/gin-gonic/autotls" - _ "github.com/jessevdk/go-assets" - _ "github.com/manucorporat/stats" - _ "github.com/thinkerou/favicon" - _ "golang.org/x/crypto/acme/autocert" - _ "golang.org/x/lint/golint" - _ "google.golang.org/grpc" -) From 5acf6601170bb49a1958c055d66d54ba152dc34b Mon Sep 17 00:00:00 2001 From: Dmitry Kutakov Date: Mon, 4 Feb 2019 04:27:00 +0300 Subject: [PATCH 059/111] fix travis freeze on concurrent test (#1761) --- gin_integration_test.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/gin_integration_test.go b/gin_integration_test.go index 3ce6d80a..b80cbb24 100644 --- a/gin_integration_test.go +++ b/gin_integration_test.go @@ -188,15 +188,12 @@ func TestConcurrentHandleContext(t *testing.T) { }) router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) - ts := httptest.NewServer(router) - defer ts.Close() - var wg sync.WaitGroup iterations := 200 wg.Add(iterations) for i := 0; i < iterations; i++ { go func() { - testRequest(t, ts.URL+"/") + testGetRequestHandler(t, router, "/") wg.Done() }() } @@ -217,3 +214,14 @@ func TestConcurrentHandleContext(t *testing.T) { // testRequest(t, "http://localhost:8033/example") // } + +func testGetRequestHandler(t *testing.T, h http.Handler, url string) { + req, err := http.NewRequest("GET", url, nil) + assert.NoError(t, err) + + w := httptest.NewRecorder() + h.ServeHTTP(w, req) + + assert.Equal(t, "it worked", w.Body.String(), "resp body should match") + assert.Equal(t, 200, w.Code, "should get a 200") +} From a768f064d53c8010d902533927441be13b1bfe17 Mon Sep 17 00:00:00 2001 From: Dmitry Kutakov Date: Mon, 18 Feb 2019 04:35:08 +0300 Subject: [PATCH 060/111] fix many redirects (#1760) (#1764) * fix many redirects (#1760) * fix @thinkerou review --- gin.go | 3 +++ gin_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/gin.go b/gin.go index cd47200f..6e5ea6d7 100644 --- a/gin.go +++ b/gin.go @@ -355,8 +355,11 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { // This can be done by setting c.Request.URL.Path to your new target. // Disclaimer: You can loop yourself to death with this, use wisely. func (engine *Engine) HandleContext(c *Context) { + oldIndexValue := c.index c.reset() engine.handleHTTPRequest(c) + + c.index = oldIndexValue } func (engine *Engine) handleHTTPRequest(c *Context) { diff --git a/gin_test.go b/gin_test.go index e013df09..f9a1c6f5 100644 --- a/gin_test.go +++ b/gin_test.go @@ -12,6 +12,8 @@ import ( "net/http" "net/http/httptest" "reflect" + "strconv" + "sync/atomic" "testing" "time" @@ -488,6 +490,43 @@ func TestEngineHandleContext(t *testing.T) { }) } +func TestEngineHandleContextManyReEntries(t *testing.T) { + expectValue := 10000 + + var handlerCounter, middlewareCounter int64 + + r := New() + r.Use(func(c *Context) { + atomic.AddInt64(&middlewareCounter, 1) + }) + r.GET("/:count", func(c *Context) { + countStr := c.Param("count") + count, err := strconv.Atoi(countStr) + assert.NoError(t, err) + + n, err := c.Writer.Write([]byte(".")) + assert.NoError(t, err) + assert.Equal(t, 1, n) + + switch { + case count > 0: + c.Request.URL.Path = "/" + strconv.Itoa(count-1) + r.HandleContext(c) + } + }, func(c *Context) { + atomic.AddInt64(&handlerCounter, 1) + }) + + assert.NotPanics(t, func() { + w := performRequest(r, "GET", "/"+strconv.Itoa(expectValue-1)) // include 0 value + assert.Equal(t, 200, w.Code) + assert.Equal(t, expectValue, w.Body.Len()) + }) + + assert.Equal(t, int64(expectValue), handlerCounter) + assert.Equal(t, int64(expectValue), middlewareCounter) +} + func assertRoutePresent(t *testing.T, gotRoutes RoutesInfo, wantRoute RouteInfo) { for _, gotRoute := range gotRoutes { if gotRoute.Path == wantRoute.Path && gotRoute.Method == wantRoute.Method { From 31bbb10f34e4673815ab66099571bac95cf4113d Mon Sep 17 00:00:00 2001 From: Dmitry Kutakov Date: Mon, 18 Feb 2019 05:10:45 +0300 Subject: [PATCH 061/111] Make silent debug info on tests (#1765) * make silent log on tests * fix coverage: check end-of-line at the end of debug msg --- debug_test.go | 2 +- deprecated_test.go | 4 +++- gin_test.go | 29 +++++++++++++++++------------ githubapi_test.go | 10 +++++----- recovery_test.go | 1 + 5 files changed, 27 insertions(+), 19 deletions(-) diff --git a/debug_test.go b/debug_test.go index b3485d70..d338f0a0 100644 --- a/debug_test.go +++ b/debug_test.go @@ -39,7 +39,7 @@ func TestDebugPrint(t *testing.T) { SetMode(TestMode) debugPrint("DEBUG this!") SetMode(DebugMode) - debugPrint("these are %d %s\n", 2, "error messages") + debugPrint("these are %d %s", 2, "error messages") SetMode(TestMode) }) assert.Equal(t, "[GIN-debug] these are 2 error messages\n", re) diff --git a/deprecated_test.go b/deprecated_test.go index 7a875fe4..f8df651c 100644 --- a/deprecated_test.go +++ b/deprecated_test.go @@ -24,7 +24,9 @@ func TestBindWith(t *testing.T) { Foo string `form:"foo"` Bar string `form:"bar"` } - assert.NoError(t, c.BindWith(&obj, binding.Form)) + captureOutput(t, func() { + assert.NoError(t, c.BindWith(&obj, binding.Form)) + }) assert.Equal(t, "foo", obj.Bar) assert.Equal(t, "bar", obj.Foo) assert.Equal(t, 0, w.Body.Len()) diff --git a/gin_test.go b/gin_test.go index f9a1c6f5..11bdd79c 100644 --- a/gin_test.go +++ b/gin_test.go @@ -27,18 +27,23 @@ func formatAsDate(t time.Time) string { func setupHTMLFiles(t *testing.T, mode string, tls bool, loadMethod func(*Engine)) *httptest.Server { SetMode(mode) - router := New() - router.Delims("{[{", "}]}") - router.SetFuncMap(template.FuncMap{ - "formatAsDate": formatAsDate, - }) - loadMethod(router) - router.GET("/test", func(c *Context) { - c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"}) - }) - router.GET("/raw", func(c *Context) { - c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{ - "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), + defer SetMode(TestMode) + + var router *Engine + captureOutput(t, func() { + router = New() + router.Delims("{[{", "}]}") + router.SetFuncMap(template.FuncMap{ + "formatAsDate": formatAsDate, + }) + loadMethod(router) + router.GET("/test", func(c *Context) { + c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"}) + }) + router.GET("/raw", func(c *Context) { + c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{ + "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), + }) }) }) diff --git a/githubapi_test.go b/githubapi_test.go index 50faca09..29aa1584 100644 --- a/githubapi_test.go +++ b/githubapi_test.go @@ -287,7 +287,7 @@ var githubAPI = []route{ func TestShouldBindUri(t *testing.T) { DefaultWriter = os.Stdout - router := Default() + router := New() type Person struct { Name string `uri:"name" binding:"required"` @@ -309,7 +309,7 @@ func TestShouldBindUri(t *testing.T) { func TestBindUri(t *testing.T) { DefaultWriter = os.Stdout - router := Default() + router := New() type Person struct { Name string `uri:"name" binding:"required"` @@ -331,7 +331,7 @@ func TestBindUri(t *testing.T) { func TestBindUriError(t *testing.T) { DefaultWriter = os.Stdout - router := Default() + router := New() type Member struct { Number string `uri:"num" binding:"required,uuid"` @@ -361,7 +361,7 @@ func githubConfigRouter(router *Engine) { func TestGithubAPI(t *testing.T) { DefaultWriter = os.Stdout - router := Default() + router := New() githubConfigRouter(router) for _, route := range githubAPI { @@ -436,7 +436,7 @@ func BenchmarkParallelGithub(b *testing.B) { func BenchmarkParallelGithubDefault(b *testing.B) { DefaultWriter = os.Stdout - router := Default() + router := New() githubConfigRouter(router) req, _ := http.NewRequest("POST", "/repos/manucorporat/sse/git/blobs", nil) diff --git a/recovery_test.go b/recovery_test.go index e886eaac..0a6d6271 100644 --- a/recovery_test.go +++ b/recovery_test.go @@ -43,6 +43,7 @@ func TestPanicInHandler(t *testing.T) { assert.Equal(t, http.StatusInternalServerError, w.Code) assert.Contains(t, buffer.String(), "GET /recovery") + SetMode(TestMode) } // TestPanicWithAbort assert that panic has been recovered even if context.Abort was used. From 5846ceba8b1074c4555491ef4df2a54e75442358 Mon Sep 17 00:00:00 2001 From: awkj Date: Wed, 20 Feb 2019 00:02:37 +0800 Subject: [PATCH 062/111] add notify accept signal (#1740) * add notify accept signal * add import * update readme,keep same with example --- README.md | 6 +++++- examples/graceful-shutdown/graceful-shutdown/server.go | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0dcaa8e9..3da8785b 100644 --- a/README.md +++ b/README.md @@ -1633,6 +1633,7 @@ import ( "net/http" "os" "os/signal" + "syscall" "time" "github.com/gin-gonic/gin" @@ -1660,7 +1661,10 @@ func main() { // 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) + // kill (no param) default send syscanll.SIGTERM + // kill -2 is syscall.SIGINT + // kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("Shutdown Server ...") diff --git a/examples/graceful-shutdown/graceful-shutdown/server.go b/examples/graceful-shutdown/graceful-shutdown/server.go index af4f2146..33be0c8f 100644 --- a/examples/graceful-shutdown/graceful-shutdown/server.go +++ b/examples/graceful-shutdown/graceful-shutdown/server.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "os/signal" + "syscall" "time" "github.com/gin-gonic/gin" @@ -35,7 +36,10 @@ func main() { // 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) + // kill (no param) default send syscanll.SIGTERM + // kill -2 is syscall.SIGINT + // kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("Shutdown Server ...") From fece76d93fd65f795bea75f59fb5e3e03320dc6d Mon Sep 17 00:00:00 2001 From: Jeremy Loy Date: Tue, 19 Feb 2019 21:41:46 -0500 Subject: [PATCH 063/111] Add NewRelic middleware example. (#1526) * Add NewRelic middleware example. * Update go.mod * Update main.go --- examples/new_relic/README.md | 30 ++++++++++++++++++++++++++ examples/new_relic/main.go | 42 ++++++++++++++++++++++++++++++++++++ go.mod | 1 + 3 files changed, 73 insertions(+) create mode 100644 examples/new_relic/README.md create mode 100644 examples/new_relic/main.go diff --git a/examples/new_relic/README.md b/examples/new_relic/README.md new file mode 100644 index 00000000..70f14942 --- /dev/null +++ b/examples/new_relic/README.md @@ -0,0 +1,30 @@ +The [New Relic Go Agent](https://github.com/newrelic/go-agent) provides a nice middleware for the stdlib handler signature. +The following is an adaptation of that middleware for Gin. + +```golang +const ( + // NewRelicTxnKey is the key used to retrieve the NewRelic Transaction from the context + NewRelicTxnKey = "NewRelicTxnKey" +) + +// NewRelicMonitoring is a middleware that starts a newrelic transaction, stores it in the context, then calls the next handler +func NewRelicMonitoring(app newrelic.Application) gin.HandlerFunc { + return func(ctx *gin.Context) { + txn := app.StartTransaction(ctx.Request.URL.Path, ctx.Writer, ctx.Request) + defer txn.End() + ctx.Set(NewRelicTxnKey, txn) + ctx.Next() + } +} +``` +and in `main.go` or equivalent... +```golang +router := gin.Default() +cfg := newrelic.NewConfig(os.Getenv("APP_NAME"), os.Getenv("NEW_RELIC_API_KEY")) +app, err := newrelic.NewApplication(cfg) +if err != nil { + log.Printf("failed to make new_relic app: %v", err) +} else { + router.Use(adapters.NewRelicMonitoring(app)) +} + ``` diff --git a/examples/new_relic/main.go b/examples/new_relic/main.go new file mode 100644 index 00000000..f85f7831 --- /dev/null +++ b/examples/new_relic/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "log" + "net/http" + "os" + + "github.com/gin-gonic/gin" + "github.com/newrelic/go-agent" +) + +const ( + // NewRelicTxnKey is the key used to retrieve the NewRelic Transaction from the context + NewRelicTxnKey = "NewRelicTxnKey" +) + +// NewRelicMonitoring is a middleware that starts a newrelic transaction, stores it in the context, then calls the next handler +func NewRelicMonitoring(app newrelic.Application) gin.HandlerFunc { + return func(ctx *gin.Context) { + txn := app.StartTransaction(ctx.Request.URL.Path, ctx.Writer, ctx.Request) + defer txn.End() + ctx.Set(NewRelicTxnKey, txn) + ctx.Next() + } +} + +func main() { + router := gin.Default() + + cfg := newrelic.NewConfig(os.Getenv("APP_NAME"), os.Getenv("NEW_RELIC_API_KEY")) + app, err := newrelic.NewApplication(cfg) + if err != nil { + log.Printf("failed to make new_relic app: %v", err) + } else { + router.Use(NewRelicMonitoring(app)) + } + + router.GET("/", func(c *gin.Context) { + c.String(http.StatusOK, "Hello World!\n") + }) + router.Run() +} diff --git a/go.mod b/go.mod index 54573a8b..6f9d68d1 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ exclude ( github.com/gin-gonic/autotls v0.0.0-20190119125636-0b5f4fc15768 github.com/jessevdk/go-assets v0.0.0-20160921144138-4f4301a06e15 github.com/manucorporat/stats v0.0.0-20180402194714-3ba42d56d227 + github.com/newrelic/go-agent v2.5.0+incompatible github.com/thinkerou/favicon v0.1.0 golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1 From 90587c7787a37e3b32375627717b936a17ab8e94 Mon Sep 17 00:00:00 2001 From: ffhelicopter <32922889+ffhelicopter@users.noreply.github.com> Date: Wed, 20 Feb 2019 13:24:29 +0800 Subject: [PATCH 064/111] Update: examples/graceful-shutdown/server.go (#1530) * Update server.go It's necessary that catching ctx.Done() * Update server.go * Update server.go * Update README.md * Update README.md --- README.md | 5 +++++ examples/graceful-shutdown/graceful-shutdown/server.go | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/README.md b/README.md index 3da8785b..90f6e1d1 100644 --- a/README.md +++ b/README.md @@ -1673,6 +1673,11 @@ func main() { if err := srv.Shutdown(ctx); err != nil { log.Fatal("Server Shutdown:", err) } + // catching ctx.Done(). timeout of 5 seconds. + select { + case <-ctx.Done(): + log.Println("timeout of 5 seconds.") + } log.Println("Server exiting") } ``` diff --git a/examples/graceful-shutdown/graceful-shutdown/server.go b/examples/graceful-shutdown/graceful-shutdown/server.go index 33be0c8f..999a209e 100644 --- a/examples/graceful-shutdown/graceful-shutdown/server.go +++ b/examples/graceful-shutdown/graceful-shutdown/server.go @@ -48,5 +48,10 @@ func main() { if err := srv.Shutdown(ctx); err != nil { log.Fatal("Server Shutdown:", err) } + // catching ctx.Done(). timeout of 5 seconds. + select { + case <-ctx.Done(): + log.Println("timeout of 5 seconds.") + } log.Println("Server exiting") } From a58a2f9bf368a7976d26dba4c16c90fad4c1db6e Mon Sep 17 00:00:00 2001 From: Olivier Robardet Date: Wed, 20 Feb 2019 14:14:16 +0100 Subject: [PATCH 065/111] Add a function to force color in console output (#1724) Add a function `ForceConsoleColor`, like `DisableConsoleColor` but to force coloring the output. It usefull when some IDE's integrated console (like IntelliJ or Goland) are not detected as TTY, but can display colors. Also helps if one want to output color in log file (#1590) and as a workaround for #1547. --- README.md | 45 ++++++++++++++++++++++++++++++++++++++++++--- logger.go | 10 ++++++++-- logger_test.go | 7 +++++++ 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 90f6e1d1..058eee2a 100644 --- a/README.md +++ b/README.md @@ -215,9 +215,6 @@ $ go build -tags=jsoniter . ```go func main() { - // Disable Console Color - // gin.DisableConsoleColor() - // Creates a gin router with default middleware: // logger and recovery (crash-free) middleware router := gin.Default() @@ -570,6 +567,48 @@ func main() { ::1 - [Fri, 07 Dec 2018 17:04:38 JST] "GET /ping HTTP/1.1 200 122.767µs "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36" " ``` +### Controlling Log output coloring + +By default, logs output on console should be colorized depending on the detected TTY. + +Never colorize logs: + +```go +func main() { + // Disable log's color + gin.DisableConsoleColor() + + // Creates a gin router with default middleware: + // logger and recovery (crash-free) middleware + router := gin.Default() + + router.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + + router.Run(":8080") +} +``` + +Always colorize logs: + +```go +func main() { + // Force log's color + gin.ForceConsoleColor() + + // Creates a gin router with default middleware: + // logger and recovery (crash-free) middleware + router := gin.Default() + + router.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + + router.Run(":8080") +} +``` + ### Model binding and validation To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML and standard form values (foo=bar&boo=baz). diff --git a/logger.go b/logger.go index d2869f51..6d8f838e 100644 --- a/logger.go +++ b/logger.go @@ -24,6 +24,7 @@ var ( cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109}) reset = string([]byte{27, 91, 48, 109}) disableColor = false + forceColor = false ) // LoggerConfig defines the config for Logger middleware. @@ -90,6 +91,11 @@ func DisableConsoleColor() { disableColor = true } +// ForceConsoleColor force color output in the console. +func ForceConsoleColor() { + forceColor = true +} + // ErrorLogger returns a handlerfunc for any error type. func ErrorLogger() HandlerFunc { return ErrorLoggerT(ErrorTypeAny) @@ -144,9 +150,9 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc { isTerm := true - if w, ok := out.(*os.File); !ok || + if w, ok := out.(*os.File); (!ok || (os.Getenv("TERM") == "dumb" || (!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd()))) || - disableColor { + disableColor) && !forceColor { isTerm = false } diff --git a/logger_test.go b/logger_test.go index d0169251..8770b5fb 100644 --- a/logger_test.go +++ b/logger_test.go @@ -340,3 +340,10 @@ func TestDisableConsoleColor(t *testing.T) { DisableConsoleColor() assert.True(t, disableColor) } + +func TestForceConsoleColor(t *testing.T) { + New() + assert.False(t, forceColor) + ForceConsoleColor() + assert.True(t, forceColor) +} From e6886e1539d89365f9970e1b5d9af1a2ccea16d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Thu, 21 Feb 2019 20:32:55 +0800 Subject: [PATCH 066/111] chore: fix Make script when failed (#1774) --- Makefile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7211144a..7ab57e27 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,10 @@ test: exit 1; \ elif grep -q "build failed" tmp.out; then \ rm tmp.out; \ - exit; \ + exit 1; \ + elif grep -q "setup failed" tmp.out; then \ + rm tmp.out; \ + exit 1; \ fi; \ if [ -f profile.out ]; then \ cat profile.out | grep -v "mode:" >> coverage.out; \ From 4e86b17e734074af0317f9b0c40167f45fd0ca91 Mon Sep 17 00:00:00 2001 From: Mara Kim Date: Thu, 21 Feb 2019 22:45:32 -0500 Subject: [PATCH 067/111] Set socket to recieve writes (#1134) * Set socket to recieve writes * Update gin.go --- gin.go | 1 + 1 file changed, 1 insertion(+) diff --git a/gin.go b/gin.go index 6e5ea6d7..ad64c35f 100644 --- a/gin.go +++ b/gin.go @@ -318,6 +318,7 @@ func (engine *Engine) RunUnix(file string) (err error) { return } defer listener.Close() + os.Chmod(file, 0777) err = http.Serve(listener, engine) return } From 48f6c6137c9e326dc0aea110a43ad5a7a590073e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bazaglia?= Date: Fri, 22 Feb 2019 01:23:52 -0300 Subject: [PATCH 068/111] allow ignoring field on form mapping (#1733) --- binding/binding_test.go | 25 +++++++++++++++++++++++++ binding/form_mapping.go | 3 +++ 2 files changed, 28 insertions(+) diff --git a/binding/binding_test.go b/binding/binding_test.go index 1044e6c2..c9dea347 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -61,6 +61,10 @@ type FooStructForMapType struct { MapFoo map[string]interface{} `form:"map_foo"` } +type FooStructForIgnoreFormTag struct { + Foo *string `form:"-"` +} + type InvalidNameType struct { TestName string `invalid_name:"test_name"` } @@ -278,6 +282,12 @@ func TestBindingFormForTime2(t *testing.T) { "", "") } +func TestFormBindingIgnoreField(t *testing.T) { + testFormBindingIgnoreField(t, "POST", + "/", "/", + "-=bar", "") +} + func TestBindingFormInvalidName(t *testing.T) { testFormBindingInvalidName(t, "POST", "/", "/", @@ -860,6 +870,21 @@ func testFormBindingForTimeFailLocation(t *testing.T, method, path, badPath, bod assert.Error(t, err) } +func testFormBindingIgnoreField(t *testing.T, method, path, badPath, body, badBody string) { + b := Form + assert.Equal(t, "form", b.Name()) + + obj := FooStructForIgnoreFormTag{} + req := requestWithBody(method, path, body) + if method == "POST" { + req.Header.Add("Content-Type", MIMEPOSTForm) + } + err := b.Bind(req, &obj) + assert.NoError(t, err) + + assert.Nil(t, obj.Foo) +} + func testFormBindingInvalidName(t *testing.T, method, path, badPath, body, badBody string) { b := Form assert.Equal(t, "form", b.Name()) diff --git a/binding/form_mapping.go b/binding/form_mapping.go index 8900ab70..8eb5c0d1 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -41,6 +41,9 @@ func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error { defaultValue = defaultList[1] } } + if inputFieldName == "-" { + continue + } if inputFieldName == "" { inputFieldName = typeField.Name From d7daffc26b212514adb756e1ba807a6b8242dd37 Mon Sep 17 00:00:00 2001 From: David Zhang Date: Fri, 22 Feb 2019 12:53:47 +0800 Subject: [PATCH 069/111] Use camel case instead of ALL_CAPS (#1419) * Use camel case instead of ALL_CAPS * Update mode.go --- mode.go | 6 +++--- mode_test.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mode.go b/mode.go index f787b5ca..8aa84aa8 100644 --- a/mode.go +++ b/mode.go @@ -11,8 +11,8 @@ import ( "github.com/gin-gonic/gin/binding" ) -// ENV_GIN_MODE indicates environment name for gin mode. -const ENV_GIN_MODE = "GIN_MODE" +// EnvGinMode indicates environment name for gin mode. +const EnvGinMode = "GIN_MODE" const ( // DebugMode indicates gin mode is debug. @@ -44,7 +44,7 @@ var ginMode = debugCode var modeName = DebugMode func init() { - mode := os.Getenv(ENV_GIN_MODE) + mode := os.Getenv(EnvGinMode) SetMode(mode) } diff --git a/mode_test.go b/mode_test.go index cf27acd8..3dba5150 100644 --- a/mode_test.go +++ b/mode_test.go @@ -13,13 +13,13 @@ import ( ) func init() { - os.Setenv(ENV_GIN_MODE, TestMode) + os.Setenv(EnvGinMode, TestMode) } func TestSetMode(t *testing.T) { assert.Equal(t, testCode, ginMode) assert.Equal(t, TestMode, Mode()) - os.Unsetenv(ENV_GIN_MODE) + os.Unsetenv(EnvGinMode) SetMode("") assert.Equal(t, debugCode, ginMode) From 184661cfa2f5ad6000c9893a0ffba8da8c1bd2ec Mon Sep 17 00:00:00 2001 From: Pierre Massat Date: Thu, 21 Feb 2019 21:12:05 -0800 Subject: [PATCH 070/111] Add response size to LogFormatterParams (#1752) --- logger.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/logger.go b/logger.go index 6d8f838e..bd28a11c 100644 --- a/logger.go +++ b/logger.go @@ -64,6 +64,8 @@ type LogFormatterParams struct { ErrorMessage string // IsTerm shows whether does gin's output descriptor refers to a terminal. IsTerm bool + // BodySize is the size of the Response Body + BodySize int } // defaultLogFormatter is the default log format function Logger middleware uses. @@ -191,6 +193,8 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc { param.StatusCode = c.Writer.Status() param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String() + param.BodySize = c.Writer.Size() + if raw != "" { path = path + "?" + raw } From 7b1081a73fe4559697f4fb5a44261fbdc1d130b2 Mon Sep 17 00:00:00 2001 From: songjiayang Date: Fri, 22 Feb 2019 14:20:24 +0800 Subject: [PATCH 071/111] issue_1721: fix render writeHeaders to make it the same as http.Header.Set (#1722) --- render/reader.go | 4 ++-- render/render_test.go | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/render/reader.go b/render/reader.go index ab60e53a..312af741 100644 --- a/render/reader.go +++ b/render/reader.go @@ -36,8 +36,8 @@ func (r Reader) WriteContentType(w http.ResponseWriter) { func (r Reader) writeHeaders(w http.ResponseWriter, headers map[string]string) { header := w.Header() for k, v := range headers { - if val := header[k]; len(val) == 0 { - header[k] = []string{v} + if header.Get(k) == "" { + header.Set(k, v) } } } diff --git a/render/render_test.go b/render/render_test.go index 3df04a17..76e29eeb 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -470,6 +470,7 @@ func TestRenderReader(t *testing.T) { body := "#!PNG some raw data" headers := make(map[string]string) headers["Content-Disposition"] = `attachment; filename="filename.png"` + headers["x-request-id"] = "requestId" err := (Reader{ ContentLength: int64(len(body)), @@ -483,4 +484,5 @@ func TestRenderReader(t *testing.T) { assert.Equal(t, "image/png", w.Header().Get("Content-Type")) assert.Equal(t, strconv.Itoa(len(body)), w.Header().Get("Content-Length")) assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition")) + assert.Equal(t, headers["x-request-id"], w.Header().Get("x-request-id")) } From e6288e90eb313122410bf1335d502a062c1ae610 Mon Sep 17 00:00:00 2001 From: Sai Date: Fri, 22 Feb 2019 17:48:55 +0900 Subject: [PATCH 072/111] Change color methods in using defaultLogger function to public (#1771) Fix https://github.com/gin-gonic/gin/issues/1768 --- logger.go | 85 ++++++++++++++++++++++++++++---------------------- logger_test.go | 19 +++++++++++ 2 files changed, 67 insertions(+), 37 deletions(-) diff --git a/logger.go b/logger.go index bd28a11c..dc639975 100644 --- a/logger.go +++ b/logger.go @@ -68,13 +68,58 @@ type LogFormatterParams struct { BodySize int } +// StatusCodeColor is the ANSI color for appropriately logging http status code to a terminal. +func (p *LogFormatterParams) StatusCodeColor() string { + code := p.StatusCode + + switch { + case code >= http.StatusOK && code < http.StatusMultipleChoices: + return green + case code >= http.StatusMultipleChoices && code < http.StatusBadRequest: + return white + case code >= http.StatusBadRequest && code < http.StatusInternalServerError: + return yellow + default: + return red + } +} + +// MethodColor is the ANSI color for appropriately logging http method to a terminal. +func (p *LogFormatterParams) MethodColor() string { + method := p.Method + + switch method { + case "GET": + return blue + case "POST": + return cyan + case "PUT": + return yellow + case "DELETE": + return red + case "PATCH": + return green + case "HEAD": + return magenta + case "OPTIONS": + return white + default: + return reset + } +} + +// ResetColor resets all escape attributes. +func (p *LogFormatterParams) ResetColor() string { + return reset +} + // defaultLogFormatter is the default log format function Logger middleware uses. var defaultLogFormatter = func(param LogFormatterParams) string { var statusColor, methodColor, resetColor string if param.IsTerm { - statusColor = colorForStatus(param.StatusCode) - methodColor = colorForMethod(param.Method) - resetColor = reset + statusColor = param.StatusCodeColor() + methodColor = param.MethodColor() + resetColor = param.ResetColor() } return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s", @@ -205,37 +250,3 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc { } } } - -func colorForStatus(code int) string { - switch { - case code >= http.StatusOK && code < http.StatusMultipleChoices: - return green - case code >= http.StatusMultipleChoices && code < http.StatusBadRequest: - return white - case code >= http.StatusBadRequest && code < http.StatusInternalServerError: - return yellow - default: - return red - } -} - -func colorForMethod(method string) string { - switch method { - case "GET": - return blue - case "POST": - return cyan - case "PUT": - return yellow - case "DELETE": - return red - case "PATCH": - return green - case "HEAD": - return magenta - case "OPTIONS": - return white - default: - return reset - } -} diff --git a/logger_test.go b/logger_test.go index 8770b5fb..c551677a 100644 --- a/logger_test.go +++ b/logger_test.go @@ -257,6 +257,13 @@ func TestDefaultLogFormatter(t *testing.T) { } func TestColorForMethod(t *testing.T) { + colorForMethod := func(method string) string { + p := LogFormatterParams{ + Method: method, + } + return p.MethodColor() + } + assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 52, 109}), colorForMethod("GET"), "get should be blue") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 54, 109}), colorForMethod("POST"), "post should be cyan") assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForMethod("PUT"), "put should be yellow") @@ -268,12 +275,24 @@ func TestColorForMethod(t *testing.T) { } func TestColorForStatus(t *testing.T) { + colorForStatus := func(code int) string { + p := LogFormatterParams{ + StatusCode: code, + } + return p.StatusCodeColor() + } + assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForStatus(http.StatusOK), "2xx should be green") assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForStatus(http.StatusMovedPermanently), "3xx should be white") assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForStatus(http.StatusNotFound), "4xx should be yellow") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForStatus(2), "other things should be red") } +func TestResetColor(t *testing.T) { + p := LogFormatterParams{} + assert.Equal(t, string([]byte{27, 91, 48, 109}), p.ResetColor()) +} + func TestErrorLogger(t *testing.T) { router := New() router.Use(ErrorLogger()) From d6adc8d0cc4e5a87e249923f511d093ffa552c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Tue, 26 Feb 2019 10:45:44 +0800 Subject: [PATCH 073/111] chore: add go1.12 support (#1780) * chore: add go1.12 support * Update .travis.yml --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 2eeb0b3d..be196feb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ go: - 1.9.x - 1.10.x - 1.11.x + - 1.12.x - master matrix: @@ -14,6 +15,8 @@ matrix: include: - go: 1.11.x env: GO111MODULE=on + - go: 1.12.x + env: GO111MODULE=on git: depth: 10 From 62749f0db4aa5ee1045779b457fe1f28a553f467 Mon Sep 17 00:00:00 2001 From: Luis GG Date: Tue, 26 Feb 2019 01:15:40 -0300 Subject: [PATCH 074/111] Add context.HandlerNames() (#1729) * Add context.HandlerNames() This change adds a HandlerNames method that will return all registered handles in the context, in descending order This is useful for debugging and troubleshooting purposes, especially in large apps * Tests Add tests for HandlerNames * Fix HandlerNames test * Simplify test --- context.go | 10 ++++++++++ context_test.go | 16 ++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/context.go b/context.go index 26badfc3..fa63ec8b 100644 --- a/context.go +++ b/context.go @@ -91,6 +91,16 @@ func (c *Context) HandlerName() string { return nameOfFunction(c.handlers.Last()) } +// HandlerNames returns a list of all registered handlers for this context in descending order, +// following the semantics of HandlerName() +func (c *Context) HandlerNames() []string { + hn := make([]string, 0, len(c.handlers)) + for _, val := range c.handlers { + hn = append(hn, nameOfFunction(val)) + } + return hn +} + // Handler returns the main handler. func (c *Context) Handler() HandlerFunc { return c.handlers.Last() diff --git a/context_test.go b/context_test.go index ea936b85..34cc71a5 100644 --- a/context_test.go +++ b/context_test.go @@ -340,10 +340,26 @@ func TestContextHandlerName(t *testing.T) { assert.Regexp(t, "^(.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest$", c.HandlerName()) } +func TestContextHandlerNames(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + c.handlers = HandlersChain{func(c *Context) {}, handlerNameTest, func(c *Context) {}, handlerNameTest2} + + names := c.HandlerNames() + + assert.True(t, len(names) == 4) + for _, name := range names { + assert.Regexp(t, `^(.*/vendor/)?(github\.com/gin-gonic/gin\.){1}(TestContextHandlerNames\.func.*){0,1}(handlerNameTest.*){0,1}`, name) + } +} + func handlerNameTest(c *Context) { } +func handlerNameTest2(c *Context) { + +} + var handlerTest HandlerFunc = func(c *Context) { } From e207a3ce65323c3dda7cd8133fe14f4f1efd2500 Mon Sep 17 00:00:00 2001 From: Raphael Gavache Date: Tue, 26 Feb 2019 08:10:16 +0100 Subject: [PATCH 075/111] Fix context.Copy() race condition (#1020) * Fix context.Copy race condition * Update githubapi_test.go * fix code format --- context.go | 4 ++++ context_test.go | 2 ++ githubapi_test.go | 23 +++++++++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/context.go b/context.go index fa63ec8b..7618cef5 100644 --- a/context.go +++ b/context.go @@ -82,6 +82,10 @@ func (c *Context) Copy() *Context { cp.Writer = &cp.writermem cp.index = abortIndex cp.handlers = nil + cp.Keys = map[string]interface{}{} + for k, v := range c.Keys { + cp.Keys[k] = v + } return &cp } diff --git a/context_test.go b/context_test.go index 34cc71a5..dc8ac306 100644 --- a/context_test.go +++ b/context_test.go @@ -331,6 +331,8 @@ func TestContextCopy(t *testing.T) { assert.Equal(t, cp.Keys, c.Keys) assert.Equal(t, cp.engine, c.engine) assert.Equal(t, cp.Params, c.Params) + cp.Set("foo", "notBar") + assert.False(t, cp.Keys["foo"] == c.Keys["foo"]) } func TestContextHandlerName(t *testing.T) { diff --git a/githubapi_test.go b/githubapi_test.go index 29aa1584..fb74d659 100644 --- a/githubapi_test.go +++ b/githubapi_test.go @@ -346,6 +346,29 @@ func TestBindUriError(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w1.Code) } +func TestRaceContextCopy(t *testing.T) { + DefaultWriter = os.Stdout + router := Default() + router.GET("/test/copy/race", func(c *Context) { + c.Set("1", 0) + c.Set("2", 0) + + // Sending a copy of the Context to two separate routines + go readWriteKeys(c.Copy()) + go readWriteKeys(c.Copy()) + c.String(http.StatusOK, "run OK, no panics") + }) + w := performRequest(router, "GET", "/test/copy/race") + assert.Equal(t, "run OK, no panics", w.Body.String()) +} + +func readWriteKeys(c *Context) { + for { + c.Set("1", rand.Int()) + c.Set("2", c.Value("1")) + } +} + func githubConfigRouter(router *Engine) { for _, route := range githubAPI { router.Handle(route.method, route.path, func(c *Context) { From ccb105dbcb48cbcec988df8a46e90af3adf7c0ff Mon Sep 17 00:00:00 2001 From: Tudor Roman Date: Wed, 27 Feb 2019 13:56:29 +0200 Subject: [PATCH 076/111] add prefix from X-Forwarded-Prefix in redirectTrailingSlash (#1238) * add prefix from X-Forwarded-Prefix in redirectTrailingSlash * added test * fix path import --- gin.go | 14 +++++++++----- routes_test.go | 17 ++++++++++++++++- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/gin.go b/gin.go index ad64c35f..e28e9579 100644 --- a/gin.go +++ b/gin.go @@ -10,6 +10,7 @@ import ( "net" "net/http" "os" + "path" "sync" "github.com/gin-gonic/gin/render" @@ -438,17 +439,20 @@ func serveError(c *Context, code int, defaultMessage []byte) { func redirectTrailingSlash(c *Context) { req := c.Request - path := req.URL.Path + p := req.URL.Path + if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." { + p = prefix + "/" + req.URL.Path + } code := http.StatusMovedPermanently // Permanent redirect, request with GET method if req.Method != "GET" { code = http.StatusTemporaryRedirect } - req.URL.Path = path + "/" - if length := len(path); length > 1 && path[length-1] == '/' { - req.URL.Path = path[:length-1] + req.URL.Path = p + "/" + if length := len(p); length > 1 && p[length-1] == '/' { + req.URL.Path = p[:length-1] } - debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String()) + debugPrint("redirecting request %d: %s --> %s", code, p, req.URL.String()) http.Redirect(c.Writer, req, req.URL.String(), code) c.writermem.WriteHeaderNow() } diff --git a/routes_test.go b/routes_test.go index 8d50292d..a842704f 100644 --- a/routes_test.go +++ b/routes_test.go @@ -16,8 +16,16 @@ import ( "github.com/stretchr/testify/assert" ) -func performRequest(r http.Handler, method, path string) *httptest.ResponseRecorder { +type header struct { + Key string + Value string +} + +func performRequest(r http.Handler, method, path string, headers ...header) *httptest.ResponseRecorder { req, _ := http.NewRequest(method, path, nil) + for _, h := range headers { + req.Header.Add(h.Key, h.Value) + } w := httptest.NewRecorder() r.ServeHTTP(w, req) return w @@ -170,6 +178,13 @@ func TestRouteRedirectTrailingSlash(t *testing.T) { w = performRequest(router, "PUT", "/path4/") assert.Equal(t, http.StatusOK, w.Code) + w = performRequest(router, "GET", "/path2", header{Key: "X-Forwarded-Prefix", Value: "/api"}) + assert.Equal(t, "/api/path2/", w.Header().Get("Location")) + assert.Equal(t, 301, w.Code) + + w = performRequest(router, "GET", "/path2/", header{Key: "X-Forwarded-Prefix", Value: "/api/"}) + assert.Equal(t, 200, w.Code) + router.RedirectTrailingSlash = false w = performRequest(router, "GET", "/path/") From 7dfa6c936a3f3169979f1bbfa074c6915f055881 Mon Sep 17 00:00:00 2001 From: Dmitry Kutakov Date: Thu, 28 Feb 2019 17:43:27 +0300 Subject: [PATCH 077/111] fix #1784: correct error comparison on tests (#1785) --- context_test.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/context_test.go b/context_test.go index dc8ac306..060a8e8d 100644 --- a/context_test.go +++ b/context_test.go @@ -1242,22 +1242,24 @@ func TestContextError(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) assert.Empty(t, c.Errors) - c.Error(errors.New("first error")) // nolint: errcheck + firstErr := errors.New("first error") + c.Error(firstErr) // nolint: errcheck assert.Len(t, c.Errors, 1) assert.Equal(t, "Error #01: first error\n", c.Errors.String()) + secondErr := errors.New("second error") c.Error(&Error{ // nolint: errcheck - Err: errors.New("second error"), + Err: secondErr, Meta: "some data 2", Type: ErrorTypePublic, }) assert.Len(t, c.Errors, 2) - assert.Equal(t, errors.New("first error"), c.Errors[0].Err) + assert.Equal(t, firstErr, c.Errors[0].Err) assert.Nil(t, c.Errors[0].Meta) assert.Equal(t, ErrorTypePrivate, c.Errors[0].Type) - assert.Equal(t, errors.New("second error"), c.Errors[1].Err) + assert.Equal(t, secondErr, c.Errors[1].Err) assert.Equal(t, "some data 2", c.Errors[1].Meta) assert.Equal(t, ErrorTypePublic, c.Errors[1].Type) From 9bacadd3eab2e7271c456eedf2b02e4e09357d83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Fri, 1 Mar 2019 07:11:02 +0800 Subject: [PATCH 078/111] remove docs dir (#1786) the post doc move https://gin-gonic.com/blog/ --- docs/how-to-build-an-effective-middleware.md | 137 ------------------- 1 file changed, 137 deletions(-) delete mode 100644 docs/how-to-build-an-effective-middleware.md diff --git a/docs/how-to-build-an-effective-middleware.md b/docs/how-to-build-an-effective-middleware.md deleted file mode 100644 index 568d5720..00000000 --- a/docs/how-to-build-an-effective-middleware.md +++ /dev/null @@ -1,137 +0,0 @@ -# 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 -``` From 2dd31930060c978a84507571f7a3106afbf9673b Mon Sep 17 00:00:00 2001 From: Equim Date: Fri, 1 Mar 2019 10:03:14 +0800 Subject: [PATCH 079/111] Support negotiation wildcards, fix #391 (#1112) * support negotiation wildcards, fix #391 * fix typo --- context.go | 13 ++++++++++++- context_test.go | 28 ++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/context.go b/context.go index 7618cef5..08c420b1 100644 --- a/context.go +++ b/context.go @@ -952,7 +952,18 @@ func (c *Context) NegotiateFormat(offered ...string) string { } for _, accepted := range c.Accepted { for _, offert := range offered { - if accepted == offert { + // According to RFC 2616 and RFC 2396, non-ASCII characters are not allowed in headers, + // therefore we can just iterate over the string without casting it into []rune + i := 0 + for ; i < len(accepted); i++ { + if accepted[i] == '*' || offert[i] == '*' { + return offert + } + if accepted[i] != offert[i] { + break + } + } + if i == len(accepted) { return offert } } diff --git a/context_test.go b/context_test.go index 060a8e8d..483e1680 100644 --- a/context_test.go +++ b/context_test.go @@ -1158,17 +1158,41 @@ func TestContextNegotiationFormat(t *testing.T) { func TestContextNegotiationFormatWithAccept(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") + c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9;q=0.8") assert.Equal(t, MIMEXML, c.NegotiateFormat(MIMEJSON, MIMEXML)) assert.Equal(t, MIMEHTML, c.NegotiateFormat(MIMEXML, MIMEHTML)) assert.Empty(t, c.NegotiateFormat(MIMEJSON)) } +func TestContextNegotiationFormatWithWildcardAccept(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + c.Request, _ = http.NewRequest("POST", "/", nil) + c.Request.Header.Add("Accept", "*/*") + + assert.Equal(t, c.NegotiateFormat("*/*"), "*/*") + assert.Equal(t, c.NegotiateFormat("text/*"), "text/*") + assert.Equal(t, c.NegotiateFormat("application/*"), "application/*") + assert.Equal(t, c.NegotiateFormat(MIMEJSON), MIMEJSON) + assert.Equal(t, c.NegotiateFormat(MIMEXML), MIMEXML) + assert.Equal(t, c.NegotiateFormat(MIMEHTML), MIMEHTML) + + c, _ = CreateTestContext(httptest.NewRecorder()) + c.Request, _ = http.NewRequest("POST", "/", nil) + c.Request.Header.Add("Accept", "text/*") + + assert.Equal(t, c.NegotiateFormat("*/*"), "*/*") + assert.Equal(t, c.NegotiateFormat("text/*"), "text/*") + assert.Equal(t, c.NegotiateFormat("application/*"), "") + assert.Equal(t, c.NegotiateFormat(MIMEJSON), "") + assert.Equal(t, c.NegotiateFormat(MIMEXML), "") + assert.Equal(t, c.NegotiateFormat(MIMEHTML), MIMEHTML) +} + 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") + c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9;q=0.8") c.Accepted = nil c.SetAccepted(MIMEJSON, MIMEXML) From ccb9e902956d15e354867918b105e1595a2f370a Mon Sep 17 00:00:00 2001 From: Emmanuel Goh Date: Fri, 1 Mar 2019 10:17:47 +0800 Subject: [PATCH 080/111] Extend context.File to allow for the content-dispositon attachments via a new method context.Attachment (#1260) * Add FileAttachment method to context to allow instant downloads with filenames * Add relevant tests for FileAttachment method --- context.go | 8 ++++++++ context_test.go | 13 +++++++++++++ 2 files changed, 21 insertions(+) diff --git a/context.go b/context.go index 08c420b1..e9735d28 100644 --- a/context.go +++ b/context.go @@ -6,6 +6,7 @@ package gin import ( "errors" + "fmt" "io" "io/ioutil" "math" @@ -880,6 +881,13 @@ func (c *Context) File(filepath string) { http.ServeFile(c.Writer, c.Request, filepath) } +// FileAttachment writes the specified file into the body stream in an efficient way +// On the client side, the file will typically be downloaded with the given filename +func (c *Context) FileAttachment(filepath, filename string) { + c.Writer.Header().Set("content-disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) + http.ServeFile(c.Writer, c.Request, filepath) +} + // SSEvent writes a Server-Sent Event into the body stream. func (c *Context) SSEvent(name string, message interface{}) { c.Render(-1, sse.Event{ diff --git a/context_test.go b/context_test.go index 483e1680..0da5fbe6 100644 --- a/context_test.go +++ b/context_test.go @@ -979,6 +979,19 @@ func TestContextRenderFile(t *testing.T) { assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type")) } +func TestContextRenderAttachment(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + newFilename := "new_filename.go" + + c.Request, _ = http.NewRequest("GET", "/", nil) + c.FileAttachment("./gin.go", newFilename) + + assert.Equal(t, 200, w.Code) + assert.Contains(t, w.Body.String(), "func New() *Engine {") + assert.Equal(t, fmt.Sprintf("attachment; filename=\"%s\"", newFilename), w.HeaderMap.Get("Content-Disposition")) +} + // TestContextRenderYAML tests that the response is serialized as YAML // and Content-Type is set to application/x-yaml func TestContextRenderYAML(t *testing.T) { From 0feaf8cbd80da13be634b13fd28bfb2d6e357839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Fri, 1 Mar 2019 23:42:41 +0800 Subject: [PATCH 081/111] Split examples to alone repo (#1776) * split examples to alone repo * vendor * fix package error * add examples/README.md --- .travis.yml | 1 - Makefile | 9 +- README.md | 6 - examples/README.md | 3 + examples/app-engine/README.md | 8 - examples/app-engine/app.yaml | 8 - examples/app-engine/hello.go | 24 -- examples/assets-in-binary/README.md | 33 --- examples/assets-in-binary/assets.go | 34 --- examples/assets-in-binary/html/bar.tmpl | 4 - examples/assets-in-binary/html/index.tmpl | 4 - examples/assets-in-binary/main.go | 48 ---- examples/auto-tls/example1/main.go | 19 -- examples/auto-tls/example2/main.go | 26 --- examples/basic/main.go | 65 ------ examples/basic/main_test.go | 20 -- examples/custom-validation/server.go | 50 ----- examples/favicon/favicon.ico | Bin 1150 -> 0 bytes examples/favicon/main.go | 17 -- examples/graceful-shutdown/close/server.go | 45 ---- .../graceful-shutdown/server.go | 57 ----- examples/grpc/README.md | 19 -- examples/grpc/gin/main.go | 46 ---- examples/grpc/grpc/server.go | 34 --- examples/grpc/pb/helloworld.pb.go | 151 ------------- examples/grpc/pb/helloworld.proto | 37 ---- examples/http-pusher/assets/app.js | 1 - examples/http-pusher/main.go | 41 ---- examples/http-pusher/testdata/ca.pem | 15 -- examples/http-pusher/testdata/server.key | 16 -- examples/http-pusher/testdata/server.pem | 16 -- examples/http2/README.md | 18 -- examples/http2/main.go | 38 ---- examples/http2/testdata/ca.pem | 15 -- examples/http2/testdata/server.key | 16 -- examples/http2/testdata/server.pem | 16 -- examples/multiple-service/main.go | 74 ------- examples/new_relic/README.md | 30 --- examples/new_relic/main.go | 42 ---- examples/realtime-advanced/Makefile | 10 - examples/realtime-advanced/main.go | 42 ---- .../resources/room_login.templ.html | 208 ------------------ .../resources/static/epoch.min.css | 1 - .../resources/static/epoch.min.js | 114 ---------- .../resources/static/prismjs.min.css | 137 ------------ .../resources/static/prismjs.min.js | 5 - .../resources/static/realtime.js | 144 ------------ examples/realtime-advanced/rooms.go | 25 --- examples/realtime-advanced/routes.go | 96 -------- examples/realtime-advanced/stats.go | 59 ----- examples/realtime-chat/Makefile | 9 - examples/realtime-chat/main.go | 59 ----- examples/realtime-chat/rooms.go | 33 --- examples/realtime-chat/template.go | 44 ---- examples/struct-lvl-validations/README.md | 50 ----- examples/struct-lvl-validations/server.go | 64 ------ examples/template/main.go | 32 --- examples/upload-file/multiple/main.go | 39 ---- .../upload-file/multiple/public/index.html | 17 -- examples/upload-file/single/main.go | 36 --- examples/upload-file/single/public/index.html | 16 -- go.mod | 24 +- go.sum | 42 +--- vendor/vendor.json | 6 + 64 files changed, 25 insertions(+), 2393 deletions(-) create mode 100644 examples/README.md delete mode 100644 examples/app-engine/README.md delete mode 100644 examples/app-engine/app.yaml delete mode 100644 examples/app-engine/hello.go delete mode 100644 examples/assets-in-binary/README.md delete mode 100644 examples/assets-in-binary/assets.go delete mode 100644 examples/assets-in-binary/html/bar.tmpl delete mode 100644 examples/assets-in-binary/html/index.tmpl delete mode 100644 examples/assets-in-binary/main.go delete mode 100644 examples/auto-tls/example1/main.go delete mode 100644 examples/auto-tls/example2/main.go delete mode 100644 examples/basic/main.go delete mode 100644 examples/basic/main_test.go delete mode 100644 examples/custom-validation/server.go delete mode 100644 examples/favicon/favicon.ico delete mode 100644 examples/favicon/main.go delete mode 100644 examples/graceful-shutdown/close/server.go delete mode 100644 examples/graceful-shutdown/graceful-shutdown/server.go delete mode 100644 examples/grpc/README.md delete mode 100644 examples/grpc/gin/main.go delete mode 100644 examples/grpc/grpc/server.go delete mode 100644 examples/grpc/pb/helloworld.pb.go delete mode 100644 examples/grpc/pb/helloworld.proto delete mode 100644 examples/http-pusher/assets/app.js delete mode 100644 examples/http-pusher/main.go delete mode 100644 examples/http-pusher/testdata/ca.pem delete mode 100644 examples/http-pusher/testdata/server.key delete mode 100644 examples/http-pusher/testdata/server.pem delete mode 100644 examples/http2/README.md delete mode 100644 examples/http2/main.go delete mode 100644 examples/http2/testdata/ca.pem delete mode 100644 examples/http2/testdata/server.key delete mode 100644 examples/http2/testdata/server.pem delete mode 100644 examples/multiple-service/main.go delete mode 100644 examples/new_relic/README.md delete mode 100644 examples/new_relic/main.go delete mode 100644 examples/realtime-advanced/Makefile delete mode 100644 examples/realtime-advanced/main.go delete mode 100644 examples/realtime-advanced/resources/room_login.templ.html delete mode 100644 examples/realtime-advanced/resources/static/epoch.min.css delete mode 100644 examples/realtime-advanced/resources/static/epoch.min.js delete mode 100644 examples/realtime-advanced/resources/static/prismjs.min.css delete mode 100644 examples/realtime-advanced/resources/static/prismjs.min.js delete mode 100644 examples/realtime-advanced/resources/static/realtime.js delete mode 100644 examples/realtime-advanced/rooms.go delete mode 100644 examples/realtime-advanced/routes.go delete mode 100644 examples/realtime-advanced/stats.go delete mode 100644 examples/realtime-chat/Makefile delete mode 100644 examples/realtime-chat/main.go delete mode 100644 examples/realtime-chat/rooms.go delete mode 100644 examples/realtime-chat/template.go delete mode 100644 examples/struct-lvl-validations/README.md delete mode 100644 examples/struct-lvl-validations/server.go delete mode 100644 examples/template/main.go delete mode 100644 examples/upload-file/multiple/main.go delete mode 100644 examples/upload-file/multiple/public/index.html delete mode 100644 examples/upload-file/single/main.go delete mode 100644 examples/upload-file/single/public/index.html diff --git a/.travis.yml b/.travis.yml index be196feb..00393750 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,7 +34,6 @@ go_import_path: github.com/gin-gonic/gin script: - make vet - make fmt-check - - make embedmd - make misspell-check - make test diff --git a/Makefile b/Makefile index 7ab57e27..51a6b916 100644 --- a/Makefile +++ b/Makefile @@ -52,12 +52,6 @@ deps: @hash govendor > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ $(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; \ - fi - -embedmd: - embedmd -d *.md .PHONY: lint lint: @@ -83,5 +77,4 @@ misspell: .PHONY: tools tools: go install golang.org/x/lint/golint; \ - go install github.com/client9/misspell/cmd/misspell; \ - go install github.com/campoy/embedmd; + go install github.com/client9/misspell/cmd/misspell; diff --git a/README.md b/README.md index 058eee2a..a4ced64e 100644 --- a/README.md +++ b/README.md @@ -728,7 +728,6 @@ When running the above example using the above the `curl` command, it returns er It is also possible to register custom validators. See the [example code](examples/custom-validation/server.go). -[embedmd]:# (examples/custom-validation/server.go go) ```go package main @@ -1501,7 +1500,6 @@ func main() { example for 1-line LetsEncrypt HTTPS servers. -[embedmd]:# (examples/auto-tls/example1/main.go go) ```go package main @@ -1526,7 +1524,6 @@ func main() { example for custom autocert manager. -[embedmd]:# (examples/auto-tls/example2/main.go go) ```go package main @@ -1560,7 +1557,6 @@ func main() { See the [question](https://github.com/gin-gonic/gin/issues/346) and try the following example: -[embedmd]:# (examples/multiple-service/main.go go) ```go package main @@ -1660,7 +1656,6 @@ An alternative to endless: If you are using Go 1.8, you may not need to use this library! Consider using http.Server's built-in [Shutdown()](https://golang.org/pkg/net/http/#Server.Shutdown) method for graceful shutdowns. See the full [graceful-shutdown](./examples/graceful-shutdown) example with gin. -[embedmd]:# (examples/graceful-shutdown/graceful-shutdown/server.go go) ```go // +build go1.8 @@ -1919,7 +1914,6 @@ performance (See [#1341](https://github.com/gin-gonic/gin/pull/1341)). 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 diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..4b3b718c --- /dev/null +++ b/examples/README.md @@ -0,0 +1,3 @@ +# Gin Examples + +## TODO diff --git a/examples/app-engine/README.md b/examples/app-engine/README.md deleted file mode 100644 index b3dd7c78..00000000 --- a/examples/app-engine/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Guide to run Gin under App Engine LOCAL Development Server - -1. Download, install and setup Go in your computer. (That includes setting your `$GOPATH`.) -2. Download SDK for your platform from [here](https://cloud.google.com/appengine/docs/standard/go/download): `https://cloud.google.com/appengine/docs/standard/go/download` -3. Download Gin source code using: `$ go get github.com/gin-gonic/gin` -4. Navigate to examples folder: `$ cd $GOPATH/src/github.com/gin-gonic/gin/examples/app-engine/` -5. Run it: `$ dev_appserver.py .` (notice that you have to run this script by Python2) - diff --git a/examples/app-engine/app.yaml b/examples/app-engine/app.yaml deleted file mode 100644 index 5f20cf3f..00000000 --- a/examples/app-engine/app.yaml +++ /dev/null @@ -1,8 +0,0 @@ -application: hello -version: 1 -runtime: go -api_version: go1 - -handlers: -- url: /.* - script: _go_app \ No newline at end of file diff --git a/examples/app-engine/hello.go b/examples/app-engine/hello.go deleted file mode 100644 index f569dadb..00000000 --- a/examples/app-engine/hello.go +++ /dev/null @@ -1,24 +0,0 @@ -package hello - -import ( - "net/http" - - "github.com/gin-gonic/gin" -) - -// This function's name is a must. App Engine uses it to drive the requests properly. -func init() { - // Starts a new Gin instance with no middle-ware - r := gin.New() - - // Define your handlers - r.GET("/", func(c *gin.Context) { - c.String(http.StatusOK, "Hello World!") - }) - r.GET("/ping", func(c *gin.Context) { - c.String(http.StatusOK, "pong") - }) - - // Handle all requests using net/http - http.Handle("/", r) -} diff --git a/examples/assets-in-binary/README.md b/examples/assets-in-binary/README.md deleted file mode 100644 index 0c23bb0d..00000000 --- a/examples/assets-in-binary/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# Building a single binary containing templates - -This is a complete example to create a single binary with the -[gin-gonic/gin][gin] Web Server with HTML templates. - -[gin]: https://github.com/gin-gonic/gin - -## How to use - -### Prepare Packages - -``` -go get github.com/gin-gonic/gin -go get github.com/jessevdk/go-assets-builder -``` - -### Generate assets.go - -``` -go-assets-builder html -o assets.go -``` - -### Build the server - -``` -go build -o assets-in-binary -``` - -### Run - -``` -./assets-in-binary -``` diff --git a/examples/assets-in-binary/assets.go b/examples/assets-in-binary/assets.go deleted file mode 100644 index dcc5c465..00000000 --- a/examples/assets-in-binary/assets.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -import ( - "time" - - "github.com/jessevdk/go-assets" -) - -var _Assetsbfa8d115ce0617d89507412d5393a462f8e9b003 = "\n\n

Can you see this? → {{.Bar}}

\n\n" -var _Assets3737a75b5254ed1f6d588b40a3449721f9ea86c2 = "\n\n

Hello, {{.Foo}}

\n\n" - -// Assets returns go-assets FileSystem -var Assets = assets.NewFileSystem(map[string][]string{"/": {"html"}, "/html": {"bar.tmpl", "index.tmpl"}}, map[string]*assets.File{ - "/": { - Path: "/", - FileMode: 0x800001ed, - Mtime: time.Unix(1524365738, 1524365738517125470), - Data: nil, - }, "/html": { - Path: "/html", - FileMode: 0x800001ed, - Mtime: time.Unix(1524365491, 1524365491289799093), - Data: nil, - }, "/html/bar.tmpl": { - Path: "/html/bar.tmpl", - FileMode: 0x1a4, - Mtime: time.Unix(1524365491, 1524365491289611557), - Data: []byte(_Assetsbfa8d115ce0617d89507412d5393a462f8e9b003), - }, "/html/index.tmpl": { - Path: "/html/index.tmpl", - FileMode: 0x1a4, - Mtime: time.Unix(1524365491, 1524365491289995821), - Data: []byte(_Assets3737a75b5254ed1f6d588b40a3449721f9ea86c2), - }}, "") diff --git a/examples/assets-in-binary/html/bar.tmpl b/examples/assets-in-binary/html/bar.tmpl deleted file mode 100644 index c8e1c0ff..00000000 --- a/examples/assets-in-binary/html/bar.tmpl +++ /dev/null @@ -1,4 +0,0 @@ - - -

Can you see this? → {{.Bar}}

- diff --git a/examples/assets-in-binary/html/index.tmpl b/examples/assets-in-binary/html/index.tmpl deleted file mode 100644 index 6904fd58..00000000 --- a/examples/assets-in-binary/html/index.tmpl +++ /dev/null @@ -1,4 +0,0 @@ - - -

Hello, {{.Foo}}

- diff --git a/examples/assets-in-binary/main.go b/examples/assets-in-binary/main.go deleted file mode 100644 index 27bc3b17..00000000 --- a/examples/assets-in-binary/main.go +++ /dev/null @@ -1,48 +0,0 @@ -package main - -import ( - "html/template" - "io/ioutil" - "net/http" - "strings" - - "github.com/gin-gonic/gin" -) - -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", gin.H{ - "Foo": "World", - }) - }) - r.GET("/bar", func(c *gin.Context) { - c.HTML(http.StatusOK, "/html/bar.tmpl", gin.H{ - "Bar": "World", - }) - }) - r.Run(":8080") -} - -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 -} diff --git a/examples/auto-tls/example1/main.go b/examples/auto-tls/example1/main.go deleted file mode 100644 index fa9f4008..00000000 --- a/examples/auto-tls/example1/main.go +++ /dev/null @@ -1,19 +0,0 @@ -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")) -} diff --git a/examples/auto-tls/example2/main.go b/examples/auto-tls/example2/main.go deleted file mode 100644 index 01718689..00000000 --- a/examples/auto-tls/example2/main.go +++ /dev/null @@ -1,26 +0,0 @@ -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)) -} diff --git a/examples/basic/main.go b/examples/basic/main.go deleted file mode 100644 index 1c9e0ac4..00000000 --- a/examples/basic/main.go +++ /dev/null @@ -1,65 +0,0 @@ -package main - -import ( - "net/http" - - "github.com/gin-gonic/gin" -) - -var db = make(map[string]string) - -func setupRouter() *gin.Engine { - // Disable Console Color - // gin.DisableConsoleColor() - r := gin.Default() - - // Ping test - r.GET("/ping", func(c *gin.Context) { - 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] - if ok { - c.JSON(http.StatusOK, gin.H{"user": user, "value": value}) - } else { - c.JSON(http.StatusOK, gin.H{"user": user, "status": "no value"}) - } - }) - - // Authorized group (uses gin.BasicAuth() middleware) - // Same than: - // authorized := r.Group("/") - // authorized.Use(gin.BasicAuth(gin.Credentials{ - // "foo": "bar", - // "manu": "123", - //})) - authorized := r.Group("/", gin.BasicAuth(gin.Accounts{ - "foo": "bar", // user:foo password:bar - "manu": "123", // user:manu password:123 - })) - - authorized.POST("admin", func(c *gin.Context) { - user := c.MustGet(gin.AuthUserKey).(string) - - // Parse JSON - var json struct { - Value string `json:"value" binding:"required"` - } - - if c.Bind(&json) == nil { - db[user] = json.Value - c.JSON(http.StatusOK, gin.H{"status": "ok"}) - } - }) - - return r -} - -func main() { - r := setupRouter() - // Listen and Server in 0.0.0.0:8080 - r.Run(":8080") -} diff --git a/examples/basic/main_test.go b/examples/basic/main_test.go deleted file mode 100644 index 5eb85240..00000000 --- a/examples/basic/main_test.go +++ /dev/null @@ -1,20 +0,0 @@ -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, 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 deleted file mode 100644 index 9238200b..00000000 --- a/examples/custom-validation/server.go +++ /dev/null @@ -1,50 +0,0 @@ -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()}) - } -} diff --git a/examples/favicon/favicon.ico b/examples/favicon/favicon.ico deleted file mode 100644 index 3959cd7f9b13333df2f132f736c5f1d562278895..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1150 zcmZvaZAep57{~u-FK%{oZPV!zqumdgW0lj0f*e`-B@(PiL9v7&C`hOvv*<%AsnCmM z1{IQ-Wf{?n%U;YZA+mQGi`IuSL&;vX7hRjRdwR}RV$i+(&bjCOpZ|03bM6B$x{XHA zXu~=U5Dy@Qpe@>o?9&907Ar*vum3Z+TrJ4TYEn6!ZB~b)J=Nj3J|jKdZO+d>z?Lj& z_>Uv9Ww+Nrr5c6J){<rb+kFDFx&2;ZqPs+vPbDNJxiNe8wtw=N&0AqM zcOowC@WP1`Pd?LV&T_`u9s(V?#9GE$d$rm#++a9y!(ypRwpflKCMItzha??@B<-87 z(;f9PR?mT^uRv=S;09x7Del60;CM)-s^@zB;Z{kiHbqD84*MC~Los%yRyWo#)=b{h z#QD3xRReH|V%h}LfOCC7GksU{PmQKwjaDo2mXP-%)qNq6u}&Y*MP9+}6F@NoTK@R2 zP_C|fe|5$>T2-osD8^2c?j<~Pfu3)`8}jxNo_zpKI7zsP<59r#D-m79ynF#Xumbn# z{X@j3OvZlrfgou=h@NW3g#Qq6m8hUU|CWjodXQ=udBa%0jlhHneqtnDl9WM7;#`tK zJSwVhe?o%ri~4U8^*X)&Xh zx9Du{kiWw?dGcUb7csINYBHJn)mG~eoK8P-ayVQrWot$TR|xKKe11%47^~HG!({SM zZ$F)pmNxuOXf7B3o{^zsFId1abLIf$23D`;g80HheyyN@^XzQDUzU9OoZinxyEuqKAfmpL|X4q(TQVDg3yLC>a53eU?Md^Kmz%hLJtUsn|s zegCv^qr!`egXgQOL#DDaWy~9SFd{Xz2M&iX)o%ZKv$T|#)YK?vcJ2(^FE0&)6%-lPA8Q&?sB<0v3N1ZvSkD1^X3h2@%dik{d=zVczBH0%jvIh>Q6<# zXwkPLxw(DI3kp7ra|274KB09EcI_HTuB;3gtEvM3=#7mZP**pKy?X=J%F5yR+S;FE kdSEK>WfFCrj=Hk~C=--X$BaX)h1R8x#EIB`qMMHYHxij({r~^~ diff --git a/examples/favicon/main.go b/examples/favicon/main.go deleted file mode 100644 index d32ca098..00000000 --- a/examples/favicon/main.go +++ /dev/null @@ -1,17 +0,0 @@ -package main - -import ( - "net/http" - - "github.com/gin-gonic/gin" - "github.com/thinkerou/favicon" -) - -func main() { - app := gin.Default() - app.Use(favicon.New("./favicon.ico")) - app.GET("/ping", func(c *gin.Context) { - c.String(http.StatusOK, "Hello favicon.") - }) - app.Run(":8080") -} diff --git a/examples/graceful-shutdown/close/server.go b/examples/graceful-shutdown/close/server.go deleted file mode 100644 index 9c4e90fa..00000000 --- a/examples/graceful-shutdown/close/server.go +++ /dev/null @@ -1,45 +0,0 @@ -// +build go1.8 - -package main - -import ( - "log" - "net/http" - "os" - "os/signal" - - "github.com/gin-gonic/gin" -) - -func main() { - router := gin.Default() - router.GET("/", func(c *gin.Context) { - c.String(http.StatusOK, "Welcome Gin Server") - }) - - server := &http.Server{ - Addr: ":8080", - Handler: router, - } - - quit := make(chan os.Signal) - signal.Notify(quit, os.Interrupt) - - go func() { - <-quit - log.Println("receive interrupt signal") - if err := server.Close(); err != nil { - log.Fatal("Server Close:", err) - } - }() - - if err := server.ListenAndServe(); err != nil { - if err == http.ErrServerClosed { - log.Println("Server closed under request") - } else { - log.Fatal("Server closed unexpect") - } - } - - log.Println("Server exiting") -} diff --git a/examples/graceful-shutdown/graceful-shutdown/server.go b/examples/graceful-shutdown/graceful-shutdown/server.go deleted file mode 100644 index 999a209e..00000000 --- a/examples/graceful-shutdown/graceful-shutdown/server.go +++ /dev/null @@ -1,57 +0,0 @@ -// +build go1.8 - -package main - -import ( - "context" - "log" - "net/http" - "os" - "os/signal" - "syscall" - "time" - - "github.com/gin-gonic/gin" -) - -func main() { - router := gin.Default() - router.GET("/", func(c *gin.Context) { - time.Sleep(5 * time.Second) - c.String(http.StatusOK, "Welcome Gin Server") - }) - - srv := &http.Server{ - Addr: ":8080", - Handler: router, - } - - go func() { - // service connections - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatalf("listen: %s\n", err) - } - }() - - // Wait for interrupt signal to gracefully shutdown the server with - // a timeout of 5 seconds. - quit := make(chan os.Signal) - // kill (no param) default send syscanll.SIGTERM - // kill -2 is syscall.SIGINT - // kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - <-quit - log.Println("Shutdown Server ...") - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := srv.Shutdown(ctx); err != nil { - log.Fatal("Server Shutdown:", err) - } - // catching ctx.Done(). timeout of 5 seconds. - select { - case <-ctx.Done(): - log.Println("timeout of 5 seconds.") - } - log.Println("Server exiting") -} diff --git a/examples/grpc/README.md b/examples/grpc/README.md deleted file mode 100644 index a96d3c1c..00000000 --- a/examples/grpc/README.md +++ /dev/null @@ -1,19 +0,0 @@ -## 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 deleted file mode 100644 index 820e65a3..00000000 --- a/examples/grpc/gin/main.go +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index d9bf9fc5..00000000 --- a/examples/grpc/grpc/server.go +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index c8c8942a..00000000 --- a/examples/grpc/pb/helloworld.pb.go +++ /dev/null @@ -1,151 +0,0 @@ -// 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 deleted file mode 100644 index d79a6a0d..00000000 --- a/examples/grpc/pb/helloworld.proto +++ /dev/null @@ -1,37 +0,0 @@ -// 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 deleted file mode 100644 index 05271b67..00000000 --- a/examples/http-pusher/assets/app.js +++ /dev/null @@ -1 +0,0 @@ -console.log("http2 pusher"); diff --git a/examples/http-pusher/main.go b/examples/http-pusher/main.go deleted file mode 100644 index d4f33aa0..00000000 --- a/examples/http-pusher/main.go +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index 6c8511a7..00000000 --- a/examples/http-pusher/testdata/ca.pem +++ /dev/null @@ -1,15 +0,0 @@ ------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 deleted file mode 100644 index 143a5b87..00000000 --- a/examples/http-pusher/testdata/server.key +++ /dev/null @@ -1,16 +0,0 @@ ------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 deleted file mode 100644 index f3d43fcc..00000000 --- a/examples/http-pusher/testdata/server.pem +++ /dev/null @@ -1,16 +0,0 @@ ------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/README.md b/examples/http2/README.md deleted file mode 100644 index 42dd4b8a..00000000 --- a/examples/http2/README.md +++ /dev/null @@ -1,18 +0,0 @@ -## How to generate RSA private key and digital certificate - -1. Install Openssl - -Please visit https://github.com/openssl/openssl to get pkg and install. - -2. Generate RSA private key - -```sh -$ mkdir testdata -$ openssl genrsa -out ./testdata/server.key 2048 -``` - -3. Generate digital certificate - -```sh -$ openssl req -new -x509 -key ./testdata/server.key -out ./testdata/server.pem -days 365 -``` diff --git a/examples/http2/main.go b/examples/http2/main.go deleted file mode 100644 index 6598a4c9..00000000 --- a/examples/http2/main.go +++ /dev/null @@ -1,38 +0,0 @@ -package main - -import ( - "html/template" - "log" - "net/http" - "os" - - "github.com/gin-gonic/gin" -) - -var html = template.Must(template.New("https").Parse(` - - - Https Test - - -

Welcome, Ginner!

- - -`)) - -func main() { - logger := log.New(os.Stderr, "", 0) - logger.Println("[WARNING] DON'T USE THE EMBED CERTS FROM THIS EXAMPLE IN PRODUCTION ENVIRONMENT, GENERATE YOUR OWN!") - - r := gin.Default() - r.SetHTMLTemplate(html) - - r.GET("/welcome", func(c *gin.Context) { - c.HTML(http.StatusOK, "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/http2/testdata/ca.pem b/examples/http2/testdata/ca.pem deleted file mode 100644 index 6c8511a7..00000000 --- a/examples/http2/testdata/ca.pem +++ /dev/null @@ -1,15 +0,0 @@ ------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/http2/testdata/server.key b/examples/http2/testdata/server.key deleted file mode 100644 index 143a5b87..00000000 --- a/examples/http2/testdata/server.key +++ /dev/null @@ -1,16 +0,0 @@ ------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/http2/testdata/server.pem b/examples/http2/testdata/server.pem deleted file mode 100644 index f3d43fcc..00000000 --- a/examples/http2/testdata/server.pem +++ /dev/null @@ -1,16 +0,0 @@ ------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/multiple-service/main.go b/examples/multiple-service/main.go deleted file mode 100644 index ceddaa2e..00000000 --- a/examples/multiple-service/main.go +++ /dev/null @@ -1,74 +0,0 @@ -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) - } -} diff --git a/examples/new_relic/README.md b/examples/new_relic/README.md deleted file mode 100644 index 70f14942..00000000 --- a/examples/new_relic/README.md +++ /dev/null @@ -1,30 +0,0 @@ -The [New Relic Go Agent](https://github.com/newrelic/go-agent) provides a nice middleware for the stdlib handler signature. -The following is an adaptation of that middleware for Gin. - -```golang -const ( - // NewRelicTxnKey is the key used to retrieve the NewRelic Transaction from the context - NewRelicTxnKey = "NewRelicTxnKey" -) - -// NewRelicMonitoring is a middleware that starts a newrelic transaction, stores it in the context, then calls the next handler -func NewRelicMonitoring(app newrelic.Application) gin.HandlerFunc { - return func(ctx *gin.Context) { - txn := app.StartTransaction(ctx.Request.URL.Path, ctx.Writer, ctx.Request) - defer txn.End() - ctx.Set(NewRelicTxnKey, txn) - ctx.Next() - } -} -``` -and in `main.go` or equivalent... -```golang -router := gin.Default() -cfg := newrelic.NewConfig(os.Getenv("APP_NAME"), os.Getenv("NEW_RELIC_API_KEY")) -app, err := newrelic.NewApplication(cfg) -if err != nil { - log.Printf("failed to make new_relic app: %v", err) -} else { - router.Use(adapters.NewRelicMonitoring(app)) -} - ``` diff --git a/examples/new_relic/main.go b/examples/new_relic/main.go deleted file mode 100644 index f85f7831..00000000 --- a/examples/new_relic/main.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - "log" - "net/http" - "os" - - "github.com/gin-gonic/gin" - "github.com/newrelic/go-agent" -) - -const ( - // NewRelicTxnKey is the key used to retrieve the NewRelic Transaction from the context - NewRelicTxnKey = "NewRelicTxnKey" -) - -// NewRelicMonitoring is a middleware that starts a newrelic transaction, stores it in the context, then calls the next handler -func NewRelicMonitoring(app newrelic.Application) gin.HandlerFunc { - return func(ctx *gin.Context) { - txn := app.StartTransaction(ctx.Request.URL.Path, ctx.Writer, ctx.Request) - defer txn.End() - ctx.Set(NewRelicTxnKey, txn) - ctx.Next() - } -} - -func main() { - router := gin.Default() - - cfg := newrelic.NewConfig(os.Getenv("APP_NAME"), os.Getenv("NEW_RELIC_API_KEY")) - app, err := newrelic.NewApplication(cfg) - if err != nil { - log.Printf("failed to make new_relic app: %v", err) - } else { - router.Use(NewRelicMonitoring(app)) - } - - router.GET("/", func(c *gin.Context) { - c.String(http.StatusOK, "Hello World!\n") - }) - router.Run() -} diff --git a/examples/realtime-advanced/Makefile b/examples/realtime-advanced/Makefile deleted file mode 100644 index 104ce809..00000000 --- a/examples/realtime-advanced/Makefile +++ /dev/null @@ -1,10 +0,0 @@ -all: deps build - -.PHONY: deps -deps: - go get -d -v github.com/dustin/go-broadcast/... - go get -d -v github.com/manucorporat/stats/... - -.PHONY: build -build: deps - go build -o realtime-advanced main.go rooms.go routes.go stats.go diff --git a/examples/realtime-advanced/main.go b/examples/realtime-advanced/main.go deleted file mode 100644 index f3ead476..00000000 --- a/examples/realtime-advanced/main.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - "fmt" - "runtime" - - "github.com/gin-gonic/gin" -) - -func main() { - ConfigRuntime() - StartWorkers() - 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) - - router := gin.New() - router.Use(rateLimit, gin.Recovery()) - router.LoadHTMLGlob("resources/*.templ.html") - router.Static("/static", "resources/static") - router.GET("/", index) - router.GET("/room/:roomid", roomGET) - router.POST("/room-post/:roomid", roomPOST) - router.GET("/stream/:roomid", streamRoom) - - router.Run(":80") -} diff --git a/examples/realtime-advanced/resources/room_login.templ.html b/examples/realtime-advanced/resources/room_login.templ.html deleted file mode 100644 index 905c012f..00000000 --- a/examples/realtime-advanced/resources/room_login.templ.html +++ /dev/null @@ -1,208 +0,0 @@ - - - - - - - Server-Sent Events. Room "{{.roomid}}" - - - - - - - - - - - - - - - - - - - - - - - -
-
-

Server-Sent Events in Go

-

Server-sent events (SSE) is a technology where a browser receives automatic updates from a server via HTTP connection. It is not websockets. Learn more.

-

The chat and the charts data is provided in realtime using the SSE implementation of Gin Framework.

-
-
-
- - - - - - - - -
NickMessage
-
- {{if .nick}} -
-
- -
-
{{.nick}}
- -
-
- -
- {{else}} -
- Join the SSE real-time chat -
- -
-
- -
-
- {{end}} -
-
-
-

- ◼︎ Users
- ◼︎ Inbound messages / sec
- ◼︎ Outbound messages / sec
-

-
-
-
-
-
-
-

Realtime server Go stats

-
-

Memory usage

-

-

-

-

- ◼︎ Heap bytes
- ◼︎ Stack bytes
-

-
-
-

Allocations per second

-

-

-

-

- ◼︎ Mallocs / sec
- ◼︎ Frees / sec
-

-
-
-
-

MIT Open Sourced

- -
- -

Server-side (Go)

-
func streamRoom(c *gin.Context) {
-    roomid := c.ParamValue("roomid")
-    listener := openListener(roomid)
-    statsTicker := time.NewTicker(1 * time.Second)
-    defer closeListener(roomid, listener)
-    defer statsTicker.Stop()
-
-    c.Stream(func(w io.Writer) bool {
-        select {
-        case msg := <-listener:
-            c.SSEvent("message", msg)
-        case <-statsTicker.C:
-            c.SSEvent("stats", Stats())
-        }
-        return true
-    })
-}
-
-
-

Client-side (JS)

-
function StartSSE(roomid) {
-    var source = new EventSource('/stream/'+roomid);
-    source.addEventListener('message', newChatMessage, false);
-    source.addEventListener('stats', stats, false);
-}
-
-
-
-
-

SSE package

-
import "github.com/manucorporat/sse"
-
-func httpHandler(w http.ResponseWriter, req *http.Request) {
-    // data can be a primitive like a string, an integer or a float
-    sse.Encode(w, sse.Event{
-        Event: "message",
-        Data:  "some data\nmore data",
-    })
-
-    // also a complex type, like a map, a struct or a slice
-    sse.Encode(w, sse.Event{
-        Id:    "124",
-        Event: "message",
-        Data: map[string]interface{}{
-            "user":    "manu",
-            "date":    time.Now().Unix(),
-            "content": "hi!",
-        },
-    })
-}
-
event: message
-data: some data\\nmore data
-
-id: 124
-event: message
-data: {"content":"hi!","date":1431540810,"user":"manu"}
-
-
-
- -
- - diff --git a/examples/realtime-advanced/resources/static/epoch.min.css b/examples/realtime-advanced/resources/static/epoch.min.css deleted file mode 100644 index 47a80cdc..00000000 --- a/examples/realtime-advanced/resources/static/epoch.min.css +++ /dev/null @@ -1 +0,0 @@ -.epoch .axis path,.epoch .axis line{shape-rendering:crispEdges;}.epoch .axis.canvas .tick line{shape-rendering:geometricPrecision;}div#_canvas_css_reference{width:0;height:0;position:absolute;top:-1000px;left:-1000px;}div#_canvas_css_reference svg{position:absolute;width:0;height:0;top:-1000px;left:-1000px;}.epoch{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12pt;}.epoch .axis path,.epoch .axis line{fill:none;stroke:#000;}.epoch .axis .tick text{font-size:9pt;}.epoch .line{fill:none;stroke-width:2px;}.epoch.sparklines .line{stroke-width:1px;}.epoch .area{stroke:none;}.epoch .arc.pie{stroke:#fff;stroke-width:1.5px;}.epoch .arc.pie text{stroke:none;fill:white;font-size:9pt;}.epoch .gauge-labels .value{text-anchor:middle;font-size:140%;fill:#666;}.epoch.gauge-tiny{width:120px;height:90px;}.epoch.gauge-tiny .gauge-labels .value{font-size:80%;}.epoch.gauge-tiny .gauge .arc.outer{stroke-width:2px;}.epoch.gauge-small{width:180px;height:135px;}.epoch.gauge-small .gauge-labels .value{font-size:120%;}.epoch.gauge-small .gauge .arc.outer{stroke-width:3px;}.epoch.gauge-medium{width:240px;height:180px;}.epoch.gauge-medium .gauge .arc.outer{stroke-width:3px;}.epoch.gauge-large{width:320px;height:240px;}.epoch.gauge-large .gauge-labels .value{font-size:180%;}.epoch .gauge .arc.outer{stroke-width:4px;stroke:#666;}.epoch .gauge .arc.inner{stroke-width:1px;stroke:#555;}.epoch .gauge .tick{stroke-width:1px;stroke:#555;}.epoch .gauge .needle{fill:orange;}.epoch .gauge .needle-base{fill:#666;}.epoch div.ref.category1,.epoch.category10 div.ref.category1{background-color:#1f77b4;}.epoch .category1 .line,.epoch.category10 .category1 .line{stroke:#1f77b4;}.epoch .category1 .area,.epoch .category1 .dot,.epoch.category10 .category1 .area,.epoch.category10 .category1 .dot{fill:#1f77b4;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category1 path,.epoch.category10 .arc.category1 path{fill:#1f77b4;}.epoch .bar.category1,.epoch.category10 .bar.category1{fill:#1f77b4;}.epoch div.ref.category2,.epoch.category10 div.ref.category2{background-color:#ff7f0e;}.epoch .category2 .line,.epoch.category10 .category2 .line{stroke:#ff7f0e;}.epoch .category2 .area,.epoch .category2 .dot,.epoch.category10 .category2 .area,.epoch.category10 .category2 .dot{fill:#ff7f0e;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category2 path,.epoch.category10 .arc.category2 path{fill:#ff7f0e;}.epoch .bar.category2,.epoch.category10 .bar.category2{fill:#ff7f0e;}.epoch div.ref.category3,.epoch.category10 div.ref.category3{background-color:#2ca02c;}.epoch .category3 .line,.epoch.category10 .category3 .line{stroke:#2ca02c;}.epoch .category3 .area,.epoch .category3 .dot,.epoch.category10 .category3 .area,.epoch.category10 .category3 .dot{fill:#2ca02c;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category3 path,.epoch.category10 .arc.category3 path{fill:#2ca02c;}.epoch .bar.category3,.epoch.category10 .bar.category3{fill:#2ca02c;}.epoch div.ref.category4,.epoch.category10 div.ref.category4{background-color:#d62728;}.epoch .category4 .line,.epoch.category10 .category4 .line{stroke:#d62728;}.epoch .category4 .area,.epoch .category4 .dot,.epoch.category10 .category4 .area,.epoch.category10 .category4 .dot{fill:#d62728;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category4 path,.epoch.category10 .arc.category4 path{fill:#d62728;}.epoch .bar.category4,.epoch.category10 .bar.category4{fill:#d62728;}.epoch div.ref.category5,.epoch.category10 div.ref.category5{background-color:#9467bd;}.epoch .category5 .line,.epoch.category10 .category5 .line{stroke:#9467bd;}.epoch .category5 .area,.epoch .category5 .dot,.epoch.category10 .category5 .area,.epoch.category10 .category5 .dot{fill:#9467bd;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category5 path,.epoch.category10 .arc.category5 path{fill:#9467bd;}.epoch .bar.category5,.epoch.category10 .bar.category5{fill:#9467bd;}.epoch div.ref.category6,.epoch.category10 div.ref.category6{background-color:#8c564b;}.epoch .category6 .line,.epoch.category10 .category6 .line{stroke:#8c564b;}.epoch .category6 .area,.epoch .category6 .dot,.epoch.category10 .category6 .area,.epoch.category10 .category6 .dot{fill:#8c564b;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category6 path,.epoch.category10 .arc.category6 path{fill:#8c564b;}.epoch .bar.category6,.epoch.category10 .bar.category6{fill:#8c564b;}.epoch div.ref.category7,.epoch.category10 div.ref.category7{background-color:#e377c2;}.epoch .category7 .line,.epoch.category10 .category7 .line{stroke:#e377c2;}.epoch .category7 .area,.epoch .category7 .dot,.epoch.category10 .category7 .area,.epoch.category10 .category7 .dot{fill:#e377c2;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category7 path,.epoch.category10 .arc.category7 path{fill:#e377c2;}.epoch .bar.category7,.epoch.category10 .bar.category7{fill:#e377c2;}.epoch div.ref.category8,.epoch.category10 div.ref.category8{background-color:#7f7f7f;}.epoch .category8 .line,.epoch.category10 .category8 .line{stroke:#7f7f7f;}.epoch .category8 .area,.epoch .category8 .dot,.epoch.category10 .category8 .area,.epoch.category10 .category8 .dot{fill:#7f7f7f;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category8 path,.epoch.category10 .arc.category8 path{fill:#7f7f7f;}.epoch .bar.category8,.epoch.category10 .bar.category8{fill:#7f7f7f;}.epoch div.ref.category9,.epoch.category10 div.ref.category9{background-color:#bcbd22;}.epoch .category9 .line,.epoch.category10 .category9 .line{stroke:#bcbd22;}.epoch .category9 .area,.epoch .category9 .dot,.epoch.category10 .category9 .area,.epoch.category10 .category9 .dot{fill:#bcbd22;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category9 path,.epoch.category10 .arc.category9 path{fill:#bcbd22;}.epoch .bar.category9,.epoch.category10 .bar.category9{fill:#bcbd22;}.epoch div.ref.category10,.epoch.category10 div.ref.category10{background-color:#17becf;}.epoch .category10 .line,.epoch.category10 .category10 .line{stroke:#17becf;}.epoch .category10 .area,.epoch .category10 .dot,.epoch.category10 .category10 .area,.epoch.category10 .category10 .dot{fill:#17becf;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category10 path,.epoch.category10 .arc.category10 path{fill:#17becf;}.epoch .bar.category10,.epoch.category10 .bar.category10{fill:#17becf;}.epoch.category20 div.ref.category1{background-color:#1f77b4;}.epoch.category20 .category1 .line{stroke:#1f77b4;}.epoch.category20 .category1 .area,.epoch.category20 .category1 .dot{fill:#1f77b4;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category1 path{fill:#1f77b4;}.epoch.category20 .bar.category1{fill:#1f77b4;}.epoch.category20 div.ref.category2{background-color:#aec7e8;}.epoch.category20 .category2 .line{stroke:#aec7e8;}.epoch.category20 .category2 .area,.epoch.category20 .category2 .dot{fill:#aec7e8;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category2 path{fill:#aec7e8;}.epoch.category20 .bar.category2{fill:#aec7e8;}.epoch.category20 div.ref.category3{background-color:#ff7f0e;}.epoch.category20 .category3 .line{stroke:#ff7f0e;}.epoch.category20 .category3 .area,.epoch.category20 .category3 .dot{fill:#ff7f0e;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category3 path{fill:#ff7f0e;}.epoch.category20 .bar.category3{fill:#ff7f0e;}.epoch.category20 div.ref.category4{background-color:#ffbb78;}.epoch.category20 .category4 .line{stroke:#ffbb78;}.epoch.category20 .category4 .area,.epoch.category20 .category4 .dot{fill:#ffbb78;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category4 path{fill:#ffbb78;}.epoch.category20 .bar.category4{fill:#ffbb78;}.epoch.category20 div.ref.category5{background-color:#2ca02c;}.epoch.category20 .category5 .line{stroke:#2ca02c;}.epoch.category20 .category5 .area,.epoch.category20 .category5 .dot{fill:#2ca02c;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category5 path{fill:#2ca02c;}.epoch.category20 .bar.category5{fill:#2ca02c;}.epoch.category20 div.ref.category6{background-color:#98df8a;}.epoch.category20 .category6 .line{stroke:#98df8a;}.epoch.category20 .category6 .area,.epoch.category20 .category6 .dot{fill:#98df8a;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category6 path{fill:#98df8a;}.epoch.category20 .bar.category6{fill:#98df8a;}.epoch.category20 div.ref.category7{background-color:#d62728;}.epoch.category20 .category7 .line{stroke:#d62728;}.epoch.category20 .category7 .area,.epoch.category20 .category7 .dot{fill:#d62728;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category7 path{fill:#d62728;}.epoch.category20 .bar.category7{fill:#d62728;}.epoch.category20 div.ref.category8{background-color:#ff9896;}.epoch.category20 .category8 .line{stroke:#ff9896;}.epoch.category20 .category8 .area,.epoch.category20 .category8 .dot{fill:#ff9896;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category8 path{fill:#ff9896;}.epoch.category20 .bar.category8{fill:#ff9896;}.epoch.category20 div.ref.category9{background-color:#9467bd;}.epoch.category20 .category9 .line{stroke:#9467bd;}.epoch.category20 .category9 .area,.epoch.category20 .category9 .dot{fill:#9467bd;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category9 path{fill:#9467bd;}.epoch.category20 .bar.category9{fill:#9467bd;}.epoch.category20 div.ref.category10{background-color:#c5b0d5;}.epoch.category20 .category10 .line{stroke:#c5b0d5;}.epoch.category20 .category10 .area,.epoch.category20 .category10 .dot{fill:#c5b0d5;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category10 path{fill:#c5b0d5;}.epoch.category20 .bar.category10{fill:#c5b0d5;}.epoch.category20 div.ref.category11{background-color:#8c564b;}.epoch.category20 .category11 .line{stroke:#8c564b;}.epoch.category20 .category11 .area,.epoch.category20 .category11 .dot{fill:#8c564b;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category11 path{fill:#8c564b;}.epoch.category20 .bar.category11{fill:#8c564b;}.epoch.category20 div.ref.category12{background-color:#c49c94;}.epoch.category20 .category12 .line{stroke:#c49c94;}.epoch.category20 .category12 .area,.epoch.category20 .category12 .dot{fill:#c49c94;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category12 path{fill:#c49c94;}.epoch.category20 .bar.category12{fill:#c49c94;}.epoch.category20 div.ref.category13{background-color:#e377c2;}.epoch.category20 .category13 .line{stroke:#e377c2;}.epoch.category20 .category13 .area,.epoch.category20 .category13 .dot{fill:#e377c2;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category13 path{fill:#e377c2;}.epoch.category20 .bar.category13{fill:#e377c2;}.epoch.category20 div.ref.category14{background-color:#f7b6d2;}.epoch.category20 .category14 .line{stroke:#f7b6d2;}.epoch.category20 .category14 .area,.epoch.category20 .category14 .dot{fill:#f7b6d2;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category14 path{fill:#f7b6d2;}.epoch.category20 .bar.category14{fill:#f7b6d2;}.epoch.category20 div.ref.category15{background-color:#7f7f7f;}.epoch.category20 .category15 .line{stroke:#7f7f7f;}.epoch.category20 .category15 .area,.epoch.category20 .category15 .dot{fill:#7f7f7f;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category15 path{fill:#7f7f7f;}.epoch.category20 .bar.category15{fill:#7f7f7f;}.epoch.category20 div.ref.category16{background-color:#c7c7c7;}.epoch.category20 .category16 .line{stroke:#c7c7c7;}.epoch.category20 .category16 .area,.epoch.category20 .category16 .dot{fill:#c7c7c7;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category16 path{fill:#c7c7c7;}.epoch.category20 .bar.category16{fill:#c7c7c7;}.epoch.category20 div.ref.category17{background-color:#bcbd22;}.epoch.category20 .category17 .line{stroke:#bcbd22;}.epoch.category20 .category17 .area,.epoch.category20 .category17 .dot{fill:#bcbd22;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category17 path{fill:#bcbd22;}.epoch.category20 .bar.category17{fill:#bcbd22;}.epoch.category20 div.ref.category18{background-color:#dbdb8d;}.epoch.category20 .category18 .line{stroke:#dbdb8d;}.epoch.category20 .category18 .area,.epoch.category20 .category18 .dot{fill:#dbdb8d;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category18 path{fill:#dbdb8d;}.epoch.category20 .bar.category18{fill:#dbdb8d;}.epoch.category20 div.ref.category19{background-color:#17becf;}.epoch.category20 .category19 .line{stroke:#17becf;}.epoch.category20 .category19 .area,.epoch.category20 .category19 .dot{fill:#17becf;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category19 path{fill:#17becf;}.epoch.category20 .bar.category19{fill:#17becf;}.epoch.category20 div.ref.category20{background-color:#9edae5;}.epoch.category20 .category20 .line{stroke:#9edae5;}.epoch.category20 .category20 .area,.epoch.category20 .category20 .dot{fill:#9edae5;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category20 path{fill:#9edae5;}.epoch.category20 .bar.category20{fill:#9edae5;}.epoch.category20b div.ref.category1{background-color:#393b79;}.epoch.category20b .category1 .line{stroke:#393b79;}.epoch.category20b .category1 .area,.epoch.category20b .category1 .dot{fill:#393b79;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category1 path{fill:#393b79;}.epoch.category20b .bar.category1{fill:#393b79;}.epoch.category20b div.ref.category2{background-color:#5254a3;}.epoch.category20b .category2 .line{stroke:#5254a3;}.epoch.category20b .category2 .area,.epoch.category20b .category2 .dot{fill:#5254a3;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category2 path{fill:#5254a3;}.epoch.category20b .bar.category2{fill:#5254a3;}.epoch.category20b div.ref.category3{background-color:#6b6ecf;}.epoch.category20b .category3 .line{stroke:#6b6ecf;}.epoch.category20b .category3 .area,.epoch.category20b .category3 .dot{fill:#6b6ecf;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category3 path{fill:#6b6ecf;}.epoch.category20b .bar.category3{fill:#6b6ecf;}.epoch.category20b div.ref.category4{background-color:#9c9ede;}.epoch.category20b .category4 .line{stroke:#9c9ede;}.epoch.category20b .category4 .area,.epoch.category20b .category4 .dot{fill:#9c9ede;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category4 path{fill:#9c9ede;}.epoch.category20b .bar.category4{fill:#9c9ede;}.epoch.category20b div.ref.category5{background-color:#637939;}.epoch.category20b .category5 .line{stroke:#637939;}.epoch.category20b .category5 .area,.epoch.category20b .category5 .dot{fill:#637939;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category5 path{fill:#637939;}.epoch.category20b .bar.category5{fill:#637939;}.epoch.category20b div.ref.category6{background-color:#8ca252;}.epoch.category20b .category6 .line{stroke:#8ca252;}.epoch.category20b .category6 .area,.epoch.category20b .category6 .dot{fill:#8ca252;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category6 path{fill:#8ca252;}.epoch.category20b .bar.category6{fill:#8ca252;}.epoch.category20b div.ref.category7{background-color:#b5cf6b;}.epoch.category20b .category7 .line{stroke:#b5cf6b;}.epoch.category20b .category7 .area,.epoch.category20b .category7 .dot{fill:#b5cf6b;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category7 path{fill:#b5cf6b;}.epoch.category20b .bar.category7{fill:#b5cf6b;}.epoch.category20b div.ref.category8{background-color:#cedb9c;}.epoch.category20b .category8 .line{stroke:#cedb9c;}.epoch.category20b .category8 .area,.epoch.category20b .category8 .dot{fill:#cedb9c;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category8 path{fill:#cedb9c;}.epoch.category20b .bar.category8{fill:#cedb9c;}.epoch.category20b div.ref.category9{background-color:#8c6d31;}.epoch.category20b .category9 .line{stroke:#8c6d31;}.epoch.category20b .category9 .area,.epoch.category20b .category9 .dot{fill:#8c6d31;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category9 path{fill:#8c6d31;}.epoch.category20b .bar.category9{fill:#8c6d31;}.epoch.category20b div.ref.category10{background-color:#bd9e39;}.epoch.category20b .category10 .line{stroke:#bd9e39;}.epoch.category20b .category10 .area,.epoch.category20b .category10 .dot{fill:#bd9e39;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category10 path{fill:#bd9e39;}.epoch.category20b .bar.category10{fill:#bd9e39;}.epoch.category20b div.ref.category11{background-color:#e7ba52;}.epoch.category20b .category11 .line{stroke:#e7ba52;}.epoch.category20b .category11 .area,.epoch.category20b .category11 .dot{fill:#e7ba52;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category11 path{fill:#e7ba52;}.epoch.category20b .bar.category11{fill:#e7ba52;}.epoch.category20b div.ref.category12{background-color:#e7cb94;}.epoch.category20b .category12 .line{stroke:#e7cb94;}.epoch.category20b .category12 .area,.epoch.category20b .category12 .dot{fill:#e7cb94;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category12 path{fill:#e7cb94;}.epoch.category20b .bar.category12{fill:#e7cb94;}.epoch.category20b div.ref.category13{background-color:#843c39;}.epoch.category20b .category13 .line{stroke:#843c39;}.epoch.category20b .category13 .area,.epoch.category20b .category13 .dot{fill:#843c39;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category13 path{fill:#843c39;}.epoch.category20b .bar.category13{fill:#843c39;}.epoch.category20b div.ref.category14{background-color:#ad494a;}.epoch.category20b .category14 .line{stroke:#ad494a;}.epoch.category20b .category14 .area,.epoch.category20b .category14 .dot{fill:#ad494a;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category14 path{fill:#ad494a;}.epoch.category20b .bar.category14{fill:#ad494a;}.epoch.category20b div.ref.category15{background-color:#d6616b;}.epoch.category20b .category15 .line{stroke:#d6616b;}.epoch.category20b .category15 .area,.epoch.category20b .category15 .dot{fill:#d6616b;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category15 path{fill:#d6616b;}.epoch.category20b .bar.category15{fill:#d6616b;}.epoch.category20b div.ref.category16{background-color:#e7969c;}.epoch.category20b .category16 .line{stroke:#e7969c;}.epoch.category20b .category16 .area,.epoch.category20b .category16 .dot{fill:#e7969c;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category16 path{fill:#e7969c;}.epoch.category20b .bar.category16{fill:#e7969c;}.epoch.category20b div.ref.category17{background-color:#7b4173;}.epoch.category20b .category17 .line{stroke:#7b4173;}.epoch.category20b .category17 .area,.epoch.category20b .category17 .dot{fill:#7b4173;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category17 path{fill:#7b4173;}.epoch.category20b .bar.category17{fill:#7b4173;}.epoch.category20b div.ref.category18{background-color:#a55194;}.epoch.category20b .category18 .line{stroke:#a55194;}.epoch.category20b .category18 .area,.epoch.category20b .category18 .dot{fill:#a55194;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category18 path{fill:#a55194;}.epoch.category20b .bar.category18{fill:#a55194;}.epoch.category20b div.ref.category19{background-color:#ce6dbd;}.epoch.category20b .category19 .line{stroke:#ce6dbd;}.epoch.category20b .category19 .area,.epoch.category20b .category19 .dot{fill:#ce6dbd;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category19 path{fill:#ce6dbd;}.epoch.category20b .bar.category19{fill:#ce6dbd;}.epoch.category20b div.ref.category20{background-color:#de9ed6;}.epoch.category20b .category20 .line{stroke:#de9ed6;}.epoch.category20b .category20 .area,.epoch.category20b .category20 .dot{fill:#de9ed6;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category20 path{fill:#de9ed6;}.epoch.category20b .bar.category20{fill:#de9ed6;}.epoch.category20c div.ref.category1{background-color:#3182bd;}.epoch.category20c .category1 .line{stroke:#3182bd;}.epoch.category20c .category1 .area,.epoch.category20c .category1 .dot{fill:#3182bd;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category1 path{fill:#3182bd;}.epoch.category20c .bar.category1{fill:#3182bd;}.epoch.category20c div.ref.category2{background-color:#6baed6;}.epoch.category20c .category2 .line{stroke:#6baed6;}.epoch.category20c .category2 .area,.epoch.category20c .category2 .dot{fill:#6baed6;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category2 path{fill:#6baed6;}.epoch.category20c .bar.category2{fill:#6baed6;}.epoch.category20c div.ref.category3{background-color:#9ecae1;}.epoch.category20c .category3 .line{stroke:#9ecae1;}.epoch.category20c .category3 .area,.epoch.category20c .category3 .dot{fill:#9ecae1;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category3 path{fill:#9ecae1;}.epoch.category20c .bar.category3{fill:#9ecae1;}.epoch.category20c div.ref.category4{background-color:#c6dbef;}.epoch.category20c .category4 .line{stroke:#c6dbef;}.epoch.category20c .category4 .area,.epoch.category20c .category4 .dot{fill:#c6dbef;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category4 path{fill:#c6dbef;}.epoch.category20c .bar.category4{fill:#c6dbef;}.epoch.category20c div.ref.category5{background-color:#e6550d;}.epoch.category20c .category5 .line{stroke:#e6550d;}.epoch.category20c .category5 .area,.epoch.category20c .category5 .dot{fill:#e6550d;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category5 path{fill:#e6550d;}.epoch.category20c .bar.category5{fill:#e6550d;}.epoch.category20c div.ref.category6{background-color:#fd8d3c;}.epoch.category20c .category6 .line{stroke:#fd8d3c;}.epoch.category20c .category6 .area,.epoch.category20c .category6 .dot{fill:#fd8d3c;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category6 path{fill:#fd8d3c;}.epoch.category20c .bar.category6{fill:#fd8d3c;}.epoch.category20c div.ref.category7{background-color:#fdae6b;}.epoch.category20c .category7 .line{stroke:#fdae6b;}.epoch.category20c .category7 .area,.epoch.category20c .category7 .dot{fill:#fdae6b;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category7 path{fill:#fdae6b;}.epoch.category20c .bar.category7{fill:#fdae6b;}.epoch.category20c div.ref.category8{background-color:#fdd0a2;}.epoch.category20c .category8 .line{stroke:#fdd0a2;}.epoch.category20c .category8 .area,.epoch.category20c .category8 .dot{fill:#fdd0a2;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category8 path{fill:#fdd0a2;}.epoch.category20c .bar.category8{fill:#fdd0a2;}.epoch.category20c div.ref.category9{background-color:#31a354;}.epoch.category20c .category9 .line{stroke:#31a354;}.epoch.category20c .category9 .area,.epoch.category20c .category9 .dot{fill:#31a354;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category9 path{fill:#31a354;}.epoch.category20c .bar.category9{fill:#31a354;}.epoch.category20c div.ref.category10{background-color:#74c476;}.epoch.category20c .category10 .line{stroke:#74c476;}.epoch.category20c .category10 .area,.epoch.category20c .category10 .dot{fill:#74c476;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category10 path{fill:#74c476;}.epoch.category20c .bar.category10{fill:#74c476;}.epoch.category20c div.ref.category11{background-color:#a1d99b;}.epoch.category20c .category11 .line{stroke:#a1d99b;}.epoch.category20c .category11 .area,.epoch.category20c .category11 .dot{fill:#a1d99b;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category11 path{fill:#a1d99b;}.epoch.category20c .bar.category11{fill:#a1d99b;}.epoch.category20c div.ref.category12{background-color:#c7e9c0;}.epoch.category20c .category12 .line{stroke:#c7e9c0;}.epoch.category20c .category12 .area,.epoch.category20c .category12 .dot{fill:#c7e9c0;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category12 path{fill:#c7e9c0;}.epoch.category20c .bar.category12{fill:#c7e9c0;}.epoch.category20c div.ref.category13{background-color:#756bb1;}.epoch.category20c .category13 .line{stroke:#756bb1;}.epoch.category20c .category13 .area,.epoch.category20c .category13 .dot{fill:#756bb1;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category13 path{fill:#756bb1;}.epoch.category20c .bar.category13{fill:#756bb1;}.epoch.category20c div.ref.category14{background-color:#9e9ac8;}.epoch.category20c .category14 .line{stroke:#9e9ac8;}.epoch.category20c .category14 .area,.epoch.category20c .category14 .dot{fill:#9e9ac8;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category14 path{fill:#9e9ac8;}.epoch.category20c .bar.category14{fill:#9e9ac8;}.epoch.category20c div.ref.category15{background-color:#bcbddc;}.epoch.category20c .category15 .line{stroke:#bcbddc;}.epoch.category20c .category15 .area,.epoch.category20c .category15 .dot{fill:#bcbddc;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category15 path{fill:#bcbddc;}.epoch.category20c .bar.category15{fill:#bcbddc;}.epoch.category20c div.ref.category16{background-color:#dadaeb;}.epoch.category20c .category16 .line{stroke:#dadaeb;}.epoch.category20c .category16 .area,.epoch.category20c .category16 .dot{fill:#dadaeb;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category16 path{fill:#dadaeb;}.epoch.category20c .bar.category16{fill:#dadaeb;}.epoch.category20c div.ref.category17{background-color:#636363;}.epoch.category20c .category17 .line{stroke:#636363;}.epoch.category20c .category17 .area,.epoch.category20c .category17 .dot{fill:#636363;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category17 path{fill:#636363;}.epoch.category20c .bar.category17{fill:#636363;}.epoch.category20c div.ref.category18{background-color:#969696;}.epoch.category20c .category18 .line{stroke:#969696;}.epoch.category20c .category18 .area,.epoch.category20c .category18 .dot{fill:#969696;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category18 path{fill:#969696;}.epoch.category20c .bar.category18{fill:#969696;}.epoch.category20c div.ref.category19{background-color:#bdbdbd;}.epoch.category20c .category19 .line{stroke:#bdbdbd;}.epoch.category20c .category19 .area,.epoch.category20c .category19 .dot{fill:#bdbdbd;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category19 path{fill:#bdbdbd;}.epoch.category20c .bar.category19{fill:#bdbdbd;}.epoch.category20c div.ref.category20{background-color:#d9d9d9;}.epoch.category20c .category20 .line{stroke:#d9d9d9;}.epoch.category20c .category20 .area,.epoch.category20c .category20 .dot{fill:#d9d9d9;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category20 path{fill:#d9d9d9;}.epoch.category20c .bar.category20{fill:#d9d9d9;}.epoch .category1 .bucket,.epoch.heatmap5 .category1 .bucket{fill:#1f77b4;}.epoch .category2 .bucket,.epoch.heatmap5 .category2 .bucket{fill:#2ca02c;}.epoch .category3 .bucket,.epoch.heatmap5 .category3 .bucket{fill:#d62728;}.epoch .category4 .bucket,.epoch.heatmap5 .category4 .bucket{fill:#8c564b;}.epoch .category5 .bucket,.epoch.heatmap5 .category5 .bucket{fill:#7f7f7f;}.epoch-theme-dark .epoch .axis path,.epoch-theme-dark .epoch .axis line{stroke:#d0d0d0;}.epoch-theme-dark .epoch .axis .tick text{fill:#d0d0d0;}.epoch-theme-dark .arc.pie{stroke:#333;}.epoch-theme-dark .arc.pie text{fill:#333;}.epoch-theme-dark .epoch .gauge-labels .value{fill:#BBB;}.epoch-theme-dark .epoch .gauge .arc.outer{stroke:#999;}.epoch-theme-dark .epoch .gauge .arc.inner{stroke:#AAA;}.epoch-theme-dark .epoch .gauge .tick{stroke:#AAA;}.epoch-theme-dark .epoch .gauge .needle{fill:#F3DE88;}.epoch-theme-dark .epoch .gauge .needle-base{fill:#999;}.epoch-theme-dark .epoch div.ref.category1,.epoch-theme-dark .epoch.category10 div.ref.category1{background-color:#909CFF;}.epoch-theme-dark .epoch .category1 .line,.epoch-theme-dark .epoch.category10 .category1 .line{stroke:#909CFF;}.epoch-theme-dark .epoch .category1 .area,.epoch-theme-dark .epoch .category1 .dot,.epoch-theme-dark .epoch.category10 .category1 .area,.epoch-theme-dark .epoch.category10 .category1 .dot{fill:#909CFF;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category1 path,.epoch-theme-dark .epoch.category10 .arc.category1 path{fill:#909CFF;}.epoch-theme-dark .epoch .bar.category1,.epoch-theme-dark .epoch.category10 .bar.category1{fill:#909CFF;}.epoch-theme-dark .epoch div.ref.category2,.epoch-theme-dark .epoch.category10 div.ref.category2{background-color:#FFAC89;}.epoch-theme-dark .epoch .category2 .line,.epoch-theme-dark .epoch.category10 .category2 .line{stroke:#FFAC89;}.epoch-theme-dark .epoch .category2 .area,.epoch-theme-dark .epoch .category2 .dot,.epoch-theme-dark .epoch.category10 .category2 .area,.epoch-theme-dark .epoch.category10 .category2 .dot{fill:#FFAC89;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category2 path,.epoch-theme-dark .epoch.category10 .arc.category2 path{fill:#FFAC89;}.epoch-theme-dark .epoch .bar.category2,.epoch-theme-dark .epoch.category10 .bar.category2{fill:#FFAC89;}.epoch-theme-dark .epoch div.ref.category3,.epoch-theme-dark .epoch.category10 div.ref.category3{background-color:#E889E8;}.epoch-theme-dark .epoch .category3 .line,.epoch-theme-dark .epoch.category10 .category3 .line{stroke:#E889E8;}.epoch-theme-dark .epoch .category3 .area,.epoch-theme-dark .epoch .category3 .dot,.epoch-theme-dark .epoch.category10 .category3 .area,.epoch-theme-dark .epoch.category10 .category3 .dot{fill:#E889E8;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category3 path,.epoch-theme-dark .epoch.category10 .arc.category3 path{fill:#E889E8;}.epoch-theme-dark .epoch .bar.category3,.epoch-theme-dark .epoch.category10 .bar.category3{fill:#E889E8;}.epoch-theme-dark .epoch div.ref.category4,.epoch-theme-dark .epoch.category10 div.ref.category4{background-color:#78E8D3;}.epoch-theme-dark .epoch .category4 .line,.epoch-theme-dark .epoch.category10 .category4 .line{stroke:#78E8D3;}.epoch-theme-dark .epoch .category4 .area,.epoch-theme-dark .epoch .category4 .dot,.epoch-theme-dark .epoch.category10 .category4 .area,.epoch-theme-dark .epoch.category10 .category4 .dot{fill:#78E8D3;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category4 path,.epoch-theme-dark .epoch.category10 .arc.category4 path{fill:#78E8D3;}.epoch-theme-dark .epoch .bar.category4,.epoch-theme-dark .epoch.category10 .bar.category4{fill:#78E8D3;}.epoch-theme-dark .epoch div.ref.category5,.epoch-theme-dark .epoch.category10 div.ref.category5{background-color:#C2FF97;}.epoch-theme-dark .epoch .category5 .line,.epoch-theme-dark .epoch.category10 .category5 .line{stroke:#C2FF97;}.epoch-theme-dark .epoch .category5 .area,.epoch-theme-dark .epoch .category5 .dot,.epoch-theme-dark .epoch.category10 .category5 .area,.epoch-theme-dark .epoch.category10 .category5 .dot{fill:#C2FF97;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category5 path,.epoch-theme-dark .epoch.category10 .arc.category5 path{fill:#C2FF97;}.epoch-theme-dark .epoch .bar.category5,.epoch-theme-dark .epoch.category10 .bar.category5{fill:#C2FF97;}.epoch-theme-dark .epoch div.ref.category6,.epoch-theme-dark .epoch.category10 div.ref.category6{background-color:#B7BCD1;}.epoch-theme-dark .epoch .category6 .line,.epoch-theme-dark .epoch.category10 .category6 .line{stroke:#B7BCD1;}.epoch-theme-dark .epoch .category6 .area,.epoch-theme-dark .epoch .category6 .dot,.epoch-theme-dark .epoch.category10 .category6 .area,.epoch-theme-dark .epoch.category10 .category6 .dot{fill:#B7BCD1;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category6 path,.epoch-theme-dark .epoch.category10 .arc.category6 path{fill:#B7BCD1;}.epoch-theme-dark .epoch .bar.category6,.epoch-theme-dark .epoch.category10 .bar.category6{fill:#B7BCD1;}.epoch-theme-dark .epoch div.ref.category7,.epoch-theme-dark .epoch.category10 div.ref.category7{background-color:#FF857F;}.epoch-theme-dark .epoch .category7 .line,.epoch-theme-dark .epoch.category10 .category7 .line{stroke:#FF857F;}.epoch-theme-dark .epoch .category7 .area,.epoch-theme-dark .epoch .category7 .dot,.epoch-theme-dark .epoch.category10 .category7 .area,.epoch-theme-dark .epoch.category10 .category7 .dot{fill:#FF857F;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category7 path,.epoch-theme-dark .epoch.category10 .arc.category7 path{fill:#FF857F;}.epoch-theme-dark .epoch .bar.category7,.epoch-theme-dark .epoch.category10 .bar.category7{fill:#FF857F;}.epoch-theme-dark .epoch div.ref.category8,.epoch-theme-dark .epoch.category10 div.ref.category8{background-color:#F3DE88;}.epoch-theme-dark .epoch .category8 .line,.epoch-theme-dark .epoch.category10 .category8 .line{stroke:#F3DE88;}.epoch-theme-dark .epoch .category8 .area,.epoch-theme-dark .epoch .category8 .dot,.epoch-theme-dark .epoch.category10 .category8 .area,.epoch-theme-dark .epoch.category10 .category8 .dot{fill:#F3DE88;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category8 path,.epoch-theme-dark .epoch.category10 .arc.category8 path{fill:#F3DE88;}.epoch-theme-dark .epoch .bar.category8,.epoch-theme-dark .epoch.category10 .bar.category8{fill:#F3DE88;}.epoch-theme-dark .epoch div.ref.category9,.epoch-theme-dark .epoch.category10 div.ref.category9{background-color:#C9935E;}.epoch-theme-dark .epoch .category9 .line,.epoch-theme-dark .epoch.category10 .category9 .line{stroke:#C9935E;}.epoch-theme-dark .epoch .category9 .area,.epoch-theme-dark .epoch .category9 .dot,.epoch-theme-dark .epoch.category10 .category9 .area,.epoch-theme-dark .epoch.category10 .category9 .dot{fill:#C9935E;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category9 path,.epoch-theme-dark .epoch.category10 .arc.category9 path{fill:#C9935E;}.epoch-theme-dark .epoch .bar.category9,.epoch-theme-dark .epoch.category10 .bar.category9{fill:#C9935E;}.epoch-theme-dark .epoch div.ref.category10,.epoch-theme-dark .epoch.category10 div.ref.category10{background-color:#A488FF;}.epoch-theme-dark .epoch .category10 .line,.epoch-theme-dark .epoch.category10 .category10 .line{stroke:#A488FF;}.epoch-theme-dark .epoch .category10 .area,.epoch-theme-dark .epoch .category10 .dot,.epoch-theme-dark .epoch.category10 .category10 .area,.epoch-theme-dark .epoch.category10 .category10 .dot{fill:#A488FF;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category10 path,.epoch-theme-dark .epoch.category10 .arc.category10 path{fill:#A488FF;}.epoch-theme-dark .epoch .bar.category10,.epoch-theme-dark .epoch.category10 .bar.category10{fill:#A488FF;}.epoch-theme-dark .epoch.category20 div.ref.category1{background-color:#909CFF;}.epoch-theme-dark .epoch.category20 .category1 .line{stroke:#909CFF;}.epoch-theme-dark .epoch.category20 .category1 .area,.epoch-theme-dark .epoch.category20 .category1 .dot{fill:#909CFF;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category1 path{fill:#909CFF;}.epoch-theme-dark .epoch.category20 .bar.category1{fill:#909CFF;}.epoch-theme-dark .epoch.category20 div.ref.category2{background-color:#626AAD;}.epoch-theme-dark .epoch.category20 .category2 .line{stroke:#626AAD;}.epoch-theme-dark .epoch.category20 .category2 .area,.epoch-theme-dark .epoch.category20 .category2 .dot{fill:#626AAD;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category2 path{fill:#626AAD;}.epoch-theme-dark .epoch.category20 .bar.category2{fill:#626AAD;}.epoch-theme-dark .epoch.category20 div.ref.category3{background-color:#FFAC89;}.epoch-theme-dark .epoch.category20 .category3 .line{stroke:#FFAC89;}.epoch-theme-dark .epoch.category20 .category3 .area,.epoch-theme-dark .epoch.category20 .category3 .dot{fill:#FFAC89;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category3 path{fill:#FFAC89;}.epoch-theme-dark .epoch.category20 .bar.category3{fill:#FFAC89;}.epoch-theme-dark .epoch.category20 div.ref.category4{background-color:#BD7F66;}.epoch-theme-dark .epoch.category20 .category4 .line{stroke:#BD7F66;}.epoch-theme-dark .epoch.category20 .category4 .area,.epoch-theme-dark .epoch.category20 .category4 .dot{fill:#BD7F66;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category4 path{fill:#BD7F66;}.epoch-theme-dark .epoch.category20 .bar.category4{fill:#BD7F66;}.epoch-theme-dark .epoch.category20 div.ref.category5{background-color:#E889E8;}.epoch-theme-dark .epoch.category20 .category5 .line{stroke:#E889E8;}.epoch-theme-dark .epoch.category20 .category5 .area,.epoch-theme-dark .epoch.category20 .category5 .dot{fill:#E889E8;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category5 path{fill:#E889E8;}.epoch-theme-dark .epoch.category20 .bar.category5{fill:#E889E8;}.epoch-theme-dark .epoch.category20 div.ref.category6{background-color:#995A99;}.epoch-theme-dark .epoch.category20 .category6 .line{stroke:#995A99;}.epoch-theme-dark .epoch.category20 .category6 .area,.epoch-theme-dark .epoch.category20 .category6 .dot{fill:#995A99;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category6 path{fill:#995A99;}.epoch-theme-dark .epoch.category20 .bar.category6{fill:#995A99;}.epoch-theme-dark .epoch.category20 div.ref.category7{background-color:#78E8D3;}.epoch-theme-dark .epoch.category20 .category7 .line{stroke:#78E8D3;}.epoch-theme-dark .epoch.category20 .category7 .area,.epoch-theme-dark .epoch.category20 .category7 .dot{fill:#78E8D3;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category7 path{fill:#78E8D3;}.epoch-theme-dark .epoch.category20 .bar.category7{fill:#78E8D3;}.epoch-theme-dark .epoch.category20 div.ref.category8{background-color:#4F998C;}.epoch-theme-dark .epoch.category20 .category8 .line{stroke:#4F998C;}.epoch-theme-dark .epoch.category20 .category8 .area,.epoch-theme-dark .epoch.category20 .category8 .dot{fill:#4F998C;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category8 path{fill:#4F998C;}.epoch-theme-dark .epoch.category20 .bar.category8{fill:#4F998C;}.epoch-theme-dark .epoch.category20 div.ref.category9{background-color:#C2FF97;}.epoch-theme-dark .epoch.category20 .category9 .line{stroke:#C2FF97;}.epoch-theme-dark .epoch.category20 .category9 .area,.epoch-theme-dark .epoch.category20 .category9 .dot{fill:#C2FF97;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category9 path{fill:#C2FF97;}.epoch-theme-dark .epoch.category20 .bar.category9{fill:#C2FF97;}.epoch-theme-dark .epoch.category20 div.ref.category10{background-color:#789E5E;}.epoch-theme-dark .epoch.category20 .category10 .line{stroke:#789E5E;}.epoch-theme-dark .epoch.category20 .category10 .area,.epoch-theme-dark .epoch.category20 .category10 .dot{fill:#789E5E;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category10 path{fill:#789E5E;}.epoch-theme-dark .epoch.category20 .bar.category10{fill:#789E5E;}.epoch-theme-dark .epoch.category20 div.ref.category11{background-color:#B7BCD1;}.epoch-theme-dark .epoch.category20 .category11 .line{stroke:#B7BCD1;}.epoch-theme-dark .epoch.category20 .category11 .area,.epoch-theme-dark .epoch.category20 .category11 .dot{fill:#B7BCD1;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category11 path{fill:#B7BCD1;}.epoch-theme-dark .epoch.category20 .bar.category11{fill:#B7BCD1;}.epoch-theme-dark .epoch.category20 div.ref.category12{background-color:#7F8391;}.epoch-theme-dark .epoch.category20 .category12 .line{stroke:#7F8391;}.epoch-theme-dark .epoch.category20 .category12 .area,.epoch-theme-dark .epoch.category20 .category12 .dot{fill:#7F8391;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category12 path{fill:#7F8391;}.epoch-theme-dark .epoch.category20 .bar.category12{fill:#7F8391;}.epoch-theme-dark .epoch.category20 div.ref.category13{background-color:#CCB889;}.epoch-theme-dark .epoch.category20 .category13 .line{stroke:#CCB889;}.epoch-theme-dark .epoch.category20 .category13 .area,.epoch-theme-dark .epoch.category20 .category13 .dot{fill:#CCB889;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category13 path{fill:#CCB889;}.epoch-theme-dark .epoch.category20 .bar.category13{fill:#CCB889;}.epoch-theme-dark .epoch.category20 div.ref.category14{background-color:#A1906B;}.epoch-theme-dark .epoch.category20 .category14 .line{stroke:#A1906B;}.epoch-theme-dark .epoch.category20 .category14 .area,.epoch-theme-dark .epoch.category20 .category14 .dot{fill:#A1906B;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category14 path{fill:#A1906B;}.epoch-theme-dark .epoch.category20 .bar.category14{fill:#A1906B;}.epoch-theme-dark .epoch.category20 div.ref.category15{background-color:#F3DE88;}.epoch-theme-dark .epoch.category20 .category15 .line{stroke:#F3DE88;}.epoch-theme-dark .epoch.category20 .category15 .area,.epoch-theme-dark .epoch.category20 .category15 .dot{fill:#F3DE88;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category15 path{fill:#F3DE88;}.epoch-theme-dark .epoch.category20 .bar.category15{fill:#F3DE88;}.epoch-theme-dark .epoch.category20 div.ref.category16{background-color:#A89A5E;}.epoch-theme-dark .epoch.category20 .category16 .line{stroke:#A89A5E;}.epoch-theme-dark .epoch.category20 .category16 .area,.epoch-theme-dark .epoch.category20 .category16 .dot{fill:#A89A5E;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category16 path{fill:#A89A5E;}.epoch-theme-dark .epoch.category20 .bar.category16{fill:#A89A5E;}.epoch-theme-dark .epoch.category20 div.ref.category17{background-color:#FF857F;}.epoch-theme-dark .epoch.category20 .category17 .line{stroke:#FF857F;}.epoch-theme-dark .epoch.category20 .category17 .area,.epoch-theme-dark .epoch.category20 .category17 .dot{fill:#FF857F;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category17 path{fill:#FF857F;}.epoch-theme-dark .epoch.category20 .bar.category17{fill:#FF857F;}.epoch-theme-dark .epoch.category20 div.ref.category18{background-color:#BA615D;}.epoch-theme-dark .epoch.category20 .category18 .line{stroke:#BA615D;}.epoch-theme-dark .epoch.category20 .category18 .area,.epoch-theme-dark .epoch.category20 .category18 .dot{fill:#BA615D;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category18 path{fill:#BA615D;}.epoch-theme-dark .epoch.category20 .bar.category18{fill:#BA615D;}.epoch-theme-dark .epoch.category20 div.ref.category19{background-color:#A488FF;}.epoch-theme-dark .epoch.category20 .category19 .line{stroke:#A488FF;}.epoch-theme-dark .epoch.category20 .category19 .area,.epoch-theme-dark .epoch.category20 .category19 .dot{fill:#A488FF;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category19 path{fill:#A488FF;}.epoch-theme-dark .epoch.category20 .bar.category19{fill:#A488FF;}.epoch-theme-dark .epoch.category20 div.ref.category20{background-color:#7662B8;}.epoch-theme-dark .epoch.category20 .category20 .line{stroke:#7662B8;}.epoch-theme-dark .epoch.category20 .category20 .area,.epoch-theme-dark .epoch.category20 .category20 .dot{fill:#7662B8;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category20 path{fill:#7662B8;}.epoch-theme-dark .epoch.category20 .bar.category20{fill:#7662B8;}.epoch-theme-dark .epoch.category20b div.ref.category1{background-color:#909CFF;}.epoch-theme-dark .epoch.category20b .category1 .line{stroke:#909CFF;}.epoch-theme-dark .epoch.category20b .category1 .area,.epoch-theme-dark .epoch.category20b .category1 .dot{fill:#909CFF;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category1 path{fill:#909CFF;}.epoch-theme-dark .epoch.category20b .bar.category1{fill:#909CFF;}.epoch-theme-dark .epoch.category20b div.ref.category2{background-color:#7680D1;}.epoch-theme-dark .epoch.category20b .category2 .line{stroke:#7680D1;}.epoch-theme-dark .epoch.category20b .category2 .area,.epoch-theme-dark .epoch.category20b .category2 .dot{fill:#7680D1;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category2 path{fill:#7680D1;}.epoch-theme-dark .epoch.category20b .bar.category2{fill:#7680D1;}.epoch-theme-dark .epoch.category20b div.ref.category3{background-color:#656DB2;}.epoch-theme-dark .epoch.category20b .category3 .line{stroke:#656DB2;}.epoch-theme-dark .epoch.category20b .category3 .area,.epoch-theme-dark .epoch.category20b .category3 .dot{fill:#656DB2;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category3 path{fill:#656DB2;}.epoch-theme-dark .epoch.category20b .bar.category3{fill:#656DB2;}.epoch-theme-dark .epoch.category20b div.ref.category4{background-color:#525992;}.epoch-theme-dark .epoch.category20b .category4 .line{stroke:#525992;}.epoch-theme-dark .epoch.category20b .category4 .area,.epoch-theme-dark .epoch.category20b .category4 .dot{fill:#525992;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category4 path{fill:#525992;}.epoch-theme-dark .epoch.category20b .bar.category4{fill:#525992;}.epoch-theme-dark .epoch.category20b div.ref.category5{background-color:#FFAC89;}.epoch-theme-dark .epoch.category20b .category5 .line{stroke:#FFAC89;}.epoch-theme-dark .epoch.category20b .category5 .area,.epoch-theme-dark .epoch.category20b .category5 .dot{fill:#FFAC89;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category5 path{fill:#FFAC89;}.epoch-theme-dark .epoch.category20b .bar.category5{fill:#FFAC89;}.epoch-theme-dark .epoch.category20b div.ref.category6{background-color:#D18D71;}.epoch-theme-dark .epoch.category20b .category6 .line{stroke:#D18D71;}.epoch-theme-dark .epoch.category20b .category6 .area,.epoch-theme-dark .epoch.category20b .category6 .dot{fill:#D18D71;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category6 path{fill:#D18D71;}.epoch-theme-dark .epoch.category20b .bar.category6{fill:#D18D71;}.epoch-theme-dark .epoch.category20b div.ref.category7{background-color:#AB735C;}.epoch-theme-dark .epoch.category20b .category7 .line{stroke:#AB735C;}.epoch-theme-dark .epoch.category20b .category7 .area,.epoch-theme-dark .epoch.category20b .category7 .dot{fill:#AB735C;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category7 path{fill:#AB735C;}.epoch-theme-dark .epoch.category20b .bar.category7{fill:#AB735C;}.epoch-theme-dark .epoch.category20b div.ref.category8{background-color:#92624E;}.epoch-theme-dark .epoch.category20b .category8 .line{stroke:#92624E;}.epoch-theme-dark .epoch.category20b .category8 .area,.epoch-theme-dark .epoch.category20b .category8 .dot{fill:#92624E;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category8 path{fill:#92624E;}.epoch-theme-dark .epoch.category20b .bar.category8{fill:#92624E;}.epoch-theme-dark .epoch.category20b div.ref.category9{background-color:#E889E8;}.epoch-theme-dark .epoch.category20b .category9 .line{stroke:#E889E8;}.epoch-theme-dark .epoch.category20b .category9 .area,.epoch-theme-dark .epoch.category20b .category9 .dot{fill:#E889E8;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category9 path{fill:#E889E8;}.epoch-theme-dark .epoch.category20b .bar.category9{fill:#E889E8;}.epoch-theme-dark .epoch.category20b div.ref.category10{background-color:#BA6EBA;}.epoch-theme-dark .epoch.category20b .category10 .line{stroke:#BA6EBA;}.epoch-theme-dark .epoch.category20b .category10 .area,.epoch-theme-dark .epoch.category20b .category10 .dot{fill:#BA6EBA;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category10 path{fill:#BA6EBA;}.epoch-theme-dark .epoch.category20b .bar.category10{fill:#BA6EBA;}.epoch-theme-dark .epoch.category20b div.ref.category11{background-color:#9B5C9B;}.epoch-theme-dark .epoch.category20b .category11 .line{stroke:#9B5C9B;}.epoch-theme-dark .epoch.category20b .category11 .area,.epoch-theme-dark .epoch.category20b .category11 .dot{fill:#9B5C9B;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category11 path{fill:#9B5C9B;}.epoch-theme-dark .epoch.category20b .bar.category11{fill:#9B5C9B;}.epoch-theme-dark .epoch.category20b div.ref.category12{background-color:#7B487B;}.epoch-theme-dark .epoch.category20b .category12 .line{stroke:#7B487B;}.epoch-theme-dark .epoch.category20b .category12 .area,.epoch-theme-dark .epoch.category20b .category12 .dot{fill:#7B487B;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category12 path{fill:#7B487B;}.epoch-theme-dark .epoch.category20b .bar.category12{fill:#7B487B;}.epoch-theme-dark .epoch.category20b div.ref.category13{background-color:#78E8D3;}.epoch-theme-dark .epoch.category20b .category13 .line{stroke:#78E8D3;}.epoch-theme-dark .epoch.category20b .category13 .area,.epoch-theme-dark .epoch.category20b .category13 .dot{fill:#78E8D3;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category13 path{fill:#78E8D3;}.epoch-theme-dark .epoch.category20b .bar.category13{fill:#78E8D3;}.epoch-theme-dark .epoch.category20b div.ref.category14{background-color:#60BAAA;}.epoch-theme-dark .epoch.category20b .category14 .line{stroke:#60BAAA;}.epoch-theme-dark .epoch.category20b .category14 .area,.epoch-theme-dark .epoch.category20b .category14 .dot{fill:#60BAAA;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category14 path{fill:#60BAAA;}.epoch-theme-dark .epoch.category20b .bar.category14{fill:#60BAAA;}.epoch-theme-dark .epoch.category20b div.ref.category15{background-color:#509B8D;}.epoch-theme-dark .epoch.category20b .category15 .line{stroke:#509B8D;}.epoch-theme-dark .epoch.category20b .category15 .area,.epoch-theme-dark .epoch.category20b .category15 .dot{fill:#509B8D;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category15 path{fill:#509B8D;}.epoch-theme-dark .epoch.category20b .bar.category15{fill:#509B8D;}.epoch-theme-dark .epoch.category20b div.ref.category16{background-color:#3F7B70;}.epoch-theme-dark .epoch.category20b .category16 .line{stroke:#3F7B70;}.epoch-theme-dark .epoch.category20b .category16 .area,.epoch-theme-dark .epoch.category20b .category16 .dot{fill:#3F7B70;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category16 path{fill:#3F7B70;}.epoch-theme-dark .epoch.category20b .bar.category16{fill:#3F7B70;}.epoch-theme-dark .epoch.category20b div.ref.category17{background-color:#C2FF97;}.epoch-theme-dark .epoch.category20b .category17 .line{stroke:#C2FF97;}.epoch-theme-dark .epoch.category20b .category17 .area,.epoch-theme-dark .epoch.category20b .category17 .dot{fill:#C2FF97;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category17 path{fill:#C2FF97;}.epoch-theme-dark .epoch.category20b .bar.category17{fill:#C2FF97;}.epoch-theme-dark .epoch.category20b div.ref.category18{background-color:#9FD17C;}.epoch-theme-dark .epoch.category20b .category18 .line{stroke:#9FD17C;}.epoch-theme-dark .epoch.category20b .category18 .area,.epoch-theme-dark .epoch.category20b .category18 .dot{fill:#9FD17C;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category18 path{fill:#9FD17C;}.epoch-theme-dark .epoch.category20b .bar.category18{fill:#9FD17C;}.epoch-theme-dark .epoch.category20b div.ref.category19{background-color:#7DA361;}.epoch-theme-dark .epoch.category20b .category19 .line{stroke:#7DA361;}.epoch-theme-dark .epoch.category20b .category19 .area,.epoch-theme-dark .epoch.category20b .category19 .dot{fill:#7DA361;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category19 path{fill:#7DA361;}.epoch-theme-dark .epoch.category20b .bar.category19{fill:#7DA361;}.epoch-theme-dark .epoch.category20b div.ref.category20{background-color:#65854E;}.epoch-theme-dark .epoch.category20b .category20 .line{stroke:#65854E;}.epoch-theme-dark .epoch.category20b .category20 .area,.epoch-theme-dark .epoch.category20b .category20 .dot{fill:#65854E;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category20 path{fill:#65854E;}.epoch-theme-dark .epoch.category20b .bar.category20{fill:#65854E;}.epoch-theme-dark .epoch.category20c div.ref.category1{background-color:#B7BCD1;}.epoch-theme-dark .epoch.category20c .category1 .line{stroke:#B7BCD1;}.epoch-theme-dark .epoch.category20c .category1 .area,.epoch-theme-dark .epoch.category20c .category1 .dot{fill:#B7BCD1;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category1 path{fill:#B7BCD1;}.epoch-theme-dark .epoch.category20c .bar.category1{fill:#B7BCD1;}.epoch-theme-dark .epoch.category20c div.ref.category2{background-color:#979DAD;}.epoch-theme-dark .epoch.category20c .category2 .line{stroke:#979DAD;}.epoch-theme-dark .epoch.category20c .category2 .area,.epoch-theme-dark .epoch.category20c .category2 .dot{fill:#979DAD;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category2 path{fill:#979DAD;}.epoch-theme-dark .epoch.category20c .bar.category2{fill:#979DAD;}.epoch-theme-dark .epoch.category20c div.ref.category3{background-color:#6E717D;}.epoch-theme-dark .epoch.category20c .category3 .line{stroke:#6E717D;}.epoch-theme-dark .epoch.category20c .category3 .area,.epoch-theme-dark .epoch.category20c .category3 .dot{fill:#6E717D;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category3 path{fill:#6E717D;}.epoch-theme-dark .epoch.category20c .bar.category3{fill:#6E717D;}.epoch-theme-dark .epoch.category20c div.ref.category4{background-color:#595C66;}.epoch-theme-dark .epoch.category20c .category4 .line{stroke:#595C66;}.epoch-theme-dark .epoch.category20c .category4 .area,.epoch-theme-dark .epoch.category20c .category4 .dot{fill:#595C66;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category4 path{fill:#595C66;}.epoch-theme-dark .epoch.category20c .bar.category4{fill:#595C66;}.epoch-theme-dark .epoch.category20c div.ref.category5{background-color:#FF857F;}.epoch-theme-dark .epoch.category20c .category5 .line{stroke:#FF857F;}.epoch-theme-dark .epoch.category20c .category5 .area,.epoch-theme-dark .epoch.category20c .category5 .dot{fill:#FF857F;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category5 path{fill:#FF857F;}.epoch-theme-dark .epoch.category20c .bar.category5{fill:#FF857F;}.epoch-theme-dark .epoch.category20c div.ref.category6{background-color:#DE746E;}.epoch-theme-dark .epoch.category20c .category6 .line{stroke:#DE746E;}.epoch-theme-dark .epoch.category20c .category6 .area,.epoch-theme-dark .epoch.category20c .category6 .dot{fill:#DE746E;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category6 path{fill:#DE746E;}.epoch-theme-dark .epoch.category20c .bar.category6{fill:#DE746E;}.epoch-theme-dark .epoch.category20c div.ref.category7{background-color:#B55F5A;}.epoch-theme-dark .epoch.category20c .category7 .line{stroke:#B55F5A;}.epoch-theme-dark .epoch.category20c .category7 .area,.epoch-theme-dark .epoch.category20c .category7 .dot{fill:#B55F5A;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category7 path{fill:#B55F5A;}.epoch-theme-dark .epoch.category20c .bar.category7{fill:#B55F5A;}.epoch-theme-dark .epoch.category20c div.ref.category8{background-color:#964E4B;}.epoch-theme-dark .epoch.category20c .category8 .line{stroke:#964E4B;}.epoch-theme-dark .epoch.category20c .category8 .area,.epoch-theme-dark .epoch.category20c .category8 .dot{fill:#964E4B;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category8 path{fill:#964E4B;}.epoch-theme-dark .epoch.category20c .bar.category8{fill:#964E4B;}.epoch-theme-dark .epoch.category20c div.ref.category9{background-color:#F3DE88;}.epoch-theme-dark .epoch.category20c .category9 .line{stroke:#F3DE88;}.epoch-theme-dark .epoch.category20c .category9 .area,.epoch-theme-dark .epoch.category20c .category9 .dot{fill:#F3DE88;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category9 path{fill:#F3DE88;}.epoch-theme-dark .epoch.category20c .bar.category9{fill:#F3DE88;}.epoch-theme-dark .epoch.category20c div.ref.category10{background-color:#DBC87B;}.epoch-theme-dark .epoch.category20c .category10 .line{stroke:#DBC87B;}.epoch-theme-dark .epoch.category20c .category10 .area,.epoch-theme-dark .epoch.category20c .category10 .dot{fill:#DBC87B;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category10 path{fill:#DBC87B;}.epoch-theme-dark .epoch.category20c .bar.category10{fill:#DBC87B;}.epoch-theme-dark .epoch.category20c div.ref.category11{background-color:#BAAA68;}.epoch-theme-dark .epoch.category20c .category11 .line{stroke:#BAAA68;}.epoch-theme-dark .epoch.category20c .category11 .area,.epoch-theme-dark .epoch.category20c .category11 .dot{fill:#BAAA68;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category11 path{fill:#BAAA68;}.epoch-theme-dark .epoch.category20c .bar.category11{fill:#BAAA68;}.epoch-theme-dark .epoch.category20c div.ref.category12{background-color:#918551;}.epoch-theme-dark .epoch.category20c .category12 .line{stroke:#918551;}.epoch-theme-dark .epoch.category20c .category12 .area,.epoch-theme-dark .epoch.category20c .category12 .dot{fill:#918551;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category12 path{fill:#918551;}.epoch-theme-dark .epoch.category20c .bar.category12{fill:#918551;}.epoch-theme-dark .epoch.category20c div.ref.category13{background-color:#C9935E;}.epoch-theme-dark .epoch.category20c .category13 .line{stroke:#C9935E;}.epoch-theme-dark .epoch.category20c .category13 .area,.epoch-theme-dark .epoch.category20c .category13 .dot{fill:#C9935E;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category13 path{fill:#C9935E;}.epoch-theme-dark .epoch.category20c .bar.category13{fill:#C9935E;}.epoch-theme-dark .epoch.category20c div.ref.category14{background-color:#B58455;}.epoch-theme-dark .epoch.category20c .category14 .line{stroke:#B58455;}.epoch-theme-dark .epoch.category20c .category14 .area,.epoch-theme-dark .epoch.category20c .category14 .dot{fill:#B58455;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category14 path{fill:#B58455;}.epoch-theme-dark .epoch.category20c .bar.category14{fill:#B58455;}.epoch-theme-dark .epoch.category20c div.ref.category15{background-color:#997048;}.epoch-theme-dark .epoch.category20c .category15 .line{stroke:#997048;}.epoch-theme-dark .epoch.category20c .category15 .area,.epoch-theme-dark .epoch.category20c .category15 .dot{fill:#997048;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category15 path{fill:#997048;}.epoch-theme-dark .epoch.category20c .bar.category15{fill:#997048;}.epoch-theme-dark .epoch.category20c div.ref.category16{background-color:#735436;}.epoch-theme-dark .epoch.category20c .category16 .line{stroke:#735436;}.epoch-theme-dark .epoch.category20c .category16 .area,.epoch-theme-dark .epoch.category20c .category16 .dot{fill:#735436;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category16 path{fill:#735436;}.epoch-theme-dark .epoch.category20c .bar.category16{fill:#735436;}.epoch-theme-dark .epoch.category20c div.ref.category17{background-color:#A488FF;}.epoch-theme-dark .epoch.category20c .category17 .line{stroke:#A488FF;}.epoch-theme-dark .epoch.category20c .category17 .area,.epoch-theme-dark .epoch.category20c .category17 .dot{fill:#A488FF;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category17 path{fill:#A488FF;}.epoch-theme-dark .epoch.category20c .bar.category17{fill:#A488FF;}.epoch-theme-dark .epoch.category20c div.ref.category18{background-color:#8670D1;}.epoch-theme-dark .epoch.category20c .category18 .line{stroke:#8670D1;}.epoch-theme-dark .epoch.category20c .category18 .area,.epoch-theme-dark .epoch.category20c .category18 .dot{fill:#8670D1;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category18 path{fill:#8670D1;}.epoch-theme-dark .epoch.category20c .bar.category18{fill:#8670D1;}.epoch-theme-dark .epoch.category20c div.ref.category19{background-color:#705CAD;}.epoch-theme-dark .epoch.category20c .category19 .line{stroke:#705CAD;}.epoch-theme-dark .epoch.category20c .category19 .area,.epoch-theme-dark .epoch.category20c .category19 .dot{fill:#705CAD;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category19 path{fill:#705CAD;}.epoch-theme-dark .epoch.category20c .bar.category19{fill:#705CAD;}.epoch-theme-dark .epoch.category20c div.ref.category20{background-color:#52447F;}.epoch-theme-dark .epoch.category20c .category20 .line{stroke:#52447F;}.epoch-theme-dark .epoch.category20c .category20 .area,.epoch-theme-dark .epoch.category20c .category20 .dot{fill:#52447F;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category20 path{fill:#52447F;}.epoch-theme-dark .epoch.category20c .bar.category20{fill:#52447F;} \ No newline at end of file diff --git a/examples/realtime-advanced/resources/static/epoch.min.js b/examples/realtime-advanced/resources/static/epoch.min.js deleted file mode 100644 index 0c654b86..00000000 --- a/examples/realtime-advanced/resources/static/epoch.min.js +++ /dev/null @@ -1,114 +0,0 @@ -(function(){var e;null==window.Epoch&&(window.Epoch={});null==(e=window.Epoch).Chart&&(e.Chart={});null==(e=window.Epoch).Time&&(e.Time={});null==(e=window.Epoch).Util&&(e.Util={});null==(e=window.Epoch).Formats&&(e.Formats={});Epoch.warn=function(g){return(console.warn||console.log)("Epoch Warning: "+g)};Epoch.exception=function(g){throw"Epoch Error: "+g;}}).call(this); -(function(){Epoch.TestContext=function(){function e(){var c,a,d;this._log=[];a=0;for(d=g.length;ac){if((c|0)!==c||d)c=c.toFixed(a);return c}f="KMGTPEZY".split("");for(h in f)if(k=f[h],b=Math.pow(10,3*((h|0)+1)),c>=b&&cc){if(0!==c%1||d)c=c.toFixed(a);return""+c+" B"}f="KB MB GB TB PB EB ZB YB".split(" ");for(h in f)if(k=f[h],b=Math.pow(1024,(h|0)+1),c>=b&&cf;k=1<=f?++a:--a)q.push(arguments[k]);return q}.apply(this,arguments);c=this._events[a];m=[];f=0;for(q=c.length;fthis.options.windowSize+1&&a.values.shift();b=[this._ticks[0],this._ticks[this._ticks.length-1]];a=b[0];b=b[1];null!=b&&b.enter&&(b.enter=!1,b.opacity=1);null!=a&&a.exit&&this._shiftTick();this.animation.frame=0;this.trigger("transition:end");if(0this.options.queueSize&&this._queue.splice(this.options.queueSize,this._queue.length-this.options.queueSize);if(this._queue.length===this.options.queueSize)return!1;this._queue.push(a.map(function(a){return function(b){return a._prepareEntry(b)}}(this)));this.trigger("push");if(!this.inTransition())return this._startTransition()}; -a.prototype._shift=function(){var a,b,c,d;this.trigger("before:shift");a=this._queue.shift();d=this.data;for(b in d)c=d[b],c.values.push(a[b]);this._updateTicks(a[0].time);this._transitionRangeAxes();return this.trigger("after:shift")};a.prototype._transitionRangeAxes=function(){this.hasAxis("left")&&this.svg.selectAll(".y.axis.left").transition().duration(500).ease("linear").call(this.leftAxis());if(this.hasAxis("right"))return this.svg.selectAll(".y.axis.right").transition().duration(500).ease("linear").call(this.rightAxis())}; -a.prototype._animate=function(){if(this.inTransition())return++this.animation.frame===this.animation.duration&&this._stopTransition(),this.draw(this.animation.frame*this.animation.delta()),this._updateTimeAxes()};a.prototype.y=function(){return d3.scale.linear().domain(this.extent(function(a){return a.y})).range([this.innerHeight(),0])};a.prototype.ySvg=function(){return d3.scale.linear().domain(this.extent(function(a){return a.y})).range([this.innerHeight()/this.pixelRatio,0])};a.prototype.w=function(){return this.innerWidth()/ -this.options.windowSize};a.prototype._updateTicks=function(a){if(this.hasAxis("top")||this.hasAxis("bottom"))if(++this._tickTimer%this.options.ticks.time||this._pushTick(this.options.windowSize,a,!0),!(0<=this._ticks[0].x-this.w()/this.pixelRatio))return this._ticks[0].exit=!0};a.prototype._pushTick=function(a,b,c,d){null==c&&(c=!1);null==d&&(d=!1);if(this.hasAxis("top")||this.hasAxis("bottom"))return b={time:b,x:a*(this.w()/this.pixelRatio)+this._offsetX(),opacity:c?0:1,enter:c?!0:!1,exit:!1},this.hasAxis("bottom")&& -(a=this.bottomAxis.append("g").attr("class","tick major").attr("transform","translate("+(b.x+1)+",0)").style("opacity",b.opacity),a.append("line").attr("y2",6),a.append("text").attr("text-anchor","middle").attr("dy",19).text(this.options.tickFormats.bottom(b.time)),b.bottomEl=a),this.hasAxis("top")&&(a=this.topAxis.append("g").attr("class","tick major").attr("transform","translate("+(b.x+1)+",0)").style("opacity",b.opacity),a.append("line").attr("y2",-6),a.append("text").attr("text-anchor","middle").attr("dy", --10).text(this.options.tickFormats.top(b.time)),b.topEl=a),d?this._ticks.unshift(b):this._ticks.push(b),b};a.prototype._shiftTick=function(){var a;if(0f;b=0<=f?++c:--c)k=0,e.push(function(){var a,c,d,f;d=this.data;f=[];a=0;for(c=d.length;ag;a=0<=g?++f:--f){b=e=k=0;for(m=this.data.length;0<=m?em;b=0<=m?++e:--e)k+=this.data[b].values[a].y;k>c&&(c=k)}return[0,c]};return a}(Epoch.Time.Plot)}).call(this); -(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Time.Area=function(c){function a(){return a.__super__.constructor.apply(this,arguments)}g(a,c);a.prototype.setStyles=function(a){a=null!=a.className?this.getStyles("g."+a.className.replace(/\s/g,".")+" path.area"):this.getStyles("g path.area");this.ctx.fillStyle=a.fill;null!=a.stroke&&(this.ctx.strokeStyle= -a.stroke);if(null!=a["stroke-width"])return this.ctx.lineWidth=a["stroke-width"].replace("px","")};a.prototype._drawAreas=function(a){var b,c,k,f,e,g,m,l,n,p;null==a&&(a=0);g=[this.y(),this.w()];m=g[0];g=g[1];p=[];for(c=l=n=this.data.length-1;0>=n?0>=l:0<=l;c=0>=n?++l:--l){f=this.data[c];this.setStyles(f);this.ctx.beginPath();e=[this.options.windowSize,f.values.length,this.inTransition()];c=e[0];k=e[1];for(e=e[2];-2<=--c&&0<=--k;)b=f.values[k],b=[(c+1)*g+a,m(b.y+b.y0)],e&&(b[0]+=g),c===this.options.windowSize- -1?this.ctx.moveTo.apply(this.ctx,b):this.ctx.lineTo.apply(this.ctx,b);c=e?(c+3)*g+a:(c+2)*g+a;this.ctx.lineTo(c,this.innerHeight());this.ctx.lineTo(this.width*this.pixelRatio+g+a,this.innerHeight());this.ctx.closePath();p.push(this.ctx.fill())}return p};a.prototype._drawStrokes=function(a){var b,c,k,f,e,g,m,l,n,p;null==a&&(a=0);c=[this.y(),this.w()];m=c[0];g=c[1];p=[];for(c=l=n=this.data.length-1;0>=n?0>=l:0<=l;c=0>=n?++l:--l){f=this.data[c];this.setStyles(f);this.ctx.beginPath();e=[this.options.windowSize, -f.values.length,this.inTransition()];c=e[0];k=e[1];for(e=e[2];-2<=--c&&0<=--k;)b=f.values[k],b=[(c+1)*g+a,m(b.y+b.y0)],e&&(b[0]+=g),c===this.options.windowSize-1?this.ctx.moveTo.apply(this.ctx,b):this.ctx.lineTo.apply(this.ctx,b);p.push(this.ctx.stroke())}return p};a.prototype.draw=function(c){null==c&&(c=0);this.clear();this._drawAreas(c);this._drawStrokes(c);return a.__super__.draw.call(this)};return a}(Epoch.Time.Stack)}).call(this); -(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Time.Bar=function(c){function a(){return a.__super__.constructor.apply(this,arguments)}g(a,c);a.prototype._offsetX=function(){return 0.5*this.w()/this.pixelRatio};a.prototype.setStyles=function(a){a=this.getStyles("rect.bar."+a.replace(/\s/g,"."));this.ctx.fillStyle=a.fill;this.ctx.strokeStyle= -null==a.stroke||"none"===a.stroke?"transparent":a.stroke;if(null!=a["stroke-width"])return this.ctx.lineWidth=a["stroke-width"].replace("px","")};a.prototype.draw=function(c){var b,h,k,f,e,g,m,l,n,p,r,s,t;null==c&&(c=0);this.clear();f=[this.y(),this.w()];p=f[0];n=f[1];t=this.data;r=0;for(s=t.length;r=e&&0<=--g;)b=m.values[g],k=[f*n+c, -b.y,b.y0],b=k[0],h=k[1],k=k[2],l&&(b+=n),b=[b+1,p(h+k),n-2,this.innerHeight()-p(h)+0.5*this.pixelRatio],this.ctx.fillRect.apply(this.ctx,b),this.ctx.strokeRect.apply(this.ctx,b);return a.__super__.draw.call(this)};return a}(Epoch.Time.Stack)}).call(this); -(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Time.Gauge=function(c){function a(c){this.options=null!=c?c:{};a.__super__.constructor.call(this,this.options=Epoch.Util.defaults(this.options,d));this.value=this.options.value||0;"absolute"!==this.el.style("position")&&"relative"!==this.el.style("position")&&this.el.style("position","relative"); -this.svg=this.el.insert("svg",":first-child").attr("width",this.width).attr("height",this.height).attr("class","gauge-labels");this.svg.style({position:"absolute","z-index":"1"});this.svg.append("g").attr("transform","translate("+this.textX()+", "+this.textY()+")").append("text").attr("class","value").text(this.options.format(this.value));this.animation={interval:null,active:!1,delta:0,target:0};this._animate=function(a){return function(){Math.abs(a.animation.target-a.value)=t;b=0<=t?++s:--s)b=l(b),b=[Math.cos(b),Math.sin(b)],c=b[0],m=b[1],b=c*(g-n)+d,r=m*(g-n)+e,c=c*(g-n-p)+d,m=m*(g-n-p)+e,this.ctx.moveTo(b,r),this.ctx.lineTo(c,m);this.ctx.stroke();this.setStyles(".epoch .gauge .arc.outer");this.ctx.beginPath();this.ctx.arc(d,e,g,-1.125* -Math.PI,0.125*Math.PI,!1);this.ctx.stroke();this.setStyles(".epoch .gauge .arc.inner");this.ctx.beginPath();this.ctx.arc(d,e,g-10,-1.125*Math.PI,0.125*Math.PI,!1);this.ctx.stroke();this.drawNeedle();return a.__super__.draw.call(this)};a.prototype.drawNeedle=function(){var a,b,c;c=[this.centerX(),this.centerY(),this.radius()];a=c[0];b=c[1];c=c[2];this.setStyles(".epoch .gauge .needle");this.ctx.beginPath();this.ctx.save();this.ctx.translate(a,b);this.ctx.rotate(this.getAngle(this.value));this.ctx.moveTo(4* -this.pixelRatio,0);this.ctx.lineTo(-4*this.pixelRatio,0);this.ctx.lineTo(-1*this.pixelRatio,19-c);this.ctx.lineTo(1,19-c);this.ctx.fill();this.setStyles(".epoch .gauge .needle-base");this.ctx.beginPath();this.ctx.arc(0,0,this.getWidth()/25,0,2*Math.PI);this.ctx.fill();return this.ctx.restore()};a.prototype.domainChanged=function(){return this.draw()};a.prototype.ticksChanged=function(){return this.draw()};a.prototype.tickSizeChanged=function(){return this.draw()};a.prototype.tickOffsetChanged=function(){return this.draw()}; -a.prototype.formatChanged=function(){return this.svg.select("text.value").text(this.options.format(this.value))};return a}(Epoch.Chart.Canvas)}).call(this); -(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Time.Heatmap=function(c){function a(c){this.options=c;a.__super__.constructor.call(this,this.options=Epoch.Util.defaults(this.options,b));this._setOpacityFunction();this._setupPaintCanvas();this.onAll(e)}var d,b,e;g(a,c);b={buckets:10,bucketRange:[0,100],opacity:"linear",bucketPadding:2,paintZeroValues:!1, -cutOutliers:!1};d={root:function(a,b){return Math.pow(a/b,0.5)},linear:function(a,b){return a/b},quadratic:function(a,b){return Math.pow(a/b,2)},cubic:function(a,b){return Math.pow(a/b,3)},quartic:function(a,b){return Math.pow(a/b,4)},quintic:function(a,b){return Math.pow(a/b,5)}};e={"option:buckets":"bucketsChanged","option:bucketRange":"bucketRangeChanged","option:opacity":"opacityChanged","option:bucketPadding":"bucketPaddingChanged","option:paintZeroValues":"paintZeroValuesChanged","option:cutOutliers":"cutOutliersChanged"}; -a.prototype._setOpacityFunction=function(){if(Epoch.isString(this.options.opacity)){if(this._opacityFn=d[this.options.opacity],null==this._opacityFn)return Epoch.exception("Unknown coloring function provided '"+this.options.opacity+"'")}else return Epoch.isFunction(this.options.opacity)?this._opacityFn=this.options.opacity:Epoch.exception("Unknown type for provided coloring function.")};a.prototype.setData=function(b){var c,d,e,g;a.__super__.setData.call(this,b);e=this.data;g=[];c=0;for(d=e.length;c< -d;c++)b=e[c],g.push(b.values=b.values.map(function(a){return function(b){return a._prepareEntry(b)}}(this)));return g};a.prototype._getBuckets=function(a){var b,c,d,e,g;e=a.time;g=[];b=0;for(d=this.options.buckets;0<=d?bd;0<=d?++b:--b)g.push(0);e={time:e,max:0,buckets:g};b=(this.options.bucketRange[1]-this.options.bucketRange[0])/this.options.buckets;g=a.histogram;for(c in g)a=g[c],d=parseInt((c-this.options.bucketRange[0])/b),this.options.cutOutliers&&(0>d||d>=this.options.buckets)||(0>d?d= -0:d>=this.options.buckets&&(d=this.options.buckets-1),e.buckets[d]+=parseInt(a));c=a=0;for(b=e.buckets.length;0<=b?ab;c=0<=b?++a:--a)e.max=Math.max(e.max,e.buckets[c]);return e};a.prototype.y=function(){return d3.scale.linear().domain(this.options.bucketRange).range([this.innerHeight(),0])};a.prototype.ySvg=function(){return d3.scale.linear().domain(this.options.bucketRange).range([this.innerHeight()/this.pixelRatio,0])};a.prototype.h=function(){return this.innerHeight()/this.options.buckets}; -a.prototype._offsetX=function(){return 0.5*this.w()/this.pixelRatio};a.prototype._setupPaintCanvas=function(){this.paintWidth=(this.options.windowSize+1)*this.w();this.paintHeight=this.height*this.pixelRatio;this.paint=document.createElement("CANVAS");this.paint.width=this.paintWidth;this.paint.height=this.paintHeight;this.p=Epoch.Util.getContext(this.paint);this.redraw();this.on("after:shift","_paintEntry");this.on("transition:end","_shiftPaintCanvas");return this.on("transition:end",function(a){return function(){return a.draw(a.animation.frame* -a.animation.delta())}}(this))};a.prototype.redraw=function(){var a,b;b=this.data[0].values.length;a=this.options.windowSize;for(this.inTransition()&&a++;0<=--b&&0<=--a;)this._paintEntry(b,a);return this.draw(this.animation.frame*this.animation.delta())};a.prototype._computeColor=function(a,b,c){return Epoch.Util.toRGBA(c,this._opacityFn(a,b))};a.prototype._paintEntry=function(a,b){var c,d,e,g,h,p,r,s,t,v,y,w,A,z;null==a&&(a=null);null==b&&(b=null);g=[this.w(),this.h()];y=g[0];p=g[1];null==a&&(a=this.data[0].values.length- -1);null==b&&(b=this.options.windowSize);g=[];var x;x=[];h=0;for(v=this.options.buckets;0<=v?hv;0<=v?++h:--h)x.push(0);v=0;t=this.data;d=0;for(r=t.length;d code[class*="language-"], -pre[class*="language-"] { - background: #f5f2f0; -} - -/* Inline code */ -:not(pre) > code[class*="language-"] { - padding: .1em; - border-radius: .3em; -} - -.token.comment, -.token.prolog, -.token.doctype, -.token.cdata { - color: slategray; -} - -.token.punctuation { - color: #999; -} - -.namespace { - opacity: .7; -} - -.token.property, -.token.tag, -.token.boolean, -.token.number, -.token.constant, -.token.symbol, -.token.deleted { - color: #905; -} - -.token.selector, -.token.attr-name, -.token.string, -.token.char, -.token.builtin, -.token.inserted { - color: #690; -} - -.token.operator, -.token.entity, -.token.url, -.language-css .token.string, -.style .token.string { - color: #a67f59; - background: hsla(0, 0%, 100%, .5); -} - -.token.atrule, -.token.attr-value, -.token.keyword { - color: #07a; -} - -.token.function { - color: #DD4A68; -} - -.token.regex, -.token.important, -.token.variable { - color: #e90; -} - -.token.important, -.token.bold { - font-weight: bold; -} -.token.italic { - font-style: italic; -} - -.token.entity { - cursor: help; -} - diff --git a/examples/realtime-advanced/resources/static/prismjs.min.js b/examples/realtime-advanced/resources/static/prismjs.min.js deleted file mode 100644 index a6855a78..00000000 --- a/examples/realtime-advanced/resources/static/prismjs.min.js +++ /dev/null @@ -1,5 +0,0 @@ -/* http://prismjs.com/download.html?themes=prism&languages=clike+javascript+go */ -self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{};var Prism=function(){var e=/\blang(?:uage)?-(?!\*)(\w+)\b/i,t=self.Prism={util:{encode:function(e){return e instanceof n?new n(e.type,t.util.encode(e.content),e.alias):"Array"===t.util.type(e)?e.map(t.util.encode):e.replace(/&/g,"&").replace(/e.length)break e;if(!(d instanceof a)){u.lastIndex=0;var m=u.exec(d);if(m){c&&(f=m[1].length);var y=m.index-1+f,m=m[0].slice(f),v=m.length,k=y+v,b=d.slice(0,y+1),w=d.slice(k+1),N=[p,1];b&&N.push(b);var O=new a(l,g?t.tokenize(m,g):m,h);N.push(O),w&&N.push(w),Array.prototype.splice.apply(r,N)}}}}}return r},hooks:{all:{},add:function(e,n){var a=t.hooks.all;a[e]=a[e]||[],a[e].push(n)},run:function(e,n){var a=t.hooks.all[e];if(a&&a.length)for(var r,i=0;r=a[i++];)r(n)}}},n=t.Token=function(e,t,n){this.type=e,this.content=t,this.alias=n};if(n.stringify=function(e,a,r){if("string"==typeof e)return e;if("Array"===t.util.type(e))return e.map(function(t){return n.stringify(t,a,e)}).join("");var i={type:e.type,content:n.stringify(e.content,a,r),tag:"span",classes:["token",e.type],attributes:{},language:a,parent:r};if("comment"==i.type&&(i.attributes.spellcheck="true"),e.alias){var l="Array"===t.util.type(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(i.classes,l)}t.hooks.run("wrap",i);var s="";for(var o in i.attributes)s+=o+'="'+(i.attributes[o]||"")+'"';return"<"+i.tag+' class="'+i.classes.join(" ")+'" '+s+">"+i.content+""},!self.document)return self.addEventListener?(self.addEventListener("message",function(e){var n=JSON.parse(e.data),a=n.language,r=n.code;self.postMessage(JSON.stringify(t.util.encode(t.tokenize(r,t.languages[a])))),self.close()},!1),self.Prism):self.Prism;var a=document.getElementsByTagName("script");return a=a[a.length-1],a&&(t.filename=a.src,document.addEventListener&&!a.hasAttribute("data-manual")&&document.addEventListener("DOMContentLoaded",t.highlightAll)),self.Prism}();"undefined"!=typeof module&&module.exports&&(module.exports=Prism);; -Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\w\W]*?\*\//,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0}],string:/("|')(\\\n|\\?.)*?\1/,"class-name":{pattern:/((?:(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/i,lookbehind:!0,inside:{punctuation:/(\.|\\)/}},keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,"boolean":/\b(true|false)\b/,"function":{pattern:/[a-z0-9_]+\(/i,inside:{punctuation:/\(/}},number:/\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?)\b/,operator:/[-+]{1,2}|!|<=?|>=?|={1,3}|&{1,2}|\|?\||\?|\*|\/|~|\^|%/,ignore:/&(lt|gt|amp);/i,punctuation:/[{}[\];(),.:]/};; -Prism.languages.javascript=Prism.languages.extend("clike",{keyword:/\b(break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|false|finally|for|function|get|if|implements|import|in|instanceof|interface|let|new|null|package|private|protected|public|return|set|static|super|switch|this|throw|true|try|typeof|var|void|while|with|yield)\b/,number:/\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee][+-]?\d+)?|NaN|-?Infinity)\b/,"function":/(?!\d)[a-z0-9_$]+(?=\()/i}),Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/,lookbehind:!0}}),Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{script:{pattern:/[\w\W]*?<\/script>/i,inside:{tag:{pattern:/|<\/script>/i,inside:Prism.languages.markup.tag.inside},rest:Prism.languages.javascript},alias:"language-javascript"}});; -Prism.languages.go=Prism.languages.extend("clike",{keyword:/\b(break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go(to)?|if|import|interface|map|package|range|return|select|struct|switch|type|var)\b/,builtin:/\b(bool|byte|complex(64|128)|error|float(32|64)|rune|string|u?int(8|16|32|64|)|uintptr|append|cap|close|complex|copy|delete|imag|len|make|new|panic|print(ln)?|real|recover)\b/,"boolean":/\b(_|iota|nil|true|false)\b/,operator:/([(){}\[\]]|[*\/%^!]=?|\+[=+]?|-[>=-]?|\|[=|]?|>[=>]?|<(<|[=-])?|==?|&(&|=|^=?)?|\.(\.\.)?|[,;]|:=?)/,number:/\b(-?(0x[a-f\d]+|(\d+\.?\d*|\.\d+)(e[-+]?\d+)?)i?)\b/i,string:/("|'|`)(\\?.|\r|\n)*?\1/}),delete Prism.languages.go["class-name"];; diff --git a/examples/realtime-advanced/resources/static/realtime.js b/examples/realtime-advanced/resources/static/realtime.js deleted file mode 100644 index 919dae26..00000000 --- a/examples/realtime-advanced/resources/static/realtime.js +++ /dev/null @@ -1,144 +0,0 @@ - - -function StartRealtime(roomid, timestamp) { - StartEpoch(timestamp); - StartSSE(roomid); - StartForm(); -} - -function StartForm() { - $('#chat-message').focus(); - $('#chat-form').ajaxForm(function() { - $('#chat-message').val(''); - $('#chat-message').focus(); - }); -} - -function StartEpoch(timestamp) { - var windowSize = 60; - var height = 200; - var defaultData = histogram(windowSize, timestamp); - - window.heapChart = $('#heapChart').epoch({ - type: 'time.area', - axes: ['bottom', 'left'], - height: height, - historySize: 10, - data: [ - {values: defaultData}, - {values: defaultData} - ] - }); - - window.mallocsChart = $('#mallocsChart').epoch({ - type: 'time.area', - axes: ['bottom', 'left'], - height: height, - historySize: 10, - data: [ - {values: defaultData}, - {values: defaultData} - ] - }); - - window.messagesChart = $('#messagesChart').epoch({ - type: 'time.line', - axes: ['bottom', 'left'], - height: 240, - historySize: 10, - data: [ - {values: defaultData}, - {values: defaultData}, - {values: defaultData} - ] - }); -} - -function StartSSE(roomid) { - if (!window.EventSource) { - alert("EventSource is not enabled in this browser"); - return; - } - var source = new EventSource('/stream/'+roomid); - source.addEventListener('message', newChatMessage, false); - source.addEventListener('stats', stats, false); -} - -function stats(e) { - var data = parseJSONStats(e.data); - heapChart.push(data.heap); - mallocsChart.push(data.mallocs); - messagesChart.push(data.messages); -} - -function parseJSONStats(e) { - var data = jQuery.parseJSON(e); - var timestamp = data.timestamp; - - var heap = [ - {time: timestamp, y: data.HeapInuse}, - {time: timestamp, y: data.StackInuse} - ]; - - var mallocs = [ - {time: timestamp, y: data.Mallocs}, - {time: timestamp, y: data.Frees} - ]; - var messages = [ - {time: timestamp, y: data.Connected}, - {time: timestamp, y: data.Inbound}, - {time: timestamp, y: data.Outbound} - ]; - - return { - heap: heap, - mallocs: mallocs, - messages: messages - } -} - -function newChatMessage(e) { - var data = jQuery.parseJSON(e.data); - var nick = data.nick; - var message = data.message; - var style = rowStyle(nick); - var html = ""+nick+""+message+""; - $('#chat').append(html); - - $("#chat-scroll").scrollTop($("#chat-scroll")[0].scrollHeight); -} - -function histogram(windowSize, timestamp) { - var entries = new Array(windowSize); - for(var i = 0; i < windowSize; i++) { - entries[i] = {time: (timestamp-windowSize+i-1), y:0}; - } - return entries; -} - -var entityMap = { - "&": "&", - "<": "<", - ">": ">", - '"': '"', - "'": ''', - "/": '/' -}; - -function rowStyle(nick) { - var classes = ['active', 'success', 'info', 'warning', 'danger']; - var index = hashCode(nick)%5; - return classes[index]; -} - -function hashCode(s){ - return Math.abs(s.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0)); -} - -function escapeHtml(string) { - return String(string).replace(/[&<>"'\/]/g, function (s) { - return entityMap[s]; - }); -} - -window.StartRealtime = StartRealtime diff --git a/examples/realtime-advanced/rooms.go b/examples/realtime-advanced/rooms.go deleted file mode 100644 index 82396ba3..00000000 --- a/examples/realtime-advanced/rooms.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import "github.com/dustin/go-broadcast" - -var roomChannels = make(map[string]broadcast.Broadcaster) - -func openListener(roomid string) chan interface{} { - listener := make(chan interface{}) - room(roomid).Register(listener) - return listener -} - -func closeListener(roomid string, listener chan interface{}) { - room(roomid).Unregister(listener) - close(listener) -} - -func room(roomid string) broadcast.Broadcaster { - b, ok := roomChannels[roomid] - if !ok { - b = broadcast.NewBroadcaster(10) - roomChannels[roomid] = b - } - return b -} diff --git a/examples/realtime-advanced/routes.go b/examples/realtime-advanced/routes.go deleted file mode 100644 index 03c69910..00000000 --- a/examples/realtime-advanced/routes.go +++ /dev/null @@ -1,96 +0,0 @@ -package main - -import ( - "fmt" - "html" - "io" - "net/http" - "strings" - "time" - - "github.com/gin-gonic/gin" -) - -func rateLimit(c *gin.Context) { - ip := c.ClientIP() - value := int(ips.Add(ip, 1)) - if value%50 == 0 { - fmt.Printf("ip: %s, count: %d\n", ip, value) - } - if value >= 200 { - if value%200 == 0 { - fmt.Println("ip blocked") - } - c.Abort() - c.String(http.StatusServiceUnavailable, "you were automatically banned :)") - } -} - -func index(c *gin.Context) { - c.Redirect(http.StatusMovedPermanently, "/room/hn") -} - -func roomGET(c *gin.Context) { - roomid := c.Param("roomid") - nick := c.Query("nick") - if len(nick) < 2 { - nick = "" - } - if len(nick) > 13 { - nick = nick[0:12] + "..." - } - c.HTML(http.StatusOK, "room_login.templ.html", gin.H{ - "roomid": roomid, - "nick": nick, - "timestamp": time.Now().Unix(), - }) - -} - -func roomPOST(c *gin.Context) { - roomid := c.Param("roomid") - nick := c.Query("nick") - message := c.PostForm("message") - message = strings.TrimSpace(message) - - validMessage := len(message) > 1 && len(message) < 200 - validNick := len(nick) > 1 && len(nick) < 14 - if !validMessage || !validNick { - c.JSON(http.StatusBadRequest, gin.H{ - "status": "failed", - "error": "the message or nickname is too long", - }) - return - } - - post := gin.H{ - "nick": html.EscapeString(nick), - "message": html.EscapeString(message), - } - messages.Add("inbound", 1) - room(roomid).Submit(post) - c.JSON(http.StatusOK, post) -} - -func streamRoom(c *gin.Context) { - roomid := c.Param("roomid") - listener := openListener(roomid) - ticker := time.NewTicker(1 * time.Second) - users.Add("connected", 1) - defer func() { - closeListener(roomid, listener) - ticker.Stop() - users.Add("disconnected", 1) - }() - - c.Stream(func(w io.Writer) bool { - select { - case msg := <-listener: - messages.Add("outbound", 1) - c.SSEvent("message", msg) - case <-ticker.C: - c.SSEvent("stats", Stats()) - } - return true - }) -} diff --git a/examples/realtime-advanced/stats.go b/examples/realtime-advanced/stats.go deleted file mode 100644 index a6488035..00000000 --- a/examples/realtime-advanced/stats.go +++ /dev/null @@ -1,59 +0,0 @@ -package main - -import ( - "runtime" - "sync" - "time" - - "github.com/manucorporat/stats" -) - -var ( - ips = stats.New() - messages = stats.New() - users = stats.New() - mutexStats sync.RWMutex - savedStats map[string]uint64 -) - -func statsWorker() { - c := time.Tick(1 * time.Second) - var lastMallocs uint64 - var lastFrees uint64 - for range c { - var stats runtime.MemStats - runtime.ReadMemStats(&stats) - - mutexStats.Lock() - savedStats = map[string]uint64{ - "timestamp": uint64(time.Now().Unix()), - "HeapInuse": stats.HeapInuse, - "StackInuse": stats.StackInuse, - "Mallocs": stats.Mallocs - lastMallocs, - "Frees": stats.Frees - lastFrees, - "Inbound": uint64(messages.Get("inbound")), - "Outbound": uint64(messages.Get("outbound")), - "Connected": connectedUsers(), - } - lastMallocs = stats.Mallocs - lastFrees = stats.Frees - messages.Reset() - mutexStats.Unlock() - } -} - -func connectedUsers() uint64 { - connected := users.Get("connected") - users.Get("disconnected") - if connected < 0 { - return 0 - } - return uint64(connected) -} - -// Stats returns savedStats data. -func Stats() map[string]uint64 { - mutexStats.RLock() - defer mutexStats.RUnlock() - - return savedStats -} diff --git a/examples/realtime-chat/Makefile b/examples/realtime-chat/Makefile deleted file mode 100644 index dea583df..00000000 --- a/examples/realtime-chat/Makefile +++ /dev/null @@ -1,9 +0,0 @@ -all: deps build - -.PHONY: deps -deps: - go get -d -v github.com/dustin/go-broadcast/... - -.PHONY: build -build: deps - go build -o realtime-chat main.go rooms.go template.go diff --git a/examples/realtime-chat/main.go b/examples/realtime-chat/main.go deleted file mode 100644 index 5741fcba..00000000 --- a/examples/realtime-chat/main.go +++ /dev/null @@ -1,59 +0,0 @@ -package main - -import ( - "fmt" - "io" - "math/rand" - "net/http" - - "github.com/gin-gonic/gin" -) - -func main() { - router := gin.Default() - router.SetHTMLTemplate(html) - - router.GET("/room/:roomid", roomGET) - router.POST("/room/:roomid", roomPOST) - router.DELETE("/room/:roomid", roomDELETE) - router.GET("/stream/:roomid", stream) - - router.Run(":8080") -} - -func stream(c *gin.Context) { - roomid := c.Param("roomid") - listener := openListener(roomid) - defer closeListener(roomid, listener) - - c.Stream(func(w io.Writer) bool { - c.SSEvent("message", <-listener) - return true - }) -} - -func roomGET(c *gin.Context) { - roomid := c.Param("roomid") - userid := fmt.Sprint(rand.Int31()) - c.HTML(http.StatusOK, "chat_room", gin.H{ - "roomid": roomid, - "userid": userid, - }) -} - -func roomPOST(c *gin.Context) { - roomid := c.Param("roomid") - userid := c.PostForm("user") - message := c.PostForm("message") - room(roomid).Submit(userid + ": " + message) - - c.JSON(http.StatusOK, gin.H{ - "status": "success", - "message": message, - }) -} - -func roomDELETE(c *gin.Context) { - roomid := c.Param("roomid") - deleteBroadcast(roomid) -} diff --git a/examples/realtime-chat/rooms.go b/examples/realtime-chat/rooms.go deleted file mode 100644 index 8c62bece..00000000 --- a/examples/realtime-chat/rooms.go +++ /dev/null @@ -1,33 +0,0 @@ -package main - -import "github.com/dustin/go-broadcast" - -var roomChannels = make(map[string]broadcast.Broadcaster) - -func openListener(roomid string) chan interface{} { - listener := make(chan interface{}) - room(roomid).Register(listener) - return listener -} - -func closeListener(roomid string, listener chan interface{}) { - room(roomid).Unregister(listener) - close(listener) -} - -func deleteBroadcast(roomid string) { - b, ok := roomChannels[roomid] - if ok { - b.Close() - delete(roomChannels, roomid) - } -} - -func room(roomid string) broadcast.Broadcaster { - b, ok := roomChannels[roomid] - if !ok { - b = broadcast.NewBroadcaster(10) - roomChannels[roomid] = b - } - return b -} diff --git a/examples/realtime-chat/template.go b/examples/realtime-chat/template.go deleted file mode 100644 index cc1ab9bc..00000000 --- a/examples/realtime-chat/template.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import "html/template" - -var html = template.Must(template.New("chat_room").Parse(` - - - {{.roomid}} - - - - - - -

Welcome to {{.roomid}} room

-
-
- User: - Message: - -
- - -`)) diff --git a/examples/struct-lvl-validations/README.md b/examples/struct-lvl-validations/README.md deleted file mode 100644 index 1bd57f03..00000000 --- a/examples/struct-lvl-validations/README.md +++ /dev/null @@ -1,50 +0,0 @@ -## Struct level validations - -Validations can also be registered at the `struct` level when field level validations -don't make much sense. This can also be used to solve cross-field validation elegantly. -Additionally, it can be combined with tag validations. Struct Level validations run after -the structs tag validations. - -### Example requests - -```shell -# Validation errors are generated for struct tags as well as at the struct level -$ curl -s -X POST http://localhost:8085/user \ - -H 'content-type: application/json' \ - -d '{}' | jq -{ - "error": "Key: 'User.Email' Error:Field validation for 'Email' failed on the 'required' tag\nKey: 'User.FirstName' Error:Field validation for 'FirstName' failed on the 'fnameorlname' tag\nKey: 'User.LastName' Error:Field validation for 'LastName' failed on the 'fnameorlname' tag", - "message": "User validation failed!" -} - -# Validation fails at the struct level because neither first name nor last name are present -$ curl -s -X POST http://localhost:8085/user \ - -H 'content-type: application/json' \ - -d '{"email": "george@vandaley.com"}' | jq -{ - "error": "Key: 'User.FirstName' Error:Field validation for 'FirstName' failed on the 'fnameorlname' tag\nKey: 'User.LastName' Error:Field validation for 'LastName' failed on the 'fnameorlname' tag", - "message": "User validation failed!" -} - -# No validation errors when either first name or last name is present -$ curl -X POST http://localhost:8085/user \ - -H 'content-type: application/json' \ - -d '{"fname": "George", "email": "george@vandaley.com"}' -{"message":"User validation successful."} - -$ curl -X POST http://localhost:8085/user \ - -H 'content-type: application/json' \ - -d '{"lname": "Contanza", "email": "george@vandaley.com"}' -{"message":"User validation successful."} - -$ curl -X POST http://localhost:8085/user \ - -H 'content-type: application/json' \ - -d '{"fname": "George", "lname": "Costanza", "email": "george@vandaley.com"}' -{"message":"User validation successful."} -``` - -### Useful links - -- Validator docs - https://godoc.org/gopkg.in/go-playground/validator.v8#Validate.RegisterStructValidation -- Struct level example - https://github.com/go-playground/validator/blob/v8.18.2/examples/struct-level/struct_level.go -- Validator release notes - https://github.com/go-playground/validator/releases/tag/v8.7 diff --git a/examples/struct-lvl-validations/server.go b/examples/struct-lvl-validations/server.go deleted file mode 100644 index be807b78..00000000 --- a/examples/struct-lvl-validations/server.go +++ /dev/null @@ -1,64 +0,0 @@ -package main - -import ( - "net/http" - "reflect" - - "github.com/gin-gonic/gin" - "github.com/gin-gonic/gin/binding" - validator "gopkg.in/go-playground/validator.v8" -) - -// User contains user information. -type User struct { - FirstName string `json:"fname"` - LastName string `json:"lname"` - Email string `binding:"required,email"` -} - -// UserStructLevelValidation contains custom struct level validations that don't always -// make sense at the field validation level. For example, this function validates that either -// FirstName or LastName exist; could have done that with a custom field validation but then -// would have had to add it to both fields duplicating the logic + overhead, this way it's -// only validated once. -// -// NOTE: you may ask why wouldn't not just do this outside of validator. Doing this way -// hooks right into validator and you can combine with validation tags and still have a -// common error output format. -func UserStructLevelValidation(v *validator.Validate, structLevel *validator.StructLevel) { - user := structLevel.CurrentStruct.Interface().(User) - - if len(user.FirstName) == 0 && len(user.LastName) == 0 { - structLevel.ReportError( - reflect.ValueOf(user.FirstName), "FirstName", "fname", "fnameorlname", - ) - structLevel.ReportError( - reflect.ValueOf(user.LastName), "LastName", "lname", "fnameorlname", - ) - } - - // plus can to more, even with different tag than "fnameorlname" -} - -func main() { - route := gin.Default() - - if v, ok := binding.Validator.Engine().(*validator.Validate); ok { - v.RegisterStructValidation(UserStructLevelValidation, User{}) - } - - route.POST("/user", validateUser) - route.Run(":8085") -} - -func validateUser(c *gin.Context) { - var u User - if err := c.ShouldBindJSON(&u); err == nil { - c.JSON(http.StatusOK, gin.H{"message": "User validation successful."}) - } else { - c.JSON(http.StatusBadRequest, gin.H{ - "message": "User validation failed!", - "error": err.Error(), - }) - } -} diff --git a/examples/template/main.go b/examples/template/main.go deleted file mode 100644 index e20a3b98..00000000 --- a/examples/template/main.go +++ /dev/null @@ -1,32 +0,0 @@ -package main - -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") -} diff --git a/examples/upload-file/multiple/main.go b/examples/upload-file/multiple/main.go deleted file mode 100644 index 2b9d6d91..00000000 --- a/examples/upload-file/multiple/main.go +++ /dev/null @@ -1,39 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "path/filepath" - - "github.com/gin-gonic/gin" -) - -func main() { - router := gin.Default() - // Set a lower memory limit for multipart forms (default is 32 MiB) - router.MaxMultipartMemory = 8 << 20 // 8 MiB - router.Static("/", "./public") - router.POST("/upload", func(c *gin.Context) { - name := c.PostForm("name") - email := c.PostForm("email") - - // Multipart form - form, err := c.MultipartForm() - if err != nil { - c.String(http.StatusBadRequest, fmt.Sprintf("get form err: %s", err.Error())) - return - } - files := form.File["files"] - - for _, file := range files { - filename := filepath.Base(file.Filename) - if err := c.SaveUploadedFile(file, filename); err != nil { - c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error())) - return - } - } - - c.String(http.StatusOK, fmt.Sprintf("Uploaded successfully %d files with fields name=%s and email=%s.", len(files), name, email)) - }) - router.Run(":8080") -} diff --git a/examples/upload-file/multiple/public/index.html b/examples/upload-file/multiple/public/index.html deleted file mode 100644 index b8463601..00000000 --- a/examples/upload-file/multiple/public/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - Multiple file upload - - -

Upload multiple files with fields

- -
- Name:
- Email:
- Files:

- -
- - diff --git a/examples/upload-file/single/main.go b/examples/upload-file/single/main.go deleted file mode 100644 index ba289f54..00000000 --- a/examples/upload-file/single/main.go +++ /dev/null @@ -1,36 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "path/filepath" - - "github.com/gin-gonic/gin" -) - -func main() { - router := gin.Default() - // Set a lower memory limit for multipart forms (default is 32 MiB) - router.MaxMultipartMemory = 8 << 20 // 8 MiB - router.Static("/", "./public") - router.POST("/upload", func(c *gin.Context) { - name := c.PostForm("name") - email := c.PostForm("email") - - // Source - file, err := c.FormFile("file") - if err != nil { - c.String(http.StatusBadRequest, fmt.Sprintf("get form err: %s", err.Error())) - return - } - - filename := filepath.Base(file.Filename) - if err := c.SaveUploadedFile(file, filename); err != nil { - c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error())) - return - } - - c.String(http.StatusOK, fmt.Sprintf("File %s uploaded successfully with fields name=%s and email=%s.", file.Filename, name, email)) - }) - router.Run(":8080") -} diff --git a/examples/upload-file/single/public/index.html b/examples/upload-file/single/public/index.html deleted file mode 100644 index b0c2a808..00000000 --- a/examples/upload-file/single/public/index.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - Single file upload - - -

Upload single file with fields

- -
- Name:
- Email:
- Files:

- -
- diff --git a/go.mod b/go.mod index 6f9d68d1..5963e014 100644 --- a/go.mod +++ b/go.mod @@ -1,32 +1,18 @@ module github.com/gin-gonic/gin require ( - github.com/gin-contrib/sse v0.0.0-20190124093953-61b50c2ef482 + github.com/gin-contrib/sse v0.0.0-20190125020943-a7658810eb74 github.com/golang/protobuf v1.2.0 github.com/json-iterator/go v1.1.5 github.com/mattn/go-isatty v0.0.4 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.1 // indirect github.com/stretchr/testify v1.3.0 - github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 - golang.org/x/net v0.0.0-20190119204137-ed066c81e75e - golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 - golang.org/x/sys v0.0.0-20190124100055-b90733256f2e // indirect + github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43 + golang.org/x/net v0.0.0-20190213061140-3a22650c66bd + golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect + golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0 // indirect gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/go-playground/validator.v8 v8.18.2 gopkg.in/yaml.v2 v2.2.2 ) - -exclude ( - github.com/campoy/embedmd v0.0.0-20181127031020-97c13d6e4160 - github.com/client9/misspell v0.3.4 - github.com/dustin/go-broadcast v0.0.0-20171205050544-f664265f5a66 - github.com/gin-gonic/autotls v0.0.0-20190119125636-0b5f4fc15768 - github.com/jessevdk/go-assets v0.0.0-20160921144138-4f4301a06e15 - github.com/manucorporat/stats v0.0.0-20180402194714-3ba42d56d227 - github.com/newrelic/go-agent v2.5.0+incompatible - github.com/thinkerou/favicon v0.1.0 - golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b - golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1 - google.golang.org/grpc v1.18.0 -) diff --git a/go.sum b/go.sum index 95e2b4f6..d864be2f 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,10 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/campoy/embedmd v0.0.0-20181127031020-97c13d6e4160 h1:HJpuhXOHC4EkXDARsLjmXAV9FhlY6qFDnKI/MJM6eoE= -github.com/campoy/embedmd v0.0.0-20181127031020-97c13d6e4160/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= -github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gin-contrib/sse v0.0.0-20190124093953-61b50c2ef482 h1:iOz5sIQUvuOlpiC7Q6+MmJQpWnlneYX98QIGf+2m50Y= -github.com/gin-contrib/sse v0.0.0-20190124093953-61b50c2ef482/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/gin-contrib/sse v0.0.0-20190125020943-a7658810eb74 h1:FaI7wNyesdMBSkIRVUuEEYEvmzufs7EqQvRAxfEXGbQ= +github.com/gin-contrib/sse v0.0.0-20190125020943-a7658810eb74/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -22,28 +13,16 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 h1:EICbibRW4JNKMcY+LsWmuwob+CRS1BmdRdjphAm9mH4= -github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3 h1:x/bBzNauLQAlE3fLku/xy92Y8QwKX5HZymrMz2IiKFc= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190119204137-ed066c81e75e h1:MDa3fSUp6MdYHouVmCCNz/zaH2a6CRcxY3VhT/K3C5Q= -golang.org/x/net v0.0.0-20190119204137-ed066c81e75e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +github.com/ugorji/go v1.1.2 h1:JON3E2/GPW2iDNGoSAusl1KDf5TRQ8k8q7Tp097pZGs= +github.com/ugorji/go v1.1.2/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= +github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43 h1:BasDe+IErOQKrMVXab7UayvSlIpiyGwRvuX3EKYY7UA= +github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43/go.mod h1:iT03XoTwV7xq/+UGwKO3UbC1nNNlopQiY61beSdrtOA= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd h1:HuTn7WObtcDo9uEEU7rEqL0jYthdXAmZ6PP+meazmaU= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190124100055-b90733256f2e h1:3GIlrlVLfkoipSReOMNAgApI0ajnalyLa/EZHHca/XI= -golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/grpc v1.18.0 h1:IZl7mfBGfbhYx2p2rKRtYgDFw6SBz+kclmxYrCksPPA= -google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0 h1:bzeyCHgoAyjZjAhvTpks+qM7sdlh4cCSitmXeCEO3B4= +golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= @@ -51,4 +30,3 @@ gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2G gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/vendor/vendor.json b/vendor/vendor.json index af1a0148..6050e8f6 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -40,6 +40,12 @@ "version": "v0.0", "versionExact": "v0.0.4" }, + { + "checksumSHA1": "LuFv4/jlrmFNnDb/5SCSEPAM9vU=", + "path": "github.com/pmezard/go-difflib/difflib", + "revision": "5d4384ee4fb2527b0a1256a821ebfc92f91efefc", + "revisionTime": "2018-12-26T10:54:42Z" + }, { "checksumSHA1": "c6pbpF7eowwO59phRTpF8cQ80Z0=", "path": "github.com/stretchr/testify/assert", From 688eb1281c6c027fbf44d9af9aea9abe32794d07 Mon Sep 17 00:00:00 2001 From: Dang Nguyen Date: Sat, 2 Mar 2019 15:04:21 +0700 Subject: [PATCH 082/111] update examples link in README (#1789) --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a4ced64e..6cb0e78c 100644 --- a/README.md +++ b/README.md @@ -359,7 +359,7 @@ ids: map[b:hello a:1234], names: map[second:tianou first:thinkerou] #### Single file -References issue [#774](https://github.com/gin-gonic/gin/issues/774) and detail [example code](examples/upload-file/single). +References issue [#774](https://github.com/gin-gonic/gin/issues/774) and detail [example code](https://github.com/gin-gonic/examples/tree/master/upload-file/single). `file.Filename` **SHOULD NOT** be trusted. See [`Content-Disposition` on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#Directives) and [#1693](https://github.com/gin-gonic/gin/issues/1693) @@ -394,7 +394,7 @@ curl -X POST http://localhost:8080/upload \ #### Multiple files -See the detail [example code](examples/upload-file/multiple). +See the detail [example code](https://github.com/gin-gonic/examples/tree/master/upload-file/multiple). ```go func main() { @@ -726,7 +726,7 @@ When running the above example using the above the `curl` command, it returns er ### Custom Validators -It is also possible to register custom validators. See the [example code](examples/custom-validation/server.go). +It is also possible to register custom validators. See the [example code](https://github.com/gin-gonic/examples/tree/master/custom-validation/server.go). ```go package main @@ -790,7 +790,7 @@ $ curl "localhost:8085/bookable?check_in=2018-03-08&check_out=2018-03-09" ``` [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. +See the [struct-lvl-validation example](https://github.com/gin-gonic/examples/tree/master/struct-lvl-validations) to learn more. ### Only Bind Query String @@ -1280,7 +1280,7 @@ You may use custom delims #### Custom Template Funcs -See the detail [example code](examples/template). +See the detail [example code](https://github.com/gin-gonic/examples/tree/master/template). main.go @@ -1654,7 +1654,7 @@ An alternative to endless: * [graceful](https://github.com/tylerb/graceful): Graceful is a Go package enabling graceful shutdown of an http.Handler server. * [grace](https://github.com/facebookgo/grace): Graceful restart & zero downtime deploy for Go servers. -If you are using Go 1.8, you may not need to use this library! Consider using http.Server's built-in [Shutdown()](https://golang.org/pkg/net/http/#Server.Shutdown) method for graceful shutdowns. See the full [graceful-shutdown](./examples/graceful-shutdown) example with gin. +If you are using Go 1.8, you may not need to use this library! Consider using http.Server's built-in [Shutdown()](https://golang.org/pkg/net/http/#Server.Shutdown) method for graceful shutdowns. See the full [graceful-shutdown](https://github.com/gin-gonic/examples/tree/master/graceful-shutdown) example with gin. ```go // +build go1.8 @@ -1758,7 +1758,7 @@ func loadTemplate() (*template.Template, error) { } ``` -See a complete example in the `examples/assets-in-binary` directory. +See a complete example in the `https://github.com/gin-gonic/examples/tree/master/assets-in-binary` directory. ### Bind form-data request with custom struct From 8c8002d7449979ab65d22794be159751dbc1c20f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Sat, 2 Mar 2019 19:21:10 +0800 Subject: [PATCH 083/111] chore: add examples repo link to README (#1788) --- README.md | 2 ++ examples/README.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6cb0e78c..20e3e58d 100644 --- a/README.md +++ b/README.md @@ -211,6 +211,8 @@ $ go build -tags=jsoniter . ## API Examples +You can find a number of ready-to-run examples at [Gin examples repository](https://github.com/gin-gonic/examples). + ### Using GET, POST, PUT, PATCH, DELETE and OPTIONS ```go diff --git a/examples/README.md b/examples/README.md index 4b3b718c..b02deae4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,3 +1,3 @@ # Gin Examples -## TODO +⚠️ **NOTICE:** All gin examples has moved as alone repository to [here](https://github.com/gin-gonic/examples). From 3b84a430d0a6307b1f788952520db268bc3effe4 Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Sat, 2 Mar 2019 20:19:42 +0800 Subject: [PATCH 084/111] Drone switch from gin to go-chi in 1.0 version. (#1790) --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 20e3e58d..a22440f9 100644 --- a/README.md +++ b/README.md @@ -2082,7 +2082,6 @@ func TestPingRoute(t *testing.T) { 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. * [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. From 893c6cae07ef564cbdd2796589c449dd2ac87d21 Mon Sep 17 00:00:00 2001 From: Daniel Krom Date: Sat, 2 Mar 2019 17:07:37 +0200 Subject: [PATCH 085/111] Added stream flag indicates if client disconnected in middle of streaming (#1252) --- context.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/context.go b/context.go index e9735d28..5dc7f8a0 100644 --- a/context.go +++ b/context.go @@ -896,19 +896,20 @@ func (c *Context) SSEvent(name string, message interface{}) { }) } -// Stream sends a streaming response. -func (c *Context) Stream(step func(w io.Writer) bool) { +// Stream sends a streaming response and returns a boolean +// indicates "Is client disconnected in middle of stream" +func (c *Context) Stream(step func(w io.Writer) bool) bool { w := c.Writer clientGone := w.CloseNotify() for { select { case <-clientGone: - return + return true default: keepOpen := step(w) w.Flush() if !keepOpen { - return + return false } } } From 0d50ce859745354fa83dcf2bf4c972abed25e53b Mon Sep 17 00:00:00 2001 From: Dmitry Kutakov Date: Sun, 3 Mar 2019 09:39:43 +0300 Subject: [PATCH 086/111] refactor(form_mapping.go): mapping ptr, struct and map (#1749) * refactor(form_mapping.go): mapping ptr, struct and map * fix #1672 correct work with ptr - not create value if field is not set * avoid allocations on strings.Split() - change to strings.Index() * fix #610 tag value "-" is mean ignoring field * struct fields mapped like json.Unmarshal * map fields mapped like json.Unmarshal * fix after @thinkerou review --- README.md | 18 -- binding/binding_test.go | 134 ++++++++++-- binding/form_mapping.go | 278 +++++++++++++++---------- binding/form_mapping_benchmark_test.go | 61 ++++++ 4 files changed, 341 insertions(+), 150 deletions(-) create mode 100644 binding/form_mapping_benchmark_test.go diff --git a/README.md b/README.md index a22440f9..eb9415fd 100644 --- a/README.md +++ b/README.md @@ -1836,24 +1836,6 @@ $ curl "http://localhost:8080/getd?field_x=hello&field_d=world" {"d":"world","x":{"FieldX":"hello"}} ``` -**NOTE**: NOT support the follow style struct: - -```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 -} -``` - -In a word, only support nested custom struct which have no `form` now. - ### Try to bind body into different structs The normal methods for binding request body consumes `c.Request.Body` and they diff --git a/binding/binding_test.go b/binding/binding_test.go index c9dea347..16ca2027 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -12,6 +12,7 @@ import ( "mime/multipart" "net/http" "strconv" + "strings" "testing" "time" @@ -57,7 +58,6 @@ type FooStructForTimeTypeFailLocation struct { } type FooStructForMapType struct { - // Unknown type: not support map MapFoo map[string]interface{} `form:"map_foo"` } @@ -303,7 +303,7 @@ func TestBindingFormInvalidName2(t *testing.T) { func TestBindingFormForType(t *testing.T) { testFormBindingForType(t, "POST", "/", "/", - "map_foo=", "bar2=1", "Map") + "map_foo={\"bar\":123}", "map_foo=1", "Map") testFormBindingForType(t, "POST", "/", "/", @@ -508,20 +508,30 @@ func TestBindingYAMLFail(t *testing.T) { `foo:\nbar`, `bar: foo`) } -func createFormPostRequest() *http.Request { - req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar&bar=foo")) +func createFormPostRequest(t *testing.T) *http.Request { + req, err := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar&bar=foo")) + assert.NoError(t, err) req.Header.Set("Content-Type", MIMEPOSTForm) return req } -func createDefaultFormPostRequest() *http.Request { - req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar")) +func createDefaultFormPostRequest(t *testing.T) *http.Request { + req, err := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar")) + assert.NoError(t, err) req.Header.Set("Content-Type", MIMEPOSTForm) return req } -func createFormPostRequestFail() *http.Request { - req, _ := http.NewRequest("POST", "/?map_foo=getfoo", bytes.NewBufferString("map_foo=bar")) +func createFormPostRequestForMap(t *testing.T) *http.Request { + req, err := http.NewRequest("POST", "/?map_foo=getfoo", bytes.NewBufferString("map_foo={\"bar\":123}")) + assert.NoError(t, err) + req.Header.Set("Content-Type", MIMEPOSTForm) + return req +} + +func createFormPostRequestForMapFail(t *testing.T) *http.Request { + req, err := http.NewRequest("POST", "/?map_foo=getfoo", bytes.NewBufferString("map_foo=hello")) + assert.NoError(t, err) req.Header.Set("Content-Type", MIMEPOSTForm) return req } @@ -535,26 +545,42 @@ func createFormMultipartRequest(t *testing.T) *http.Request { assert.NoError(t, mw.SetBoundary(boundary)) assert.NoError(t, mw.WriteField("foo", "bar")) assert.NoError(t, mw.WriteField("bar", "foo")) - req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body) + req, err := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body) + assert.NoError(t, err) req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary) return req } -func createFormMultipartRequestFail(t *testing.T) *http.Request { +func createFormMultipartRequestForMap(t *testing.T) *http.Request { boundary := "--testboundary" body := new(bytes.Buffer) mw := multipart.NewWriter(body) defer mw.Close() assert.NoError(t, mw.SetBoundary(boundary)) - assert.NoError(t, mw.WriteField("map_foo", "bar")) - req, _ := http.NewRequest("POST", "/?map_foo=getfoo", body) + assert.NoError(t, mw.WriteField("map_foo", "{\"bar\":123, \"name\":\"thinkerou\", \"pai\": 3.14}")) + req, err := http.NewRequest("POST", "/?map_foo=getfoo", body) + assert.NoError(t, err) + req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary) + return req +} + +func createFormMultipartRequestForMapFail(t *testing.T) *http.Request { + boundary := "--testboundary" + body := new(bytes.Buffer) + mw := multipart.NewWriter(body) + defer mw.Close() + + assert.NoError(t, mw.SetBoundary(boundary)) + assert.NoError(t, mw.WriteField("map_foo", "3.14")) + req, err := http.NewRequest("POST", "/?map_foo=getfoo", body) + assert.NoError(t, err) req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary) return req } func TestBindingFormPost(t *testing.T) { - req := createFormPostRequest() + req := createFormPostRequest(t) var obj FooBarStruct assert.NoError(t, FormPost.Bind(req, &obj)) @@ -564,7 +590,7 @@ func TestBindingFormPost(t *testing.T) { } func TestBindingDefaultValueFormPost(t *testing.T) { - req := createDefaultFormPostRequest() + req := createDefaultFormPostRequest(t) var obj FooDefaultBarStruct assert.NoError(t, FormPost.Bind(req, &obj)) @@ -572,8 +598,16 @@ func TestBindingDefaultValueFormPost(t *testing.T) { assert.Equal(t, "hello", obj.Bar) } -func TestBindingFormPostFail(t *testing.T) { - req := createFormPostRequestFail() +func TestBindingFormPostForMap(t *testing.T) { + req := createFormPostRequestForMap(t) + var obj FooStructForMapType + err := FormPost.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, float64(123), obj.MapFoo["bar"].(float64)) +} + +func TestBindingFormPostForMapFail(t *testing.T) { + req := createFormPostRequestForMapFail(t) var obj FooStructForMapType err := FormPost.Bind(req, &obj) assert.Error(t, err) @@ -589,8 +623,18 @@ func TestBindingFormMultipart(t *testing.T) { assert.Equal(t, "foo", obj.Bar) } -func TestBindingFormMultipartFail(t *testing.T) { - req := createFormMultipartRequestFail(t) +func TestBindingFormMultipartForMap(t *testing.T) { + req := createFormMultipartRequestForMap(t) + var obj FooStructForMapType + err := FormMultipart.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, float64(123), obj.MapFoo["bar"].(float64)) + assert.Equal(t, "thinkerou", obj.MapFoo["name"].(string)) + assert.Equal(t, float64(3.14), obj.MapFoo["pai"].(float64)) +} + +func TestBindingFormMultipartForMapFail(t *testing.T) { + req := createFormMultipartRequestForMapFail(t) var obj FooStructForMapType err := FormMultipart.Bind(req, &obj) assert.Error(t, err) @@ -773,6 +817,17 @@ func TestFormBindingFail(t *testing.T) { assert.Error(t, err) } +func TestFormBindingMultipartFail(t *testing.T) { + obj := FooBarStruct{} + req, err := http.NewRequest("POST", "/", strings.NewReader("foo=bar")) + assert.NoError(t, err) + req.Header.Set("Content-Type", MIMEMultipartPOSTForm+";boundary=testboundary") + _, err = req.MultipartReader() + assert.NoError(t, err) + err = Form.Bind(req, &obj) + assert.Error(t, err) +} + func TestFormPostBindingFail(t *testing.T) { b := FormPost assert.Equal(t, "form-urlencoded", b.Name()) @@ -1109,7 +1164,8 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s case "Map": obj := FooStructForMapType{} err := b.Bind(req, &obj) - assert.Error(t, err) + assert.NoError(t, err) + assert.Equal(t, float64(123), obj.MapFoo["bar"].(float64)) case "SliceMap": obj := FooStructForSliceMapType{} err := b.Bind(req, &obj) @@ -1317,3 +1373,43 @@ func TestCanSet(t *testing.T) { var c CanSetStruct assert.Nil(t, mapForm(&c, nil)) } + +func formPostRequest(path, body string) *http.Request { + req := requestWithBody("POST", path, body) + req.Header.Add("Content-Type", MIMEPOSTForm) + return req +} + +func TestBindingSliceDefault(t *testing.T) { + var s struct { + Friends []string `form:"friends,default=mike"` + } + req := formPostRequest("", "") + err := Form.Bind(req, &s) + assert.NoError(t, err) + + assert.Len(t, s.Friends, 1) + assert.Equal(t, "mike", s.Friends[0]) +} + +func TestBindingStructField(t *testing.T) { + var s struct { + Opts struct { + Port int + } `form:"opts"` + } + req := formPostRequest("", `opts={"Port": 8000}`) + err := Form.Bind(req, &s) + assert.NoError(t, err) + assert.Equal(t, 8000, s.Opts.Port) +} + +func TestBindingUnknownTypeChan(t *testing.T) { + var s struct { + Stop chan bool `form:"stop"` + } + req := formPostRequest("", "stop=true") + err := Form.Bind(req, &s) + assert.Error(t, err) + assert.Equal(t, errUnknownType, err) +} diff --git a/binding/form_mapping.go b/binding/form_mapping.go index 8eb5c0d1..1109e4d0 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -5,6 +5,7 @@ package binding import ( + "encoding/json" "errors" "reflect" "strconv" @@ -12,6 +13,8 @@ import ( "time" ) +var errUnknownType = errors.New("Unknown type") + func mapUri(ptr interface{}, m map[string][]string) error { return mapFormByTag(ptr, m, "uri") } @@ -20,124 +23,153 @@ func mapForm(ptr interface{}, form map[string][]string) error { return mapFormByTag(ptr, form, "form") } +var emptyField = reflect.StructField{} + 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++ { - typeField := typ.Field(i) - structField := val.Field(i) - if !structField.CanSet() { - continue - } - - structFieldKind := structField.Kind() - inputFieldName := typeField.Tag.Get(tag) - inputFieldNameList := strings.Split(inputFieldName, ",") - inputFieldName = inputFieldNameList[0] - var defaultValue string - if len(inputFieldNameList) > 1 { - defaultList := strings.SplitN(inputFieldNameList[1], "=", 2) - if defaultList[0] == "default" { - defaultValue = defaultList[1] - } - } - if inputFieldName == "-" { - continue - } - if inputFieldName == "" { - inputFieldName = typeField.Name - - // if "form" tag is nil, we inspect if the field is a struct or struct pointer. - // this would not make sense for JSON parsing but it does for a form - // since data is flatten - if structFieldKind == reflect.Ptr { - if !structField.Elem().IsValid() { - structField.Set(reflect.New(structField.Type().Elem())) - } - structField = structField.Elem() - structFieldKind = structField.Kind() - } - if structFieldKind == reflect.Struct { - err := mapFormByTag(structField.Addr().Interface(), form, tag) - if err != nil { - return err - } - continue - } - } - inputValue, exists := form[inputFieldName] - - if !exists { - if defaultValue == "" { - continue - } - inputValue = make([]string, 1) - inputValue[0] = defaultValue - } - - numElems := len(inputValue) - if structFieldKind == reflect.Slice && numElems > 0 { - sliceOf := structField.Type().Elem().Kind() - slice := reflect.MakeSlice(structField.Type(), numElems, numElems) - for i := 0; i < numElems; i++ { - if err := setWithProperType(sliceOf, inputValue[i], slice.Index(i)); err != nil { - return err - } - } - val.Field(i).Set(slice) - continue - } - if _, isTime := structField.Interface().(time.Time); isTime { - if err := setTimeField(inputValue[0], typeField, structField); err != nil { - return err - } - continue - } - if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil { - return err - } - } - return nil + _, err := mapping(reflect.ValueOf(ptr), emptyField, form, tag) + return err } -func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error { - switch valueKind { - case reflect.Int: - return setIntField(val, 0, structField) - case reflect.Int8: - return setIntField(val, 8, structField) - case reflect.Int16: - return setIntField(val, 16, structField) - case reflect.Int32: - return setIntField(val, 32, structField) - case reflect.Int64: - return setIntField(val, 64, structField) - case reflect.Uint: - return setUintField(val, 0, structField) - case reflect.Uint8: - return setUintField(val, 8, structField) - case reflect.Uint16: - return setUintField(val, 16, structField) - case reflect.Uint32: - return setUintField(val, 32, structField) - case reflect.Uint64: - return setUintField(val, 64, structField) - case reflect.Bool: - return setBoolField(val, structField) - case reflect.Float32: - return setFloatField(val, 32, structField) - case reflect.Float64: - return setFloatField(val, 64, structField) - case reflect.String: - structField.SetString(val) - case reflect.Ptr: - if !structField.Elem().IsValid() { - structField.Set(reflect.New(structField.Type().Elem())) +func mapping(value reflect.Value, field reflect.StructField, form map[string][]string, tag string) (bool, error) { + var vKind = value.Kind() + + if vKind == reflect.Ptr { + var isNew bool + vPtr := value + if value.IsNil() { + isNew = true + vPtr = reflect.New(value.Type().Elem()) } - structFieldElem := structField.Elem() - return setWithProperType(structFieldElem.Kind(), val, structFieldElem) + isSetted, err := mapping(vPtr.Elem(), field, form, tag) + if err != nil { + return false, err + } + if isNew && isSetted { + value.Set(vPtr) + } + return isSetted, nil + } + + ok, err := tryToSetValue(value, field, form, tag) + if err != nil { + return false, err + } + if ok { + return true, nil + } + + if vKind == reflect.Struct { + tValue := value.Type() + + var isSetted bool + for i := 0; i < value.NumField(); i++ { + if !value.Field(i).CanSet() { + continue + } + ok, err := mapping(value.Field(i), tValue.Field(i), form, tag) + if err != nil { + return false, err + } + isSetted = isSetted || ok + } + return isSetted, nil + } + return false, nil +} + +func tryToSetValue(value reflect.Value, field reflect.StructField, form map[string][]string, tag string) (bool, error) { + var tagValue, defaultValue string + var isDefaultExists bool + + tagValue = field.Tag.Get(tag) + tagValue, opts := head(tagValue, ",") + + if tagValue == "-" { // just ignoring this field + return false, nil + } + if tagValue == "" { // default value is FieldName + tagValue = field.Name + } + if tagValue == "" { // when field is "emptyField" variable + return false, nil + } + + var opt string + for len(opts) > 0 { + opt, opts = head(opts, ",") + + k, v := head(opt, "=") + switch k { + case "default": + isDefaultExists = true + defaultValue = v + } + } + + vs, ok := form[tagValue] + if !ok && !isDefaultExists { + return false, nil + } + + switch value.Kind() { + case reflect.Slice: + if !ok { + vs = []string{defaultValue} + } + return true, setSlice(vs, value, field) default: - return errors.New("Unknown type") + var val string + if !ok { + val = defaultValue + } + + if len(vs) > 0 { + val = vs[0] + } + return true, setWithProperType(val, value, field) + } +} + +func setWithProperType(val string, value reflect.Value, field reflect.StructField) error { + switch value.Kind() { + case reflect.Int: + return setIntField(val, 0, value) + case reflect.Int8: + return setIntField(val, 8, value) + case reflect.Int16: + return setIntField(val, 16, value) + case reflect.Int32: + return setIntField(val, 32, value) + case reflect.Int64: + return setIntField(val, 64, value) + case reflect.Uint: + return setUintField(val, 0, value) + case reflect.Uint8: + return setUintField(val, 8, value) + case reflect.Uint16: + return setUintField(val, 16, value) + case reflect.Uint32: + return setUintField(val, 32, value) + case reflect.Uint64: + return setUintField(val, 64, value) + case reflect.Bool: + return setBoolField(val, value) + case reflect.Float32: + return setFloatField(val, 32, value) + case reflect.Float64: + return setFloatField(val, 64, value) + case reflect.String: + value.SetString(val) + case reflect.Struct: + switch value.Interface().(type) { + case time.Time: + return setTimeField(val, field, value) + } + return json.Unmarshal([]byte(val), value.Addr().Interface()) + case reflect.Map: + return json.Unmarshal([]byte(val), value.Addr().Interface()) + default: + return errUnknownType } return nil } @@ -218,3 +250,23 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val value.Set(reflect.ValueOf(t)) return nil } + +func setSlice(vals []string, value reflect.Value, field reflect.StructField) error { + slice := reflect.MakeSlice(value.Type(), len(vals), len(vals)) + for i, s := range vals { + err := setWithProperType(s, slice.Index(i), field) + if err != nil { + return err + } + } + value.Set(slice) + return nil +} + +func head(str, sep string) (head string, tail string) { + idx := strings.Index(str, sep) + if idx < 0 { + return str, "" + } + return str[:idx], str[idx+len(sep):] +} diff --git a/binding/form_mapping_benchmark_test.go b/binding/form_mapping_benchmark_test.go new file mode 100644 index 00000000..0ef08f00 --- /dev/null +++ b/binding/form_mapping_benchmark_test.go @@ -0,0 +1,61 @@ +// Copyright 2019 Gin Core Team. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +var form = map[string][]string{ + "name": {"mike"}, + "friends": {"anna", "nicole"}, + "id_number": {"12345678"}, + "id_date": {"2018-01-20"}, +} + +type structFull struct { + Name string `form:"name"` + Age int `form:"age,default=25"` + Friends []string `form:"friends"` + ID *struct { + Number string `form:"id_number"` + DateOfIssue time.Time `form:"id_date" time_format:"2006-01-02" time_utc:"true"` + } + Nationality *string `form:"nationality"` +} + +func BenchmarkMapFormFull(b *testing.B) { + var s structFull + for i := 0; i < b.N; i++ { + mapForm(&s, form) + } + b.StopTimer() + + t := b + assert.Equal(t, "mike", s.Name) + assert.Equal(t, 25, s.Age) + assert.Equal(t, []string{"anna", "nicole"}, s.Friends) + assert.Equal(t, "12345678", s.ID.Number) + assert.Equal(t, time.Date(2018, 1, 20, 0, 0, 0, 0, time.UTC), s.ID.DateOfIssue) + assert.Nil(t, s.Nationality) +} + +type structName struct { + Name string `form:"name"` +} + +func BenchmarkMapFormName(b *testing.B) { + var s structName + for i := 0; i < b.N; i++ { + mapForm(&s, form) + } + b.StopTimer() + + t := b + assert.Equal(t, "mike", s.Name) +} From df366c7840199276b3828c55d2a48588b5c15633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Mon, 4 Mar 2019 07:28:03 +0800 Subject: [PATCH 087/111] chore: update go mod package (#1792) --- go.mod | 12 ++++++------ go.sum | 31 +++++++++++++++++++------------ 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 5963e014..01227574 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,17 @@ module github.com/gin-gonic/gin +go 1.12 + require ( - github.com/gin-contrib/sse v0.0.0-20190125020943-a7658810eb74 - github.com/golang/protobuf v1.2.0 + github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 + github.com/golang/protobuf v1.3.0 github.com/json-iterator/go v1.1.5 - github.com/mattn/go-isatty v0.0.4 + github.com/mattn/go-isatty v0.0.6 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.1 // indirect github.com/stretchr/testify v1.3.0 github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43 - golang.org/x/net v0.0.0-20190213061140-3a22650c66bd - golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect - golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0 // indirect + golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95 gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/go-playground/validator.v8 v8.18.2 gopkg.in/yaml.v2 v2.2.2 diff --git a/go.sum b/go.sum index d864be2f..84cf8378 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,18 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gin-contrib/sse v0.0.0-20190125020943-a7658810eb74 h1:FaI7wNyesdMBSkIRVUuEEYEvmzufs7EqQvRAxfEXGbQ= -github.com/gin-contrib/sse v0.0.0-20190125020943-a7658810eb74/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= -github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g= +github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk= +github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= +github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE= github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.6 h1:SrwhHcpV4nWrMGdNcC2kXpMfcBVYGDuTArqyhocJgvA= +github.com/mattn/go-isatty v0.0.6/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= @@ -17,14 +21,17 @@ github.com/ugorji/go v1.1.2 h1:JON3E2/GPW2iDNGoSAusl1KDf5TRQ8k8q7Tp097pZGs= github.com/ugorji/go v1.1.2/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43 h1:BasDe+IErOQKrMVXab7UayvSlIpiyGwRvuX3EKYY7UA= github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43/go.mod h1:iT03XoTwV7xq/+UGwKO3UbC1nNNlopQiY61beSdrtOA= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd h1:HuTn7WObtcDo9uEEU7rEqL0jYthdXAmZ6PP+meazmaU= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0 h1:bzeyCHgoAyjZjAhvTpks+qM7sdlh4cCSitmXeCEO3B4= -golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95 h1:fY7Dsw114eJN4boqzVSbpVHO6rTdhq6/GnXeu+PKnzU= +golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= From f8f145961971af45de677fd81e7eae0b07b820c8 Mon Sep 17 00:00:00 2001 From: Kumar McMillan Date: Sun, 3 Mar 2019 18:06:46 -0600 Subject: [PATCH 088/111] Fix URL to starter template in the docs (#1795) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eb9415fd..28ebd740 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ $ 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 +$ curl https://raw.githubusercontent.com/gin-gonic/examples/master/basic/main.go > main.go ``` 5. Run your project From 805b2d490481d348856fb652473e73b25daf5aa2 Mon Sep 17 00:00:00 2001 From: Dmitry Kutakov Date: Mon, 4 Mar 2019 06:37:46 +0300 Subject: [PATCH 089/111] add support time.Duration on mapping (#1794) --- binding/binding_test.go | 17 +++++++++++++++++ binding/form_mapping.go | 13 +++++++++++++ 2 files changed, 30 insertions(+) diff --git a/binding/binding_test.go b/binding/binding_test.go index 16ca2027..5ae87957 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -1413,3 +1413,20 @@ func TestBindingUnknownTypeChan(t *testing.T) { assert.Error(t, err) assert.Equal(t, errUnknownType, err) } + +func TestBindingTimeDuration(t *testing.T) { + var s struct { + Timeout time.Duration `form:"timeout"` + } + + // ok + req := formPostRequest("", "timeout=5s") + err := Form.Bind(req, &s) + assert.NoError(t, err) + assert.Equal(t, 5*time.Second, s.Timeout) + + // error + req = formPostRequest("", "timeout=wrong") + err = Form.Bind(req, &s) + assert.Error(t, err) +} diff --git a/binding/form_mapping.go b/binding/form_mapping.go index 1109e4d0..91fadcfd 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -141,6 +141,10 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel case reflect.Int32: return setIntField(val, 32, value) case reflect.Int64: + switch value.Interface().(type) { + case time.Duration: + return setTimeDuration(val, value, field) + } return setIntField(val, 64, value) case reflect.Uint: return setUintField(val, 0, value) @@ -263,6 +267,15 @@ func setSlice(vals []string, value reflect.Value, field reflect.StructField) err return nil } +func setTimeDuration(val string, value reflect.Value, field reflect.StructField) error { + d, err := time.ParseDuration(val) + if err != nil { + return err + } + value.Set(reflect.ValueOf(d)) + return nil +} + func head(str, sep string) (head string, tail string) { idx := strings.Index(str, sep) if idx < 0 { From a5dda62cdc30f28fd27f7a7fb2facbd06eb3520f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Tue, 5 Mar 2019 06:46:18 +0800 Subject: [PATCH 090/111] chore: use internal/json (#1791) --- binding/form_mapping.go | 3 ++- internal/json/json.go | 2 ++ internal/json/jsoniter.go | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/binding/form_mapping.go b/binding/form_mapping.go index 91fadcfd..87edfbb2 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -5,12 +5,13 @@ package binding import ( - "encoding/json" "errors" "reflect" "strconv" "strings" "time" + + "github.com/gin-gonic/gin/internal/json" ) var errUnknownType = errors.New("Unknown type") diff --git a/internal/json/json.go b/internal/json/json.go index 419d35f2..480e8bff 100644 --- a/internal/json/json.go +++ b/internal/json/json.go @@ -11,6 +11,8 @@ import "encoding/json" var ( // Marshal is exported by gin/json package. Marshal = json.Marshal + // Unmarshal is exported by gin/json package. + Unmarshal = json.Unmarshal // MarshalIndent is exported by gin/json package. MarshalIndent = json.MarshalIndent // NewDecoder is exported by gin/json package. diff --git a/internal/json/jsoniter.go b/internal/json/jsoniter.go index 2021c53c..fabd7b84 100644 --- a/internal/json/jsoniter.go +++ b/internal/json/jsoniter.go @@ -12,6 +12,8 @@ var ( json = jsoniter.ConfigCompatibleWithStandardLibrary // Marshal is exported by gin/json package. Marshal = json.Marshal + // Unmarshal is exported by gin/json package. + Unmarshal = json.Unmarshal // MarshalIndent is exported by gin/json package. MarshalIndent = json.MarshalIndent // NewDecoder is exported by gin/json package. From 057f63b1bb1cca059173363d10c3de9512ee1110 Mon Sep 17 00:00:00 2001 From: Riverside Date: Tue, 5 Mar 2019 09:41:37 +0800 Subject: [PATCH 091/111] spell check (#1796) * spell check * variable path collides with imported package name * spell check --- errors.go | 2 +- gin.go | 18 +++++++++--------- recovery.go | 10 +++++----- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/errors.go b/errors.go index ab13ca61..6070ff55 100644 --- a/errors.go +++ b/errors.go @@ -53,7 +53,7 @@ func (msg *Error) SetMeta(data interface{}) *Error { return msg } -// JSON creates a properly formated JSON +// JSON creates a properly formatted JSON func (msg *Error) JSON() interface{} { json := H{} if msg.Meta != nil { diff --git a/gin.go b/gin.go index e28e9579..2d24092f 100644 --- a/gin.go +++ b/gin.go @@ -225,7 +225,7 @@ func (engine *Engine) NoMethod(handlers ...HandlerFunc) { engine.rebuild405Handlers() } -// Use attachs a global middleware to the router. ie. the middleware attached though Use() will be +// Use attaches a global middleware to the router. ie. the middleware attached though Use() will be // included in the handlers chain for every single request. Even 404, 405, static files... // For example, this is the right place for a logger or error management middleware. func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes { @@ -366,10 +366,10 @@ func (engine *Engine) HandleContext(c *Context) { func (engine *Engine) handleHTTPRequest(c *Context) { httpMethod := c.Request.Method - path := c.Request.URL.Path + rPath := c.Request.URL.Path unescape := false if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 { - path = c.Request.URL.RawPath + rPath = c.Request.URL.RawPath unescape = engine.UnescapePathValues } @@ -381,7 +381,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) { } root := t[i].root // Find route in tree - handlers, params, tsr := root.getValue(path, c.Params, unescape) + handlers, params, tsr := root.getValue(rPath, c.Params, unescape) if handlers != nil { c.handlers = handlers c.Params = params @@ -389,7 +389,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) { c.writermem.WriteHeaderNow() return } - if httpMethod != "CONNECT" && path != "/" { + if httpMethod != "CONNECT" && rPath != "/" { if tsr && engine.RedirectTrailingSlash { redirectTrailingSlash(c) return @@ -406,7 +406,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) { if tree.method == httpMethod { continue } - if handlers, _, _ := tree.root.getValue(path, nil, unescape); handlers != nil { + if handlers, _, _ := tree.root.getValue(rPath, nil, unescape); handlers != nil { c.handlers = engine.allNoMethod serveError(c, http.StatusMethodNotAllowed, default405Body) return @@ -459,15 +459,15 @@ func redirectTrailingSlash(c *Context) { func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool { req := c.Request - path := req.URL.Path + rPath := req.URL.Path - if fixedPath, ok := root.findCaseInsensitivePath(cleanPath(path), trailingSlash); ok { + if fixedPath, ok := root.findCaseInsensitivePath(cleanPath(rPath), trailingSlash); ok { code := http.StatusMovedPermanently // Permanent redirect, request with GET method if req.Method != "GET" { code = http.StatusTemporaryRedirect } req.URL.Path = string(fixedPath) - debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String()) + debugPrint("redirecting request %d: %s --> %s", code, rPath, req.URL.String()) http.Redirect(c.Writer, req, req.URL.String(), code) c.writermem.WriteHeaderNow() return true diff --git a/recovery.go b/recovery.go index 0e35968f..9e893e1b 100644 --- a/recovery.go +++ b/recovery.go @@ -52,12 +52,12 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc { } if logger != nil { stack := stack(3) - httprequest, _ := httputil.DumpRequest(c.Request, false) + httpRequest, _ := httputil.DumpRequest(c.Request, false) if brokenPipe { - logger.Printf("%s\n%s%s", err, string(httprequest), reset) + logger.Printf("%s\n%s%s", err, string(httpRequest), reset) } else if IsDebugging() { logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", - timeFormat(time.Now()), string(httprequest), err, stack, reset) + timeFormat(time.Now()), string(httpRequest), err, stack, reset) } else { logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s", timeFormat(time.Now()), err, stack, reset) @@ -128,8 +128,8 @@ func function(pc uintptr) []byte { // *T.ptrmethod // Also the package path might contains dot (e.g. code.google.com/...), // so first eliminate the path prefix - if lastslash := bytes.LastIndex(name, slash); lastslash >= 0 { - name = name[lastslash+1:] + if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 { + name = name[lastSlash+1:] } if period := bytes.Index(name, dot); period >= 0 { name = name[period+1:] From 3dc247893e9772d0b95c47f4b32dad3d28feb488 Mon Sep 17 00:00:00 2001 From: Adam Zielinski Date: Wed, 6 Mar 2019 20:47:31 -0500 Subject: [PATCH 092/111] make context.Keys available as LogFormatterParams (#1779) * make context available as LogFormatterParams * pass context Keys to LogFormatterParams * update logger test to check for Key param --- logger.go | 3 +++ logger_test.go | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/logger.go b/logger.go index dc639975..2ecaed7d 100644 --- a/logger.go +++ b/logger.go @@ -66,6 +66,8 @@ type LogFormatterParams struct { IsTerm bool // BodySize is the size of the Response Body BodySize int + // Keys are the keys set on the request's context. + Keys map[string]interface{} } // StatusCodeColor is the ANSI color for appropriately logging http status code to a terminal. @@ -227,6 +229,7 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc { param := LogFormatterParams{ Request: c.Request, IsTerm: isTerm, + Keys: c.Keys, } // Stop timer diff --git a/logger_test.go b/logger_test.go index c551677a..a2041773 100644 --- a/logger_test.go +++ b/logger_test.go @@ -181,6 +181,7 @@ func TestLoggerWithFormatter(t *testing.T) { func TestLoggerWithConfigFormatting(t *testing.T) { var gotParam LogFormatterParams + var gotKeys map[string]interface{} buffer := new(bytes.Buffer) router := New() @@ -204,6 +205,7 @@ func TestLoggerWithConfigFormatting(t *testing.T) { router.GET("/example", func(c *Context) { // set dummy ClientIP c.Request.Header.Set("X-Forwarded-For", "20.20.20.20") + gotKeys = c.Keys }) performRequest(router, "GET", "/example?a=100") @@ -223,6 +225,8 @@ func TestLoggerWithConfigFormatting(t *testing.T) { assert.Equal(t, "GET", gotParam.Method) assert.Equal(t, "/example?a=100", gotParam.Path) assert.Empty(t, gotParam.ErrorMessage) + assert.Empty(t, gotParam.ErrorMessage) + assert.Equal(t, gotKeys, gotParam.Keys) } From f7079a861e6db66385aae1d865b9c9bbe10d1bb1 Mon Sep 17 00:00:00 2001 From: Sai Date: Fri, 8 Mar 2019 20:44:39 +0900 Subject: [PATCH 093/111] Delete dupilicated test (#1801) --- logger_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/logger_test.go b/logger_test.go index a2041773..36231371 100644 --- a/logger_test.go +++ b/logger_test.go @@ -225,7 +225,6 @@ func TestLoggerWithConfigFormatting(t *testing.T) { assert.Equal(t, "GET", gotParam.Method) assert.Equal(t, "/example?a=100", gotParam.Path) assert.Empty(t, gotParam.ErrorMessage) - assert.Empty(t, gotParam.ErrorMessage) assert.Equal(t, gotKeys, gotParam.Keys) } From 70a0aba3e423246be37462cfdaedd510c26c566e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Fri, 8 Mar 2019 23:18:52 +0800 Subject: [PATCH 094/111] travisci: use go module when go11+ (#1800) --- .travis.yml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 00393750..b38adcb1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,22 +1,20 @@ language: go sudo: false -go: - - 1.6.x - - 1.7.x - - 1.8.x - - 1.9.x - - 1.10.x - - 1.11.x - - 1.12.x - - master matrix: fast_finish: true include: + - go: 1.6.x + - go: 1.7.x + - go: 1.8.x + - go: 1.9.x + - go: 1.10.x - go: 1.11.x env: GO111MODULE=on - go: 1.12.x env: GO111MODULE=on + - go: master + env: GO111MODULE=on git: depth: 10 From 4a23c4f7b9ced6b8e4476f2e021a61165153b71d Mon Sep 17 00:00:00 2001 From: Sai Date: Mon, 11 Mar 2019 11:52:47 +0900 Subject: [PATCH 095/111] fix #1804 which is caused by calling middleware twice. (#1805) Fix: https://github.com/gin-gonic/gin/issues/1804 `allNoRoute` contains middlewares such as `gin.Logger`, `gin.Recovery`, so on. The correct code is to use `noRoute`. cc: @MetalBreaker --- routergroup.go | 2 +- routes_test.go | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/routergroup.go b/routergroup.go index 297d3574..a1e6c928 100644 --- a/routergroup.go +++ b/routergroup.go @@ -195,7 +195,7 @@ func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileS // Check if file exists and/or if we have permission to access it if _, err := fs.Open(file); err != nil { c.Writer.WriteHeader(http.StatusNotFound) - c.handlers = group.engine.allNoRoute + c.handlers = group.engine.noRoute // Reset index c.index = -1 return diff --git a/routes_test.go b/routes_test.go index a842704f..de363a8c 100644 --- a/routes_test.go +++ b/routes_test.go @@ -429,7 +429,6 @@ func TestRouterNotFound(t *testing.T) { func TestRouterStaticFSNotFound(t *testing.T) { router := New() - router.StaticFS("/", http.FileSystem(http.Dir("/thisreallydoesntexist/"))) router.NoRoute(func(c *Context) { c.String(404, "non existent") @@ -452,6 +451,27 @@ func TestRouterStaticFSFileNotFound(t *testing.T) { }) } +// Reproduction test for the bug of issue #1805 +func TestMiddlewareCalledOnceByRouterStaticFSNotFound(t *testing.T) { + router := New() + + // Middleware must be called just only once by per request. + middlewareCalledNum := 0 + router.Use(func(c *Context) { + middlewareCalledNum += 1 + }) + + router.StaticFS("/", http.FileSystem(http.Dir("/thisreallydoesntexist/"))) + + // First access + performRequest(router, "GET", "/nonexistent") + assert.Equal(t, 1, middlewareCalledNum) + + // Second access + performRequest(router, "HEAD", "/nonexistent") + assert.Equal(t, 2, middlewareCalledNum) +} + func TestRouteRawPath(t *testing.T) { route := New() route.UseRawPath = true From e5261480fde106ac5385f00fb00f9bc6b4035485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Tue, 12 Mar 2019 14:01:12 +0800 Subject: [PATCH 096/111] chore(readme.md): fix invalid link (#1807) --- README.md | 2 +- testdata/assets/console.png | Bin 0 -> 59545 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 testdata/assets/console.png diff --git a/README.md b/README.md index 28ebd740..df5302e1 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ 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. -![Gin console logger](https://gin-gonic.github.io/gin/other/console.png) +![Gin console logger](testdata/assets/console.png) ## Contents diff --git a/testdata/assets/console.png b/testdata/assets/console.png new file mode 100644 index 0000000000000000000000000000000000000000..7a695718fa31f9b1b42cfa547c16da394378aeae GIT binary patch literal 59545 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 literal 0 HcmV?d00001 From cab0749b4f3fadc8e9a23a5daf7083d3597fdbfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Thu, 14 Mar 2019 13:23:35 +0800 Subject: [PATCH 097/111] chore: update readme (#1793) * update readme * add multi-language version doc link * add multi-language version doc link * update readme * update * update readme * update readme * update readme --- README.md | 2011 +---------------------------------------------------- 1 file changed, 37 insertions(+), 1974 deletions(-) diff --git a/README.md b/README.md index df5302e1..f9c10b22 100644 --- a/README.md +++ b/README.md @@ -13,123 +13,35 @@ 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. -![Gin console logger](testdata/assets/console.png) +**The key features of Gin are:** -## Contents +- Zero allocation router +- Fast +- Middleware support +- Crash-free +- JSON validation +- Routes grouping +- Error management +- Rendering built-in +- Extendable -- [Installation](#installation) -- [Prerequisite](#prerequisite) -- [Quick start](#quick-start) -- [Benchmarks](#benchmarks) -- [Gin v1.stable](#gin-v1-stable) -- [Build with jsoniter](#build-with-jsoniter) -- [API Examples](#api-examples) - - [Using GET,POST,PUT,PATCH,DELETE and OPTIONS](#using-get-post-put-patch-delete-and-options) - - [Parameters in path](#parameters-in-path) - - [Querystring parameters](#querystring-parameters) - - [Multipart/Urlencoded Form](#multiparturlencoded-form) - - [Another example: query + post form](#another-example-query--post-form) - - [Map as querystring or postform parameters](#map-as-querystring-or-postform-parameters) - - [Upload files](#upload-files) - - [Grouping routes](#grouping-routes) - - [Blank Gin without middleware by default](#blank-gin-without-middleware-by-default) - - [Using middleware](#using-middleware) - - [How to write log file](#how-to-write-log-file) - - [Custom Log Format](#custom-log-format) - - [Model binding and validation](#model-binding-and-validation) - - [Custom Validators](#custom-validators) - - [Only Bind Query String](#only-bind-query-string) - - [Bind Query String or Post Data](#bind-query-string-or-post-data) - - [Bind Uri](#bind-uri) - - [Bind HTML checkboxes](#bind-html-checkboxes) - - [Multipart/Urlencoded binding](#multiparturlencoded-binding) - - [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering) - - [JSONP rendering](#jsonp) - - [Serving static files](#serving-static-files) - - [Serving data from reader](#serving-data-from-reader) - - [HTML rendering](#html-rendering) - - [Multitemplate](#multitemplate) - - [Redirects](#redirects) - - [Custom Middleware](#custom-middleware) - - [Using BasicAuth() middleware](#using-basicauth-middleware) - - [Goroutines inside a middleware](#goroutines-inside-a-middleware) - - [Custom HTTP configuration](#custom-http-configuration) - - [Support Let's Encrypt](#support-lets-encrypt) - - [Run multiple service using Gin](#run-multiple-service-using-gin) - - [Graceful restart or stop](#graceful-restart-or-stop) - - [Build a single binary with templates](#build-a-single-binary-with-templates) - - [Bind form-data request with custom struct](#bind-form-data-request-with-custom-struct) - - [Try to bind body into different structs](#try-to-bind-body-into-different-structs) - - [http2 server push](#http2-server-push) - - [Define format for the log of routes](#define-format-for-the-log-of-routes) - - [Set and get a cookie](#set-and-get-a-cookie) -- [Testing](#testing) -- [Users](#users) +For more feature details, please see the [Gin website introduction](https://gin-gonic.com/docs/introduction/). -## Installation +## Getting started -To install Gin package, you need to install Go and set your Go workspace first. +### Getting Gin -1. Download and install it: +The first need [Go](https://golang.org/) installed (version 1.6+ is required), then you can use the below Go command to install Gin. ```sh $ go get -u github.com/gin-gonic/gin ``` -2. Import it in your code: +For more installation guides such as vendor tool, please check out [Gin quickstart](https://gin-gonic.com/docs/quickstart/). -```go -import "github.com/gin-gonic/gin" -``` +### Running 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/examples/master/basic/main.go > main.go -``` - -5. Run your project - -```sh -$ go run main.go -``` - -## Prerequisite - -Now Gin requires Go 1.6 or later and Go 1.7 will be required soon. - -## Quick start - -```sh -# assume the following codes in example.go file -$ cat example.go -``` +First you need to import Gin package for using Gin, one simplest example likes the follow `example.go`: ```go package main @@ -147,6 +59,8 @@ func main() { } ``` +And use the Go command to run the demo: + ``` # run example.go and visit 0.0.0.0:8080/ping on browser $ go run example.go @@ -154,9 +68,7 @@ $ go run example.go ## Benchmarks -Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httprouter) - -[See all benchmarks](/BENCHMARKS.md) +Please see all benchmarks details from [Gin website](https://gin-gonic.com/docs/benchmarks/). Benchmark name | (1) | (2) | (3) | (4) --------------------------------------------|-----------:|------------:|-----------:|---------: @@ -193,1879 +105,30 @@ BenchmarkVulcan_GithubAll | 5000 | 394253 | 19894 - (3): Heap Memory (B/op), lower is better - (4): Average Allocations per Repetition (allocs/op), lower is better -## Gin v1. stable +## Middlewares -- [x] Zero allocation router. -- [x] Still the fastest http router and framework. From routing to writing. -- [x] Complete suite of unit tests -- [x] Battle tested -- [x] API frozen, new releases will not break your code. +You can find many useful Gin middlewares at [gin-contrib](https://github.com/gin-contrib). -## Build with [jsoniter](https://github.com/json-iterator/go) +## Documentation -Gin uses `encoding/json` as default json package but you can change to [jsoniter](https://github.com/json-iterator/go) by build from other tags. +All documentation is available on the Gin website. -```sh -$ go build -tags=jsoniter . -``` +- [English](https://gin-gonic.com/docs/) +- [简体中文](https://gin-gonic.com/zh-cn/docs/) +- [繁體中文](https://gin-gonic.com/zh-tw/docs/) +- [にほんご](https://gin-gonic.com/ja/docs/) -## API Examples +## Examples -You can find a number of ready-to-run examples at [Gin examples repository](https://github.com/gin-gonic/examples). - -### Using GET, POST, PUT, PATCH, DELETE and OPTIONS - -```go -func main() { - // Creates a gin router with default middleware: - // logger and recovery (crash-free) middleware - router := gin.Default() - - router.GET("/someGet", getting) - router.POST("/somePost", posting) - router.PUT("/somePut", putting) - router.DELETE("/someDelete", deleting) - router.PATCH("/somePatch", patching) - router.HEAD("/someHead", head) - router.OPTIONS("/someOptions", options) - - // By default it serves on :8080 unless a - // PORT environment variable was defined. - router.Run() - // router.Run(":3000") for a hard coded port -} -``` - -### Parameters in path - -```go -func main() { - router := gin.Default() - - // This handler will match /user/john but will not match /user/ or /user - router.GET("/user/:name", func(c *gin.Context) { - name := c.Param("name") - c.String(http.StatusOK, "Hello %s", name) - }) - - // However, this one will match /user/john/ and also /user/john/send - // If no other routers match /user/john, it will redirect to /user/john/ - router.GET("/user/:name/*action", func(c *gin.Context) { - name := c.Param("name") - action := c.Param("action") - message := name + " is " + action - c.String(http.StatusOK, message) - }) - - router.Run(":8080") -} -``` - -### Querystring parameters - -```go -func main() { - router := gin.Default() - - // Query string parameters are parsed using the existing underlying request object. - // The request responds to a url matching: /welcome?firstname=Jane&lastname=Doe - router.GET("/welcome", func(c *gin.Context) { - firstname := c.DefaultQuery("firstname", "Guest") - lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname") - - c.String(http.StatusOK, "Hello %s %s", firstname, lastname) - }) - router.Run(":8080") -} -``` - -### Multipart/Urlencoded Form - -```go -func main() { - router := gin.Default() - - router.POST("/form_post", func(c *gin.Context) { - message := c.PostForm("message") - nick := c.DefaultPostForm("nick", "anonymous") - - c.JSON(200, gin.H{ - "status": "posted", - "message": message, - "nick": nick, - }) - }) - router.Run(":8080") -} -``` - -### Another example: query + post form - -``` -POST /post?id=1234&page=1 HTTP/1.1 -Content-Type: application/x-www-form-urlencoded - -name=manu&message=this_is_great -``` - -```go -func main() { - router := gin.Default() - - router.POST("/post", func(c *gin.Context) { - - id := c.Query("id") - page := c.DefaultQuery("page", "0") - name := c.PostForm("name") - message := c.PostForm("message") - - fmt.Printf("id: %s; page: %s; name: %s; message: %s", id, page, name, message) - }) - router.Run(":8080") -} -``` - -``` -id: 1234; page: 1; name: manu; message: this_is_great -``` - -### Map as querystring or postform parameters - -``` -POST /post?ids[a]=1234&ids[b]=hello HTTP/1.1 -Content-Type: application/x-www-form-urlencoded - -names[first]=thinkerou&names[second]=tianou -``` - -```go -func main() { - router := gin.Default() - - router.POST("/post", func(c *gin.Context) { - - ids := c.QueryMap("ids") - names := c.PostFormMap("names") - - fmt.Printf("ids: %v; names: %v", ids, names) - }) - router.Run(":8080") -} -``` - -``` -ids: map[b:hello a:1234], names: map[second:tianou first:thinkerou] -``` - -### Upload files - -#### Single file - -References issue [#774](https://github.com/gin-gonic/gin/issues/774) and detail [example code](https://github.com/gin-gonic/examples/tree/master/upload-file/single). - -`file.Filename` **SHOULD NOT** be trusted. See [`Content-Disposition` on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#Directives) and [#1693](https://github.com/gin-gonic/gin/issues/1693) - -> The filename is always optional and must not be used blindly by the application: path information should be stripped, and conversion to the server file system rules should be done. - -```go -func main() { - router := gin.Default() - // Set a lower memory limit for multipart forms (default is 32 MiB) - // router.MaxMultipartMemory = 8 << 20 // 8 MiB - router.POST("/upload", func(c *gin.Context) { - // single file - file, _ := c.FormFile("file") - log.Println(file.Filename) - - // Upload the file to specific dst. - // c.SaveUploadedFile(file, dst) - - c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename)) - }) - router.Run(":8080") -} -``` - -How to `curl`: - -```bash -curl -X POST http://localhost:8080/upload \ - -F "file=@/Users/appleboy/test.zip" \ - -H "Content-Type: multipart/form-data" -``` - -#### Multiple files - -See the detail [example code](https://github.com/gin-gonic/examples/tree/master/upload-file/multiple). - -```go -func main() { - router := gin.Default() - // Set a lower memory limit for multipart forms (default is 32 MiB) - // router.MaxMultipartMemory = 8 << 20 // 8 MiB - router.POST("/upload", func(c *gin.Context) { - // Multipart form - form, _ := c.MultipartForm() - files := form.File["upload[]"] - - for _, file := range files { - log.Println(file.Filename) - - // Upload the file to specific dst. - // c.SaveUploadedFile(file, dst) - } - c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files))) - }) - router.Run(":8080") -} -``` - -How to `curl`: - -```bash -curl -X POST http://localhost:8080/upload \ - -F "upload[]=@/Users/appleboy/test1.zip" \ - -F "upload[]=@/Users/appleboy/test2.zip" \ - -H "Content-Type: multipart/form-data" -``` - -### Grouping routes - -```go -func main() { - router := gin.Default() - - // Simple group: v1 - v1 := router.Group("/v1") - { - v1.POST("/login", loginEndpoint) - v1.POST("/submit", submitEndpoint) - v1.POST("/read", readEndpoint) - } - - // Simple group: v2 - v2 := router.Group("/v2") - { - v2.POST("/login", loginEndpoint) - v2.POST("/submit", submitEndpoint) - v2.POST("/read", readEndpoint) - } - - router.Run(":8080") -} -``` - -### Blank Gin without middleware by default - -Use - -```go -r := gin.New() -``` - -instead of - -```go -// Default With the Logger and Recovery middleware already attached -r := gin.Default() -``` - - -### Using middleware -```go -func main() { - // Creates a router without any middleware by default - r := gin.New() - - // Global middleware - // Logger middleware will write the logs to gin.DefaultWriter even if you set with GIN_MODE=release. - // By default gin.DefaultWriter = os.Stdout - r.Use(gin.Logger()) - - // Recovery middleware recovers from any panics and writes a 500 if there was one. - r.Use(gin.Recovery()) - - // Per route middleware, you can add as many as you desire. - r.GET("/benchmark", MyBenchLogger(), benchEndpoint) - - // Authorization group - // authorized := r.Group("/", AuthRequired()) - // exactly the same as: - authorized := r.Group("/") - // per group middleware! in this case we use the custom created - // AuthRequired() middleware just in the "authorized" group. - authorized.Use(AuthRequired()) - { - authorized.POST("/login", loginEndpoint) - authorized.POST("/submit", submitEndpoint) - authorized.POST("/read", readEndpoint) - - // nested group - testing := authorized.Group("testing") - testing.GET("/analytics", analyticsEndpoint) - } - - // Listen and serve on 0.0.0.0:8080 - r.Run(":8080") -} -``` - -### How to write log file -```go -func main() { - // Disable Console Color, you don't need console color when writing the logs to file. - gin.DisableConsoleColor() - - // Logging to a file. - f, _ := os.Create("gin.log") - gin.DefaultWriter = io.MultiWriter(f) - - // Use the following code if you need to write the logs to file and console at the same time. - // gin.DefaultWriter = io.MultiWriter(f, os.Stdout) - - router := gin.Default() - router.GET("/ping", func(c *gin.Context) { - c.String(200, "pong") - }) - -    router.Run(":8080") -} -``` - -### Custom Log Format -```go -func main() { - router := gin.New() - - // LoggerWithFormatter middleware will write the logs to gin.DefaultWriter - // By default gin.DefaultWriter = os.Stdout - router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { - - // your custom format - return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n", - param.ClientIP, - param.TimeStamp.Format(time.RFC1123), - param.Method, - param.Path, - param.Request.Proto, - param.StatusCode, - param.Latency, - param.Request.UserAgent(), - param.ErrorMessage, - ) - })) - router.Use(gin.Recovery()) - - router.GET("/ping", func(c *gin.Context) { - c.String(200, "pong") - }) - - router.Run(":8080") -} -``` - -**Sample Output** -``` -::1 - [Fri, 07 Dec 2018 17:04:38 JST] "GET /ping HTTP/1.1 200 122.767µs "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36" " -``` - -### Controlling Log output coloring - -By default, logs output on console should be colorized depending on the detected TTY. - -Never colorize logs: - -```go -func main() { - // Disable log's color - gin.DisableConsoleColor() - - // Creates a gin router with default middleware: - // logger and recovery (crash-free) middleware - router := gin.Default() - - router.GET("/ping", func(c *gin.Context) { - c.String(200, "pong") - }) - - router.Run(":8080") -} -``` - -Always colorize logs: - -```go -func main() { - // Force log's color - gin.ForceConsoleColor() - - // Creates a gin router with default middleware: - // logger and recovery (crash-free) middleware - router := gin.Default() - - router.GET("/ping", func(c *gin.Context) { - c.String(200, "pong") - }) - - router.Run(":8080") -} -``` - -### Model binding and validation - -To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML and standard form values (foo=bar&boo=baz). - -Gin uses [**go-playground/validator.v8**](https://github.com/go-playground/validator) for validation. Check the full docs on tags usage [here](http://godoc.org/gopkg.in/go-playground/validator.v8#hdr-Baked_In_Validators_and_Tags). - -Note that you need to set the corresponding binding tag on all fields you want to bind. For example, when binding from JSON, set `json:"fieldname"`. - -Also, Gin provides two sets of methods for binding: -- **Type** - Must bind - - **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`, `BindYAML` - - **Behavior** - These methods use `MustBindWith` under the hood. If there is a binding error, the request is aborted with `c.AbortWithError(400, err).SetType(ErrorTypeBind)`. This sets the response status code to 400 and the `Content-Type` header is set to `text/plain; charset=utf-8`. Note that if you try to set the response code after this, it will result in a warning `[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422`. If you wish to have greater control over the behavior, consider using the `ShouldBind` equivalent method. -- **Type** - Should bind - - **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindXML`, `ShouldBindQuery`, `ShouldBindYAML` - - **Behavior** - These methods use `ShouldBindWith` under the hood. If there is a binding error, the error is returned and it is the developer's responsibility to handle the request and error appropriately. - -When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use `MustBindWith` or `ShouldBindWith`. - -You can also specify that specific fields are required. If a field is decorated with `binding:"required"` and has a empty value when binding, an error will be returned. - -```go -// Binding from JSON -type Login struct { - User string `form:"user" json:"user" xml:"user" binding:"required"` - Password string `form:"password" json:"password" xml:"password" binding:"required"` -} - -func main() { - router := gin.Default() - - // Example for binding JSON ({"user": "manu", "password": "123"}) - router.POST("/loginJSON", func(c *gin.Context) { - var json Login - if err := c.ShouldBindJSON(&json); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - if json.User != "manu" || json.Password != "123" { - c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) - return - } - - c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) - }) - - // Example for binding XML ( - // - // - // user - // 123 - // ) - router.POST("/loginXML", func(c *gin.Context) { - var xml Login - if err := c.ShouldBindXML(&xml); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - if xml.User != "manu" || xml.Password != "123" { - c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) - return - } - - c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) - }) - - // Example for binding a HTML form (user=manu&password=123) - router.POST("/loginForm", func(c *gin.Context) { - var form Login - // This will infer what binder to use depending on the content-type header. - if err := c.ShouldBind(&form); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - if form.User != "manu" || form.Password != "123" { - c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) - return - } - - c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) - }) - - // Listen and serve on 0.0.0.0:8080 - router.Run(":8080") -} -``` - -**Sample request** -```shell -$ curl -v -X POST \ - http://localhost:8080/loginJSON \ - -H 'content-type: application/json' \ - -d '{ "user": "manu" }' -> POST /loginJSON HTTP/1.1 -> Host: localhost:8080 -> User-Agent: curl/7.51.0 -> Accept: */* -> content-type: application/json -> Content-Length: 18 -> -* upload completely sent off: 18 out of 18 bytes -< HTTP/1.1 400 Bad Request -< Content-Type: application/json; charset=utf-8 -< Date: Fri, 04 Aug 2017 03:51:31 GMT -< Content-Length: 100 -< -{"error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'required' tag"} -``` - -**Skip validate** - -When running the above example using the above the `curl` command, it returns error. Because the example use `binding:"required"` for `Password`. If use `binding:"-"` for `Password`, then it will not return error when running the above example again. - -### Custom Validators - -It is also possible to register custom validators. See the [example code](https://github.com/gin-gonic/examples/tree/master/custom-validation/server.go). - -```go -package main - -import ( - "net/http" - "reflect" - "time" - - "github.com/gin-gonic/gin" - "github.com/gin-gonic/gin/binding" - "gopkg.in/go-playground/validator.v8" -) - -// Booking contains binded and validated data. -type Booking struct { - CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"` - CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"` -} - -func bookableDate( - v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value, - field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string, -) bool { - if date, ok := field.Interface().(time.Time); ok { - today := time.Now() - if today.Year() > date.Year() || today.YearDay() > date.YearDay() { - return false - } - } - return true -} - -func main() { - route := gin.Default() - - if v, ok := binding.Validator.Engine().(*validator.Validate); ok { - v.RegisterValidation("bookabledate", bookableDate) - } - - route.GET("/bookable", getBookable) - route.Run(":8085") -} - -func getBookable(c *gin.Context) { - var b Booking - if err := c.ShouldBindWith(&b, binding.Query); err == nil { - c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"}) - } else { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - } -} -``` - -```console -$ curl "localhost:8085/bookable?check_in=2018-04-16&check_out=2018-04-17" -{"message":"Booking dates are valid!"} - -$ curl "localhost:8085/bookable?check_in=2018-03-08&check_out=2018-03-09" -{"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag"} -``` - -[Struct level validations](https://github.com/go-playground/validator/releases/tag/v8.7) can also be registered this way. -See the [struct-lvl-validation example](https://github.com/gin-gonic/examples/tree/master/struct-lvl-validations) to learn more. - -### Only Bind Query String - -`ShouldBindQuery` function only binds the query params and not the post data. See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-315953017). - -```go -package main - -import ( - "log" - - "github.com/gin-gonic/gin" -) - -type Person struct { - Name string `form:"name"` - Address string `form:"address"` -} - -func main() { - route := gin.Default() - route.Any("/testing", startPage) - route.Run(":8085") -} - -func startPage(c *gin.Context) { - var person Person - if c.ShouldBindQuery(&person) == nil { - log.Println("====== Only Bind By Query String ======") - log.Println(person.Name) - log.Println(person.Address) - } - c.String(200, "Success") -} - -``` - -### Bind Query String or Post Data - -See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-264681292). - -```go -package main - -import ( - "log" - "time" - - "github.com/gin-gonic/gin" -) - -type Person struct { - Name string `form:"name"` - Address string `form:"address"` - Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"` -} - -func main() { - route := gin.Default() - route.GET("/testing", startPage) - route.Run(":8085") -} - -func startPage(c *gin.Context) { - var person Person - // If `GET`, only `Form` binding engine (`query`) used. - // If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`). - // See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48 - if c.ShouldBind(&person) == nil { - log.Println(person.Name) - log.Println(person.Address) - log.Println(person.Birthday) - } - - c.String(200, "Success") -} -``` - -Test it with: -```sh -$ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15" -``` - -### Bind Uri - -See the [detail information](https://github.com/gin-gonic/gin/issues/846). - -```go -package main - -import "github.com/gin-gonic/gin" - -type Person struct { - ID string `uri:"id" binding:"required,uuid"` - Name string `uri:"name" binding:"required"` -} - -func main() { - route := gin.Default() - route.GET("/:name/:id", func(c *gin.Context) { - var person Person - if err := c.ShouldBindUri(&person); err != nil { - c.JSON(400, gin.H{"msg": err}) - return - } - c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID}) - }) - route.Run(":8088") -} -``` - -Test it with: -```sh -$ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3 -$ curl -v localhost:8088/thinkerou/not-uuid -``` - -### Bind HTML checkboxes - -See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092) - -main.go - -```go -... - -type myForm struct { - Colors []string `form:"colors[]"` -} - -... - -func formHandler(c *gin.Context) { - var fakeForm myForm - c.ShouldBind(&fakeForm) - c.JSON(200, gin.H{"color": fakeForm.Colors}) -} - -... - -``` - -form.html - -```html -
-

Check some colors

- - - - - - - -
-``` - -result: - -``` -{"color":["red","green","blue"]} -``` - -### Multipart/Urlencoded binding - -```go -package main - -import ( - "github.com/gin-gonic/gin" -) - -type LoginForm struct { - User string `form:"user" binding:"required"` - Password string `form:"password" binding:"required"` -} - -func main() { - router := gin.Default() - router.POST("/login", func(c *gin.Context) { - // you can bind multipart form with explicit binding declaration: - // c.ShouldBindWith(&form, binding.Form) - // or you can simply use autobinding with ShouldBind method: - var form LoginForm - // in this case proper binding will be automatically selected - if c.ShouldBind(&form) == nil { - if form.User == "user" && form.Password == "password" { - c.JSON(200, gin.H{"status": "you are logged in"}) - } else { - c.JSON(401, gin.H{"status": "unauthorized"}) - } - } - }) - router.Run(":8080") -} -``` - -Test it with: -```sh -$ curl -v --form user=user --form password=password http://localhost:8080/login -``` - -### XML, JSON, YAML and ProtoBuf rendering - -```go -func main() { - r := gin.Default() - - // gin.H is a shortcut for map[string]interface{} - r.GET("/someJSON", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK}) - }) - - r.GET("/moreJSON", func(c *gin.Context) { - // You also can use a struct - var msg struct { - Name string `json:"user"` - Message string - Number int - } - msg.Name = "Lena" - msg.Message = "hey" - msg.Number = 123 - // Note that msg.Name becomes "user" in the JSON - // Will output : {"user": "Lena", "Message": "hey", "Number": 123} - c.JSON(http.StatusOK, msg) - }) - - r.GET("/someXML", func(c *gin.Context) { - c.XML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK}) - }) - - r.GET("/someYAML", func(c *gin.Context) { - c.YAML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK}) - }) - - r.GET("/someProtoBuf", func(c *gin.Context) { - reps := []int64{int64(1), int64(2)} - label := "test" - // The specific definition of protobuf is written in the testdata/protoexample file. - data := &protoexample.Test{ - Label: &label, - Reps: reps, - } - // Note that data becomes binary data in the response - // Will output protoexample.Test protobuf serialized data - c.ProtoBuf(http.StatusOK, data) - }) - - // Listen and serve on 0.0.0.0:8080 - r.Run(":8080") -} -``` - -#### SecureJSON - -Using SecureJSON to prevent json hijacking. Default prepends `"while(1),"` to response body if the given struct is array values. - -```go -func main() { - r := gin.Default() - - // You can also use your own secure json prefix - // r.SecureJsonPrefix(")]}',\n") - - r.GET("/someJSON", func(c *gin.Context) { - names := []string{"lena", "austin", "foo"} - - // Will output : while(1);["lena","austin","foo"] - c.SecureJSON(http.StatusOK, names) - }) - - // Listen and serve on 0.0.0.0:8080 - r.Run(":8080") -} -``` -#### JSONP - -Using JSONP to request data from a server in a different domain. Add callback to response body if the query parameter callback exists. - -```go -func main() { - r := gin.Default() - - r.GET("/JSONP?callback=x", func(c *gin.Context) { - data := map[string]interface{}{ - "foo": "bar", - } - - //callback is x - // Will output : x({\"foo\":\"bar\"}) - c.JSONP(http.StatusOK, data) - }) - - // Listen and serve on 0.0.0.0:8080 - r.Run(":8080") -} -``` - -#### AsciiJSON - -Using AsciiJSON to Generates ASCII-only JSON with escaped non-ASCII chracters. - -```go -func main() { - r := gin.Default() - - r.GET("/someJSON", func(c *gin.Context) { - data := map[string]interface{}{ - "lang": "GO语言", - "tag": "
", - } - - // will output : {"lang":"GO\u8bed\u8a00","tag":"\u003cbr\u003e"} - c.AsciiJSON(http.StatusOK, data) - }) - - // Listen and serve on 0.0.0.0:8080 - r.Run(":8080") -} -``` - -#### PureJSON - -Normally, JSON replaces special HTML characters with their unicode entities, e.g. `<` becomes `\u003c`. If you want to encode such characters literally, you can use PureJSON instead. -This feature is unavailable in Go 1.6 and lower. - -```go -func main() { - r := gin.Default() - - // Serves unicode entities - r.GET("/json", func(c *gin.Context) { - c.JSON(200, gin.H{ - "html": "Hello, world!", - }) - }) - - // Serves literal characters - r.GET("/purejson", func(c *gin.Context) { - c.PureJSON(200, gin.H{ - "html": "Hello, world!", - }) - }) - - // listen and serve on 0.0.0.0:8080 - r.Run(":8080") -} -``` - -### Serving static files - -```go -func main() { - router := gin.Default() - router.Static("/assets", "./assets") - router.StaticFS("/more_static", http.Dir("my_file_system")) - router.StaticFile("/favicon.ico", "./resources/favicon.ico") - - // Listen and serve on 0.0.0.0:8080 - router.Run(":8080") -} -``` - -### Serving data from reader - -```go -func main() { - router := gin.Default() - router.GET("/someDataFromReader", func(c *gin.Context) { - response, err := http.Get("https://raw.githubusercontent.com/gin-gonic/logo/master/color.png") - if err != nil || response.StatusCode != http.StatusOK { - c.Status(http.StatusServiceUnavailable) - return - } - - reader := response.Body - contentLength := response.ContentLength - contentType := response.Header.Get("Content-Type") - - extraHeaders := map[string]string{ - "Content-Disposition": `attachment; filename="gopher.png"`, - } - - c.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders) - }) - router.Run(":8080") -} -``` - -### HTML rendering - -Using LoadHTMLGlob() or LoadHTMLFiles() - -```go -func main() { - router := gin.Default() - router.LoadHTMLGlob("templates/*") - //router.LoadHTMLFiles("templates/template1.html", "templates/template2.html") - router.GET("/index", func(c *gin.Context) { - c.HTML(http.StatusOK, "index.tmpl", gin.H{ - "title": "Main website", - }) - }) - router.Run(":8080") -} -``` - -templates/index.tmpl - -```html - -

- {{ .title }} -

- -``` - -Using templates with same name in different directories - -```go -func main() { - router := gin.Default() - router.LoadHTMLGlob("templates/**/*") - router.GET("/posts/index", func(c *gin.Context) { - c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{ - "title": "Posts", - }) - }) - router.GET("/users/index", func(c *gin.Context) { - c.HTML(http.StatusOK, "users/index.tmpl", gin.H{ - "title": "Users", - }) - }) - router.Run(":8080") -} -``` - -templates/posts/index.tmpl - -```html -{{ define "posts/index.tmpl" }} -

- {{ .title }} -

-

Using posts/index.tmpl

- -{{ end }} -``` - -templates/users/index.tmpl - -```html -{{ define "users/index.tmpl" }} -

- {{ .title }} -

-

Using users/index.tmpl

- -{{ end }} -``` - -#### Custom Template renderer - -You can also use your own html template render - -```go -import "html/template" - -func main() { - router := gin.Default() - html := template.Must(template.ParseFiles("file1", "file2")) - router.SetHTMLTemplate(html) - router.Run(":8080") -} -``` - -#### Custom Delimiters - -You may use custom delims - -```go - r := gin.Default() - r.Delims("{[{", "}]}") - r.LoadHTMLGlob("/path/to/templates") -``` - -#### Custom Template Funcs - -See the detail [example code](https://github.com/gin-gonic/examples/tree/master/template). - -main.go - -```go -import ( - "fmt" - "html/template" - "net/http" - "time" - - "github.com/gin-gonic/gin" -) - -func formatAsDate(t time.Time) string { - year, month, day := t.Date() - return fmt.Sprintf("%d%02d/%02d", year, month, day) -} - -func main() { - router := gin.Default() - router.Delims("{[{", "}]}") - router.SetFuncMap(template.FuncMap{ - "formatAsDate": formatAsDate, - }) - router.LoadHTMLFiles("./testdata/template/raw.tmpl") - - router.GET("/raw", func(c *gin.Context) { - c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{ - "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), - }) - }) - - router.Run(":8080") -} - -``` - -raw.tmpl - -```html -Date: {[{.now | formatAsDate}]} -``` - -Result: -``` -Date: 2017/07/01 -``` - -### Multitemplate - -Gin allow by default use only one html.Template. Check [a multitemplate render](https://github.com/gin-contrib/multitemplate) for using features like go 1.6 `block template`. - -### Redirects - -Issuing a HTTP redirect is easy. Both internal and external locations are supported. - -```go -r.GET("/test", func(c *gin.Context) { - c.Redirect(http.StatusMovedPermanently, "http://www.google.com/") -}) -``` - - -Issuing a Router redirect, use `HandleContext` like below. - -``` go -r.GET("/test", func(c *gin.Context) { - c.Request.URL.Path = "/test2" - r.HandleContext(c) -}) -r.GET("/test2", func(c *gin.Context) { - c.JSON(200, gin.H{"hello": "world"}) -}) -``` - - -### Custom Middleware - -```go -func Logger() gin.HandlerFunc { - return func(c *gin.Context) { - t := time.Now() - - // Set example variable - c.Set("example", "12345") - - // before request - - c.Next() - - // after request - latency := time.Since(t) - log.Print(latency) - - // access the status we are sending - status := c.Writer.Status() - log.Println(status) - } -} - -func main() { - r := gin.New() - r.Use(Logger()) - - r.GET("/test", func(c *gin.Context) { - example := c.MustGet("example").(string) - - // it would print: "12345" - log.Println(example) - }) - - // Listen and serve on 0.0.0.0:8080 - r.Run(":8080") -} -``` - -### Using BasicAuth() middleware - -```go -// simulate some private data -var secrets = gin.H{ - "foo": gin.H{"email": "foo@bar.com", "phone": "123433"}, - "austin": gin.H{"email": "austin@example.com", "phone": "666"}, - "lena": gin.H{"email": "lena@guapa.com", "phone": "523443"}, -} - -func main() { - r := gin.Default() - - // Group using gin.BasicAuth() middleware - // gin.Accounts is a shortcut for map[string]string - authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{ - "foo": "bar", - "austin": "1234", - "lena": "hello2", - "manu": "4321", - })) - - // /admin/secrets endpoint - // hit "localhost:8080/admin/secrets - authorized.GET("/secrets", func(c *gin.Context) { - // get user, it was set by the BasicAuth middleware - user := c.MustGet(gin.AuthUserKey).(string) - if secret, ok := secrets[user]; ok { - c.JSON(http.StatusOK, gin.H{"user": user, "secret": secret}) - } else { - c.JSON(http.StatusOK, gin.H{"user": user, "secret": "NO SECRET :("}) - } - }) - - // Listen and serve on 0.0.0.0:8080 - r.Run(":8080") -} -``` - -### Goroutines inside a middleware - -When starting new Goroutines inside a middleware or handler, you **SHOULD NOT** use the original context inside it, you have to use a read-only copy. - -```go -func main() { - r := gin.Default() - - r.GET("/long_async", func(c *gin.Context) { - // create copy to be used inside the goroutine - cCp := c.Copy() - go func() { - // simulate a long task with time.Sleep(). 5 seconds - time.Sleep(5 * time.Second) - - // note that you are using the copied context "cCp", IMPORTANT - log.Println("Done! in path " + cCp.Request.URL.Path) - }() - }) - - r.GET("/long_sync", func(c *gin.Context) { - // simulate a long task with time.Sleep(). 5 seconds - time.Sleep(5 * time.Second) - - // since we are NOT using a goroutine, we do not have to copy the context - log.Println("Done! in path " + c.Request.URL.Path) - }) - - // Listen and serve on 0.0.0.0:8080 - r.Run(":8080") -} -``` - -### Custom HTTP configuration - -Use `http.ListenAndServe()` directly, like this: - -```go -func main() { - router := gin.Default() - http.ListenAndServe(":8080", router) -} -``` -or - -```go -func main() { - router := gin.Default() - - s := &http.Server{ - Addr: ":8080", - Handler: router, - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, - MaxHeaderBytes: 1 << 20, - } - s.ListenAndServe() -} -``` - -### Support Let's Encrypt - -example for 1-line LetsEncrypt HTTPS servers. - -```go -package main - -import ( - "log" - - "github.com/gin-gonic/autotls" - "github.com/gin-gonic/gin" -) - -func main() { - r := gin.Default() - - // Ping handler - r.GET("/ping", func(c *gin.Context) { - c.String(200, "pong") - }) - - log.Fatal(autotls.Run(r, "example1.com", "example2.com")) -} -``` - -example for custom autocert manager. - -```go -package main - -import ( - "log" - - "github.com/gin-gonic/autotls" - "github.com/gin-gonic/gin" - "golang.org/x/crypto/acme/autocert" -) - -func main() { - r := gin.Default() - - // Ping handler - r.GET("/ping", func(c *gin.Context) { - c.String(200, "pong") - }) - - m := autocert.Manager{ - Prompt: autocert.AcceptTOS, - HostPolicy: autocert.HostWhitelist("example1.com", "example2.com"), - Cache: autocert.DirCache("/var/www/.cache"), - } - - log.Fatal(autotls.RunWithManager(r, &m)) -} -``` - -### Run multiple service using Gin - -See the [question](https://github.com/gin-gonic/gin/issues/346) and try the following example: - -```go -package main - -import ( - "log" - "net/http" - "time" - - "github.com/gin-gonic/gin" - "golang.org/x/sync/errgroup" -) - -var ( - g errgroup.Group -) - -func router01() http.Handler { - e := gin.New() - e.Use(gin.Recovery()) - e.GET("/", func(c *gin.Context) { - c.JSON( - http.StatusOK, - gin.H{ - "code": http.StatusOK, - "error": "Welcome server 01", - }, - ) - }) - - return e -} - -func router02() http.Handler { - e := gin.New() - e.Use(gin.Recovery()) - e.GET("/", func(c *gin.Context) { - c.JSON( - http.StatusOK, - gin.H{ - "code": http.StatusOK, - "error": "Welcome server 02", - }, - ) - }) - - return e -} - -func main() { - server01 := &http.Server{ - Addr: ":8080", - Handler: router01(), - ReadTimeout: 5 * time.Second, - WriteTimeout: 10 * time.Second, - } - - server02 := &http.Server{ - Addr: ":8081", - Handler: router02(), - ReadTimeout: 5 * time.Second, - WriteTimeout: 10 * time.Second, - } - - g.Go(func() error { - return server01.ListenAndServe() - }) - - g.Go(func() error { - return server02.ListenAndServe() - }) - - if err := g.Wait(); err != nil { - log.Fatal(err) - } -} -``` - -### Graceful restart or stop - -Do you want to graceful restart or stop your web server? -There are some ways this can be done. - -We can use [fvbock/endless](https://github.com/fvbock/endless) to replace the default `ListenAndServe`. Refer issue [#296](https://github.com/gin-gonic/gin/issues/296) for more details. - -```go -router := gin.Default() -router.GET("/", handler) -// [...] -endless.ListenAndServe(":4242", router) -``` - -An alternative to endless: - -* [manners](https://github.com/braintree/manners): A polite Go HTTP server that shuts down gracefully. -* [graceful](https://github.com/tylerb/graceful): Graceful is a Go package enabling graceful shutdown of an http.Handler server. -* [grace](https://github.com/facebookgo/grace): Graceful restart & zero downtime deploy for Go servers. - -If you are using Go 1.8, you may not need to use this library! Consider using http.Server's built-in [Shutdown()](https://golang.org/pkg/net/http/#Server.Shutdown) method for graceful shutdowns. See the full [graceful-shutdown](https://github.com/gin-gonic/examples/tree/master/graceful-shutdown) example with gin. - -```go -// +build go1.8 - -package main - -import ( - "context" - "log" - "net/http" - "os" - "os/signal" - "syscall" - "time" - - "github.com/gin-gonic/gin" -) - -func main() { - router := gin.Default() - router.GET("/", func(c *gin.Context) { - time.Sleep(5 * time.Second) - c.String(http.StatusOK, "Welcome Gin Server") - }) - - srv := &http.Server{ - Addr: ":8080", - Handler: router, - } - - go func() { - // service connections - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatalf("listen: %s\n", err) - } - }() - - // Wait for interrupt signal to gracefully shutdown the server with - // a timeout of 5 seconds. - quit := make(chan os.Signal) - // kill (no param) default send syscanll.SIGTERM - // kill -2 is syscall.SIGINT - // kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - <-quit - log.Println("Shutdown Server ...") - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := srv.Shutdown(ctx); err != nil { - log.Fatal("Server Shutdown:", err) - } - // catching ctx.Done(). timeout of 5 seconds. - select { - case <-ctx.Done(): - log.Println("timeout of 5 seconds.") - } - log.Println("Server exiting") -} -``` - -### Build a single binary with templates - -You can build a server into a single binary containing templates by using [go-assets][]. - -[go-assets]: https://github.com/jessevdk/go-assets - -```go -func main() { - r := gin.New() - - t, err := loadTemplate() - if err != nil { - panic(err) - } - r.SetHTMLTemplate(t) - - r.GET("/", func(c *gin.Context) { - c.HTML(http.StatusOK, "/html/index.tmpl",nil) - }) - r.Run(":8080") -} - -// loadTemplate loads templates embedded by go-assets-builder -func loadTemplate() (*template.Template, error) { - t := template.New("") - for name, file := range Assets.Files { - if file.IsDir() || !strings.HasSuffix(name, ".tmpl") { - continue - } - h, err := ioutil.ReadAll(file) - if err != nil { - return nil, err - } - t, err = t.New(name).Parse(string(h)) - if err != nil { - return nil, err - } - } - return t, nil -} -``` - -See a complete example in the `https://github.com/gin-gonic/examples/tree/master/assets-in-binary` directory. - -### Bind form-data request with custom struct - -The follow example using custom struct: - -```go -type StructA struct { - FieldA string `form:"field_a"` -} - -type StructB struct { - NestedStruct StructA - FieldB string `form:"field_b"` -} - -type StructC struct { - NestedStructPointer *StructA - FieldC string `form:"field_c"` -} - -type StructD struct { - NestedAnonyStruct struct { - FieldX string `form:"field_x"` - } - FieldD string `form:"field_d"` -} - -func GetDataB(c *gin.Context) { - var b StructB - c.Bind(&b) - c.JSON(200, gin.H{ - "a": b.NestedStruct, - "b": b.FieldB, - }) -} - -func GetDataC(c *gin.Context) { - var b StructC - c.Bind(&b) - c.JSON(200, gin.H{ - "a": b.NestedStructPointer, - "c": b.FieldC, - }) -} - -func GetDataD(c *gin.Context) { - var b StructD - c.Bind(&b) - c.JSON(200, gin.H{ - "x": b.NestedAnonyStruct, - "d": b.FieldD, - }) -} - -func main() { - r := gin.Default() - r.GET("/getb", GetDataB) - r.GET("/getc", GetDataC) - r.GET("/getd", GetDataD) - - r.Run() -} -``` - -Using the command `curl` command result: - -``` -$ curl "http://localhost:8080/getb?field_a=hello&field_b=world" -{"a":{"FieldA":"hello"},"b":"world"} -$ curl "http://localhost:8080/getc?field_a=hello&field_c=world" -{"a":{"FieldA":"hello"},"c":"world"} -$ curl "http://localhost:8080/getd?field_x=hello&field_d=world" -{"d":"world","x":{"FieldX":"hello"}} -``` - -### Try to bind body into different structs - -The normal methods for binding request body consumes `c.Request.Body` and they -cannot be called multiple times. - -```go -type formA struct { - Foo string `json:"foo" xml:"foo" binding:"required"` -} - -type formB struct { - Bar string `json:"bar" xml:"bar" binding:"required"` -} - -func SomeHandler(c *gin.Context) { - objA := formA{} - objB := formB{} - // This c.ShouldBind consumes c.Request.Body and it cannot be reused. - if errA := c.ShouldBind(&objA); errA == nil { - c.String(http.StatusOK, `the body should be formA`) - // Always an error is occurred by this because c.Request.Body is EOF now. - } else if errB := c.ShouldBind(&objB); errB == nil { - c.String(http.StatusOK, `the body should be formB`) - } else { - ... - } -} -``` - -For this, you can use `c.ShouldBindBodyWith`. - -```go -func SomeHandler(c *gin.Context) { - objA := formA{} - objB := formB{} - // This reads c.Request.Body and stores the result into the context. - if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil { - c.String(http.StatusOK, `the body should be formA`) - // At this time, it reuses body stored in the context. - } else if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil { - c.String(http.StatusOK, `the body should be formB JSON`) - // And it can accepts other formats - } else if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil { - c.String(http.StatusOK, `the body should be formB XML`) - } else { - ... - } -} -``` - -* `c.ShouldBindBodyWith` stores body into the context before binding. This has -a slight impact to performance, so you should not use this method if you are -enough to call binding at once. -* This feature is only needed for some formats -- `JSON`, `XML`, `MsgPack`, -`ProtoBuf`. For other formats, `Query`, `Form`, `FormPost`, `FormMultipart`, -can be called by `c.ShouldBind()` multiple times without any damage to -performance (See [#1341](https://github.com/gin-gonic/gin/pull/1341)). - -### http2 server push - -http.Pusher is supported only **go1.8+**. See the [golang blog](https://blog.golang.org/h2push) for detail information. - -```go -package main - -import ( - "html/template" - "log" - - "github.com/gin-gonic/gin" -) - -var html = template.Must(template.New("https").Parse(` - - - Https Test - - - -

Welcome, Ginner!

- - -`)) - -func main() { - r := gin.Default() - r.Static("/assets", "./assets") - r.SetHTMLTemplate(html) - - r.GET("/", func(c *gin.Context) { - if pusher := c.Writer.Pusher(); pusher != nil { - // use pusher.Push() to do server push - if err := pusher.Push("/assets/app.js", nil); err != nil { - log.Printf("Failed to push: %v", err) - } - } - c.HTML(200, "https", gin.H{ - "status": "success", - }) - }) - - // Listen and Server in https://127.0.0.1:8080 - r.RunTLS(":8080", "./testdata/server.pem", "./testdata/server.key") -} -``` - -### Define format for the log of routes - -The default log of routes is: -``` -[GIN-debug] POST /foo --> main.main.func1 (3 handlers) -[GIN-debug] GET /bar --> main.main.func2 (3 handlers) -[GIN-debug] GET /status --> main.main.func3 (3 handlers) -``` - -If you want to log this information in given format (e.g. JSON, key values or something else), then you can define this format with `gin.DebugPrintRouteFunc`. -In the example below, we log all routes with standard log package but you can use another log tools that suits of your needs. -```go -import ( - "log" - "net/http" - - "github.com/gin-gonic/gin" -) - -func main() { - r := gin.Default() - gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) { - log.Printf("endpoint %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers) - } - - r.POST("/foo", func(c *gin.Context) { - c.JSON(http.StatusOK, "foo") - }) - - r.GET("/bar", func(c *gin.Context) { - c.JSON(http.StatusOK, "bar") - }) - - r.GET("/status", func(c *gin.Context) { - c.JSON(http.StatusOK, "ok") - }) - - // Listen and Server in http://0.0.0.0:8080 - r.Run() -} -``` - -### Set and get a cookie - -```go -import ( - "fmt" - - "github.com/gin-gonic/gin" -) - -func main() { - - router := gin.Default() - - router.GET("/cookie", func(c *gin.Context) { - - cookie, err := c.Cookie("gin_cookie") - - if err != nil { - cookie = "NotSet" - c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true) - } - - fmt.Printf("Cookie value: %s \n", cookie) - }) - - router.Run() -} -``` - - -## Testing - -The `net/http/httptest` package is preferable way for HTTP testing. - -```go -package main - -func setupRouter() *gin.Engine { - r := gin.Default() - r.GET("/ping", func(c *gin.Context) { - c.String(200, "pong") - }) - return r -} - -func main() { - r := setupRouter() - r.Run(":8080") -} -``` - -Test for code example above: - -```go -package main - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestPingRoute(t *testing.T) { - router := setupRouter() - - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/ping", nil) - router.ServeHTTP(w, req) - - assert.Equal(t, 200, w.Code) - assert.Equal(t, "pong", w.Body.String()) -} -``` +A number of ready-to-run examples demonstrating various use cases of Gin on the [Gin examples](https://github.com/gin-gonic/examples) repository. ## Users -Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework. +[Gin website](https://gin-gonic.com/docs/users/) lists some awesome projects made with Gin web framework. + +## Contributing + +Gin is the work of hundreds of contributors. We appreciate your help! + +Please see [CONTRIBUTING](CONTRIBUTING.md) for details on submitting patches and the contribution workflow. -* [gorush](https://github.com/appleboy/gorush): A push notification server written in Go. -* [fnproject](https://github.com/fnproject/fn): The container native, cloud agnostic serverless platform. -* [photoprism](https://github.com/photoprism/photoprism): Personal photo management powered by Go and Google TensorFlow. -* [krakend](https://github.com/devopsfaith/krakend): Ultra performant API Gateway with middlewares. -* [picfit](https://github.com/thoas/picfit): An image resizing server written in Go. From 483f828bce15ff82fdec7414c32ee9b3f017b5ca Mon Sep 17 00:00:00 2001 From: Dmitry Kutakov Date: Thu, 14 Mar 2019 08:34:56 +0300 Subject: [PATCH 098/111] add support arrays on mapping (#1797) * add support arrays on mapping * not allow default value on array mapping --- binding/binding_test.go | 28 ++++++++++++++++++++++++++++ binding/form_mapping.go | 23 ++++++++++++++++++++--- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/binding/binding_test.go b/binding/binding_test.go index 5ae87957..b265af36 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -1430,3 +1430,31 @@ func TestBindingTimeDuration(t *testing.T) { err = Form.Bind(req, &s) assert.Error(t, err) } + +func TestBindingArray(t *testing.T) { + var s struct { + Nums [2]int `form:"nums,default=4"` + } + + // default + req := formPostRequest("", "") + err := Form.Bind(req, &s) + assert.Error(t, err) + assert.Equal(t, [2]int{0, 0}, s.Nums) + + // ok + req = formPostRequest("", "nums=3&nums=8") + err = Form.Bind(req, &s) + assert.NoError(t, err) + assert.Equal(t, [2]int{3, 8}, s.Nums) + + // not enough vals + req = formPostRequest("", "nums=3") + err = Form.Bind(req, &s) + assert.Error(t, err) + + // error + req = formPostRequest("", "nums=3&nums=wrong") + err = Form.Bind(req, &s) + assert.Error(t, err) +} diff --git a/binding/form_mapping.go b/binding/form_mapping.go index 87edfbb2..ba9d2c4f 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -6,6 +6,7 @@ package binding import ( "errors" + "fmt" "reflect" "strconv" "strings" @@ -118,6 +119,14 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, form map[stri vs = []string{defaultValue} } return true, setSlice(vs, value, field) + case reflect.Array: + if !ok { + vs = []string{defaultValue} + } + if len(vs) != value.Len() { + return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String()) + } + return true, setArray(vs, value, field) default: var val string if !ok { @@ -256,14 +265,22 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val return nil } -func setSlice(vals []string, value reflect.Value, field reflect.StructField) error { - slice := reflect.MakeSlice(value.Type(), len(vals), len(vals)) +func setArray(vals []string, value reflect.Value, field reflect.StructField) error { for i, s := range vals { - err := setWithProperType(s, slice.Index(i), field) + err := setWithProperType(s, value.Index(i), field) if err != nil { return err } } + return nil +} + +func setSlice(vals []string, value reflect.Value, field reflect.StructField) error { + slice := reflect.MakeSlice(value.Type(), len(vals), len(vals)) + err := setArray(vals, slice, field) + if err != nil { + return err + } value.Set(slice) return nil } From 242a2622c839bf883c415fd088d24fec8727fb23 Mon Sep 17 00:00:00 2001 From: Sai Date: Thu, 14 Mar 2019 17:26:51 +0900 Subject: [PATCH 099/111] Fix Japanese text hiragana -> kanji (#1812) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f9c10b22..b46c5637 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ All documentation is available on the Gin website. - [English](https://gin-gonic.com/docs/) - [简体中文](https://gin-gonic.com/zh-cn/docs/) - [繁體中文](https://gin-gonic.com/zh-tw/docs/) -- [にほんご](https://gin-gonic.com/ja/docs/) +- [日本語](https://gin-gonic.com/ja/docs/) ## Examples From 05b5c3ba7495fb3cd737cad8b35e62e0862ed1c2 Mon Sep 17 00:00:00 2001 From: David Zhang Date: Fri, 15 Mar 2019 15:39:34 +0800 Subject: [PATCH 100/111] Doc: fix gin example notice syntax (#1814) --- examples/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/README.md b/examples/README.md index b02deae4..bfebc6c0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,3 +1,3 @@ # Gin Examples -⚠️ **NOTICE:** All gin examples has moved as alone repository to [here](https://github.com/gin-gonic/examples). +⚠️ **NOTICE:** All gin examples have been moved as standalone repository to [here](https://github.com/gin-gonic/examples). From bcf36ade9f763b875bd781ad26cdb0549349c5f8 Mon Sep 17 00:00:00 2001 From: sekky0905 <20237968+sekky0905@users.noreply.github.com> Date: Sat, 16 Mar 2019 17:09:10 +0900 Subject: [PATCH 101/111] Remove sudo setting from travis.yml (#1816) --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b38adcb1..2fd9c8a2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,4 @@ language: go -sudo: false matrix: fast_finish: true From c16bfa7949c6ca59c049d20df507d24b1f2ec629 Mon Sep 17 00:00:00 2001 From: Boyi Wu Date: Mon, 18 Mar 2019 10:16:34 +0800 Subject: [PATCH 102/111] update for supporting file binding (#1264) update for supporting multipart form and file binding example: ``` type PhoptUploadForm struct { imgData *multipart.FileHeader `form:"img_data" binding:"required"` ProjectID string `form:"project_id" binding:"required"` Description string `form:"description binding:"required"` } ``` ref: https://github.com/gin-gonic/gin/issues/1263 --- binding/binding.go | 4 +- binding/binding_test.go | 94 ++++++++++++++++++++++++++++++++++++++++- binding/form.go | 5 +++ binding/form_mapping.go | 29 +++++++++++++ 4 files changed, 129 insertions(+), 3 deletions(-) diff --git a/binding/binding.go b/binding/binding.go index 26d71c9f..520c5109 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -98,7 +98,9 @@ func Default(method, contentType string) Binding { return MsgPack case MIMEYAML: return YAML - default: //case MIMEPOSTForm, MIMEMultipartPOSTForm: + case MIMEMultipartPOSTForm: + return FormMultipart + default: // case MIMEPOSTForm: return Form } } diff --git a/binding/binding_test.go b/binding/binding_test.go index b265af36..ee788225 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -8,9 +8,11 @@ import ( "bytes" "encoding/json" "errors" + "io" "io/ioutil" "mime/multipart" "net/http" + "os" "strconv" "strings" "testing" @@ -31,6 +33,18 @@ type FooBarStruct struct { Bar string `msgpack:"bar" json:"bar" form:"bar" xml:"bar" binding:"required"` } +type FooBarFileStruct struct { + FooBarStruct + File *multipart.FileHeader `form:"file" binding:"required"` +} + +type FooBarFileFailStruct struct { + FooBarStruct + File *multipart.FileHeader `invalid_name:"file" binding:"required"` + // for unexport test + data *multipart.FileHeader `form:"data" binding:"required"` +} + type FooDefaultBarStruct struct { FooStruct Bar string `msgpack:"bar" json:"bar" form:"bar,default=hello" xml:"bar" binding:"required"` @@ -187,8 +201,8 @@ func TestBindingDefault(t *testing.T) { assert.Equal(t, Form, Default("POST", MIMEPOSTForm)) assert.Equal(t, Form, Default("PUT", MIMEPOSTForm)) - assert.Equal(t, Form, Default("POST", MIMEMultipartPOSTForm)) - assert.Equal(t, Form, Default("PUT", MIMEMultipartPOSTForm)) + assert.Equal(t, FormMultipart, Default("POST", MIMEMultipartPOSTForm)) + assert.Equal(t, FormMultipart, Default("PUT", MIMEMultipartPOSTForm)) assert.Equal(t, ProtoBuf, Default("POST", MIMEPROTOBUF)) assert.Equal(t, ProtoBuf, Default("PUT", MIMEPROTOBUF)) @@ -536,6 +550,54 @@ func createFormPostRequestForMapFail(t *testing.T) *http.Request { return req } +func createFormFilesMultipartRequest(t *testing.T) *http.Request { + boundary := "--testboundary" + body := new(bytes.Buffer) + mw := multipart.NewWriter(body) + defer mw.Close() + + assert.NoError(t, mw.SetBoundary(boundary)) + assert.NoError(t, mw.WriteField("foo", "bar")) + assert.NoError(t, mw.WriteField("bar", "foo")) + + f, err := os.Open("form.go") + assert.NoError(t, err) + defer f.Close() + fw, err1 := mw.CreateFormFile("file", "form.go") + assert.NoError(t, err1) + io.Copy(fw, f) + + req, err2 := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body) + assert.NoError(t, err2) + req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary) + + return req +} + +func createFormFilesMultipartRequestFail(t *testing.T) *http.Request { + boundary := "--testboundary" + body := new(bytes.Buffer) + mw := multipart.NewWriter(body) + defer mw.Close() + + assert.NoError(t, mw.SetBoundary(boundary)) + assert.NoError(t, mw.WriteField("foo", "bar")) + assert.NoError(t, mw.WriteField("bar", "foo")) + + f, err := os.Open("form.go") + assert.NoError(t, err) + defer f.Close() + fw, err1 := mw.CreateFormFile("file_foo", "form_foo.go") + assert.NoError(t, err1) + io.Copy(fw, f) + + req, err2 := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body) + assert.NoError(t, err2) + req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary) + + return req +} + func createFormMultipartRequest(t *testing.T) *http.Request { boundary := "--testboundary" body := new(bytes.Buffer) @@ -613,6 +675,34 @@ func TestBindingFormPostForMapFail(t *testing.T) { assert.Error(t, err) } +func TestBindingFormFilesMultipart(t *testing.T) { + req := createFormFilesMultipartRequest(t) + var obj FooBarFileStruct + FormMultipart.Bind(req, &obj) + + // file from os + f, _ := os.Open("form.go") + defer f.Close() + fileActual, _ := ioutil.ReadAll(f) + + // file from multipart + mf, _ := obj.File.Open() + defer mf.Close() + fileExpect, _ := ioutil.ReadAll(mf) + + assert.Equal(t, FormMultipart.Name(), "multipart/form-data") + assert.Equal(t, obj.Foo, "bar") + assert.Equal(t, obj.Bar, "foo") + assert.Equal(t, fileExpect, fileActual) +} + +func TestBindingFormFilesMultipartFail(t *testing.T) { + req := createFormFilesMultipartRequestFail(t) + var obj FooBarFileFailStruct + err := FormMultipart.Bind(req, &obj) + assert.Error(t, err) +} + func TestBindingFormMultipart(t *testing.T) { req := createFormMultipartRequest(t) var obj FooBarStruct diff --git a/binding/form.go b/binding/form.go index 8955c95b..f1f89195 100644 --- a/binding/form.go +++ b/binding/form.go @@ -56,5 +56,10 @@ func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error { if err := mapForm(obj, req.MultipartForm.Value); err != nil { return err } + + if err := mapFiles(obj, req); err != nil { + return err + } + return validate(obj) } diff --git a/binding/form_mapping.go b/binding/form_mapping.go index ba9d2c4f..fc33b1df 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -7,6 +7,7 @@ package binding import ( "errors" "fmt" + "net/http" "reflect" "strconv" "strings" @@ -15,6 +16,34 @@ import ( "github.com/gin-gonic/gin/internal/json" ) +func mapFiles(ptr interface{}, req *http.Request) error { + typ := reflect.TypeOf(ptr).Elem() + val := reflect.ValueOf(ptr).Elem() + for i := 0; i < typ.NumField(); i++ { + typeField := typ.Field(i) + structField := val.Field(i) + + t := fmt.Sprintf("%s", typeField.Type) + if string(t) != "*multipart.FileHeader" { + continue + } + + inputFieldName := typeField.Tag.Get("form") + if inputFieldName == "" { + inputFieldName = typeField.Name + } + + _, fileHeader, err := req.FormFile(inputFieldName) + if err != nil { + return err + } + + structField.Set(reflect.ValueOf(fileHeader)) + + } + return nil +} + var errUnknownType = errors.New("Unknown type") func mapUri(ptr interface{}, m map[string][]string) error { From b40d4c175c079fa41cda2669c308485175cf2eee Mon Sep 17 00:00:00 2001 From: Sai Date: Mon, 18 Mar 2019 12:12:30 +0900 Subject: [PATCH 103/111] IsTerm flag should not be affected by DisableConsoleColor method. (#1802) * IsTerm flag should not be affected by DisableConsoleColor method. * change public property to private --- logger.go | 49 +++++++++++++++++++++++++++++------------------- logger_test.go | 51 ++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 75 insertions(+), 25 deletions(-) diff --git a/logger.go b/logger.go index 2ecaed7d..198a0192 100644 --- a/logger.go +++ b/logger.go @@ -14,17 +14,24 @@ import ( "github.com/mattn/go-isatty" ) +type consoleColorModeValue int + +const ( + autoColor consoleColorModeValue = iota + disableColor + forceColor +) + var ( - green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109}) - white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109}) - yellow = string([]byte{27, 91, 57, 48, 59, 52, 51, 109}) - red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109}) - blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109}) - magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109}) - cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109}) - reset = string([]byte{27, 91, 48, 109}) - disableColor = false - forceColor = false + green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109}) + white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109}) + yellow = string([]byte{27, 91, 57, 48, 59, 52, 51, 109}) + red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109}) + blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109}) + magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109}) + cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109}) + reset = string([]byte{27, 91, 48, 109}) + consoleColorMode = autoColor ) // LoggerConfig defines the config for Logger middleware. @@ -62,8 +69,8 @@ type LogFormatterParams struct { Path string // ErrorMessage is set if error has occurred in processing the request. ErrorMessage string - // IsTerm shows whether does gin's output descriptor refers to a terminal. - IsTerm bool + // isTerm shows whether does gin's output descriptor refers to a terminal. + isTerm bool // BodySize is the size of the Response Body BodySize int // Keys are the keys set on the request's context. @@ -115,10 +122,15 @@ func (p *LogFormatterParams) ResetColor() string { return reset } +// IsOutputColor indicates whether can colors be outputted to the log. +func (p *LogFormatterParams) IsOutputColor() bool { + return consoleColorMode == forceColor || (consoleColorMode == autoColor && p.isTerm) +} + // defaultLogFormatter is the default log format function Logger middleware uses. var defaultLogFormatter = func(param LogFormatterParams) string { var statusColor, methodColor, resetColor string - if param.IsTerm { + if param.IsOutputColor() { statusColor = param.StatusCodeColor() methodColor = param.MethodColor() resetColor = param.ResetColor() @@ -137,12 +149,12 @@ var defaultLogFormatter = func(param LogFormatterParams) string { // DisableConsoleColor disables color output in the console. func DisableConsoleColor() { - disableColor = true + consoleColorMode = disableColor } // ForceConsoleColor force color output in the console. func ForceConsoleColor() { - forceColor = true + consoleColorMode = forceColor } // ErrorLogger returns a handlerfunc for any error type. @@ -199,9 +211,8 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc { isTerm := true - if w, ok := out.(*os.File); (!ok || - (os.Getenv("TERM") == "dumb" || (!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd()))) || - disableColor) && !forceColor { + if w, ok := out.(*os.File); !ok || os.Getenv("TERM") == "dumb" || + (!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd())) { isTerm = false } @@ -228,7 +239,7 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc { if _, ok := skip[path]; !ok { param := LogFormatterParams{ Request: c.Request, - IsTerm: isTerm, + isTerm: isTerm, Keys: c.Keys, } diff --git a/logger_test.go b/logger_test.go index 36231371..11a859e6 100644 --- a/logger_test.go +++ b/logger_test.go @@ -240,7 +240,7 @@ func TestDefaultLogFormatter(t *testing.T) { Method: "GET", Path: "/", ErrorMessage: "", - IsTerm: false, + isTerm: false, } termTrueParam := LogFormatterParams{ @@ -251,7 +251,7 @@ func TestDefaultLogFormatter(t *testing.T) { Method: "GET", Path: "/", ErrorMessage: "", - IsTerm: true, + isTerm: true, } assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET /\n", defaultLogFormatter(termFalseParam)) @@ -296,6 +296,39 @@ func TestResetColor(t *testing.T) { assert.Equal(t, string([]byte{27, 91, 48, 109}), p.ResetColor()) } +func TestIsOutputColor(t *testing.T) { + // test with isTerm flag true. + p := LogFormatterParams{ + isTerm: true, + } + + consoleColorMode = autoColor + assert.Equal(t, true, p.IsOutputColor()) + + ForceConsoleColor() + assert.Equal(t, true, p.IsOutputColor()) + + DisableConsoleColor() + assert.Equal(t, false, p.IsOutputColor()) + + // test with isTerm flag false. + p = LogFormatterParams{ + isTerm: false, + } + + consoleColorMode = autoColor + assert.Equal(t, false, p.IsOutputColor()) + + ForceConsoleColor() + assert.Equal(t, true, p.IsOutputColor()) + + DisableConsoleColor() + assert.Equal(t, false, p.IsOutputColor()) + + // reset console color mode. + consoleColorMode = autoColor +} + func TestErrorLogger(t *testing.T) { router := New() router.Use(ErrorLogger()) @@ -358,14 +391,20 @@ func TestLoggerWithConfigSkippingPaths(t *testing.T) { func TestDisableConsoleColor(t *testing.T) { New() - assert.False(t, disableColor) + assert.Equal(t, autoColor, consoleColorMode) DisableConsoleColor() - assert.True(t, disableColor) + assert.Equal(t, disableColor, consoleColorMode) + + // reset console color mode. + consoleColorMode = autoColor } func TestForceConsoleColor(t *testing.T) { New() - assert.False(t, forceColor) + assert.Equal(t, autoColor, consoleColorMode) ForceConsoleColor() - assert.True(t, forceColor) + assert.Equal(t, forceColor, consoleColorMode) + + // reset console color mode. + consoleColorMode = autoColor } From 0c1f3c4e81f6e969a3e465d933faefb5578c54d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Wed, 20 Mar 2019 12:07:34 +0800 Subject: [PATCH 104/111] chore: fix invalid link (#1820) --- README.md | 4 +++- doc.go | 2 +- testdata/assets/console.png | Bin 59545 -> 0 bytes 3 files changed, 4 insertions(+), 2 deletions(-) delete mode 100644 testdata/assets/console.png diff --git a/README.md b/README.md index b46c5637..d3433ed2 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ For more feature details, please see the [Gin website introduction](https://gin- ### Getting Gin -The first need [Go](https://golang.org/) installed (version 1.6+ is required), then you can use the below Go command to install Gin. +The first need [Go](https://golang.org/) installed (**version 1.6+ is required**), then you can use the below Go command to install Gin. ```sh $ go get -u github.com/gin-gonic/gin @@ -111,6 +111,8 @@ You can find many useful Gin middlewares at [gin-contrib](https://github.com/gin ## Documentation +See [API documentation and descriptions](https://godoc.org/github.com/gin-gonic/gin) for package. + All documentation is available on the Gin website. - [English](https://gin-gonic.com/docs/) diff --git a/doc.go b/doc.go index 01ac4a90..1bd03864 100644 --- a/doc.go +++ b/doc.go @@ -1,6 +1,6 @@ /* Package gin implements a HTTP web framework called gin. -See https://gin-gonic.github.io/gin/ for more information about gin. +See https://gin-gonic.com/ for more information about gin. */ package gin // import "github.com/gin-gonic/gin" diff --git a/testdata/assets/console.png b/testdata/assets/console.png deleted file mode 100644 index 7a695718fa31f9b1b42cfa547c16da394378aeae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59545 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
?hY(7P?kZ1wD_zt6I%hfC|R7KdZM)`&t88E6Ua# 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 From 1d462bbe3713bc2fea40ed45c80a06ce856d379f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Thu, 21 Mar 2019 15:12:06 +0800 Subject: [PATCH 105/111] chore: update ginS (#1822) --- ginS/gins.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/ginS/gins.go b/ginS/gins.go index 0f08645a..3ce4a6f6 100644 --- a/ginS/gins.go +++ b/ginS/gins.go @@ -125,23 +125,35 @@ func Use(middlewares ...gin.HandlerFunc) gin.IRoutes { return engine().Use(middlewares...) } -// Run : The router is attached to a http.Server and starts listening and serving HTTP requests. +// Routes returns a slice of registered routes. +func Routes() gin.RoutesInfo { + return engine().Routes() +} + +// Run attaches to a http.Server and starts listening and serving HTTP requests. // It is a shortcut for http.ListenAndServe(addr, router) // Note: this method will block the calling goroutine indefinitely unless an error happens. func Run(addr ...string) (err error) { return engine().Run(addr...) } -// RunTLS : The router is attached to a http.Server and starts listening and serving HTTPS requests. +// RunTLS attaches to a http.Server and starts listening and serving HTTPS requests. // It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router) // Note: this method will block the calling goroutine indefinitely unless an error happens. func RunTLS(addr, certFile, keyFile string) (err error) { return engine().RunTLS(addr, certFile, keyFile) } -// RunUnix : The router is attached to a http.Server and starts listening and serving HTTP requests +// RunUnix attaches to a http.Server and starts listening and serving HTTP requests // through the specified unix socket (ie. a file) // Note: this method will block the calling goroutine indefinitely unless an error happens. func RunUnix(file string) (err error) { return engine().RunUnix(file) } + +// RunFd attaches the router to a http.Server and starts listening and serving HTTP requests +// through the specified file descriptor. +// Note: thie method will block the calling goroutine indefinitely unless on error happens. +func RunFd(fd int) (err error) { + return engine().RunFd(fd) +} From ce20f107f5dc498ec7489d7739541a25dcd48463 Mon Sep 17 00:00:00 2001 From: Dan Markham Date: Wed, 27 Mar 2019 23:14:00 -0700 Subject: [PATCH 106/111] Truncate Latency precision in long running request (#1830) fixes #1823 --- logger.go | 4 ++++ logger_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/logger.go b/logger.go index 198a0192..5ab4639e 100644 --- a/logger.go +++ b/logger.go @@ -136,6 +136,10 @@ var defaultLogFormatter = func(param LogFormatterParams) string { resetColor = param.ResetColor() } + if param.Latency > time.Minute { + // Truncate in a golang < 1.8 safe way + param.Latency = param.Latency - param.Latency%time.Second + } return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s", param.TimeStamp.Format("2006/01/02 - 15:04:05"), statusColor, param.StatusCode, resetColor, diff --git a/logger_test.go b/logger_test.go index 11a859e6..56bb3a00 100644 --- a/logger_test.go +++ b/logger_test.go @@ -253,10 +253,34 @@ func TestDefaultLogFormatter(t *testing.T) { ErrorMessage: "", isTerm: true, } + termTrueLongDurationParam := LogFormatterParams{ + TimeStamp: timeStamp, + StatusCode: 200, + Latency: time.Millisecond * 9876543210, + ClientIP: "20.20.20.20", + Method: "GET", + Path: "/", + ErrorMessage: "", + isTerm: true, + } + + termFalseLongDurationParam := LogFormatterParams{ + TimeStamp: timeStamp, + StatusCode: 200, + Latency: time.Millisecond * 9876543210, + ClientIP: "20.20.20.20", + Method: "GET", + Path: "/", + ErrorMessage: "", + isTerm: false, + } assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET /\n", defaultLogFormatter(termFalseParam)) + assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 2743h29m3s | 20.20.20.20 | GET /\n", defaultLogFormatter(termFalseLongDurationParam)) assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 5s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m /\n", defaultLogFormatter(termTrueParam)) + assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 2743h29m3s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m /\n", defaultLogFormatter(termTrueLongDurationParam)) + } func TestColorForMethod(t *testing.T) { From 2e915f4e5083995154f65a600c86582b5396d02a Mon Sep 17 00:00:00 2001 From: Dmitry Kutakov Date: Tue, 2 Apr 2019 04:01:34 +0300 Subject: [PATCH 107/111] refactor(form_mapping.go): mapping multipart request (#1829) * refactor(form_mapping.go): mapping multipart request * add checkers for a types to match with the setter interface * form_mapping.go: rename method name on setter interface, add comments * fix style of comments --- binding/form.go | 36 +++++++++++++++--- binding/form_mapping.go | 84 ++++++++++++++++++++--------------------- 2 files changed, 71 insertions(+), 49 deletions(-) diff --git a/binding/form.go b/binding/form.go index f1f89195..0b28aa8a 100644 --- a/binding/form.go +++ b/binding/form.go @@ -4,7 +4,11 @@ package binding -import "net/http" +import ( + "mime/multipart" + "net/http" + "reflect" +) const defaultMemory = 32 * 1024 * 1024 @@ -53,13 +57,33 @@ func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error { if err := req.ParseMultipartForm(defaultMemory); err != nil { return err } - if err := mapForm(obj, req.MultipartForm.Value); err != nil { - return err - } - - if err := mapFiles(obj, req); err != nil { + if err := mappingByPtr(obj, (*multipartRequest)(req), "form"); err != nil { return err } return validate(obj) } + +type multipartRequest http.Request + +var _ setter = (*multipartRequest)(nil) + +var ( + multipartFileHeaderStructType = reflect.TypeOf(multipart.FileHeader{}) +) + +// TrySet tries to set a value by the multipart request with the binding a form file +func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) { + if value.Type() == multipartFileHeaderStructType { + _, file, err := (*http.Request)(r).FormFile(key) + if err != nil { + return false, err + } + if file != nil { + value.Set(reflect.ValueOf(*file)) + return true, nil + } + } + + return setByForm(value, field, r.MultipartForm.Value, key, opt) +} diff --git a/binding/form_mapping.go b/binding/form_mapping.go index fc33b1df..aaacf6c5 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -7,7 +7,6 @@ package binding import ( "errors" "fmt" - "net/http" "reflect" "strconv" "strings" @@ -16,34 +15,6 @@ import ( "github.com/gin-gonic/gin/internal/json" ) -func mapFiles(ptr interface{}, req *http.Request) error { - typ := reflect.TypeOf(ptr).Elem() - val := reflect.ValueOf(ptr).Elem() - for i := 0; i < typ.NumField(); i++ { - typeField := typ.Field(i) - structField := val.Field(i) - - t := fmt.Sprintf("%s", typeField.Type) - if string(t) != "*multipart.FileHeader" { - continue - } - - inputFieldName := typeField.Tag.Get("form") - if inputFieldName == "" { - inputFieldName = typeField.Name - } - - _, fileHeader, err := req.FormFile(inputFieldName) - if err != nil { - return err - } - - structField.Set(reflect.ValueOf(fileHeader)) - - } - return nil -} - var errUnknownType = errors.New("Unknown type") func mapUri(ptr interface{}, m map[string][]string) error { @@ -57,11 +28,29 @@ func mapForm(ptr interface{}, form map[string][]string) error { var emptyField = reflect.StructField{} func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error { - _, err := mapping(reflect.ValueOf(ptr), emptyField, form, tag) + return mappingByPtr(ptr, formSource(form), tag) +} + +// setter tries to set value on a walking by fields of a struct +type setter interface { + TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) +} + +type formSource map[string][]string + +var _ setter = formSource(nil) + +// TrySet tries to set a value by request's form source (like map[string][]string) +func (form formSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSetted bool, err error) { + return setByForm(value, field, form, tagValue, opt) +} + +func mappingByPtr(ptr interface{}, setter setter, tag string) error { + _, err := mapping(reflect.ValueOf(ptr), emptyField, setter, tag) return err } -func mapping(value reflect.Value, field reflect.StructField, form map[string][]string, tag string) (bool, error) { +func mapping(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) { var vKind = value.Kind() if vKind == reflect.Ptr { @@ -71,7 +60,7 @@ func mapping(value reflect.Value, field reflect.StructField, form map[string][]s isNew = true vPtr = reflect.New(value.Type().Elem()) } - isSetted, err := mapping(vPtr.Elem(), field, form, tag) + isSetted, err := mapping(vPtr.Elem(), field, setter, tag) if err != nil { return false, err } @@ -81,7 +70,7 @@ func mapping(value reflect.Value, field reflect.StructField, form map[string][]s return isSetted, nil } - ok, err := tryToSetValue(value, field, form, tag) + ok, err := tryToSetValue(value, field, setter, tag) if err != nil { return false, err } @@ -97,7 +86,7 @@ func mapping(value reflect.Value, field reflect.StructField, form map[string][]s if !value.Field(i).CanSet() { continue } - ok, err := mapping(value.Field(i), tValue.Field(i), form, tag) + ok, err := mapping(value.Field(i), tValue.Field(i), setter, tag) if err != nil { return false, err } @@ -108,9 +97,14 @@ func mapping(value reflect.Value, field reflect.StructField, form map[string][]s return false, nil } -func tryToSetValue(value reflect.Value, field reflect.StructField, form map[string][]string, tag string) (bool, error) { - var tagValue, defaultValue string - var isDefaultExists bool +type setOptions struct { + isDefaultExists bool + defaultValue string +} + +func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) { + var tagValue string + var setOpt setOptions tagValue = field.Tag.Get(tag) tagValue, opts := head(tagValue, ",") @@ -132,25 +126,29 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, form map[stri k, v := head(opt, "=") switch k { case "default": - isDefaultExists = true - defaultValue = v + setOpt.isDefaultExists = true + setOpt.defaultValue = v } } + return setter.TrySet(value, field, tagValue, setOpt) +} + +func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSetted bool, err error) { vs, ok := form[tagValue] - if !ok && !isDefaultExists { + if !ok && !opt.isDefaultExists { return false, nil } switch value.Kind() { case reflect.Slice: if !ok { - vs = []string{defaultValue} + vs = []string{opt.defaultValue} } return true, setSlice(vs, value, field) case reflect.Array: if !ok { - vs = []string{defaultValue} + vs = []string{opt.defaultValue} } if len(vs) != value.Len() { return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String()) @@ -159,7 +157,7 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, form map[stri default: var val string if !ok { - val = defaultValue + val = opt.defaultValue } if len(vs) > 0 { From ffcbe77b1e6222b4e0e97eb1920adc1813fb2224 Mon Sep 17 00:00:00 2001 From: Eason Lin Date: Sat, 6 Apr 2019 21:48:33 +0800 Subject: [PATCH 108/111] chore(readme): rollback readme (#1846) #1844 #1838 Keep the documentation in readme until full available on the new website. --- README.md | 2010 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 1972 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index d3433ed2..804041f9 100644 --- a/README.md +++ b/README.md @@ -13,35 +13,122 @@ Gin is a web framework written in Go (Golang). It features a martini-like API with much better performance, up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin. -**The key features of Gin are:** -- Zero allocation router -- Fast -- Middleware support -- Crash-free -- JSON validation -- Routes grouping -- Error management -- Rendering built-in -- Extendable +## Contents -For more feature details, please see the [Gin website introduction](https://gin-gonic.com/docs/introduction/). +- [Installation](#installation) +- [Prerequisite](#prerequisite) +- [Quick start](#quick-start) +- [Benchmarks](#benchmarks) +- [Gin v1.stable](#gin-v1-stable) +- [Build with jsoniter](#build-with-jsoniter) +- [API Examples](#api-examples) + - [Using GET,POST,PUT,PATCH,DELETE and OPTIONS](#using-get-post-put-patch-delete-and-options) + - [Parameters in path](#parameters-in-path) + - [Querystring parameters](#querystring-parameters) + - [Multipart/Urlencoded Form](#multiparturlencoded-form) + - [Another example: query + post form](#another-example-query--post-form) + - [Map as querystring or postform parameters](#map-as-querystring-or-postform-parameters) + - [Upload files](#upload-files) + - [Grouping routes](#grouping-routes) + - [Blank Gin without middleware by default](#blank-gin-without-middleware-by-default) + - [Using middleware](#using-middleware) + - [How to write log file](#how-to-write-log-file) + - [Custom Log Format](#custom-log-format) + - [Model binding and validation](#model-binding-and-validation) + - [Custom Validators](#custom-validators) + - [Only Bind Query String](#only-bind-query-string) + - [Bind Query String or Post Data](#bind-query-string-or-post-data) + - [Bind Uri](#bind-uri) + - [Bind HTML checkboxes](#bind-html-checkboxes) + - [Multipart/Urlencoded binding](#multiparturlencoded-binding) + - [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering) + - [JSONP rendering](#jsonp) + - [Serving static files](#serving-static-files) + - [Serving data from reader](#serving-data-from-reader) + - [HTML rendering](#html-rendering) + - [Multitemplate](#multitemplate) + - [Redirects](#redirects) + - [Custom Middleware](#custom-middleware) + - [Using BasicAuth() middleware](#using-basicauth-middleware) + - [Goroutines inside a middleware](#goroutines-inside-a-middleware) + - [Custom HTTP configuration](#custom-http-configuration) + - [Support Let's Encrypt](#support-lets-encrypt) + - [Run multiple service using Gin](#run-multiple-service-using-gin) + - [Graceful restart or stop](#graceful-restart-or-stop) + - [Build a single binary with templates](#build-a-single-binary-with-templates) + - [Bind form-data request with custom struct](#bind-form-data-request-with-custom-struct) + - [Try to bind body into different structs](#try-to-bind-body-into-different-structs) + - [http2 server push](#http2-server-push) + - [Define format for the log of routes](#define-format-for-the-log-of-routes) + - [Set and get a cookie](#set-and-get-a-cookie) +- [Testing](#testing) +- [Users](#users) -## Getting started +## Installation -### Getting Gin +To install Gin package, you need to install Go and set your Go workspace first. -The first need [Go](https://golang.org/) installed (**version 1.6+ is required**), then you can use the below Go command to install Gin. +1. Download and install it: ```sh $ go get -u github.com/gin-gonic/gin ``` -For more installation guides such as vendor tool, please check out [Gin quickstart](https://gin-gonic.com/docs/quickstart/). +2. Import it in your code: -### Running Gin +```go +import "github.com/gin-gonic/gin" +``` -First you need to import Gin package for using Gin, one simplest example likes the follow `example.go`: +3. (Optional) Import `net/http`. This is required for example if using constants such as `http.StatusOK`. + +```go +import "net/http" +``` + +### Use a vendor tool like [Govendor](https://github.com/kardianos/govendor) + +1. `go get` govendor + +```sh +$ go get github.com/kardianos/govendor +``` +2. Create your project folder and `cd` inside + +```sh +$ mkdir -p $GOPATH/src/github.com/myusername/project && cd "$_" +``` + +3. Vendor init your project and add gin + +```sh +$ govendor init +$ govendor fetch github.com/gin-gonic/gin@v1.3 +``` + +4. Copy a starting template inside your project + +```sh +$ curl https://raw.githubusercontent.com/gin-gonic/examples/master/basic/main.go > main.go +``` + +5. Run your project + +```sh +$ go run main.go +``` + +## Prerequisite + +Now Gin requires Go 1.6 or later and Go 1.7 will be required soon. + +## Quick start + +```sh +# assume the following codes in example.go file +$ cat example.go +``` ```go package main @@ -59,8 +146,6 @@ func main() { } ``` -And use the Go command to run the demo: - ``` # run example.go and visit 0.0.0.0:8080/ping on browser $ go run example.go @@ -68,7 +153,9 @@ $ go run example.go ## Benchmarks -Please see all benchmarks details from [Gin website](https://gin-gonic.com/docs/benchmarks/). +Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httprouter) + +[See all benchmarks](/BENCHMARKS.md) Benchmark name | (1) | (2) | (3) | (4) --------------------------------------------|-----------:|------------:|-----------:|---------: @@ -105,32 +192,1879 @@ BenchmarkVulcan_GithubAll | 5000 | 394253 | 19894 - (3): Heap Memory (B/op), lower is better - (4): Average Allocations per Repetition (allocs/op), lower is better -## Middlewares +## Gin v1. stable -You can find many useful Gin middlewares at [gin-contrib](https://github.com/gin-contrib). +- [x] Zero allocation router. +- [x] Still the fastest http router and framework. From routing to writing. +- [x] Complete suite of unit tests +- [x] Battle tested +- [x] API frozen, new releases will not break your code. -## Documentation +## Build with [jsoniter](https://github.com/json-iterator/go) -See [API documentation and descriptions](https://godoc.org/github.com/gin-gonic/gin) for package. +Gin uses `encoding/json` as default json package but you can change to [jsoniter](https://github.com/json-iterator/go) by build from other tags. -All documentation is available on the Gin website. +```sh +$ go build -tags=jsoniter . +``` -- [English](https://gin-gonic.com/docs/) -- [简体中文](https://gin-gonic.com/zh-cn/docs/) -- [繁體中文](https://gin-gonic.com/zh-tw/docs/) -- [日本語](https://gin-gonic.com/ja/docs/) +## API Examples -## Examples +You can find a number of ready-to-run examples at [Gin examples repository](https://github.com/gin-gonic/examples). -A number of ready-to-run examples demonstrating various use cases of Gin on the [Gin examples](https://github.com/gin-gonic/examples) repository. +### Using GET, POST, PUT, PATCH, DELETE and OPTIONS + +```go +func main() { + // Creates a gin router with default middleware: + // logger and recovery (crash-free) middleware + router := gin.Default() + + router.GET("/someGet", getting) + router.POST("/somePost", posting) + router.PUT("/somePut", putting) + router.DELETE("/someDelete", deleting) + router.PATCH("/somePatch", patching) + router.HEAD("/someHead", head) + router.OPTIONS("/someOptions", options) + + // By default it serves on :8080 unless a + // PORT environment variable was defined. + router.Run() + // router.Run(":3000") for a hard coded port +} +``` + +### Parameters in path + +```go +func main() { + router := gin.Default() + + // This handler will match /user/john but will not match /user/ or /user + router.GET("/user/:name", func(c *gin.Context) { + name := c.Param("name") + c.String(http.StatusOK, "Hello %s", name) + }) + + // However, this one will match /user/john/ and also /user/john/send + // If no other routers match /user/john, it will redirect to /user/john/ + router.GET("/user/:name/*action", func(c *gin.Context) { + name := c.Param("name") + action := c.Param("action") + message := name + " is " + action + c.String(http.StatusOK, message) + }) + + router.Run(":8080") +} +``` + +### Querystring parameters + +```go +func main() { + router := gin.Default() + + // Query string parameters are parsed using the existing underlying request object. + // The request responds to a url matching: /welcome?firstname=Jane&lastname=Doe + router.GET("/welcome", func(c *gin.Context) { + firstname := c.DefaultQuery("firstname", "Guest") + lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname") + + c.String(http.StatusOK, "Hello %s %s", firstname, lastname) + }) + router.Run(":8080") +} +``` + +### Multipart/Urlencoded Form + +```go +func main() { + router := gin.Default() + + router.POST("/form_post", func(c *gin.Context) { + message := c.PostForm("message") + nick := c.DefaultPostForm("nick", "anonymous") + + c.JSON(200, gin.H{ + "status": "posted", + "message": message, + "nick": nick, + }) + }) + router.Run(":8080") +} +``` + +### Another example: query + post form + +``` +POST /post?id=1234&page=1 HTTP/1.1 +Content-Type: application/x-www-form-urlencoded + +name=manu&message=this_is_great +``` + +```go +func main() { + router := gin.Default() + + router.POST("/post", func(c *gin.Context) { + + id := c.Query("id") + page := c.DefaultQuery("page", "0") + name := c.PostForm("name") + message := c.PostForm("message") + + fmt.Printf("id: %s; page: %s; name: %s; message: %s", id, page, name, message) + }) + router.Run(":8080") +} +``` + +``` +id: 1234; page: 1; name: manu; message: this_is_great +``` + +### Map as querystring or postform parameters + +``` +POST /post?ids[a]=1234&ids[b]=hello HTTP/1.1 +Content-Type: application/x-www-form-urlencoded + +names[first]=thinkerou&names[second]=tianou +``` + +```go +func main() { + router := gin.Default() + + router.POST("/post", func(c *gin.Context) { + + ids := c.QueryMap("ids") + names := c.PostFormMap("names") + + fmt.Printf("ids: %v; names: %v", ids, names) + }) + router.Run(":8080") +} +``` + +``` +ids: map[b:hello a:1234], names: map[second:tianou first:thinkerou] +``` + +### Upload files + +#### Single file + +References issue [#774](https://github.com/gin-gonic/gin/issues/774) and detail [example code](https://github.com/gin-gonic/examples/tree/master/upload-file/single). + +`file.Filename` **SHOULD NOT** be trusted. See [`Content-Disposition` on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#Directives) and [#1693](https://github.com/gin-gonic/gin/issues/1693) + +> The filename is always optional and must not be used blindly by the application: path information should be stripped, and conversion to the server file system rules should be done. + +```go +func main() { + router := gin.Default() + // Set a lower memory limit for multipart forms (default is 32 MiB) + // router.MaxMultipartMemory = 8 << 20 // 8 MiB + router.POST("/upload", func(c *gin.Context) { + // single file + file, _ := c.FormFile("file") + log.Println(file.Filename) + + // Upload the file to specific dst. + // c.SaveUploadedFile(file, dst) + + c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename)) + }) + router.Run(":8080") +} +``` + +How to `curl`: + +```bash +curl -X POST http://localhost:8080/upload \ + -F "file=@/Users/appleboy/test.zip" \ + -H "Content-Type: multipart/form-data" +``` + +#### Multiple files + +See the detail [example code](https://github.com/gin-gonic/examples/tree/master/upload-file/multiple). + +```go +func main() { + router := gin.Default() + // Set a lower memory limit for multipart forms (default is 32 MiB) + // router.MaxMultipartMemory = 8 << 20 // 8 MiB + router.POST("/upload", func(c *gin.Context) { + // Multipart form + form, _ := c.MultipartForm() + files := form.File["upload[]"] + + for _, file := range files { + log.Println(file.Filename) + + // Upload the file to specific dst. + // c.SaveUploadedFile(file, dst) + } + c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files))) + }) + router.Run(":8080") +} +``` + +How to `curl`: + +```bash +curl -X POST http://localhost:8080/upload \ + -F "upload[]=@/Users/appleboy/test1.zip" \ + -F "upload[]=@/Users/appleboy/test2.zip" \ + -H "Content-Type: multipart/form-data" +``` + +### Grouping routes + +```go +func main() { + router := gin.Default() + + // Simple group: v1 + v1 := router.Group("/v1") + { + v1.POST("/login", loginEndpoint) + v1.POST("/submit", submitEndpoint) + v1.POST("/read", readEndpoint) + } + + // Simple group: v2 + v2 := router.Group("/v2") + { + v2.POST("/login", loginEndpoint) + v2.POST("/submit", submitEndpoint) + v2.POST("/read", readEndpoint) + } + + router.Run(":8080") +} +``` + +### Blank Gin without middleware by default + +Use + +```go +r := gin.New() +``` + +instead of + +```go +// Default With the Logger and Recovery middleware already attached +r := gin.Default() +``` + + +### Using middleware +```go +func main() { + // Creates a router without any middleware by default + r := gin.New() + + // Global middleware + // Logger middleware will write the logs to gin.DefaultWriter even if you set with GIN_MODE=release. + // By default gin.DefaultWriter = os.Stdout + r.Use(gin.Logger()) + + // Recovery middleware recovers from any panics and writes a 500 if there was one. + r.Use(gin.Recovery()) + + // Per route middleware, you can add as many as you desire. + r.GET("/benchmark", MyBenchLogger(), benchEndpoint) + + // Authorization group + // authorized := r.Group("/", AuthRequired()) + // exactly the same as: + authorized := r.Group("/") + // per group middleware! in this case we use the custom created + // AuthRequired() middleware just in the "authorized" group. + authorized.Use(AuthRequired()) + { + authorized.POST("/login", loginEndpoint) + authorized.POST("/submit", submitEndpoint) + authorized.POST("/read", readEndpoint) + + // nested group + testing := authorized.Group("testing") + testing.GET("/analytics", analyticsEndpoint) + } + + // Listen and serve on 0.0.0.0:8080 + r.Run(":8080") +} +``` + +### How to write log file +```go +func main() { + // Disable Console Color, you don't need console color when writing the logs to file. + gin.DisableConsoleColor() + + // Logging to a file. + f, _ := os.Create("gin.log") + gin.DefaultWriter = io.MultiWriter(f) + + // Use the following code if you need to write the logs to file and console at the same time. + // gin.DefaultWriter = io.MultiWriter(f, os.Stdout) + + router := gin.Default() + router.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + +    router.Run(":8080") +} +``` + +### Custom Log Format +```go +func main() { + router := gin.New() + + // LoggerWithFormatter middleware will write the logs to gin.DefaultWriter + // By default gin.DefaultWriter = os.Stdout + router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { + + // your custom format + return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n", + param.ClientIP, + param.TimeStamp.Format(time.RFC1123), + param.Method, + param.Path, + param.Request.Proto, + param.StatusCode, + param.Latency, + param.Request.UserAgent(), + param.ErrorMessage, + ) + })) + router.Use(gin.Recovery()) + + router.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + + router.Run(":8080") +} +``` + +**Sample Output** +``` +::1 - [Fri, 07 Dec 2018 17:04:38 JST] "GET /ping HTTP/1.1 200 122.767µs "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36" " +``` + +### Controlling Log output coloring + +By default, logs output on console should be colorized depending on the detected TTY. + +Never colorize logs: + +```go +func main() { + // Disable log's color + gin.DisableConsoleColor() + + // Creates a gin router with default middleware: + // logger and recovery (crash-free) middleware + router := gin.Default() + + router.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + + router.Run(":8080") +} +``` + +Always colorize logs: + +```go +func main() { + // Force log's color + gin.ForceConsoleColor() + + // Creates a gin router with default middleware: + // logger and recovery (crash-free) middleware + router := gin.Default() + + router.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + + router.Run(":8080") +} +``` + +### Model binding and validation + +To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML and standard form values (foo=bar&boo=baz). + +Gin uses [**go-playground/validator.v8**](https://github.com/go-playground/validator) for validation. Check the full docs on tags usage [here](http://godoc.org/gopkg.in/go-playground/validator.v8#hdr-Baked_In_Validators_and_Tags). + +Note that you need to set the corresponding binding tag on all fields you want to bind. For example, when binding from JSON, set `json:"fieldname"`. + +Also, Gin provides two sets of methods for binding: +- **Type** - Must bind + - **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`, `BindYAML` + - **Behavior** - These methods use `MustBindWith` under the hood. If there is a binding error, the request is aborted with `c.AbortWithError(400, err).SetType(ErrorTypeBind)`. This sets the response status code to 400 and the `Content-Type` header is set to `text/plain; charset=utf-8`. Note that if you try to set the response code after this, it will result in a warning `[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422`. If you wish to have greater control over the behavior, consider using the `ShouldBind` equivalent method. +- **Type** - Should bind + - **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindXML`, `ShouldBindQuery`, `ShouldBindYAML` + - **Behavior** - These methods use `ShouldBindWith` under the hood. If there is a binding error, the error is returned and it is the developer's responsibility to handle the request and error appropriately. + +When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use `MustBindWith` or `ShouldBindWith`. + +You can also specify that specific fields are required. If a field is decorated with `binding:"required"` and has a empty value when binding, an error will be returned. + +```go +// Binding from JSON +type Login struct { + User string `form:"user" json:"user" xml:"user" binding:"required"` + Password string `form:"password" json:"password" xml:"password" binding:"required"` +} + +func main() { + router := gin.Default() + + // Example for binding JSON ({"user": "manu", "password": "123"}) + router.POST("/loginJSON", func(c *gin.Context) { + var json Login + if err := c.ShouldBindJSON(&json); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if json.User != "manu" || json.Password != "123" { + c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) + return + } + + c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) + }) + + // Example for binding XML ( + // + // + // user + // 123 + // ) + router.POST("/loginXML", func(c *gin.Context) { + var xml Login + if err := c.ShouldBindXML(&xml); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if xml.User != "manu" || xml.Password != "123" { + c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) + return + } + + c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) + }) + + // Example for binding a HTML form (user=manu&password=123) + router.POST("/loginForm", func(c *gin.Context) { + var form Login + // This will infer what binder to use depending on the content-type header. + if err := c.ShouldBind(&form); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if form.User != "manu" || form.Password != "123" { + c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) + return + } + + c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) + }) + + // Listen and serve on 0.0.0.0:8080 + router.Run(":8080") +} +``` + +**Sample request** +```shell +$ curl -v -X POST \ + http://localhost:8080/loginJSON \ + -H 'content-type: application/json' \ + -d '{ "user": "manu" }' +> POST /loginJSON HTTP/1.1 +> Host: localhost:8080 +> User-Agent: curl/7.51.0 +> Accept: */* +> content-type: application/json +> Content-Length: 18 +> +* upload completely sent off: 18 out of 18 bytes +< HTTP/1.1 400 Bad Request +< Content-Type: application/json; charset=utf-8 +< Date: Fri, 04 Aug 2017 03:51:31 GMT +< Content-Length: 100 +< +{"error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'required' tag"} +``` + +**Skip validate** + +When running the above example using the above the `curl` command, it returns error. Because the example use `binding:"required"` for `Password`. If use `binding:"-"` for `Password`, then it will not return error when running the above example again. + +### Custom Validators + +It is also possible to register custom validators. See the [example code](https://github.com/gin-gonic/examples/tree/master/custom-validation/server.go). + +```go +package main + +import ( + "net/http" + "reflect" + "time" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "gopkg.in/go-playground/validator.v8" +) + +// Booking contains binded and validated data. +type Booking struct { + CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"` + CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"` +} + +func bookableDate( + v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value, + field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string, +) bool { + if date, ok := field.Interface().(time.Time); ok { + today := time.Now() + if today.Year() > date.Year() || today.YearDay() > date.YearDay() { + return false + } + } + return true +} + +func main() { + route := gin.Default() + + if v, ok := binding.Validator.Engine().(*validator.Validate); ok { + v.RegisterValidation("bookabledate", bookableDate) + } + + route.GET("/bookable", getBookable) + route.Run(":8085") +} + +func getBookable(c *gin.Context) { + var b Booking + if err := c.ShouldBindWith(&b, binding.Query); err == nil { + c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"}) + } else { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } +} +``` + +```console +$ curl "localhost:8085/bookable?check_in=2018-04-16&check_out=2018-04-17" +{"message":"Booking dates are valid!"} + +$ curl "localhost:8085/bookable?check_in=2018-03-08&check_out=2018-03-09" +{"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag"} +``` + +[Struct level validations](https://github.com/go-playground/validator/releases/tag/v8.7) can also be registered this way. +See the [struct-lvl-validation example](https://github.com/gin-gonic/examples/tree/master/struct-lvl-validations) to learn more. + +### Only Bind Query String + +`ShouldBindQuery` function only binds the query params and not the post data. See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-315953017). + +```go +package main + +import ( + "log" + + "github.com/gin-gonic/gin" +) + +type Person struct { + Name string `form:"name"` + Address string `form:"address"` +} + +func main() { + route := gin.Default() + route.Any("/testing", startPage) + route.Run(":8085") +} + +func startPage(c *gin.Context) { + var person Person + if c.ShouldBindQuery(&person) == nil { + log.Println("====== Only Bind By Query String ======") + log.Println(person.Name) + log.Println(person.Address) + } + c.String(200, "Success") +} + +``` + +### Bind Query String or Post Data + +See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-264681292). + +```go +package main + +import ( + "log" + "time" + + "github.com/gin-gonic/gin" +) + +type Person struct { + Name string `form:"name"` + Address string `form:"address"` + Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"` +} + +func main() { + route := gin.Default() + route.GET("/testing", startPage) + route.Run(":8085") +} + +func startPage(c *gin.Context) { + var person Person + // If `GET`, only `Form` binding engine (`query`) used. + // If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`). + // See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48 + if c.ShouldBind(&person) == nil { + log.Println(person.Name) + log.Println(person.Address) + log.Println(person.Birthday) + } + + c.String(200, "Success") +} +``` + +Test it with: +```sh +$ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15" +``` + +### Bind Uri + +See the [detail information](https://github.com/gin-gonic/gin/issues/846). + +```go +package main + +import "github.com/gin-gonic/gin" + +type Person struct { + ID string `uri:"id" binding:"required,uuid"` + Name string `uri:"name" binding:"required"` +} + +func main() { + route := gin.Default() + route.GET("/:name/:id", func(c *gin.Context) { + var person Person + if err := c.ShouldBindUri(&person); err != nil { + c.JSON(400, gin.H{"msg": err}) + return + } + c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID}) + }) + route.Run(":8088") +} +``` + +Test it with: +```sh +$ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3 +$ curl -v localhost:8088/thinkerou/not-uuid +``` + +### Bind HTML checkboxes + +See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092) + +main.go + +```go +... + +type myForm struct { + Colors []string `form:"colors[]"` +} + +... + +func formHandler(c *gin.Context) { + var fakeForm myForm + c.ShouldBind(&fakeForm) + c.JSON(200, gin.H{"color": fakeForm.Colors}) +} + +... + +``` + +form.html + +```html +
+

Check some colors

+ + + + + + + +
+``` + +result: + +``` +{"color":["red","green","blue"]} +``` + +### Multipart/Urlencoded binding + +```go +package main + +import ( + "github.com/gin-gonic/gin" +) + +type LoginForm struct { + User string `form:"user" binding:"required"` + Password string `form:"password" binding:"required"` +} + +func main() { + router := gin.Default() + router.POST("/login", func(c *gin.Context) { + // you can bind multipart form with explicit binding declaration: + // c.ShouldBindWith(&form, binding.Form) + // or you can simply use autobinding with ShouldBind method: + var form LoginForm + // in this case proper binding will be automatically selected + if c.ShouldBind(&form) == nil { + if form.User == "user" && form.Password == "password" { + c.JSON(200, gin.H{"status": "you are logged in"}) + } else { + c.JSON(401, gin.H{"status": "unauthorized"}) + } + } + }) + router.Run(":8080") +} +``` + +Test it with: +```sh +$ curl -v --form user=user --form password=password http://localhost:8080/login +``` + +### XML, JSON, YAML and ProtoBuf rendering + +```go +func main() { + r := gin.Default() + + // gin.H is a shortcut for map[string]interface{} + r.GET("/someJSON", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK}) + }) + + r.GET("/moreJSON", func(c *gin.Context) { + // You also can use a struct + var msg struct { + Name string `json:"user"` + Message string + Number int + } + msg.Name = "Lena" + msg.Message = "hey" + msg.Number = 123 + // Note that msg.Name becomes "user" in the JSON + // Will output : {"user": "Lena", "Message": "hey", "Number": 123} + c.JSON(http.StatusOK, msg) + }) + + r.GET("/someXML", func(c *gin.Context) { + c.XML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK}) + }) + + r.GET("/someYAML", func(c *gin.Context) { + c.YAML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK}) + }) + + r.GET("/someProtoBuf", func(c *gin.Context) { + reps := []int64{int64(1), int64(2)} + label := "test" + // The specific definition of protobuf is written in the testdata/protoexample file. + data := &protoexample.Test{ + Label: &label, + Reps: reps, + } + // Note that data becomes binary data in the response + // Will output protoexample.Test protobuf serialized data + c.ProtoBuf(http.StatusOK, data) + }) + + // Listen and serve on 0.0.0.0:8080 + r.Run(":8080") +} +``` + +#### SecureJSON + +Using SecureJSON to prevent json hijacking. Default prepends `"while(1),"` to response body if the given struct is array values. + +```go +func main() { + r := gin.Default() + + // You can also use your own secure json prefix + // r.SecureJsonPrefix(")]}',\n") + + r.GET("/someJSON", func(c *gin.Context) { + names := []string{"lena", "austin", "foo"} + + // Will output : while(1);["lena","austin","foo"] + c.SecureJSON(http.StatusOK, names) + }) + + // Listen and serve on 0.0.0.0:8080 + r.Run(":8080") +} +``` +#### JSONP + +Using JSONP to request data from a server in a different domain. Add callback to response body if the query parameter callback exists. + +```go +func main() { + r := gin.Default() + + r.GET("/JSONP?callback=x", func(c *gin.Context) { + data := map[string]interface{}{ + "foo": "bar", + } + + //callback is x + // Will output : x({\"foo\":\"bar\"}) + c.JSONP(http.StatusOK, data) + }) + + // Listen and serve on 0.0.0.0:8080 + r.Run(":8080") +} +``` + +#### AsciiJSON + +Using AsciiJSON to Generates ASCII-only JSON with escaped non-ASCII chracters. + +```go +func main() { + r := gin.Default() + + r.GET("/someJSON", func(c *gin.Context) { + data := map[string]interface{}{ + "lang": "GO语言", + "tag": "
", + } + + // will output : {"lang":"GO\u8bed\u8a00","tag":"\u003cbr\u003e"} + c.AsciiJSON(http.StatusOK, data) + }) + + // Listen and serve on 0.0.0.0:8080 + r.Run(":8080") +} +``` + +#### PureJSON + +Normally, JSON replaces special HTML characters with their unicode entities, e.g. `<` becomes `\u003c`. If you want to encode such characters literally, you can use PureJSON instead. +This feature is unavailable in Go 1.6 and lower. + +```go +func main() { + r := gin.Default() + + // Serves unicode entities + r.GET("/json", func(c *gin.Context) { + c.JSON(200, gin.H{ + "html": "Hello, world!", + }) + }) + + // Serves literal characters + r.GET("/purejson", func(c *gin.Context) { + c.PureJSON(200, gin.H{ + "html": "Hello, world!", + }) + }) + + // listen and serve on 0.0.0.0:8080 + r.Run(":8080") +} +``` + +### Serving static files + +```go +func main() { + router := gin.Default() + router.Static("/assets", "./assets") + router.StaticFS("/more_static", http.Dir("my_file_system")) + router.StaticFile("/favicon.ico", "./resources/favicon.ico") + + // Listen and serve on 0.0.0.0:8080 + router.Run(":8080") +} +``` + +### Serving data from reader + +```go +func main() { + router := gin.Default() + router.GET("/someDataFromReader", func(c *gin.Context) { + response, err := http.Get("https://raw.githubusercontent.com/gin-gonic/logo/master/color.png") + if err != nil || response.StatusCode != http.StatusOK { + c.Status(http.StatusServiceUnavailable) + return + } + + reader := response.Body + contentLength := response.ContentLength + contentType := response.Header.Get("Content-Type") + + extraHeaders := map[string]string{ + "Content-Disposition": `attachment; filename="gopher.png"`, + } + + c.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders) + }) + router.Run(":8080") +} +``` + +### HTML rendering + +Using LoadHTMLGlob() or LoadHTMLFiles() + +```go +func main() { + router := gin.Default() + router.LoadHTMLGlob("templates/*") + //router.LoadHTMLFiles("templates/template1.html", "templates/template2.html") + router.GET("/index", func(c *gin.Context) { + c.HTML(http.StatusOK, "index.tmpl", gin.H{ + "title": "Main website", + }) + }) + router.Run(":8080") +} +``` + +templates/index.tmpl + +```html + +

+ {{ .title }} +

+ +``` + +Using templates with same name in different directories + +```go +func main() { + router := gin.Default() + router.LoadHTMLGlob("templates/**/*") + router.GET("/posts/index", func(c *gin.Context) { + c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{ + "title": "Posts", + }) + }) + router.GET("/users/index", func(c *gin.Context) { + c.HTML(http.StatusOK, "users/index.tmpl", gin.H{ + "title": "Users", + }) + }) + router.Run(":8080") +} +``` + +templates/posts/index.tmpl + +```html +{{ define "posts/index.tmpl" }} +

+ {{ .title }} +

+

Using posts/index.tmpl

+ +{{ end }} +``` + +templates/users/index.tmpl + +```html +{{ define "users/index.tmpl" }} +

+ {{ .title }} +

+

Using users/index.tmpl

+ +{{ end }} +``` + +#### Custom Template renderer + +You can also use your own html template render + +```go +import "html/template" + +func main() { + router := gin.Default() + html := template.Must(template.ParseFiles("file1", "file2")) + router.SetHTMLTemplate(html) + router.Run(":8080") +} +``` + +#### Custom Delimiters + +You may use custom delims + +```go + r := gin.Default() + r.Delims("{[{", "}]}") + r.LoadHTMLGlob("/path/to/templates") +``` + +#### Custom Template Funcs + +See the detail [example code](https://github.com/gin-gonic/examples/tree/master/template). + +main.go + +```go +import ( + "fmt" + "html/template" + "net/http" + "time" + + "github.com/gin-gonic/gin" +) + +func formatAsDate(t time.Time) string { + year, month, day := t.Date() + return fmt.Sprintf("%d%02d/%02d", year, month, day) +} + +func main() { + router := gin.Default() + router.Delims("{[{", "}]}") + router.SetFuncMap(template.FuncMap{ + "formatAsDate": formatAsDate, + }) + router.LoadHTMLFiles("./testdata/template/raw.tmpl") + + router.GET("/raw", func(c *gin.Context) { + c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{ + "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), + }) + }) + + router.Run(":8080") +} + +``` + +raw.tmpl + +```html +Date: {[{.now | formatAsDate}]} +``` + +Result: +``` +Date: 2017/07/01 +``` + +### Multitemplate + +Gin allow by default use only one html.Template. Check [a multitemplate render](https://github.com/gin-contrib/multitemplate) for using features like go 1.6 `block template`. + +### Redirects + +Issuing a HTTP redirect is easy. Both internal and external locations are supported. + +```go +r.GET("/test", func(c *gin.Context) { + c.Redirect(http.StatusMovedPermanently, "http://www.google.com/") +}) +``` + + +Issuing a Router redirect, use `HandleContext` like below. + +``` go +r.GET("/test", func(c *gin.Context) { + c.Request.URL.Path = "/test2" + r.HandleContext(c) +}) +r.GET("/test2", func(c *gin.Context) { + c.JSON(200, gin.H{"hello": "world"}) +}) +``` + + +### Custom Middleware + +```go +func Logger() gin.HandlerFunc { + return func(c *gin.Context) { + t := time.Now() + + // Set example variable + c.Set("example", "12345") + + // before request + + c.Next() + + // after request + latency := time.Since(t) + log.Print(latency) + + // access the status we are sending + status := c.Writer.Status() + log.Println(status) + } +} + +func main() { + r := gin.New() + r.Use(Logger()) + + r.GET("/test", func(c *gin.Context) { + example := c.MustGet("example").(string) + + // it would print: "12345" + log.Println(example) + }) + + // Listen and serve on 0.0.0.0:8080 + r.Run(":8080") +} +``` + +### Using BasicAuth() middleware + +```go +// simulate some private data +var secrets = gin.H{ + "foo": gin.H{"email": "foo@bar.com", "phone": "123433"}, + "austin": gin.H{"email": "austin@example.com", "phone": "666"}, + "lena": gin.H{"email": "lena@guapa.com", "phone": "523443"}, +} + +func main() { + r := gin.Default() + + // Group using gin.BasicAuth() middleware + // gin.Accounts is a shortcut for map[string]string + authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{ + "foo": "bar", + "austin": "1234", + "lena": "hello2", + "manu": "4321", + })) + + // /admin/secrets endpoint + // hit "localhost:8080/admin/secrets + authorized.GET("/secrets", func(c *gin.Context) { + // get user, it was set by the BasicAuth middleware + user := c.MustGet(gin.AuthUserKey).(string) + if secret, ok := secrets[user]; ok { + c.JSON(http.StatusOK, gin.H{"user": user, "secret": secret}) + } else { + c.JSON(http.StatusOK, gin.H{"user": user, "secret": "NO SECRET :("}) + } + }) + + // Listen and serve on 0.0.0.0:8080 + r.Run(":8080") +} +``` + +### Goroutines inside a middleware + +When starting new Goroutines inside a middleware or handler, you **SHOULD NOT** use the original context inside it, you have to use a read-only copy. + +```go +func main() { + r := gin.Default() + + r.GET("/long_async", func(c *gin.Context) { + // create copy to be used inside the goroutine + cCp := c.Copy() + go func() { + // simulate a long task with time.Sleep(). 5 seconds + time.Sleep(5 * time.Second) + + // note that you are using the copied context "cCp", IMPORTANT + log.Println("Done! in path " + cCp.Request.URL.Path) + }() + }) + + r.GET("/long_sync", func(c *gin.Context) { + // simulate a long task with time.Sleep(). 5 seconds + time.Sleep(5 * time.Second) + + // since we are NOT using a goroutine, we do not have to copy the context + log.Println("Done! in path " + c.Request.URL.Path) + }) + + // Listen and serve on 0.0.0.0:8080 + r.Run(":8080") +} +``` + +### Custom HTTP configuration + +Use `http.ListenAndServe()` directly, like this: + +```go +func main() { + router := gin.Default() + http.ListenAndServe(":8080", router) +} +``` +or + +```go +func main() { + router := gin.Default() + + s := &http.Server{ + Addr: ":8080", + Handler: router, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxHeaderBytes: 1 << 20, + } + s.ListenAndServe() +} +``` + +### Support Let's Encrypt + +example for 1-line LetsEncrypt HTTPS servers. + +```go +package main + +import ( + "log" + + "github.com/gin-gonic/autotls" + "github.com/gin-gonic/gin" +) + +func main() { + r := gin.Default() + + // Ping handler + r.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + + log.Fatal(autotls.Run(r, "example1.com", "example2.com")) +} +``` + +example for custom autocert manager. + +```go +package main + +import ( + "log" + + "github.com/gin-gonic/autotls" + "github.com/gin-gonic/gin" + "golang.org/x/crypto/acme/autocert" +) + +func main() { + r := gin.Default() + + // Ping handler + r.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + + m := autocert.Manager{ + Prompt: autocert.AcceptTOS, + HostPolicy: autocert.HostWhitelist("example1.com", "example2.com"), + Cache: autocert.DirCache("/var/www/.cache"), + } + + log.Fatal(autotls.RunWithManager(r, &m)) +} +``` + +### Run multiple service using Gin + +See the [question](https://github.com/gin-gonic/gin/issues/346) and try the following example: + +```go +package main + +import ( + "log" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "golang.org/x/sync/errgroup" +) + +var ( + g errgroup.Group +) + +func router01() http.Handler { + e := gin.New() + e.Use(gin.Recovery()) + e.GET("/", func(c *gin.Context) { + c.JSON( + http.StatusOK, + gin.H{ + "code": http.StatusOK, + "error": "Welcome server 01", + }, + ) + }) + + return e +} + +func router02() http.Handler { + e := gin.New() + e.Use(gin.Recovery()) + e.GET("/", func(c *gin.Context) { + c.JSON( + http.StatusOK, + gin.H{ + "code": http.StatusOK, + "error": "Welcome server 02", + }, + ) + }) + + return e +} + +func main() { + server01 := &http.Server{ + Addr: ":8080", + Handler: router01(), + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + } + + server02 := &http.Server{ + Addr: ":8081", + Handler: router02(), + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + } + + g.Go(func() error { + return server01.ListenAndServe() + }) + + g.Go(func() error { + return server02.ListenAndServe() + }) + + if err := g.Wait(); err != nil { + log.Fatal(err) + } +} +``` + +### Graceful restart or stop + +Do you want to graceful restart or stop your web server? +There are some ways this can be done. + +We can use [fvbock/endless](https://github.com/fvbock/endless) to replace the default `ListenAndServe`. Refer issue [#296](https://github.com/gin-gonic/gin/issues/296) for more details. + +```go +router := gin.Default() +router.GET("/", handler) +// [...] +endless.ListenAndServe(":4242", router) +``` + +An alternative to endless: + +* [manners](https://github.com/braintree/manners): A polite Go HTTP server that shuts down gracefully. +* [graceful](https://github.com/tylerb/graceful): Graceful is a Go package enabling graceful shutdown of an http.Handler server. +* [grace](https://github.com/facebookgo/grace): Graceful restart & zero downtime deploy for Go servers. + +If you are using Go 1.8, you may not need to use this library! Consider using http.Server's built-in [Shutdown()](https://golang.org/pkg/net/http/#Server.Shutdown) method for graceful shutdowns. See the full [graceful-shutdown](https://github.com/gin-gonic/examples/tree/master/graceful-shutdown) example with gin. + +```go +// +build go1.8 + +package main + +import ( + "context" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/gin-gonic/gin" +) + +func main() { + router := gin.Default() + router.GET("/", func(c *gin.Context) { + time.Sleep(5 * time.Second) + c.String(http.StatusOK, "Welcome Gin Server") + }) + + srv := &http.Server{ + Addr: ":8080", + Handler: router, + } + + go func() { + // service connections + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("listen: %s\n", err) + } + }() + + // Wait for interrupt signal to gracefully shutdown the server with + // a timeout of 5 seconds. + quit := make(chan os.Signal) + // kill (no param) default send syscanll.SIGTERM + // kill -2 is syscall.SIGINT + // kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + log.Println("Shutdown Server ...") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + log.Fatal("Server Shutdown:", err) + } + // catching ctx.Done(). timeout of 5 seconds. + select { + case <-ctx.Done(): + log.Println("timeout of 5 seconds.") + } + log.Println("Server exiting") +} +``` + +### Build a single binary with templates + +You can build a server into a single binary containing templates by using [go-assets][]. + +[go-assets]: https://github.com/jessevdk/go-assets + +```go +func main() { + r := gin.New() + + t, err := loadTemplate() + if err != nil { + panic(err) + } + r.SetHTMLTemplate(t) + + r.GET("/", func(c *gin.Context) { + c.HTML(http.StatusOK, "/html/index.tmpl",nil) + }) + r.Run(":8080") +} + +// loadTemplate loads templates embedded by go-assets-builder +func loadTemplate() (*template.Template, error) { + t := template.New("") + for name, file := range Assets.Files { + if file.IsDir() || !strings.HasSuffix(name, ".tmpl") { + continue + } + h, err := ioutil.ReadAll(file) + if err != nil { + return nil, err + } + t, err = t.New(name).Parse(string(h)) + if err != nil { + return nil, err + } + } + return t, nil +} +``` + +See a complete example in the `https://github.com/gin-gonic/examples/tree/master/assets-in-binary` directory. + +### Bind form-data request with custom struct + +The follow example using custom struct: + +```go +type StructA struct { + FieldA string `form:"field_a"` +} + +type StructB struct { + NestedStruct StructA + FieldB string `form:"field_b"` +} + +type StructC struct { + NestedStructPointer *StructA + FieldC string `form:"field_c"` +} + +type StructD struct { + NestedAnonyStruct struct { + FieldX string `form:"field_x"` + } + FieldD string `form:"field_d"` +} + +func GetDataB(c *gin.Context) { + var b StructB + c.Bind(&b) + c.JSON(200, gin.H{ + "a": b.NestedStruct, + "b": b.FieldB, + }) +} + +func GetDataC(c *gin.Context) { + var b StructC + c.Bind(&b) + c.JSON(200, gin.H{ + "a": b.NestedStructPointer, + "c": b.FieldC, + }) +} + +func GetDataD(c *gin.Context) { + var b StructD + c.Bind(&b) + c.JSON(200, gin.H{ + "x": b.NestedAnonyStruct, + "d": b.FieldD, + }) +} + +func main() { + r := gin.Default() + r.GET("/getb", GetDataB) + r.GET("/getc", GetDataC) + r.GET("/getd", GetDataD) + + r.Run() +} +``` + +Using the command `curl` command result: + +``` +$ curl "http://localhost:8080/getb?field_a=hello&field_b=world" +{"a":{"FieldA":"hello"},"b":"world"} +$ curl "http://localhost:8080/getc?field_a=hello&field_c=world" +{"a":{"FieldA":"hello"},"c":"world"} +$ curl "http://localhost:8080/getd?field_x=hello&field_d=world" +{"d":"world","x":{"FieldX":"hello"}} +``` + +### Try to bind body into different structs + +The normal methods for binding request body consumes `c.Request.Body` and they +cannot be called multiple times. + +```go +type formA struct { + Foo string `json:"foo" xml:"foo" binding:"required"` +} + +type formB struct { + Bar string `json:"bar" xml:"bar" binding:"required"` +} + +func SomeHandler(c *gin.Context) { + objA := formA{} + objB := formB{} + // This c.ShouldBind consumes c.Request.Body and it cannot be reused. + if errA := c.ShouldBind(&objA); errA == nil { + c.String(http.StatusOK, `the body should be formA`) + // Always an error is occurred by this because c.Request.Body is EOF now. + } else if errB := c.ShouldBind(&objB); errB == nil { + c.String(http.StatusOK, `the body should be formB`) + } else { + ... + } +} +``` + +For this, you can use `c.ShouldBindBodyWith`. + +```go +func SomeHandler(c *gin.Context) { + objA := formA{} + objB := formB{} + // This reads c.Request.Body and stores the result into the context. + if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil { + c.String(http.StatusOK, `the body should be formA`) + // At this time, it reuses body stored in the context. + } else if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil { + c.String(http.StatusOK, `the body should be formB JSON`) + // And it can accepts other formats + } else if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil { + c.String(http.StatusOK, `the body should be formB XML`) + } else { + ... + } +} +``` + +* `c.ShouldBindBodyWith` stores body into the context before binding. This has +a slight impact to performance, so you should not use this method if you are +enough to call binding at once. +* This feature is only needed for some formats -- `JSON`, `XML`, `MsgPack`, +`ProtoBuf`. For other formats, `Query`, `Form`, `FormPost`, `FormMultipart`, +can be called by `c.ShouldBind()` multiple times without any damage to +performance (See [#1341](https://github.com/gin-gonic/gin/pull/1341)). + +### http2 server push + +http.Pusher is supported only **go1.8+**. See the [golang blog](https://blog.golang.org/h2push) for detail information. + +```go +package main + +import ( + "html/template" + "log" + + "github.com/gin-gonic/gin" +) + +var html = template.Must(template.New("https").Parse(` + + + Https Test + + + +

Welcome, Ginner!

+ + +`)) + +func main() { + r := gin.Default() + r.Static("/assets", "./assets") + r.SetHTMLTemplate(html) + + r.GET("/", func(c *gin.Context) { + if pusher := c.Writer.Pusher(); pusher != nil { + // use pusher.Push() to do server push + if err := pusher.Push("/assets/app.js", nil); err != nil { + log.Printf("Failed to push: %v", err) + } + } + c.HTML(200, "https", gin.H{ + "status": "success", + }) + }) + + // Listen and Server in https://127.0.0.1:8080 + r.RunTLS(":8080", "./testdata/server.pem", "./testdata/server.key") +} +``` + +### Define format for the log of routes + +The default log of routes is: +``` +[GIN-debug] POST /foo --> main.main.func1 (3 handlers) +[GIN-debug] GET /bar --> main.main.func2 (3 handlers) +[GIN-debug] GET /status --> main.main.func3 (3 handlers) +``` + +If you want to log this information in given format (e.g. JSON, key values or something else), then you can define this format with `gin.DebugPrintRouteFunc`. +In the example below, we log all routes with standard log package but you can use another log tools that suits of your needs. +```go +import ( + "log" + "net/http" + + "github.com/gin-gonic/gin" +) + +func main() { + r := gin.Default() + gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) { + log.Printf("endpoint %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers) + } + + r.POST("/foo", func(c *gin.Context) { + c.JSON(http.StatusOK, "foo") + }) + + r.GET("/bar", func(c *gin.Context) { + c.JSON(http.StatusOK, "bar") + }) + + r.GET("/status", func(c *gin.Context) { + c.JSON(http.StatusOK, "ok") + }) + + // Listen and Server in http://0.0.0.0:8080 + r.Run() +} +``` + +### Set and get a cookie + +```go +import ( + "fmt" + + "github.com/gin-gonic/gin" +) + +func main() { + + router := gin.Default() + + router.GET("/cookie", func(c *gin.Context) { + + cookie, err := c.Cookie("gin_cookie") + + if err != nil { + cookie = "NotSet" + c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true) + } + + fmt.Printf("Cookie value: %s \n", cookie) + }) + + router.Run() +} +``` + + +## Testing + +The `net/http/httptest` package is preferable way for HTTP testing. + +```go +package main + +func setupRouter() *gin.Engine { + r := gin.Default() + r.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + return r +} + +func main() { + r := setupRouter() + r.Run(":8080") +} +``` + +Test for code example above: + +```go +package main + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPingRoute(t *testing.T) { + router := setupRouter() + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/ping", nil) + router.ServeHTTP(w, req) + + assert.Equal(t, 200, w.Code) + assert.Equal(t, "pong", w.Body.String()) +} +``` ## Users -[Gin website](https://gin-gonic.com/docs/users/) lists some awesome projects made with Gin web framework. - -## Contributing - -Gin is the work of hundreds of contributors. We appreciate your help! - -Please see [CONTRIBUTING](CONTRIBUTING.md) for details on submitting patches and the contribution workflow. +Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework. +* [gorush](https://github.com/appleboy/gorush): A push notification server written in Go. +* [fnproject](https://github.com/fnproject/fn): The container native, cloud agnostic serverless platform. +* [photoprism](https://github.com/photoprism/photoprism): Personal photo management powered by Go and Google TensorFlow. +* [krakend](https://github.com/devopsfaith/krakend): Ultra performant API Gateway with middlewares. +* [picfit](https://github.com/thoas/picfit): An image resizing server written in Go. From f9de6049cbf0820198708091e2b8e01696ec1473 Mon Sep 17 00:00:00 2001 From: Abhishek Chanda Date: Thu, 18 Apr 2019 03:45:37 +0100 Subject: [PATCH 109/111] Remove contents of the Authorization header while dumping requests (#1836) This PR replaces the contents of that header with a *. This prevents credential leak in logs. --- recovery.go | 9 ++++++++- recovery_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/recovery.go b/recovery.go index 9e893e1b..bc946c03 100644 --- a/recovery.go +++ b/recovery.go @@ -53,11 +53,18 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc { if logger != nil { stack := stack(3) httpRequest, _ := httputil.DumpRequest(c.Request, false) + headers := strings.Split(string(httpRequest), "\r\n") + for idx, header := range headers { + current := strings.Split(header, ":") + if current[0] == "Authorization" { + headers[idx] = current[0] + ": *" + } + } if brokenPipe { logger.Printf("%s\n%s%s", err, string(httpRequest), reset) } else if IsDebugging() { logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", - timeFormat(time.Now()), string(httpRequest), err, stack, reset) + timeFormat(time.Now()), strings.Join(headers, "\r\n"), err, stack, reset) } else { logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s", timeFormat(time.Now()), err, stack, reset) diff --git a/recovery_test.go b/recovery_test.go index 0a6d6271..e1a0713f 100644 --- a/recovery_test.go +++ b/recovery_test.go @@ -8,6 +8,7 @@ package gin import ( "bytes" + "fmt" "net" "net/http" "os" @@ -18,6 +19,37 @@ import ( "github.com/stretchr/testify/assert" ) +func TestPanicClean(t *testing.T) { + buffer := new(bytes.Buffer) + router := New() + password := "my-super-secret-password" + router.Use(RecoveryWithWriter(buffer)) + router.GET("/recovery", func(c *Context) { + c.AbortWithStatus(http.StatusBadRequest) + panic("Oupps, Houston, we have a problem") + }) + // RUN + w := performRequest(router, "GET", "/recovery", + header{ + Key: "Host", + Value: "www.google.com", + }, + header{ + Key: "Authorization", + Value: fmt.Sprintf("Bearer %s", password), + }, + header{ + Key: "Content-Type", + Value: "application/json", + }, + ) + // TEST + assert.Equal(t, http.StatusBadRequest, w.Code) + + // Check the buffer does not have the secret key + assert.NotContains(t, buffer.String(), password) +} + // TestPanicInHandler assert that panic has been recovered. func TestPanicInHandler(t *testing.T) { buffer := new(bytes.Buffer) From 11407e73adb23e7ba4bf0fbdd02cc5336938a167 Mon Sep 17 00:00:00 2001 From: John Bampton Date: Tue, 23 Apr 2019 01:11:57 +1000 Subject: [PATCH 110/111] Fix spelling. (#1861) --- ginS/gins.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ginS/gins.go b/ginS/gins.go index 3ce4a6f6..3080fd34 100644 --- a/ginS/gins.go +++ b/ginS/gins.go @@ -118,7 +118,7 @@ func StaticFS(relativePath string, fs http.FileSystem) gin.IRoutes { return engine().StaticFS(relativePath, fs) } -// Use attachs a global middleware to the router. ie. the middlewares attached though Use() will be +// Use attaches a global middleware to the router. ie. the middlewares attached though Use() will be // included in the handlers chain for every single request. Even 404, 405, static files... // For example, this is the right place for a logger or error management middleware. func Use(middlewares ...gin.HandlerFunc) gin.IRoutes { @@ -153,7 +153,7 @@ func RunUnix(file string) (err error) { // RunFd attaches the router to a http.Server and starts listening and serving HTTP requests // through the specified file descriptor. -// Note: thie method will block the calling goroutine indefinitely unless on error happens. +// Note: the method will block the calling goroutine indefinitely unless on error happens. func RunFd(fd int) (err error) { return engine().RunFd(fd) } From 202f8fc58af47ab5c8e834662ee7fc46deacc37d Mon Sep 17 00:00:00 2001 From: DeathKing Date: Wed, 24 Apr 2019 20:21:41 +0800 Subject: [PATCH 111/111] Fix a typo syscanll.SIGTERM -> syscall.SIGTERM (#1868) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 804041f9..594c8bfa 100644 --- a/README.md +++ b/README.md @@ -1696,9 +1696,9 @@ func main() { // Wait for interrupt signal to gracefully shutdown the server with // a timeout of 5 seconds. quit := make(chan os.Signal) - // kill (no param) default send syscanll.SIGTERM + // kill (no param) default send syscall.SIGTERM // kill -2 is syscall.SIGINT - // kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it + // kill -9 is syscall.SIGKILL but can"t be catch, so don't need add it signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("Shutdown Server ...")