diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 8630bc35..e86bc98f 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,7 +1,7 @@
- With pull requests:
- Open your pull request against `master`
- Your pull request should have no more than two commits, if not you should squash them.
- - It should pass all tests in the available continuous integrations systems such as TravisCI.
+ - It should pass all tests in the available continuous integration systems such as TravisCI.
- You should add/modify tests to cover your proposed code changes.
- If your pull request contains a new feature, please document it on the README.
diff --git a/.travis.yml b/.travis.yml
index 4a4ab817..8ebae712 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,11 +3,20 @@ language: go
matrix:
fast_finish: true
include:
- - go: 1.11.x
- env: GO111MODULE=on
- go: 1.12.x
env: GO111MODULE=on
- go: 1.13.x
+ - go: 1.13.x
+ env:
+ - TESTTAGS=nomsgpack
+ - go: 1.14.x
+ - go: 1.14.x
+ env:
+ - TESTTAGS=nomsgpack
+ - go: 1.15.x
+ - go: 1.15.x
+ env:
+ - TESTTAGS=nomsgpack
- go: master
git:
@@ -17,7 +26,7 @@ before_install:
- if [[ "${GO111MODULE}" = "on" ]]; then mkdir "${HOME}/go"; export GOPATH="${HOME}/go"; fi
install:
- - if [[ "${GO111MODULE}" = "on" ]]; then go mod download; else make install; fi
+ - if [[ "${GO111MODULE}" = "on" ]]; then go mod download; fi
- if [[ "${GO111MODULE}" = "on" ]]; then export PATH="${GOPATH}/bin:${GOROOT}/bin:${PATH}"; fi
- if [[ "${GO111MODULE}" = "on" ]]; then make tools; fi
diff --git a/AUTHORS.md b/AUTHORS.md
index dda19bcf..c634e6be 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -156,7 +156,7 @@ People and companies, who have contributed, in alphabetical order.
- Fix variadic parameter in the flexible render API
- Fix Corrupted plain render
- Add Pluggable View Renderer Example
-
+
**@msemenistyi (Mykyta Semenistyi)**
- update Readme.md. Add code to String method
@@ -190,6 +190,8 @@ People and companies, who have contributed, in alphabetical order.
**@rogierlommers (Rogier Lommers)**
- Add updated static serve example
+**@rw-access (Ross Wolf)**
+- Added support to mix exact and param routes
**@se77en (Damon Zhao)**
- Improve color logging
diff --git a/BENCHMARKS.md b/BENCHMARKS.md
index 9a7df86a..c11ee99a 100644
--- a/BENCHMARKS.md
+++ b/BENCHMARKS.md
@@ -1,604 +1,666 @@
-## Benchmark System
+# Benchmark System
-**VM HOST:** DigitalOcean
-**Machine:** 4 CPU, 8 GB RAM. Ubuntu 16.04.2 x64
-**Date:** July 19th, 2017
-**Go Version:** 1.8.3 linux/amd64
-**Source:** [Go HTTP Router Benchmark](https://github.com/julienschmidt/go-http-routing-benchmark)
+**VM HOST:** Travis
+**Machine:** Ubuntu 16.04.6 LTS x64
+**Date:** May 04th, 2020
+**Version:** Gin v1.6.3
+**Go Version:** 1.14.2 linux/amd64
+**Source:** [Go HTTP Router Benchmark](https://github.com/gin-gonic/go-http-routing-benchmark)
+**Result:** [See the gist](https://gist.github.com/appleboy/b5f2ecfaf50824ae9c64dcfb9165ae5e) or [Travis result](https://travis-ci.org/github/gin-gonic/go-http-routing-benchmark/jobs/682947061)
## Static Routes: 157
-```
-Gin: 30512 Bytes
+```sh
+Gin: 34936 Bytes
-HttpServeMux: 17344 Bytes
-Ace: 30080 Bytes
-Bear: 30472 Bytes
-Beego: 96408 Bytes
-Bone: 37904 Bytes
-Denco: 10464 Bytes
-Echo: 73680 Bytes
-GocraftWeb: 55720 Bytes
-Goji: 27200 Bytes
-Gojiv2: 104464 Bytes
-GoJsonRest: 136472 Bytes
-GoRestful: 914904 Bytes
-GorillaMux: 675568 Bytes
-HttpRouter: 21128 Bytes
-HttpTreeMux: 73448 Bytes
-Kocha: 115072 Bytes
-LARS: 30120 Bytes
-Macaron: 37984 Bytes
-Martini: 310832 Bytes
-Pat: 20464 Bytes
-Possum: 91328 Bytes
-R2router: 23712 Bytes
-Rivet: 23880 Bytes
-Tango: 28008 Bytes
-TigerTonic: 80368 Bytes
-Traffic: 626480 Bytes
-Vulcan: 369064 Bytes
+HttpServeMux: 14512 Bytes
+Ace: 30680 Bytes
+Aero: 34536 Bytes
+Bear: 30456 Bytes
+Beego: 98456 Bytes
+Bone: 40224 Bytes
+Chi: 83608 Bytes
+Denco: 10216 Bytes
+Echo: 80328 Bytes
+GocraftWeb: 55288 Bytes
+Goji: 29744 Bytes
+Gojiv2: 105840 Bytes
+GoJsonRest: 137496 Bytes
+GoRestful: 816936 Bytes
+GorillaMux: 585632 Bytes
+GowwwRouter: 24968 Bytes
+HttpRouter: 21712 Bytes
+HttpTreeMux: 73448 Bytes
+Kocha: 115472 Bytes
+LARS: 30640 Bytes
+Macaron: 38592 Bytes
+Martini: 310864 Bytes
+Pat: 19696 Bytes
+Possum: 89920 Bytes
+R2router: 23712 Bytes
+Rivet: 24608 Bytes
+Tango: 28264 Bytes
+TigerTonic: 78768 Bytes
+Traffic: 538976 Bytes
+Vulcan: 369960 Bytes
```
## GithubAPI Routes: 203
-```
-Gin: 52672 Bytes
+```sh
+Gin: 58512 Bytes
-Ace: 48992 Bytes
-Bear: 161592 Bytes
-Beego: 147992 Bytes
-Bone: 97728 Bytes
-Denco: 36440 Bytes
-Echo: 95672 Bytes
-GocraftWeb: 95640 Bytes
-Goji: 86088 Bytes
-Gojiv2: 144392 Bytes
-GoJsonRest: 134648 Bytes
-GoRestful: 1410760 Bytes
-GorillaMux: 1509488 Bytes
-HttpRouter: 37464 Bytes
-HttpTreeMux: 78800 Bytes
-Kocha: 785408 Bytes
-LARS: 49032 Bytes
-Macaron: 132712 Bytes
-Martini: 564352 Bytes
-Pat: 21200 Bytes
-Possum: 83888 Bytes
-R2router: 47104 Bytes
-Rivet: 42840 Bytes
-Tango: 54584 Bytes
-TigerTonic: 96384 Bytes
-Traffic: 1061920 Bytes
-Vulcan: 465296 Bytes
+Ace: 48688 Bytes
+Aero: 318568 Bytes
+Bear: 84248 Bytes
+Beego: 150936 Bytes
+Bone: 100976 Bytes
+Chi: 95112 Bytes
+Denco: 36736 Bytes
+Echo: 100296 Bytes
+GocraftWeb: 95432 Bytes
+Goji: 49680 Bytes
+Gojiv2: 104704 Bytes
+GoJsonRest: 141976 Bytes
+GoRestful: 1241656 Bytes
+GorillaMux: 1322784 Bytes
+GowwwRouter: 80008 Bytes
+HttpRouter: 37144 Bytes
+HttpTreeMux: 78800 Bytes
+Kocha: 785120 Bytes
+LARS: 48600 Bytes
+Macaron: 92784 Bytes
+Martini: 485264 Bytes
+Pat: 21200 Bytes
+Possum: 85312 Bytes
+R2router: 47104 Bytes
+Rivet: 42840 Bytes
+Tango: 54840 Bytes
+TigerTonic: 95264 Bytes
+Traffic: 921744 Bytes
+Vulcan: 425992 Bytes
```
## GPlusAPI Routes: 13
-```
-Gin: 3968 Bytes
+```sh
+Gin: 4384 Bytes
-Ace: 3600 Bytes
-Bear: 7112 Bytes
-Beego: 10048 Bytes
-Bone: 6480 Bytes
-Denco: 3256 Bytes
-Echo: 9000 Bytes
-GocraftWeb: 7496 Bytes
-Goji: 2912 Bytes
-Gojiv2: 7376 Bytes
-GoJsonRest: 11544 Bytes
-GoRestful: 88776 Bytes
-GorillaMux: 71488 Bytes
-HttpRouter: 2712 Bytes
-HttpTreeMux: 7440 Bytes
-Kocha: 128880 Bytes
-LARS: 3640 Bytes
-Macaron: 8656 Bytes
-Martini: 23936 Bytes
-Pat: 1856 Bytes
-Possum: 7248 Bytes
-R2router: 3928 Bytes
-Rivet: 3064 Bytes
-Tango: 4912 Bytes
-TigerTonic: 9408 Bytes
-Traffic: 49472 Bytes
-Vulcan: 25496 Bytes
+Ace: 3712 Bytes
+Aero: 26056 Bytes
+Bear: 7112 Bytes
+Beego: 10272 Bytes
+Bone: 6688 Bytes
+Chi: 8024 Bytes
+Denco: 3264 Bytes
+Echo: 9688 Bytes
+GocraftWeb: 7496 Bytes
+Goji: 3152 Bytes
+Gojiv2: 7376 Bytes
+GoJsonRest: 11400 Bytes
+GoRestful: 74328 Bytes
+GorillaMux: 66208 Bytes
+GowwwRouter: 5744 Bytes
+HttpRouter: 2808 Bytes
+HttpTreeMux: 7440 Bytes
+Kocha: 128880 Bytes
+LARS: 3656 Bytes
+Macaron: 8656 Bytes
+Martini: 23920 Bytes
+Pat: 1856 Bytes
+Possum: 7248 Bytes
+R2router: 3928 Bytes
+Rivet: 3064 Bytes
+Tango: 5168 Bytes
+TigerTonic: 9408 Bytes
+Traffic: 46400 Bytes
+Vulcan: 25544 Bytes
```
## ParseAPI Routes: 26
-```
-Gin: 6928 Bytes
+```sh
+Gin: 7776 Bytes
-Ace: 6592 Bytes
-Bear: 12320 Bytes
-Beego: 18960 Bytes
-Bone: 11024 Bytes
-Denco: 4184 Bytes
-Echo: 11168 Bytes
-GocraftWeb: 12800 Bytes
-Goji: 5232 Bytes
-Gojiv2: 14464 Bytes
-GoJsonRest: 14216 Bytes
-GoRestful: 127368 Bytes
-GorillaMux: 123016 Bytes
-HttpRouter: 4976 Bytes
-HttpTreeMux: 7848 Bytes
-Kocha: 181712 Bytes
-LARS: 6632 Bytes
-Macaron: 13648 Bytes
-Martini: 45952 Bytes
-Pat: 2560 Bytes
-Possum: 9200 Bytes
-R2router: 7056 Bytes
-Rivet: 5680 Bytes
-Tango: 8664 Bytes
-TigerTonic: 9840 Bytes
-Traffic: 93480 Bytes
-Vulcan: 44504 Bytes
+Ace: 6704 Bytes
+Aero: 28488 Bytes
+Bear: 12320 Bytes
+Beego: 19280 Bytes
+Bone: 11440 Bytes
+Chi: 9744 Bytes
+Denco: 4192 Bytes
+Echo: 11664 Bytes
+GocraftWeb: 12800 Bytes
+Goji: 5680 Bytes
+Gojiv2: 14464 Bytes
+GoJsonRest: 14072 Bytes
+GoRestful: 116264 Bytes
+GorillaMux: 105880 Bytes
+GowwwRouter: 9344 Bytes
+HttpRouter: 5072 Bytes
+HttpTreeMux: 7848 Bytes
+Kocha: 181712 Bytes
+LARS: 6632 Bytes
+Macaron: 13648 Bytes
+Martini: 45888 Bytes
+Pat: 2560 Bytes
+Possum: 9200 Bytes
+R2router: 7056 Bytes
+Rivet: 5680 Bytes
+Tango: 8920 Bytes
+TigerTonic: 9840 Bytes
+Traffic: 79096 Bytes
+Vulcan: 44504 Bytes
```
## Static Routes
-```
-BenchmarkGin_StaticAll 50000 34506 ns/op 0 B/op 0 allocs/op
+```sh
+BenchmarkGin_StaticAll 62169 19319 ns/op 0 B/op 0 allocs/op
-BenchmarkAce_StaticAll 30000 49657 ns/op 0 B/op 0 allocs/op
-BenchmarkHttpServeMux_StaticAll 2000 1183737 ns/op 96 B/op 8 allocs/op
-BenchmarkBeego_StaticAll 5000 412621 ns/op 57776 B/op 628 allocs/op
-BenchmarkBear_StaticAll 10000 149242 ns/op 20336 B/op 461 allocs/op
-BenchmarkBone_StaticAll 10000 118583 ns/op 0 B/op 0 allocs/op
-BenchmarkDenco_StaticAll 100000 13247 ns/op 0 B/op 0 allocs/op
-BenchmarkEcho_StaticAll 20000 79914 ns/op 5024 B/op 157 allocs/op
-BenchmarkGocraftWeb_StaticAll 10000 211823 ns/op 46440 B/op 785 allocs/op
-BenchmarkGoji_StaticAll 10000 109390 ns/op 0 B/op 0 allocs/op
-BenchmarkGojiv2_StaticAll 3000 415533 ns/op 145696 B/op 1099 allocs/op
-BenchmarkGoJsonRest_StaticAll 5000 364403 ns/op 51653 B/op 1727 allocs/op
-BenchmarkGoRestful_StaticAll 500 2578579 ns/op 314936 B/op 3144 allocs/op
-BenchmarkGorillaMux_StaticAll 500 2704856 ns/op 115648 B/op 1578 allocs/op
-BenchmarkHttpRouter_StaticAll 100000 18541 ns/op 0 B/op 0 allocs/op
-BenchmarkHttpTreeMux_StaticAll 100000 22332 ns/op 0 B/op 0 allocs/op
-BenchmarkKocha_StaticAll 50000 31176 ns/op 0 B/op 0 allocs/op
-BenchmarkLARS_StaticAll 50000 40840 ns/op 0 B/op 0 allocs/op
-BenchmarkMacaron_StaticAll 5000 517656 ns/op 120576 B/op 1413 allocs/op
-BenchmarkMartini_StaticAll 300 4462289 ns/op 125442 B/op 1717 allocs/op
-BenchmarkPat_StaticAll 500 2157275 ns/op 533904 B/op 11123 allocs/op
-BenchmarkPossum_StaticAll 10000 254701 ns/op 65312 B/op 471 allocs/op
-BenchmarkR2router_StaticAll 10000 133956 ns/op 22608 B/op 628 allocs/op
-BenchmarkRivet_StaticAll 30000 46812 ns/op 0 B/op 0 allocs/op
-BenchmarkTango_StaticAll 5000 390613 ns/op 39225 B/op 1256 allocs/op
-BenchmarkTigerTonic_StaticAll 20000 88060 ns/op 7504 B/op 157 allocs/op
-BenchmarkTraffic_StaticAll 500 2910236 ns/op 729736 B/op 14287 allocs/op
-BenchmarkVulcan_StaticAll 5000 277366 ns/op 15386 B/op 471 allocs/op
+BenchmarkAce_StaticAll 65428 18313 ns/op 0 B/op 0 allocs/op
+BenchmarkAero_StaticAll 121132 9632 ns/op 0 B/op 0 allocs/op
+BenchmarkHttpServeMux_StaticAll 52626 22758 ns/op 0 B/op 0 allocs/op
+BenchmarkBeego_StaticAll 9962 179058 ns/op 55264 B/op 471 allocs/op
+BenchmarkBear_StaticAll 14894 80966 ns/op 20272 B/op 469 allocs/op
+BenchmarkBone_StaticAll 18718 64065 ns/op 0 B/op 0 allocs/op
+BenchmarkChi_StaticAll 10000 149827 ns/op 67824 B/op 471 allocs/op
+BenchmarkDenco_StaticAll 211393 5680 ns/op 0 B/op 0 allocs/op
+BenchmarkEcho_StaticAll 49341 24343 ns/op 0 B/op 0 allocs/op
+BenchmarkGocraftWeb_StaticAll 10000 126209 ns/op 46312 B/op 785 allocs/op
+BenchmarkGoji_StaticAll 27956 43174 ns/op 0 B/op 0 allocs/op
+BenchmarkGojiv2_StaticAll 3430 370718 ns/op 205984 B/op 1570 allocs/op
+BenchmarkGoJsonRest_StaticAll 9134 188888 ns/op 51653 B/op 1727 allocs/op
+BenchmarkGoRestful_StaticAll 706 1703330 ns/op 613280 B/op 2053 allocs/op
+BenchmarkGorillaMux_StaticAll 1268 924083 ns/op 153233 B/op 1413 allocs/op
+BenchmarkGowwwRouter_StaticAll 63374 18935 ns/op 0 B/op 0 allocs/op
+BenchmarkHttpRouter_StaticAll 109938 10902 ns/op 0 B/op 0 allocs/op
+BenchmarkHttpTreeMux_StaticAll 109166 10861 ns/op 0 B/op 0 allocs/op
+BenchmarkKocha_StaticAll 92258 12992 ns/op 0 B/op 0 allocs/op
+BenchmarkLARS_StaticAll 65200 18387 ns/op 0 B/op 0 allocs/op
+BenchmarkMacaron_StaticAll 5671 291501 ns/op 115553 B/op 1256 allocs/op
+BenchmarkMartini_StaticAll 807 1460498 ns/op 125444 B/op 1717 allocs/op
+BenchmarkPat_StaticAll 513 2342396 ns/op 602832 B/op 12559 allocs/op
+BenchmarkPossum_StaticAll 10000 128270 ns/op 65312 B/op 471 allocs/op
+BenchmarkR2router_StaticAll 16726 71760 ns/op 22608 B/op 628 allocs/op
+BenchmarkRivet_StaticAll 41722 28723 ns/op 0 B/op 0 allocs/op
+BenchmarkTango_StaticAll 7606 205082 ns/op 39209 B/op 1256 allocs/op
+BenchmarkTigerTonic_StaticAll 26247 45806 ns/op 7376 B/op 157 allocs/op
+BenchmarkTraffic_StaticAll 550 2284518 ns/op 754864 B/op 14601 allocs/op
+BenchmarkVulcan_StaticAll 10000 131343 ns/op 15386 B/op 471 allocs/op
```
## Micro Benchmarks
-```
-BenchmarkGin_Param 20000000 113 ns/op 0 B/op 0 allocs/op
+```sh
+BenchmarkGin_Param 18785022 63.9 ns/op 0 B/op 0 allocs/op
-BenchmarkAce_Param 5000000 375 ns/op 32 B/op 1 allocs/op
-BenchmarkBear_Param 1000000 1709 ns/op 456 B/op 5 allocs/op
-BenchmarkBeego_Param 1000000 2484 ns/op 368 B/op 4 allocs/op
-BenchmarkBone_Param 1000000 2391 ns/op 688 B/op 5 allocs/op
-BenchmarkDenco_Param 10000000 240 ns/op 32 B/op 1 allocs/op
-BenchmarkEcho_Param 5000000 366 ns/op 32 B/op 1 allocs/op
-BenchmarkGocraftWeb_Param 1000000 2343 ns/op 648 B/op 8 allocs/op
-BenchmarkGoji_Param 1000000 1197 ns/op 336 B/op 2 allocs/op
-BenchmarkGojiv2_Param 1000000 2771 ns/op 944 B/op 8 allocs/op
-BenchmarkGoJsonRest_Param 1000000 2993 ns/op 649 B/op 13 allocs/op
-BenchmarkGoRestful_Param 200000 8860 ns/op 2296 B/op 21 allocs/op
-BenchmarkGorillaMux_Param 500000 4461 ns/op 1056 B/op 11 allocs/op
-BenchmarkHttpRouter_Param 10000000 175 ns/op 32 B/op 1 allocs/op
-BenchmarkHttpTreeMux_Param 1000000 1167 ns/op 352 B/op 3 allocs/op
-BenchmarkKocha_Param 3000000 429 ns/op 56 B/op 3 allocs/op
-BenchmarkLARS_Param 10000000 134 ns/op 0 B/op 0 allocs/op
-BenchmarkMacaron_Param 500000 4635 ns/op 1056 B/op 10 allocs/op
-BenchmarkMartini_Param 200000 9933 ns/op 1072 B/op 10 allocs/op
-BenchmarkPat_Param 1000000 2929 ns/op 648 B/op 12 allocs/op
-BenchmarkPossum_Param 1000000 2503 ns/op 560 B/op 6 allocs/op
-BenchmarkR2router_Param 1000000 1507 ns/op 432 B/op 5 allocs/op
-BenchmarkRivet_Param 5000000 297 ns/op 48 B/op 1 allocs/op
-BenchmarkTango_Param 1000000 1862 ns/op 248 B/op 8 allocs/op
-BenchmarkTigerTonic_Param 500000 5660 ns/op 992 B/op 17 allocs/op
-BenchmarkTraffic_Param 200000 8408 ns/op 1960 B/op 21 allocs/op
-BenchmarkVulcan_Param 2000000 963 ns/op 98 B/op 3 allocs/op
-BenchmarkAce_Param5 2000000 740 ns/op 160 B/op 1 allocs/op
-BenchmarkBear_Param5 1000000 2777 ns/op 501 B/op 5 allocs/op
-BenchmarkBeego_Param5 1000000 3740 ns/op 368 B/op 4 allocs/op
-BenchmarkBone_Param5 1000000 2950 ns/op 736 B/op 5 allocs/op
-BenchmarkDenco_Param5 2000000 644 ns/op 160 B/op 1 allocs/op
-BenchmarkEcho_Param5 3000000 558 ns/op 32 B/op 1 allocs/op
-BenchmarkGin_Param5 10000000 198 ns/op 0 B/op 0 allocs/op
-BenchmarkGocraftWeb_Param5 500000 3870 ns/op 920 B/op 11 allocs/op
-BenchmarkGoji_Param5 1000000 1746 ns/op 336 B/op 2 allocs/op
-BenchmarkGojiv2_Param5 1000000 3214 ns/op 1008 B/op 8 allocs/op
-BenchmarkGoJsonRest_Param5 500000 5509 ns/op 1097 B/op 16 allocs/op
-BenchmarkGoRestful_Param5 200000 11232 ns/op 2392 B/op 21 allocs/op
-BenchmarkGorillaMux_Param5 300000 7777 ns/op 1184 B/op 11 allocs/op
-BenchmarkHttpRouter_Param5 3000000 631 ns/op 160 B/op 1 allocs/op
-BenchmarkHttpTreeMux_Param5 1000000 2800 ns/op 576 B/op 6 allocs/op
-BenchmarkKocha_Param5 1000000 2053 ns/op 440 B/op 10 allocs/op
-BenchmarkLARS_Param5 10000000 232 ns/op 0 B/op 0 allocs/op
-BenchmarkMacaron_Param5 500000 5888 ns/op 1056 B/op 10 allocs/op
-BenchmarkMartini_Param5 200000 12807 ns/op 1232 B/op 11 allocs/op
-BenchmarkPat_Param5 300000 7320 ns/op 964 B/op 32 allocs/op
-BenchmarkPossum_Param5 1000000 2495 ns/op 560 B/op 6 allocs/op
-BenchmarkR2router_Param5 1000000 1844 ns/op 432 B/op 5 allocs/op
-BenchmarkRivet_Param5 2000000 935 ns/op 240 B/op 1 allocs/op
-BenchmarkTango_Param5 1000000 2327 ns/op 360 B/op 8 allocs/op
-BenchmarkTigerTonic_Param5 100000 18514 ns/op 2551 B/op 43 allocs/op
-BenchmarkTraffic_Param5 200000 11997 ns/op 2248 B/op 25 allocs/op
-BenchmarkVulcan_Param5 1000000 1333 ns/op 98 B/op 3 allocs/op
-BenchmarkAce_Param20 1000000 2031 ns/op 640 B/op 1 allocs/op
-BenchmarkBear_Param20 200000 7285 ns/op 1664 B/op 5 allocs/op
-BenchmarkBeego_Param20 300000 6224 ns/op 368 B/op 4 allocs/op
-BenchmarkBone_Param20 200000 8023 ns/op 1903 B/op 5 allocs/op
-BenchmarkDenco_Param20 1000000 2262 ns/op 640 B/op 1 allocs/op
-BenchmarkEcho_Param20 1000000 1387 ns/op 32 B/op 1 allocs/op
-BenchmarkGin_Param20 3000000 503 ns/op 0 B/op 0 allocs/op
-BenchmarkGocraftWeb_Param20 100000 14408 ns/op 3795 B/op 15 allocs/op
-BenchmarkGoji_Param20 500000 5272 ns/op 1247 B/op 2 allocs/op
-BenchmarkGojiv2_Param20 1000000 4163 ns/op 1248 B/op 8 allocs/op
-BenchmarkGoJsonRest_Param20 100000 17866 ns/op 4485 B/op 20 allocs/op
-BenchmarkGoRestful_Param20 100000 21022 ns/op 4724 B/op 23 allocs/op
-BenchmarkGorillaMux_Param20 100000 17055 ns/op 3547 B/op 13 allocs/op
-BenchmarkHttpRouter_Param20 1000000 1748 ns/op 640 B/op 1 allocs/op
-BenchmarkHttpTreeMux_Param20 200000 12246 ns/op 3196 B/op 10 allocs/op
-BenchmarkKocha_Param20 300000 6861 ns/op 1808 B/op 27 allocs/op
-BenchmarkLARS_Param20 3000000 526 ns/op 0 B/op 0 allocs/op
-BenchmarkMacaron_Param20 100000 13069 ns/op 2906 B/op 12 allocs/op
-BenchmarkMartini_Param20 100000 23602 ns/op 3597 B/op 13 allocs/op
-BenchmarkPat_Param20 50000 32143 ns/op 4688 B/op 111 allocs/op
-BenchmarkPossum_Param20 1000000 2396 ns/op 560 B/op 6 allocs/op
-BenchmarkR2router_Param20 200000 8907 ns/op 2283 B/op 7 allocs/op
-BenchmarkRivet_Param20 1000000 3280 ns/op 1024 B/op 1 allocs/op
-BenchmarkTango_Param20 500000 4640 ns/op 856 B/op 8 allocs/op
-BenchmarkTigerTonic_Param20 20000 67581 ns/op 10532 B/op 138 allocs/op
-BenchmarkTraffic_Param20 50000 40313 ns/op 7941 B/op 45 allocs/op
-BenchmarkVulcan_Param20 1000000 2264 ns/op 98 B/op 3 allocs/op
-BenchmarkAce_ParamWrite 3000000 532 ns/op 40 B/op 2 allocs/op
-BenchmarkBear_ParamWrite 1000000 1778 ns/op 456 B/op 5 allocs/op
-BenchmarkBeego_ParamWrite 1000000 2596 ns/op 376 B/op 5 allocs/op
-BenchmarkBone_ParamWrite 1000000 2519 ns/op 688 B/op 5 allocs/op
-BenchmarkDenco_ParamWrite 5000000 411 ns/op 32 B/op 1 allocs/op
-BenchmarkEcho_ParamWrite 2000000 718 ns/op 40 B/op 2 allocs/op
-BenchmarkGin_ParamWrite 5000000 283 ns/op 0 B/op 0 allocs/op
-BenchmarkGocraftWeb_ParamWrite 1000000 2561 ns/op 656 B/op 9 allocs/op
-BenchmarkGoji_ParamWrite 1000000 1378 ns/op 336 B/op 2 allocs/op
-BenchmarkGojiv2_ParamWrite 1000000 3128 ns/op 976 B/op 10 allocs/op
-BenchmarkGoJsonRest_ParamWrite 500000 4446 ns/op 1128 B/op 18 allocs/op
-BenchmarkGoRestful_ParamWrite 200000 10291 ns/op 2304 B/op 22 allocs/op
-BenchmarkGorillaMux_ParamWrite 500000 5153 ns/op 1064 B/op 12 allocs/op
-BenchmarkHttpRouter_ParamWrite 5000000 263 ns/op 32 B/op 1 allocs/op
-BenchmarkHttpTreeMux_ParamWrite 1000000 1351 ns/op 352 B/op 3 allocs/op
-BenchmarkKocha_ParamWrite 3000000 538 ns/op 56 B/op 3 allocs/op
-BenchmarkLARS_ParamWrite 5000000 316 ns/op 0 B/op 0 allocs/op
-BenchmarkMacaron_ParamWrite 500000 5756 ns/op 1160 B/op 14 allocs/op
-BenchmarkMartini_ParamWrite 200000 13097 ns/op 1176 B/op 14 allocs/op
-BenchmarkPat_ParamWrite 500000 4954 ns/op 1072 B/op 17 allocs/op
-BenchmarkPossum_ParamWrite 1000000 2499 ns/op 560 B/op 6 allocs/op
-BenchmarkR2router_ParamWrite 1000000 1531 ns/op 432 B/op 5 allocs/op
-BenchmarkRivet_ParamWrite 3000000 570 ns/op 112 B/op 2 allocs/op
-BenchmarkTango_ParamWrite 2000000 957 ns/op 136 B/op 4 allocs/op
-BenchmarkTigerTonic_ParamWrite 200000 7025 ns/op 1424 B/op 23 allocs/op
-BenchmarkTraffic_ParamWrite 200000 10112 ns/op 2384 B/op 25 allocs/op
-BenchmarkVulcan_ParamWrite 1000000 1006 ns/op 98 B/op 3 allocs/op
+BenchmarkAce_Param 14689765 81.5 ns/op 0 B/op 0 allocs/op
+BenchmarkAero_Param 23094770 51.2 ns/op 0 B/op 0 allocs/op
+BenchmarkBear_Param 1417045 845 ns/op 456 B/op 5 allocs/op
+BenchmarkBeego_Param 1000000 1080 ns/op 352 B/op 3 allocs/op
+BenchmarkBone_Param 1000000 1463 ns/op 816 B/op 6 allocs/op
+BenchmarkChi_Param 1378756 885 ns/op 432 B/op 3 allocs/op
+BenchmarkDenco_Param 8557899 143 ns/op 32 B/op 1 allocs/op
+BenchmarkEcho_Param 16433347 75.5 ns/op 0 B/op 0 allocs/op
+BenchmarkGocraftWeb_Param 1000000 1218 ns/op 648 B/op 8 allocs/op
+BenchmarkGoji_Param 1921248 617 ns/op 336 B/op 2 allocs/op
+BenchmarkGojiv2_Param 561848 2156 ns/op 1328 B/op 11 allocs/op
+BenchmarkGoJsonRest_Param 1000000 1358 ns/op 649 B/op 13 allocs/op
+BenchmarkGoRestful_Param 224857 5307 ns/op 4192 B/op 14 allocs/op
+BenchmarkGorillaMux_Param 498313 2459 ns/op 1280 B/op 10 allocs/op
+BenchmarkGowwwRouter_Param 1864354 654 ns/op 432 B/op 3 allocs/op
+BenchmarkHttpRouter_Param 26269074 47.7 ns/op 0 B/op 0 allocs/op
+BenchmarkHttpTreeMux_Param 2109829 557 ns/op 352 B/op 3 allocs/op
+BenchmarkKocha_Param 5050216 243 ns/op 56 B/op 3 allocs/op
+BenchmarkLARS_Param 19811712 59.9 ns/op 0 B/op 0 allocs/op
+BenchmarkMacaron_Param 662746 2329 ns/op 1072 B/op 10 allocs/op
+BenchmarkMartini_Param 279902 4260 ns/op 1072 B/op 10 allocs/op
+BenchmarkPat_Param 1000000 1382 ns/op 536 B/op 11 allocs/op
+BenchmarkPossum_Param 1000000 1014 ns/op 496 B/op 5 allocs/op
+BenchmarkR2router_Param 1712559 707 ns/op 432 B/op 5 allocs/op
+BenchmarkRivet_Param 6648086 182 ns/op 48 B/op 1 allocs/op
+BenchmarkTango_Param 1221504 994 ns/op 248 B/op 8 allocs/op
+BenchmarkTigerTonic_Param 891661 2261 ns/op 776 B/op 16 allocs/op
+BenchmarkTraffic_Param 350059 3598 ns/op 1856 B/op 21 allocs/op
+BenchmarkVulcan_Param 2517823 472 ns/op 98 B/op 3 allocs/op
+BenchmarkAce_Param5 9214365 130 ns/op 0 B/op 0 allocs/op
+BenchmarkAero_Param5 15369013 77.9 ns/op 0 B/op 0 allocs/op
+BenchmarkBear_Param5 1000000 1113 ns/op 501 B/op 5 allocs/op
+BenchmarkBeego_Param5 1000000 1269 ns/op 352 B/op 3 allocs/op
+BenchmarkBone_Param5 986820 1873 ns/op 864 B/op 6 allocs/op
+BenchmarkChi_Param5 1000000 1156 ns/op 432 B/op 3 allocs/op
+BenchmarkDenco_Param5 3036331 400 ns/op 160 B/op 1 allocs/op
+BenchmarkEcho_Param5 6447133 186 ns/op 0 B/op 0 allocs/op
+BenchmarkGin_Param5 10786068 110 ns/op 0 B/op 0 allocs/op
+BenchmarkGocraftWeb_Param5 844820 1944 ns/op 920 B/op 11 allocs/op
+BenchmarkGoji_Param5 1474965 827 ns/op 336 B/op 2 allocs/op
+BenchmarkGojiv2_Param5 442820 2516 ns/op 1392 B/op 11 allocs/op
+BenchmarkGoJsonRest_Param5 507555 2711 ns/op 1097 B/op 16 allocs/op
+BenchmarkGoRestful_Param5 216481 6093 ns/op 4288 B/op 14 allocs/op
+BenchmarkGorillaMux_Param5 314402 3628 ns/op 1344 B/op 10 allocs/op
+BenchmarkGowwwRouter_Param5 1624660 733 ns/op 432 B/op 3 allocs/op
+BenchmarkHttpRouter_Param5 13167324 92.0 ns/op 0 B/op 0 allocs/op
+BenchmarkHttpTreeMux_Param5 1000000 1295 ns/op 576 B/op 6 allocs/op
+BenchmarkKocha_Param5 1000000 1138 ns/op 440 B/op 10 allocs/op
+BenchmarkLARS_Param5 11580613 105 ns/op 0 B/op 0 allocs/op
+BenchmarkMacaron_Param5 473596 2755 ns/op 1072 B/op 10 allocs/op
+BenchmarkMartini_Param5 230756 5111 ns/op 1232 B/op 11 allocs/op
+BenchmarkPat_Param5 469190 3370 ns/op 888 B/op 29 allocs/op
+BenchmarkPossum_Param5 1000000 1002 ns/op 496 B/op 5 allocs/op
+BenchmarkR2router_Param5 1422129 844 ns/op 432 B/op 5 allocs/op
+BenchmarkRivet_Param5 2263789 539 ns/op 240 B/op 1 allocs/op
+BenchmarkTango_Param5 1000000 1256 ns/op 360 B/op 8 allocs/op
+BenchmarkTigerTonic_Param5 175500 7492 ns/op 2279 B/op 39 allocs/op
+BenchmarkTraffic_Param5 233631 5816 ns/op 2208 B/op 27 allocs/op
+BenchmarkVulcan_Param5 1923416 629 ns/op 98 B/op 3 allocs/op
+BenchmarkAce_Param20 4321266 281 ns/op 0 B/op 0 allocs/op
+BenchmarkAero_Param20 31501641 35.2 ns/op 0 B/op 0 allocs/op
+BenchmarkBear_Param20 335204 3489 ns/op 1665 B/op 5 allocs/op
+BenchmarkBeego_Param20 503674 2860 ns/op 352 B/op 3 allocs/op
+BenchmarkBone_Param20 298922 4741 ns/op 2031 B/op 6 allocs/op
+BenchmarkChi_Param20 878181 1957 ns/op 432 B/op 3 allocs/op
+BenchmarkDenco_Param20 1000000 1360 ns/op 640 B/op 1 allocs/op
+BenchmarkEcho_Param20 2104946 580 ns/op 0 B/op 0 allocs/op
+BenchmarkGin_Param20 4167204 290 ns/op 0 B/op 0 allocs/op
+BenchmarkGocraftWeb_Param20 173064 7514 ns/op 3796 B/op 15 allocs/op
+BenchmarkGoji_Param20 458778 2651 ns/op 1247 B/op 2 allocs/op
+BenchmarkGojiv2_Param20 364862 3178 ns/op 1632 B/op 11 allocs/op
+BenchmarkGoJsonRest_Param20 125514 9760 ns/op 4485 B/op 20 allocs/op
+BenchmarkGoRestful_Param20 101217 11964 ns/op 6715 B/op 18 allocs/op
+BenchmarkGorillaMux_Param20 147654 8132 ns/op 3452 B/op 12 allocs/op
+BenchmarkGowwwRouter_Param20 1000000 1225 ns/op 432 B/op 3 allocs/op
+BenchmarkHttpRouter_Param20 4920895 247 ns/op 0 B/op 0 allocs/op
+BenchmarkHttpTreeMux_Param20 173202 6605 ns/op 3196 B/op 10 allocs/op
+BenchmarkKocha_Param20 345988 3620 ns/op 1808 B/op 27 allocs/op
+BenchmarkLARS_Param20 4592326 262 ns/op 0 B/op 0 allocs/op
+BenchmarkMacaron_Param20 166492 7286 ns/op 2924 B/op 12 allocs/op
+BenchmarkMartini_Param20 122162 10653 ns/op 3595 B/op 13 allocs/op
+BenchmarkPat_Param20 78630 15239 ns/op 4424 B/op 93 allocs/op
+BenchmarkPossum_Param20 1000000 1008 ns/op 496 B/op 5 allocs/op
+BenchmarkR2router_Param20 294981 4587 ns/op 2284 B/op 7 allocs/op
+BenchmarkRivet_Param20 691798 2090 ns/op 1024 B/op 1 allocs/op
+BenchmarkTango_Param20 842440 2505 ns/op 856 B/op 8 allocs/op
+BenchmarkTigerTonic_Param20 38614 31509 ns/op 9870 B/op 119 allocs/op
+BenchmarkTraffic_Param20 57633 21107 ns/op 7853 B/op 47 allocs/op
+BenchmarkVulcan_Param20 1000000 1178 ns/op 98 B/op 3 allocs/op
+BenchmarkAce_ParamWrite 7330743 180 ns/op 8 B/op 1 allocs/op
+BenchmarkAero_ParamWrite 13833598 86.7 ns/op 0 B/op 0 allocs/op
+BenchmarkBear_ParamWrite 1363321 867 ns/op 456 B/op 5 allocs/op
+BenchmarkBeego_ParamWrite 1000000 1104 ns/op 360 B/op 4 allocs/op
+BenchmarkBone_ParamWrite 1000000 1475 ns/op 816 B/op 6 allocs/op
+BenchmarkChi_ParamWrite 1320590 892 ns/op 432 B/op 3 allocs/op
+BenchmarkDenco_ParamWrite 7093605 172 ns/op 32 B/op 1 allocs/op
+BenchmarkEcho_ParamWrite 8434424 161 ns/op 8 B/op 1 allocs/op
+BenchmarkGin_ParamWrite 10377034 118 ns/op 0 B/op 0 allocs/op
+BenchmarkGocraftWeb_ParamWrite 1000000 1266 ns/op 656 B/op 9 allocs/op
+BenchmarkGoji_ParamWrite 1874168 654 ns/op 336 B/op 2 allocs/op
+BenchmarkGojiv2_ParamWrite 459032 2352 ns/op 1360 B/op 13 allocs/op
+BenchmarkGoJsonRest_ParamWrite 499434 2145 ns/op 1128 B/op 18 allocs/op
+BenchmarkGoRestful_ParamWrite 241087 5470 ns/op 4200 B/op 15 allocs/op
+BenchmarkGorillaMux_ParamWrite 425686 2522 ns/op 1280 B/op 10 allocs/op
+BenchmarkGowwwRouter_ParamWrite 922172 1778 ns/op 976 B/op 8 allocs/op
+BenchmarkHttpRouter_ParamWrite 15392049 77.7 ns/op 0 B/op 0 allocs/op
+BenchmarkHttpTreeMux_ParamWrite 1973385 597 ns/op 352 B/op 3 allocs/op
+BenchmarkKocha_ParamWrite 4262500 281 ns/op 56 B/op 3 allocs/op
+BenchmarkLARS_ParamWrite 10764410 113 ns/op 0 B/op 0 allocs/op
+BenchmarkMacaron_ParamWrite 486769 2726 ns/op 1176 B/op 14 allocs/op
+BenchmarkMartini_ParamWrite 264804 4842 ns/op 1176 B/op 14 allocs/op
+BenchmarkPat_ParamWrite 735116 2047 ns/op 960 B/op 15 allocs/op
+BenchmarkPossum_ParamWrite 1000000 1004 ns/op 496 B/op 5 allocs/op
+BenchmarkR2router_ParamWrite 1592136 768 ns/op 432 B/op 5 allocs/op
+BenchmarkRivet_ParamWrite 3582051 339 ns/op 112 B/op 2 allocs/op
+BenchmarkTango_ParamWrite 2237337 534 ns/op 136 B/op 4 allocs/op
+BenchmarkTigerTonic_ParamWrite 439608 3136 ns/op 1216 B/op 21 allocs/op
+BenchmarkTraffic_ParamWrite 306979 4328 ns/op 2280 B/op 25 allocs/op
+BenchmarkVulcan_ParamWrite 2529973 472 ns/op 98 B/op 3 allocs/op
```
## GitHub
-```
-BenchmarkGin_GithubStatic 10000000 156 ns/op 0 B/op 0 allocs/op
+```sh
+BenchmarkGin_GithubStatic 15629472 76.7 ns/op 0 B/op 0 allocs/op
-BenchmarkAce_GithubStatic 5000000 294 ns/op 0 B/op 0 allocs/op
-BenchmarkBear_GithubStatic 2000000 893 ns/op 120 B/op 3 allocs/op
-BenchmarkBeego_GithubStatic 1000000 2491 ns/op 368 B/op 4 allocs/op
-BenchmarkBone_GithubStatic 50000 25300 ns/op 2880 B/op 60 allocs/op
-BenchmarkDenco_GithubStatic 20000000 76.0 ns/op 0 B/op 0 allocs/op
-BenchmarkEcho_GithubStatic 2000000 516 ns/op 32 B/op 1 allocs/op
-BenchmarkGocraftWeb_GithubStatic 1000000 1448 ns/op 296 B/op 5 allocs/op
-BenchmarkGoji_GithubStatic 3000000 496 ns/op 0 B/op 0 allocs/op
-BenchmarkGojiv2_GithubStatic 1000000 2941 ns/op 928 B/op 7 allocs/op
-BenchmarkGoRestful_GithubStatic 100000 27256 ns/op 3224 B/op 22 allocs/op
-BenchmarkGoJsonRest_GithubStatic 1000000 2196 ns/op 329 B/op 11 allocs/op
-BenchmarkGorillaMux_GithubStatic 50000 31617 ns/op 736 B/op 10 allocs/op
-BenchmarkHttpRouter_GithubStatic 20000000 88.4 ns/op 0 B/op 0 allocs/op
-BenchmarkHttpTreeMux_GithubStatic 10000000 134 ns/op 0 B/op 0 allocs/op
-BenchmarkKocha_GithubStatic 20000000 113 ns/op 0 B/op 0 allocs/op
-BenchmarkLARS_GithubStatic 10000000 195 ns/op 0 B/op 0 allocs/op
-BenchmarkMacaron_GithubStatic 500000 3740 ns/op 768 B/op 9 allocs/op
-BenchmarkMartini_GithubStatic 50000 27673 ns/op 768 B/op 9 allocs/op
-BenchmarkPat_GithubStatic 100000 19470 ns/op 3648 B/op 76 allocs/op
-BenchmarkPossum_GithubStatic 1000000 1729 ns/op 416 B/op 3 allocs/op
-BenchmarkR2router_GithubStatic 2000000 879 ns/op 144 B/op 4 allocs/op
-BenchmarkRivet_GithubStatic 10000000 231 ns/op 0 B/op 0 allocs/op
-BenchmarkTango_GithubStatic 1000000 2325 ns/op 248 B/op 8 allocs/op
-BenchmarkTigerTonic_GithubStatic 3000000 610 ns/op 48 B/op 1 allocs/op
-BenchmarkTraffic_GithubStatic 20000 62973 ns/op 18904 B/op 148 allocs/op
-BenchmarkVulcan_GithubStatic 1000000 1447 ns/op 98 B/op 3 allocs/op
-BenchmarkAce_GithubParam 2000000 686 ns/op 96 B/op 1 allocs/op
-BenchmarkBear_GithubParam 1000000 2155 ns/op 496 B/op 5 allocs/op
-BenchmarkBeego_GithubParam 1000000 2713 ns/op 368 B/op 4 allocs/op
-BenchmarkBone_GithubParam 100000 15088 ns/op 1760 B/op 18 allocs/op
-BenchmarkDenco_GithubParam 2000000 629 ns/op 128 B/op 1 allocs/op
-BenchmarkEcho_GithubParam 2000000 653 ns/op 32 B/op 1 allocs/op
-BenchmarkGin_GithubParam 5000000 255 ns/op 0 B/op 0 allocs/op
-BenchmarkGocraftWeb_GithubParam 1000000 3145 ns/op 712 B/op 9 allocs/op
-BenchmarkGoji_GithubParam 1000000 1916 ns/op 336 B/op 2 allocs/op
-BenchmarkGojiv2_GithubParam 1000000 3975 ns/op 1024 B/op 10 allocs/op
-BenchmarkGoJsonRest_GithubParam 300000 4134 ns/op 713 B/op 14 allocs/op
-BenchmarkGoRestful_GithubParam 50000 30782 ns/op 2360 B/op 21 allocs/op
-BenchmarkGorillaMux_GithubParam 100000 17148 ns/op 1088 B/op 11 allocs/op
-BenchmarkHttpRouter_GithubParam 3000000 523 ns/op 96 B/op 1 allocs/op
-BenchmarkHttpTreeMux_GithubParam 1000000 1671 ns/op 384 B/op 4 allocs/op
-BenchmarkKocha_GithubParam 1000000 1021 ns/op 128 B/op 5 allocs/op
-BenchmarkLARS_GithubParam 5000000 283 ns/op 0 B/op 0 allocs/op
-BenchmarkMacaron_GithubParam 500000 4270 ns/op 1056 B/op 10 allocs/op
-BenchmarkMartini_GithubParam 100000 21728 ns/op 1152 B/op 11 allocs/op
-BenchmarkPat_GithubParam 200000 11208 ns/op 2464 B/op 48 allocs/op
-BenchmarkPossum_GithubParam 1000000 2334 ns/op 560 B/op 6 allocs/op
-BenchmarkR2router_GithubParam 1000000 1487 ns/op 432 B/op 5 allocs/op
-BenchmarkRivet_GithubParam 2000000 782 ns/op 96 B/op 1 allocs/op
-BenchmarkTango_GithubParam 1000000 2653 ns/op 344 B/op 8 allocs/op
-BenchmarkTigerTonic_GithubParam 300000 14073 ns/op 1440 B/op 24 allocs/op
-BenchmarkTraffic_GithubParam 50000 29164 ns/op 5992 B/op 52 allocs/op
-BenchmarkVulcan_GithubParam 1000000 2529 ns/op 98 B/op 3 allocs/op
-BenchmarkAce_GithubAll 10000 134059 ns/op 13792 B/op 167 allocs/op
-BenchmarkBear_GithubAll 5000 534445 ns/op 86448 B/op 943 allocs/op
-BenchmarkBeego_GithubAll 3000 592444 ns/op 74705 B/op 812 allocs/op
-BenchmarkBone_GithubAll 200 6957308 ns/op 698784 B/op 8453 allocs/op
-BenchmarkDenco_GithubAll 10000 158819 ns/op 20224 B/op 167 allocs/op
-BenchmarkEcho_GithubAll 10000 154700 ns/op 6496 B/op 203 allocs/op
-BenchmarkGin_GithubAll 30000 48375 ns/op 0 B/op 0 allocs/op
-BenchmarkGocraftWeb_GithubAll 3000 570806 ns/op 131656 B/op 1686 allocs/op
-BenchmarkGoji_GithubAll 2000 818034 ns/op 56112 B/op 334 allocs/op
-BenchmarkGojiv2_GithubAll 2000 1213973 ns/op 274768 B/op 3712 allocs/op
-BenchmarkGoJsonRest_GithubAll 2000 785796 ns/op 134371 B/op 2737 allocs/op
-BenchmarkGoRestful_GithubAll 300 5238188 ns/op 689672 B/op 4519 allocs/op
-BenchmarkGorillaMux_GithubAll 100 10257726 ns/op 211840 B/op 2272 allocs/op
-BenchmarkHttpRouter_GithubAll 20000 105414 ns/op 13792 B/op 167 allocs/op
-BenchmarkHttpTreeMux_GithubAll 10000 319934 ns/op 65856 B/op 671 allocs/op
-BenchmarkKocha_GithubAll 10000 209442 ns/op 23304 B/op 843 allocs/op
-BenchmarkLARS_GithubAll 20000 62565 ns/op 0 B/op 0 allocs/op
-BenchmarkMacaron_GithubAll 2000 1161270 ns/op 204194 B/op 2000 allocs/op
-BenchmarkMartini_GithubAll 200 9991713 ns/op 226549 B/op 2325 allocs/op
-BenchmarkPat_GithubAll 200 5590793 ns/op 1499568 B/op 27435 allocs/op
-BenchmarkPossum_GithubAll 10000 319768 ns/op 84448 B/op 609 allocs/op
-BenchmarkR2router_GithubAll 10000 305134 ns/op 77328 B/op 979 allocs/op
-BenchmarkRivet_GithubAll 10000 132134 ns/op 16272 B/op 167 allocs/op
-BenchmarkTango_GithubAll 3000 552754 ns/op 63826 B/op 1618 allocs/op
-BenchmarkTigerTonic_GithubAll 1000 1439483 ns/op 239104 B/op 5374 allocs/op
-BenchmarkTraffic_GithubAll 100 11383067 ns/op 2659329 B/op 21848 allocs/op
-BenchmarkVulcan_GithubAll 5000 394253 ns/op 19894 B/op 609 allocs/op
+BenchmarkAce_GithubStatic 15542612 75.9 ns/op 0 B/op 0 allocs/op
+BenchmarkAero_GithubStatic 24777151 48.5 ns/op 0 B/op 0 allocs/op
+BenchmarkBear_GithubStatic 2788894 435 ns/op 120 B/op 3 allocs/op
+BenchmarkBeego_GithubStatic 1000000 1064 ns/op 352 B/op 3 allocs/op
+BenchmarkBone_GithubStatic 93507 12838 ns/op 2880 B/op 60 allocs/op
+BenchmarkChi_GithubStatic 1387743 860 ns/op 432 B/op 3 allocs/op
+BenchmarkDenco_GithubStatic 39384996 30.4 ns/op 0 B/op 0 allocs/op
+BenchmarkEcho_GithubStatic 12076382 99.1 ns/op 0 B/op 0 allocs/op
+BenchmarkGocraftWeb_GithubStatic 1596495 756 ns/op 296 B/op 5 allocs/op
+BenchmarkGoji_GithubStatic 6364876 189 ns/op 0 B/op 0 allocs/op
+BenchmarkGojiv2_GithubStatic 550202 2098 ns/op 1312 B/op 10 allocs/op
+BenchmarkGoRestful_GithubStatic 102183 12552 ns/op 4256 B/op 13 allocs/op
+BenchmarkGoJsonRest_GithubStatic 1000000 1029 ns/op 329 B/op 11 allocs/op
+BenchmarkGorillaMux_GithubStatic 255552 5190 ns/op 976 B/op 9 allocs/op
+BenchmarkGowwwRouter_GithubStatic 15531916 77.1 ns/op 0 B/op 0 allocs/op
+BenchmarkHttpRouter_GithubStatic 27920724 43.1 ns/op 0 B/op 0 allocs/op
+BenchmarkHttpTreeMux_GithubStatic 21448953 55.8 ns/op 0 B/op 0 allocs/op
+BenchmarkKocha_GithubStatic 21405310 56.0 ns/op 0 B/op 0 allocs/op
+BenchmarkLARS_GithubStatic 13625156 89.0 ns/op 0 B/op 0 allocs/op
+BenchmarkMacaron_GithubStatic 1000000 1747 ns/op 736 B/op 8 allocs/op
+BenchmarkMartini_GithubStatic 187186 7326 ns/op 768 B/op 9 allocs/op
+BenchmarkPat_GithubStatic 109143 11563 ns/op 3648 B/op 76 allocs/op
+BenchmarkPossum_GithubStatic 1575898 770 ns/op 416 B/op 3 allocs/op
+BenchmarkR2router_GithubStatic 3046231 404 ns/op 144 B/op 4 allocs/op
+BenchmarkRivet_GithubStatic 11484826 105 ns/op 0 B/op 0 allocs/op
+BenchmarkTango_GithubStatic 1000000 1153 ns/op 248 B/op 8 allocs/op
+BenchmarkTigerTonic_GithubStatic 4929780 249 ns/op 48 B/op 1 allocs/op
+BenchmarkTraffic_GithubStatic 106351 11819 ns/op 4664 B/op 90 allocs/op
+BenchmarkVulcan_GithubStatic 1613271 722 ns/op 98 B/op 3 allocs/op
+BenchmarkAce_GithubParam 8386032 143 ns/op 0 B/op 0 allocs/op
+BenchmarkAero_GithubParam 11816200 102 ns/op 0 B/op 0 allocs/op
+BenchmarkBear_GithubParam 1000000 1012 ns/op 496 B/op 5 allocs/op
+BenchmarkBeego_GithubParam 1000000 1157 ns/op 352 B/op 3 allocs/op
+BenchmarkBone_GithubParam 184653 6912 ns/op 1888 B/op 19 allocs/op
+BenchmarkChi_GithubParam 1000000 1102 ns/op 432 B/op 3 allocs/op
+BenchmarkDenco_GithubParam 3484798 352 ns/op 128 B/op 1 allocs/op
+BenchmarkEcho_GithubParam 6337380 189 ns/op 0 B/op 0 allocs/op
+BenchmarkGin_GithubParam 9132032 131 ns/op 0 B/op 0 allocs/op
+BenchmarkGocraftWeb_GithubParam 1000000 1446 ns/op 712 B/op 9 allocs/op
+BenchmarkGoji_GithubParam 1248640 977 ns/op 336 B/op 2 allocs/op
+BenchmarkGojiv2_GithubParam 383233 2784 ns/op 1408 B/op 13 allocs/op
+BenchmarkGoJsonRest_GithubParam 1000000 1991 ns/op 713 B/op 14 allocs/op
+BenchmarkGoRestful_GithubParam 76414 16015 ns/op 4352 B/op 16 allocs/op
+BenchmarkGorillaMux_GithubParam 150026 7663 ns/op 1296 B/op 10 allocs/op
+BenchmarkGowwwRouter_GithubParam 1592044 751 ns/op 432 B/op 3 allocs/op
+BenchmarkHttpRouter_GithubParam 10420628 115 ns/op 0 B/op 0 allocs/op
+BenchmarkHttpTreeMux_GithubParam 1403755 835 ns/op 384 B/op 4 allocs/op
+BenchmarkKocha_GithubParam 2286170 533 ns/op 128 B/op 5 allocs/op
+BenchmarkLARS_GithubParam 9540374 129 ns/op 0 B/op 0 allocs/op
+BenchmarkMacaron_GithubParam 533154 2742 ns/op 1072 B/op 10 allocs/op
+BenchmarkMartini_GithubParam 119397 9638 ns/op 1152 B/op 11 allocs/op
+BenchmarkPat_GithubParam 150675 8858 ns/op 2408 B/op 48 allocs/op
+BenchmarkPossum_GithubParam 1000000 1001 ns/op 496 B/op 5 allocs/op
+BenchmarkR2router_GithubParam 1602886 761 ns/op 432 B/op 5 allocs/op
+BenchmarkRivet_GithubParam 2986579 409 ns/op 96 B/op 1 allocs/op
+BenchmarkTango_GithubParam 1000000 1356 ns/op 344 B/op 8 allocs/op
+BenchmarkTigerTonic_GithubParam 388899 3429 ns/op 1176 B/op 22 allocs/op
+BenchmarkTraffic_GithubParam 123160 9734 ns/op 2816 B/op 40 allocs/op
+BenchmarkVulcan_GithubParam 1000000 1138 ns/op 98 B/op 3 allocs/op
+BenchmarkAce_GithubAll 40543 29670 ns/op 0 B/op 0 allocs/op
+BenchmarkAero_GithubAll 57632 20648 ns/op 0 B/op 0 allocs/op
+BenchmarkBear_GithubAll 9234 216179 ns/op 86448 B/op 943 allocs/op
+BenchmarkBeego_GithubAll 7407 243496 ns/op 71456 B/op 609 allocs/op
+BenchmarkBone_GithubAll 420 2922835 ns/op 720160 B/op 8620 allocs/op
+BenchmarkChi_GithubAll 7620 238331 ns/op 87696 B/op 609 allocs/op
+BenchmarkDenco_GithubAll 18355 64494 ns/op 20224 B/op 167 allocs/op
+BenchmarkEcho_GithubAll 31251 38479 ns/op 0 B/op 0 allocs/op
+BenchmarkGin_GithubAll 43550 27364 ns/op 0 B/op 0 allocs/op
+BenchmarkGocraftWeb_GithubAll 4117 300062 ns/op 131656 B/op 1686 allocs/op
+BenchmarkGoji_GithubAll 3274 416158 ns/op 56112 B/op 334 allocs/op
+BenchmarkGojiv2_GithubAll 1402 870518 ns/op 352720 B/op 4321 allocs/op
+BenchmarkGoJsonRest_GithubAll 2976 401507 ns/op 134371 B/op 2737 allocs/op
+BenchmarkGoRestful_GithubAll 410 2913158 ns/op 910144 B/op 2938 allocs/op
+BenchmarkGorillaMux_GithubAll 346 3384987 ns/op 251650 B/op 1994 allocs/op
+BenchmarkGowwwRouter_GithubAll 10000 143025 ns/op 72144 B/op 501 allocs/op
+BenchmarkHttpRouter_GithubAll 55938 21360 ns/op 0 B/op 0 allocs/op
+BenchmarkHttpTreeMux_GithubAll 10000 153944 ns/op 65856 B/op 671 allocs/op
+BenchmarkKocha_GithubAll 10000 106315 ns/op 23304 B/op 843 allocs/op
+BenchmarkLARS_GithubAll 47779 25084 ns/op 0 B/op 0 allocs/op
+BenchmarkMacaron_GithubAll 3266 371907 ns/op 149409 B/op 1624 allocs/op
+BenchmarkMartini_GithubAll 331 3444706 ns/op 226551 B/op 2325 allocs/op
+BenchmarkPat_GithubAll 273 4381818 ns/op 1483152 B/op 26963 allocs/op
+BenchmarkPossum_GithubAll 10000 164367 ns/op 84448 B/op 609 allocs/op
+BenchmarkR2router_GithubAll 10000 160220 ns/op 77328 B/op 979 allocs/op
+BenchmarkRivet_GithubAll 14625 82453 ns/op 16272 B/op 167 allocs/op
+BenchmarkTango_GithubAll 6255 279611 ns/op 63826 B/op 1618 allocs/op
+BenchmarkTigerTonic_GithubAll 2008 687874 ns/op 193856 B/op 4474 allocs/op
+BenchmarkTraffic_GithubAll 355 3478508 ns/op 820744 B/op 14114 allocs/op
+BenchmarkVulcan_GithubAll 6885 193333 ns/op 19894 B/op 609 allocs/op
```
## Google+
-```
-BenchmarkGin_GPlusStatic 10000000 183 ns/op 0 B/op 0 allocs/op
+```sh
+BenchmarkGin_GPlusStatic 19247326 62.2 ns/op 0 B/op 0 allocs/op
-BenchmarkAce_GPlusStatic 5000000 276 ns/op 0 B/op 0 allocs/op
-BenchmarkBear_GPlusStatic 2000000 652 ns/op 104 B/op 3 allocs/op
-BenchmarkBeego_GPlusStatic 1000000 2239 ns/op 368 B/op 4 allocs/op
-BenchmarkBone_GPlusStatic 5000000 380 ns/op 32 B/op 1 allocs/op
-BenchmarkDenco_GPlusStatic 30000000 45.8 ns/op 0 B/op 0 allocs/op
-BenchmarkEcho_GPlusStatic 5000000 338 ns/op 32 B/op 1 allocs/op
-BenchmarkGocraftWeb_GPlusStatic 1000000 1158 ns/op 280 B/op 5 allocs/op
-BenchmarkGoji_GPlusStatic 5000000 331 ns/op 0 B/op 0 allocs/op
-BenchmarkGojiv2_GPlusStatic 1000000 2106 ns/op 928 B/op 7 allocs/op
-BenchmarkGoJsonRest_GPlusStatic 1000000 1626 ns/op 329 B/op 11 allocs/op
-BenchmarkGoRestful_GPlusStatic 300000 7598 ns/op 1976 B/op 20 allocs/op
-BenchmarkGorillaMux_GPlusStatic 1000000 2629 ns/op 736 B/op 10 allocs/op
-BenchmarkHttpRouter_GPlusStatic 30000000 52.5 ns/op 0 B/op 0 allocs/op
-BenchmarkHttpTreeMux_GPlusStatic 20000000 85.8 ns/op 0 B/op 0 allocs/op
-BenchmarkKocha_GPlusStatic 20000000 89.2 ns/op 0 B/op 0 allocs/op
-BenchmarkLARS_GPlusStatic 10000000 162 ns/op 0 B/op 0 allocs/op
-BenchmarkMacaron_GPlusStatic 500000 3479 ns/op 768 B/op 9 allocs/op
-BenchmarkMartini_GPlusStatic 200000 9092 ns/op 768 B/op 9 allocs/op
-BenchmarkPat_GPlusStatic 3000000 493 ns/op 96 B/op 2 allocs/op
-BenchmarkPossum_GPlusStatic 1000000 1467 ns/op 416 B/op 3 allocs/op
-BenchmarkR2router_GPlusStatic 2000000 788 ns/op 144 B/op 4 allocs/op
-BenchmarkRivet_GPlusStatic 20000000 114 ns/op 0 B/op 0 allocs/op
-BenchmarkTango_GPlusStatic 1000000 1534 ns/op 200 B/op 8 allocs/op
-BenchmarkTigerTonic_GPlusStatic 5000000 282 ns/op 32 B/op 1 allocs/op
-BenchmarkTraffic_GPlusStatic 500000 3798 ns/op 1192 B/op 15 allocs/op
-BenchmarkVulcan_GPlusStatic 2000000 1125 ns/op 98 B/op 3 allocs/op
-BenchmarkAce_GPlusParam 3000000 528 ns/op 64 B/op 1 allocs/op
-BenchmarkBear_GPlusParam 1000000 1570 ns/op 480 B/op 5 allocs/op
-BenchmarkBeego_GPlusParam 1000000 2369 ns/op 368 B/op 4 allocs/op
-BenchmarkBone_GPlusParam 1000000 2028 ns/op 688 B/op 5 allocs/op
-BenchmarkDenco_GPlusParam 5000000 385 ns/op 64 B/op 1 allocs/op
-BenchmarkEcho_GPlusParam 3000000 441 ns/op 32 B/op 1 allocs/op
-BenchmarkGin_GPlusParam 10000000 174 ns/op 0 B/op 0 allocs/op
-BenchmarkGocraftWeb_GPlusParam 1000000 2033 ns/op 648 B/op 8 allocs/op
-BenchmarkGoji_GPlusParam 1000000 1399 ns/op 336 B/op 2 allocs/op
-BenchmarkGojiv2_GPlusParam 1000000 2641 ns/op 944 B/op 8 allocs/op
-BenchmarkGoJsonRest_GPlusParam 1000000 2824 ns/op 649 B/op 13 allocs/op
-BenchmarkGoRestful_GPlusParam 200000 8875 ns/op 2296 B/op 21 allocs/op
-BenchmarkGorillaMux_GPlusParam 200000 6291 ns/op 1056 B/op 11 allocs/op
-BenchmarkHttpRouter_GPlusParam 5000000 316 ns/op 64 B/op 1 allocs/op
-BenchmarkHttpTreeMux_GPlusParam 1000000 1129 ns/op 352 B/op 3 allocs/op
-BenchmarkKocha_GPlusParam 3000000 538 ns/op 56 B/op 3 allocs/op
-BenchmarkLARS_GPlusParam 10000000 198 ns/op 0 B/op 0 allocs/op
-BenchmarkMacaron_GPlusParam 500000 3554 ns/op 1056 B/op 10 allocs/op
-BenchmarkMartini_GPlusParam 200000 9831 ns/op 1072 B/op 10 allocs/op
-BenchmarkPat_GPlusParam 1000000 2706 ns/op 688 B/op 12 allocs/op
-BenchmarkPossum_GPlusParam 1000000 2297 ns/op 560 B/op 6 allocs/op
-BenchmarkR2router_GPlusParam 1000000 1318 ns/op 432 B/op 5 allocs/op
-BenchmarkRivet_GPlusParam 5000000 399 ns/op 48 B/op 1 allocs/op
-BenchmarkTango_GPlusParam 1000000 2070 ns/op 264 B/op 8 allocs/op
-BenchmarkTigerTonic_GPlusParam 500000 4853 ns/op 1056 B/op 17 allocs/op
-BenchmarkTraffic_GPlusParam 200000 8278 ns/op 1976 B/op 21 allocs/op
-BenchmarkVulcan_GPlusParam 1000000 1243 ns/op 98 B/op 3 allocs/op
-BenchmarkAce_GPlus2Params 3000000 549 ns/op 64 B/op 1 allocs/op
-BenchmarkBear_GPlus2Params 1000000 2112 ns/op 496 B/op 5 allocs/op
-BenchmarkBeego_GPlus2Params 500000 2750 ns/op 368 B/op 4 allocs/op
-BenchmarkBone_GPlus2Params 300000 7032 ns/op 1040 B/op 9 allocs/op
-BenchmarkDenco_GPlus2Params 3000000 502 ns/op 64 B/op 1 allocs/op
-BenchmarkEcho_GPlus2Params 3000000 641 ns/op 32 B/op 1 allocs/op
-BenchmarkGin_GPlus2Params 5000000 250 ns/op 0 B/op 0 allocs/op
-BenchmarkGocraftWeb_GPlus2Params 1000000 2681 ns/op 712 B/op 9 allocs/op
-BenchmarkGoji_GPlus2Params 1000000 1926 ns/op 336 B/op 2 allocs/op
-BenchmarkGojiv2_GPlus2Params 500000 3996 ns/op 1024 B/op 11 allocs/op
-BenchmarkGoJsonRest_GPlus2Params 500000 3886 ns/op 713 B/op 14 allocs/op
-BenchmarkGoRestful_GPlus2Params 200000 10376 ns/op 2360 B/op 21 allocs/op
-BenchmarkGorillaMux_GPlus2Params 100000 14162 ns/op 1088 B/op 11 allocs/op
-BenchmarkHttpRouter_GPlus2Params 5000000 336 ns/op 64 B/op 1 allocs/op
-BenchmarkHttpTreeMux_GPlus2Params 1000000 1523 ns/op 384 B/op 4 allocs/op
-BenchmarkKocha_GPlus2Params 2000000 970 ns/op 128 B/op 5 allocs/op
-BenchmarkLARS_GPlus2Params 5000000 238 ns/op 0 B/op 0 allocs/op
-BenchmarkMacaron_GPlus2Params 500000 4016 ns/op 1056 B/op 10 allocs/op
-BenchmarkMartini_GPlus2Params 100000 21253 ns/op 1200 B/op 13 allocs/op
-BenchmarkPat_GPlus2Params 200000 8632 ns/op 2256 B/op 34 allocs/op
-BenchmarkPossum_GPlus2Params 1000000 2171 ns/op 560 B/op 6 allocs/op
-BenchmarkR2router_GPlus2Params 1000000 1340 ns/op 432 B/op 5 allocs/op
-BenchmarkRivet_GPlus2Params 3000000 557 ns/op 96 B/op 1 allocs/op
-BenchmarkTango_GPlus2Params 1000000 2186 ns/op 344 B/op 8 allocs/op
-BenchmarkTigerTonic_GPlus2Params 200000 9060 ns/op 1488 B/op 24 allocs/op
-BenchmarkTraffic_GPlus2Params 100000 20324 ns/op 3272 B/op 31 allocs/op
-BenchmarkVulcan_GPlus2Params 1000000 2039 ns/op 98 B/op 3 allocs/op
-BenchmarkAce_GPlusAll 300000 6603 ns/op 640 B/op 11 allocs/op
-BenchmarkBear_GPlusAll 100000 22363 ns/op 5488 B/op 61 allocs/op
-BenchmarkBeego_GPlusAll 50000 38757 ns/op 4784 B/op 52 allocs/op
-BenchmarkBone_GPlusAll 20000 54916 ns/op 10336 B/op 98 allocs/op
-BenchmarkDenco_GPlusAll 300000 4959 ns/op 672 B/op 11 allocs/op
-BenchmarkEcho_GPlusAll 200000 6558 ns/op 416 B/op 13 allocs/op
-BenchmarkGin_GPlusAll 500000 2757 ns/op 0 B/op 0 allocs/op
-BenchmarkGocraftWeb_GPlusAll 50000 34615 ns/op 8040 B/op 103 allocs/op
-BenchmarkGoji_GPlusAll 100000 16002 ns/op 3696 B/op 22 allocs/op
-BenchmarkGojiv2_GPlusAll 50000 35060 ns/op 12624 B/op 115 allocs/op
-BenchmarkGoJsonRest_GPlusAll 50000 41479 ns/op 8117 B/op 170 allocs/op
-BenchmarkGoRestful_GPlusAll 10000 131653 ns/op 32024 B/op 275 allocs/op
-BenchmarkGorillaMux_GPlusAll 10000 101380 ns/op 13296 B/op 142 allocs/op
-BenchmarkHttpRouter_GPlusAll 500000 3711 ns/op 640 B/op 11 allocs/op
-BenchmarkHttpTreeMux_GPlusAll 100000 14438 ns/op 4032 B/op 38 allocs/op
-BenchmarkKocha_GPlusAll 200000 8039 ns/op 976 B/op 43 allocs/op
-BenchmarkLARS_GPlusAll 500000 2630 ns/op 0 B/op 0 allocs/op
-BenchmarkMacaron_GPlusAll 30000 51123 ns/op 13152 B/op 128 allocs/op
-BenchmarkMartini_GPlusAll 10000 176157 ns/op 14016 B/op 145 allocs/op
-BenchmarkPat_GPlusAll 20000 69911 ns/op 16576 B/op 298 allocs/op
-BenchmarkPossum_GPlusAll 100000 20716 ns/op 5408 B/op 39 allocs/op
-BenchmarkR2router_GPlusAll 100000 17463 ns/op 5040 B/op 63 allocs/op
-BenchmarkRivet_GPlusAll 300000 5142 ns/op 768 B/op 11 allocs/op
-BenchmarkTango_GPlusAll 50000 27321 ns/op 3656 B/op 104 allocs/op
-BenchmarkTigerTonic_GPlusAll 20000 77597 ns/op 14512 B/op 288 allocs/op
-BenchmarkTraffic_GPlusAll 10000 151406 ns/op 37360 B/op 392 allocs/op
-BenchmarkVulcan_GPlusAll 100000 18555 ns/op 1274 B/op 39 allocs/op
+BenchmarkAce_GPlusStatic 20235060 59.2 ns/op 0 B/op 0 allocs/op
+BenchmarkAero_GPlusStatic 31978935 37.6 ns/op 0 B/op 0 allocs/op
+BenchmarkBear_GPlusStatic 3516523 341 ns/op 104 B/op 3 allocs/op
+BenchmarkBeego_GPlusStatic 1212036 991 ns/op 352 B/op 3 allocs/op
+BenchmarkBone_GPlusStatic 6736242 183 ns/op 32 B/op 1 allocs/op
+BenchmarkChi_GPlusStatic 1490640 814 ns/op 432 B/op 3 allocs/op
+BenchmarkDenco_GPlusStatic 55006856 21.8 ns/op 0 B/op 0 allocs/op
+BenchmarkEcho_GPlusStatic 17688258 67.9 ns/op 0 B/op 0 allocs/op
+BenchmarkGocraftWeb_GPlusStatic 1829181 666 ns/op 280 B/op 5 allocs/op
+BenchmarkGoji_GPlusStatic 9147451 130 ns/op 0 B/op 0 allocs/op
+BenchmarkGojiv2_GPlusStatic 594015 2063 ns/op 1312 B/op 10 allocs/op
+BenchmarkGoJsonRest_GPlusStatic 1264906 950 ns/op 329 B/op 11 allocs/op
+BenchmarkGoRestful_GPlusStatic 231558 5341 ns/op 3872 B/op 13 allocs/op
+BenchmarkGorillaMux_GPlusStatic 908418 1809 ns/op 976 B/op 9 allocs/op
+BenchmarkGowwwRouter_GPlusStatic 40684604 29.5 ns/op 0 B/op 0 allocs/op
+BenchmarkHttpRouter_GPlusStatic 46742804 25.7 ns/op 0 B/op 0 allocs/op
+BenchmarkHttpTreeMux_GPlusStatic 32567161 36.9 ns/op 0 B/op 0 allocs/op
+BenchmarkKocha_GPlusStatic 33800060 35.3 ns/op 0 B/op 0 allocs/op
+BenchmarkLARS_GPlusStatic 20431858 60.0 ns/op 0 B/op 0 allocs/op
+BenchmarkMacaron_GPlusStatic 1000000 1745 ns/op 736 B/op 8 allocs/op
+BenchmarkMartini_GPlusStatic 442248 3619 ns/op 768 B/op 9 allocs/op
+BenchmarkPat_GPlusStatic 4328004 292 ns/op 96 B/op 2 allocs/op
+BenchmarkPossum_GPlusStatic 1570753 763 ns/op 416 B/op 3 allocs/op
+BenchmarkR2router_GPlusStatic 3339474 355 ns/op 144 B/op 4 allocs/op
+BenchmarkRivet_GPlusStatic 18570961 64.7 ns/op 0 B/op 0 allocs/op
+BenchmarkTango_GPlusStatic 1388702 860 ns/op 200 B/op 8 allocs/op
+BenchmarkTigerTonic_GPlusStatic 7803543 159 ns/op 32 B/op 1 allocs/op
+BenchmarkTraffic_GPlusStatic 878605 2171 ns/op 1112 B/op 16 allocs/op
+BenchmarkVulcan_GPlusStatic 2742446 437 ns/op 98 B/op 3 allocs/op
+BenchmarkAce_GPlusParam 11626975 105 ns/op 0 B/op 0 allocs/op
+BenchmarkAero_GPlusParam 16914322 71.6 ns/op 0 B/op 0 allocs/op
+BenchmarkBear_GPlusParam 1405173 832 ns/op 480 B/op 5 allocs/op
+BenchmarkBeego_GPlusParam 1000000 1075 ns/op 352 B/op 3 allocs/op
+BenchmarkBone_GPlusParam 1000000 1557 ns/op 816 B/op 6 allocs/op
+BenchmarkChi_GPlusParam 1347926 894 ns/op 432 B/op 3 allocs/op
+BenchmarkDenco_GPlusParam 5513000 212 ns/op 64 B/op 1 allocs/op
+BenchmarkEcho_GPlusParam 11884383 101 ns/op 0 B/op 0 allocs/op
+BenchmarkGin_GPlusParam 12898952 93.1 ns/op 0 B/op 0 allocs/op
+BenchmarkGocraftWeb_GPlusParam 1000000 1194 ns/op 648 B/op 8 allocs/op
+BenchmarkGoji_GPlusParam 1857229 645 ns/op 336 B/op 2 allocs/op
+BenchmarkGojiv2_GPlusParam 520939 2322 ns/op 1328 B/op 11 allocs/op
+BenchmarkGoJsonRest_GPlusParam 1000000 1536 ns/op 649 B/op 13 allocs/op
+BenchmarkGoRestful_GPlusParam 205449 5800 ns/op 4192 B/op 14 allocs/op
+BenchmarkGorillaMux_GPlusParam 395310 3188 ns/op 1280 B/op 10 allocs/op
+BenchmarkGowwwRouter_GPlusParam 1851798 667 ns/op 432 B/op 3 allocs/op
+BenchmarkHttpRouter_GPlusParam 18420789 65.2 ns/op 0 B/op 0 allocs/op
+BenchmarkHttpTreeMux_GPlusParam 1878463 629 ns/op 352 B/op 3 allocs/op
+BenchmarkKocha_GPlusParam 4495610 273 ns/op 56 B/op 3 allocs/op
+BenchmarkLARS_GPlusParam 14615976 83.2 ns/op 0 B/op 0 allocs/op
+BenchmarkMacaron_GPlusParam 584145 2549 ns/op 1072 B/op 10 allocs/op
+BenchmarkMartini_GPlusParam 250501 4583 ns/op 1072 B/op 10 allocs/op
+BenchmarkPat_GPlusParam 1000000 1645 ns/op 576 B/op 11 allocs/op
+BenchmarkPossum_GPlusParam 1000000 1008 ns/op 496 B/op 5 allocs/op
+BenchmarkR2router_GPlusParam 1708191 688 ns/op 432 B/op 5 allocs/op
+BenchmarkRivet_GPlusParam 5795014 211 ns/op 48 B/op 1 allocs/op
+BenchmarkTango_GPlusParam 1000000 1091 ns/op 264 B/op 8 allocs/op
+BenchmarkTigerTonic_GPlusParam 760221 2489 ns/op 856 B/op 16 allocs/op
+BenchmarkTraffic_GPlusParam 309774 4039 ns/op 1872 B/op 21 allocs/op
+BenchmarkVulcan_GPlusParam 1935730 623 ns/op 98 B/op 3 allocs/op
+BenchmarkAce_GPlus2Params 9158314 134 ns/op 0 B/op 0 allocs/op
+BenchmarkAero_GPlus2Params 11300517 107 ns/op 0 B/op 0 allocs/op
+BenchmarkBear_GPlus2Params 1239238 961 ns/op 496 B/op 5 allocs/op
+BenchmarkBeego_GPlus2Params 1000000 1202 ns/op 352 B/op 3 allocs/op
+BenchmarkBone_GPlus2Params 335576 3725 ns/op 1168 B/op 10 allocs/op
+BenchmarkChi_GPlus2Params 1000000 1014 ns/op 432 B/op 3 allocs/op
+BenchmarkDenco_GPlus2Params 4394598 280 ns/op 64 B/op 1 allocs/op
+BenchmarkEcho_GPlus2Params 7851861 154 ns/op 0 B/op 0 allocs/op
+BenchmarkGin_GPlus2Params 9958588 120 ns/op 0 B/op 0 allocs/op
+BenchmarkGocraftWeb_GPlus2Params 1000000 1433 ns/op 712 B/op 9 allocs/op
+BenchmarkGoji_GPlus2Params 1325134 909 ns/op 336 B/op 2 allocs/op
+BenchmarkGojiv2_GPlus2Params 405955 2870 ns/op 1408 B/op 14 allocs/op
+BenchmarkGoJsonRest_GPlus2Params 977038 1987 ns/op 713 B/op 14 allocs/op
+BenchmarkGoRestful_GPlus2Params 205018 6142 ns/op 4384 B/op 16 allocs/op
+BenchmarkGorillaMux_GPlus2Params 205641 6015 ns/op 1296 B/op 10 allocs/op
+BenchmarkGowwwRouter_GPlus2Params 1748542 684 ns/op 432 B/op 3 allocs/op
+BenchmarkHttpRouter_GPlus2Params 14047102 87.7 ns/op 0 B/op 0 allocs/op
+BenchmarkHttpTreeMux_GPlus2Params 1418673 828 ns/op 384 B/op 4 allocs/op
+BenchmarkKocha_GPlus2Params 2334562 520 ns/op 128 B/op 5 allocs/op
+BenchmarkLARS_GPlus2Params 11954094 101 ns/op 0 B/op 0 allocs/op
+BenchmarkMacaron_GPlus2Params 491552 2890 ns/op 1072 B/op 10 allocs/op
+BenchmarkMartini_GPlus2Params 120532 9545 ns/op 1200 B/op 13 allocs/op
+BenchmarkPat_GPlus2Params 194739 6766 ns/op 2168 B/op 33 allocs/op
+BenchmarkPossum_GPlus2Params 1201224 1009 ns/op 496 B/op 5 allocs/op
+BenchmarkR2router_GPlus2Params 1575535 756 ns/op 432 B/op 5 allocs/op
+BenchmarkRivet_GPlus2Params 3698930 325 ns/op 96 B/op 1 allocs/op
+BenchmarkTango_GPlus2Params 1000000 1212 ns/op 344 B/op 8 allocs/op
+BenchmarkTigerTonic_GPlus2Params 349350 3660 ns/op 1200 B/op 22 allocs/op
+BenchmarkTraffic_GPlus2Params 169714 7862 ns/op 2248 B/op 28 allocs/op
+BenchmarkVulcan_GPlus2Params 1222288 974 ns/op 98 B/op 3 allocs/op
+BenchmarkAce_GPlusAll 845606 1398 ns/op 0 B/op 0 allocs/op
+BenchmarkAero_GPlusAll 1000000 1009 ns/op 0 B/op 0 allocs/op
+BenchmarkBear_GPlusAll 103830 11386 ns/op 5488 B/op 61 allocs/op
+BenchmarkBeego_GPlusAll 82653 14784 ns/op 4576 B/op 39 allocs/op
+BenchmarkBone_GPlusAll 36601 33123 ns/op 11744 B/op 109 allocs/op
+BenchmarkChi_GPlusAll 95264 12831 ns/op 5616 B/op 39 allocs/op
+BenchmarkDenco_GPlusAll 567681 2950 ns/op 672 B/op 11 allocs/op
+BenchmarkEcho_GPlusAll 720366 1665 ns/op 0 B/op 0 allocs/op
+BenchmarkGin_GPlusAll 1000000 1185 ns/op 0 B/op 0 allocs/op
+BenchmarkGocraftWeb_GPlusAll 71575 16365 ns/op 8040 B/op 103 allocs/op
+BenchmarkGoji_GPlusAll 136352 9191 ns/op 3696 B/op 22 allocs/op
+BenchmarkGojiv2_GPlusAll 38006 31802 ns/op 17616 B/op 154 allocs/op
+BenchmarkGoJsonRest_GPlusAll 57238 21561 ns/op 8117 B/op 170 allocs/op
+BenchmarkGoRestful_GPlusAll 15147 79276 ns/op 55520 B/op 192 allocs/op
+BenchmarkGorillaMux_GPlusAll 24446 48410 ns/op 16112 B/op 128 allocs/op
+BenchmarkGowwwRouter_GPlusAll 150112 7770 ns/op 4752 B/op 33 allocs/op
+BenchmarkHttpRouter_GPlusAll 1367820 878 ns/op 0 B/op 0 allocs/op
+BenchmarkHttpTreeMux_GPlusAll 166628 8004 ns/op 4032 B/op 38 allocs/op
+BenchmarkKocha_GPlusAll 265694 4570 ns/op 976 B/op 43 allocs/op
+BenchmarkLARS_GPlusAll 1000000 1068 ns/op 0 B/op 0 allocs/op
+BenchmarkMacaron_GPlusAll 54564 23305 ns/op 9568 B/op 104 allocs/op
+BenchmarkMartini_GPlusAll 16274 73845 ns/op 14016 B/op 145 allocs/op
+BenchmarkPat_GPlusAll 27181 44478 ns/op 15264 B/op 271 allocs/op
+BenchmarkPossum_GPlusAll 122587 10277 ns/op 5408 B/op 39 allocs/op
+BenchmarkR2router_GPlusAll 130137 9297 ns/op 5040 B/op 63 allocs/op
+BenchmarkRivet_GPlusAll 532438 3323 ns/op 768 B/op 11 allocs/op
+BenchmarkTango_GPlusAll 86054 14531 ns/op 3656 B/op 104 allocs/op
+BenchmarkTigerTonic_GPlusAll 33936 35356 ns/op 11600 B/op 242 allocs/op
+BenchmarkTraffic_GPlusAll 17833 68181 ns/op 26248 B/op 341 allocs/op
+BenchmarkVulcan_GPlusAll 120109 9861 ns/op 1274 B/op 39 allocs/op
```
## Parse.com
-```
-BenchmarkGin_ParseStatic 10000000 133 ns/op 0 B/op 0 allocs/op
+```sh
+BenchmarkGin_ParseStatic 18877833 63.5 ns/op 0 B/op 0 allocs/op
-BenchmarkAce_ParseStatic 5000000 241 ns/op 0 B/op 0 allocs/op
-BenchmarkBear_ParseStatic 2000000 728 ns/op 120 B/op 3 allocs/op
-BenchmarkBeego_ParseStatic 1000000 2623 ns/op 368 B/op 4 allocs/op
-BenchmarkBone_ParseStatic 1000000 1285 ns/op 144 B/op 3 allocs/op
-BenchmarkDenco_ParseStatic 30000000 57.8 ns/op 0 B/op 0 allocs/op
-BenchmarkEcho_ParseStatic 5000000 342 ns/op 32 B/op 1 allocs/op
-BenchmarkGocraftWeb_ParseStatic 1000000 1478 ns/op 296 B/op 5 allocs/op
-BenchmarkGoji_ParseStatic 3000000 415 ns/op 0 B/op 0 allocs/op
-BenchmarkGojiv2_ParseStatic 1000000 2087 ns/op 928 B/op 7 allocs/op
-BenchmarkGoJsonRest_ParseStatic 1000000 1712 ns/op 329 B/op 11 allocs/op
-BenchmarkGoRestful_ParseStatic 200000 11072 ns/op 3224 B/op 22 allocs/op
-BenchmarkGorillaMux_ParseStatic 500000 4129 ns/op 752 B/op 11 allocs/op
-BenchmarkHttpRouter_ParseStatic 30000000 52.4 ns/op 0 B/op 0 allocs/op
-BenchmarkHttpTreeMux_ParseStatic 20000000 109 ns/op 0 B/op 0 allocs/op
-BenchmarkKocha_ParseStatic 20000000 81.8 ns/op 0 B/op 0 allocs/op
-BenchmarkLARS_ParseStatic 10000000 150 ns/op 0 B/op 0 allocs/op
-BenchmarkMacaron_ParseStatic 1000000 3288 ns/op 768 B/op 9 allocs/op
-BenchmarkMartini_ParseStatic 200000 9110 ns/op 768 B/op 9 allocs/op
-BenchmarkPat_ParseStatic 1000000 1135 ns/op 240 B/op 5 allocs/op
-BenchmarkPossum_ParseStatic 1000000 1557 ns/op 416 B/op 3 allocs/op
-BenchmarkR2router_ParseStatic 2000000 730 ns/op 144 B/op 4 allocs/op
-BenchmarkRivet_ParseStatic 10000000 121 ns/op 0 B/op 0 allocs/op
-BenchmarkTango_ParseStatic 1000000 1688 ns/op 248 B/op 8 allocs/op
-BenchmarkTigerTonic_ParseStatic 3000000 427 ns/op 48 B/op 1 allocs/op
-BenchmarkTraffic_ParseStatic 500000 5962 ns/op 1816 B/op 20 allocs/op
-BenchmarkVulcan_ParseStatic 2000000 969 ns/op 98 B/op 3 allocs/op
-BenchmarkAce_ParseParam 3000000 497 ns/op 64 B/op 1 allocs/op
-BenchmarkBear_ParseParam 1000000 1473 ns/op 467 B/op 5 allocs/op
-BenchmarkBeego_ParseParam 1000000 2384 ns/op 368 B/op 4 allocs/op
-BenchmarkBone_ParseParam 1000000 2513 ns/op 768 B/op 6 allocs/op
-BenchmarkDenco_ParseParam 5000000 364 ns/op 64 B/op 1 allocs/op
-BenchmarkEcho_ParseParam 5000000 418 ns/op 32 B/op 1 allocs/op
-BenchmarkGin_ParseParam 10000000 163 ns/op 0 B/op 0 allocs/op
-BenchmarkGocraftWeb_ParseParam 1000000 2361 ns/op 664 B/op 8 allocs/op
-BenchmarkGoji_ParseParam 1000000 1590 ns/op 336 B/op 2 allocs/op
-BenchmarkGojiv2_ParseParam 1000000 2851 ns/op 976 B/op 9 allocs/op
-BenchmarkGoJsonRest_ParseParam 1000000 2965 ns/op 649 B/op 13 allocs/op
-BenchmarkGoRestful_ParseParam 200000 12207 ns/op 3544 B/op 23 allocs/op
-BenchmarkGorillaMux_ParseParam 500000 5187 ns/op 1088 B/op 12 allocs/op
-BenchmarkHttpRouter_ParseParam 5000000 275 ns/op 64 B/op 1 allocs/op
-BenchmarkHttpTreeMux_ParseParam 1000000 1108 ns/op 352 B/op 3 allocs/op
-BenchmarkKocha_ParseParam 3000000 495 ns/op 56 B/op 3 allocs/op
-BenchmarkLARS_ParseParam 10000000 192 ns/op 0 B/op 0 allocs/op
-BenchmarkMacaron_ParseParam 500000 4103 ns/op 1056 B/op 10 allocs/op
-BenchmarkMartini_ParseParam 200000 9878 ns/op 1072 B/op 10 allocs/op
-BenchmarkPat_ParseParam 500000 3657 ns/op 1120 B/op 17 allocs/op
-BenchmarkPossum_ParseParam 1000000 2084 ns/op 560 B/op 6 allocs/op
-BenchmarkR2router_ParseParam 1000000 1251 ns/op 432 B/op 5 allocs/op
-BenchmarkRivet_ParseParam 5000000 335 ns/op 48 B/op 1 allocs/op
-BenchmarkTango_ParseParam 1000000 1854 ns/op 280 B/op 8 allocs/op
-BenchmarkTigerTonic_ParseParam 500000 4582 ns/op 1008 B/op 17 allocs/op
-BenchmarkTraffic_ParseParam 200000 8125 ns/op 2248 B/op 23 allocs/op
-BenchmarkVulcan_ParseParam 1000000 1148 ns/op 98 B/op 3 allocs/op
-BenchmarkAce_Parse2Params 3000000 539 ns/op 64 B/op 1 allocs/op
-BenchmarkBear_Parse2Params 1000000 1778 ns/op 496 B/op 5 allocs/op
-BenchmarkBeego_Parse2Params 1000000 2519 ns/op 368 B/op 4 allocs/op
-BenchmarkBone_Parse2Params 1000000 2596 ns/op 720 B/op 5 allocs/op
-BenchmarkDenco_Parse2Params 3000000 492 ns/op 64 B/op 1 allocs/op
-BenchmarkEcho_Parse2Params 3000000 484 ns/op 32 B/op 1 allocs/op
-BenchmarkGin_Parse2Params 10000000 193 ns/op 0 B/op 0 allocs/op
-BenchmarkGocraftWeb_Parse2Params 1000000 2575 ns/op 712 B/op 9 allocs/op
-BenchmarkGoji_Parse2Params 1000000 1373 ns/op 336 B/op 2 allocs/op
-BenchmarkGojiv2_Parse2Params 500000 2416 ns/op 960 B/op 8 allocs/op
-BenchmarkGoJsonRest_Parse2Params 300000 3452 ns/op 713 B/op 14 allocs/op
-BenchmarkGoRestful_Parse2Params 100000 17719 ns/op 6008 B/op 25 allocs/op
-BenchmarkGorillaMux_Parse2Params 300000 5102 ns/op 1088 B/op 11 allocs/op
-BenchmarkHttpRouter_Parse2Params 5000000 303 ns/op 64 B/op 1 allocs/op
-BenchmarkHttpTreeMux_Parse2Params 1000000 1372 ns/op 384 B/op 4 allocs/op
-BenchmarkKocha_Parse2Params 2000000 874 ns/op 128 B/op 5 allocs/op
-BenchmarkLARS_Parse2Params 10000000 192 ns/op 0 B/op 0 allocs/op
-BenchmarkMacaron_Parse2Params 500000 3871 ns/op 1056 B/op 10 allocs/op
-BenchmarkMartini_Parse2Params 200000 9954 ns/op 1152 B/op 11 allocs/op
-BenchmarkPat_Parse2Params 500000 4194 ns/op 832 B/op 17 allocs/op
-BenchmarkPossum_Parse2Params 1000000 2121 ns/op 560 B/op 6 allocs/op
-BenchmarkR2router_Parse2Params 1000000 1415 ns/op 432 B/op 5 allocs/op
-BenchmarkRivet_Parse2Params 3000000 457 ns/op 96 B/op 1 allocs/op
-BenchmarkTango_Parse2Params 1000000 1914 ns/op 312 B/op 8 allocs/op
-BenchmarkTigerTonic_Parse2Params 300000 6895 ns/op 1408 B/op 24 allocs/op
-BenchmarkTraffic_Parse2Params 200000 8317 ns/op 2040 B/op 22 allocs/op
-BenchmarkVulcan_Parse2Params 1000000 1274 ns/op 98 B/op 3 allocs/op
-BenchmarkAce_ParseAll 200000 10401 ns/op 640 B/op 16 allocs/op
-BenchmarkBear_ParseAll 50000 37743 ns/op 8928 B/op 110 allocs/op
-BenchmarkBeego_ParseAll 20000 63193 ns/op 9568 B/op 104 allocs/op
-BenchmarkBone_ParseAll 20000 61767 ns/op 14160 B/op 131 allocs/op
-BenchmarkDenco_ParseAll 300000 7036 ns/op 928 B/op 16 allocs/op
-BenchmarkEcho_ParseAll 200000 11824 ns/op 832 B/op 26 allocs/op
-BenchmarkGin_ParseAll 300000 4199 ns/op 0 B/op 0 allocs/op
-BenchmarkGocraftWeb_ParseAll 30000 51758 ns/op 13728 B/op 181 allocs/op
-BenchmarkGoji_ParseAll 50000 29614 ns/op 5376 B/op 32 allocs/op
-BenchmarkGojiv2_ParseAll 20000 68676 ns/op 24464 B/op 199 allocs/op
-BenchmarkGoJsonRest_ParseAll 20000 76135 ns/op 13866 B/op 321 allocs/op
-BenchmarkGoRestful_ParseAll 5000 389487 ns/op 110928 B/op 600 allocs/op
-BenchmarkGorillaMux_ParseAll 10000 221250 ns/op 24864 B/op 292 allocs/op
-BenchmarkHttpRouter_ParseAll 200000 6444 ns/op 640 B/op 16 allocs/op
-BenchmarkHttpTreeMux_ParseAll 50000 30702 ns/op 5728 B/op 51 allocs/op
-BenchmarkKocha_ParseAll 200000 13712 ns/op 1112 B/op 54 allocs/op
-BenchmarkLARS_ParseAll 300000 6925 ns/op 0 B/op 0 allocs/op
-BenchmarkMacaron_ParseAll 20000 96278 ns/op 24576 B/op 250 allocs/op
-BenchmarkMartini_ParseAll 5000 271352 ns/op 25072 B/op 253 allocs/op
-BenchmarkPat_ParseAll 20000 74941 ns/op 17264 B/op 343 allocs/op
-BenchmarkPossum_ParseAll 50000 39947 ns/op 10816 B/op 78 allocs/op
-BenchmarkR2router_ParseAll 50000 42479 ns/op 8352 B/op 120 allocs/op
-BenchmarkRivet_ParseAll 200000 7726 ns/op 912 B/op 16 allocs/op
-BenchmarkTango_ParseAll 30000 50014 ns/op 7168 B/op 208 allocs/op
-BenchmarkTigerTonic_ParseAll 10000 106550 ns/op 19728 B/op 379 allocs/op
-BenchmarkTraffic_ParseAll 10000 216037 ns/op 57776 B/op 642 allocs/op
-BenchmarkVulcan_ParseAll 50000 34379 ns/op 2548 B/op 78 allocs/op
+BenchmarkAce_ParseStatic 19663731 60.8 ns/op 0 B/op 0 allocs/op
+BenchmarkAero_ParseStatic 28967341 41.5 ns/op 0 B/op 0 allocs/op
+BenchmarkBear_ParseStatic 3006984 402 ns/op 120 B/op 3 allocs/op
+BenchmarkBeego_ParseStatic 1000000 1031 ns/op 352 B/op 3 allocs/op
+BenchmarkBone_ParseStatic 1782482 675 ns/op 144 B/op 3 allocs/op
+BenchmarkChi_ParseStatic 1453261 819 ns/op 432 B/op 3 allocs/op
+BenchmarkDenco_ParseStatic 45023595 26.5 ns/op 0 B/op 0 allocs/op
+BenchmarkEcho_ParseStatic 17330470 69.3 ns/op 0 B/op 0 allocs/op
+BenchmarkGocraftWeb_ParseStatic 1644006 731 ns/op 296 B/op 5 allocs/op
+BenchmarkGoji_ParseStatic 7026930 170 ns/op 0 B/op 0 allocs/op
+BenchmarkGojiv2_ParseStatic 517618 2037 ns/op 1312 B/op 10 allocs/op
+BenchmarkGoJsonRest_ParseStatic 1227080 975 ns/op 329 B/op 11 allocs/op
+BenchmarkGoRestful_ParseStatic 192458 6659 ns/op 4256 B/op 13 allocs/op
+BenchmarkGorillaMux_ParseStatic 744062 2109 ns/op 976 B/op 9 allocs/op
+BenchmarkGowwwRouter_ParseStatic 37781062 31.8 ns/op 0 B/op 0 allocs/op
+BenchmarkHttpRouter_ParseStatic 45311223 26.5 ns/op 0 B/op 0 allocs/op
+BenchmarkHttpTreeMux_ParseStatic 21383475 56.1 ns/op 0 B/op 0 allocs/op
+BenchmarkKocha_ParseStatic 29953290 40.1 ns/op 0 B/op 0 allocs/op
+BenchmarkLARS_ParseStatic 20036196 62.7 ns/op 0 B/op 0 allocs/op
+BenchmarkMacaron_ParseStatic 1000000 1740 ns/op 736 B/op 8 allocs/op
+BenchmarkMartini_ParseStatic 404156 3801 ns/op 768 B/op 9 allocs/op
+BenchmarkPat_ParseStatic 1547180 772 ns/op 240 B/op 5 allocs/op
+BenchmarkPossum_ParseStatic 1608991 757 ns/op 416 B/op 3 allocs/op
+BenchmarkR2router_ParseStatic 3177936 385 ns/op 144 B/op 4 allocs/op
+BenchmarkRivet_ParseStatic 17783205 67.4 ns/op 0 B/op 0 allocs/op
+BenchmarkTango_ParseStatic 1210777 990 ns/op 248 B/op 8 allocs/op
+BenchmarkTigerTonic_ParseStatic 5316440 231 ns/op 48 B/op 1 allocs/op
+BenchmarkTraffic_ParseStatic 496050 2539 ns/op 1256 B/op 19 allocs/op
+BenchmarkVulcan_ParseStatic 2462798 488 ns/op 98 B/op 3 allocs/op
+BenchmarkAce_ParseParam 13393669 89.6 ns/op 0 B/op 0 allocs/op
+BenchmarkAero_ParseParam 19836619 60.4 ns/op 0 B/op 0 allocs/op
+BenchmarkBear_ParseParam 1405954 864 ns/op 467 B/op 5 allocs/op
+BenchmarkBeego_ParseParam 1000000 1065 ns/op 352 B/op 3 allocs/op
+BenchmarkBone_ParseParam 1000000 1698 ns/op 896 B/op 7 allocs/op
+BenchmarkChi_ParseParam 1356037 873 ns/op 432 B/op 3 allocs/op
+BenchmarkDenco_ParseParam 6241392 204 ns/op 64 B/op 1 allocs/op
+BenchmarkEcho_ParseParam 14088100 85.1 ns/op 0 B/op 0 allocs/op
+BenchmarkGin_ParseParam 17426064 68.9 ns/op 0 B/op 0 allocs/op
+BenchmarkGocraftWeb_ParseParam 1000000 1254 ns/op 664 B/op 8 allocs/op
+BenchmarkGoji_ParseParam 1682574 713 ns/op 336 B/op 2 allocs/op
+BenchmarkGojiv2_ParseParam 502224 2333 ns/op 1360 B/op 12 allocs/op
+BenchmarkGoJsonRest_ParseParam 1000000 1401 ns/op 649 B/op 13 allocs/op
+BenchmarkGoRestful_ParseParam 182623 7097 ns/op 4576 B/op 14 allocs/op
+BenchmarkGorillaMux_ParseParam 482332 2477 ns/op 1280 B/op 10 allocs/op
+BenchmarkGowwwRouter_ParseParam 1834873 657 ns/op 432 B/op 3 allocs/op
+BenchmarkHttpRouter_ParseParam 23593393 51.0 ns/op 0 B/op 0 allocs/op
+BenchmarkHttpTreeMux_ParseParam 2100160 574 ns/op 352 B/op 3 allocs/op
+BenchmarkKocha_ParseParam 4837220 252 ns/op 56 B/op 3 allocs/op
+BenchmarkLARS_ParseParam 18411192 66.2 ns/op 0 B/op 0 allocs/op
+BenchmarkMacaron_ParseParam 571870 2398 ns/op 1072 B/op 10 allocs/op
+BenchmarkMartini_ParseParam 286262 4268 ns/op 1072 B/op 10 allocs/op
+BenchmarkPat_ParseParam 692906 2157 ns/op 992 B/op 15 allocs/op
+BenchmarkPossum_ParseParam 1000000 1011 ns/op 496 B/op 5 allocs/op
+BenchmarkR2router_ParseParam 1722735 697 ns/op 432 B/op 5 allocs/op
+BenchmarkRivet_ParseParam 6058054 203 ns/op 48 B/op 1 allocs/op
+BenchmarkTango_ParseParam 1000000 1061 ns/op 280 B/op 8 allocs/op
+BenchmarkTigerTonic_ParseParam 890275 2277 ns/op 784 B/op 15 allocs/op
+BenchmarkTraffic_ParseParam 351322 3543 ns/op 1896 B/op 21 allocs/op
+BenchmarkVulcan_ParseParam 2076544 572 ns/op 98 B/op 3 allocs/op
+BenchmarkAce_Parse2Params 11718074 101 ns/op 0 B/op 0 allocs/op
+BenchmarkAero_Parse2Params 16264988 73.4 ns/op 0 B/op 0 allocs/op
+BenchmarkBear_Parse2Params 1238322 973 ns/op 496 B/op 5 allocs/op
+BenchmarkBeego_Parse2Params 1000000 1120 ns/op 352 B/op 3 allocs/op
+BenchmarkBone_Parse2Params 1000000 1632 ns/op 848 B/op 6 allocs/op
+BenchmarkChi_Parse2Params 1239477 955 ns/op 432 B/op 3 allocs/op
+BenchmarkDenco_Parse2Params 4944133 245 ns/op 64 B/op 1 allocs/op
+BenchmarkEcho_Parse2Params 10518286 114 ns/op 0 B/op 0 allocs/op
+BenchmarkGin_Parse2Params 14505195 82.7 ns/op 0 B/op 0 allocs/op
+BenchmarkGocraftWeb_Parse2Params 1000000 1437 ns/op 712 B/op 9 allocs/op
+BenchmarkGoji_Parse2Params 1689883 707 ns/op 336 B/op 2 allocs/op
+BenchmarkGojiv2_Parse2Params 502334 2308 ns/op 1344 B/op 11 allocs/op
+BenchmarkGoJsonRest_Parse2Params 1000000 1771 ns/op 713 B/op 14 allocs/op
+BenchmarkGoRestful_Parse2Params 159092 7583 ns/op 4928 B/op 14 allocs/op
+BenchmarkGorillaMux_Parse2Params 417548 2980 ns/op 1296 B/op 10 allocs/op
+BenchmarkGowwwRouter_Parse2Params 1751737 686 ns/op 432 B/op 3 allocs/op
+BenchmarkHttpRouter_Parse2Params 18089204 66.3 ns/op 0 B/op 0 allocs/op
+BenchmarkHttpTreeMux_Parse2Params 1556986 777 ns/op 384 B/op 4 allocs/op
+BenchmarkKocha_Parse2Params 2493082 485 ns/op 128 B/op 5 allocs/op
+BenchmarkLARS_Parse2Params 15350108 78.5 ns/op 0 B/op 0 allocs/op
+BenchmarkMacaron_Parse2Params 530974 2605 ns/op 1072 B/op 10 allocs/op
+BenchmarkMartini_Parse2Params 247069 4673 ns/op 1152 B/op 11 allocs/op
+BenchmarkPat_Parse2Params 816295 2126 ns/op 752 B/op 16 allocs/op
+BenchmarkPossum_Parse2Params 1000000 1002 ns/op 496 B/op 5 allocs/op
+BenchmarkR2router_Parse2Params 1569771 733 ns/op 432 B/op 5 allocs/op
+BenchmarkRivet_Parse2Params 4080546 295 ns/op 96 B/op 1 allocs/op
+BenchmarkTango_Parse2Params 1000000 1121 ns/op 312 B/op 8 allocs/op
+BenchmarkTigerTonic_Parse2Params 399556 3470 ns/op 1168 B/op 22 allocs/op
+BenchmarkTraffic_Parse2Params 314194 4159 ns/op 1944 B/op 22 allocs/op
+BenchmarkVulcan_Parse2Params 1827559 664 ns/op 98 B/op 3 allocs/op
+BenchmarkAce_ParseAll 478395 2503 ns/op 0 B/op 0 allocs/op
+BenchmarkAero_ParseAll 715392 1658 ns/op 0 B/op 0 allocs/op
+BenchmarkBear_ParseAll 59191 20124 ns/op 8928 B/op 110 allocs/op
+BenchmarkBeego_ParseAll 45507 27266 ns/op 9152 B/op 78 allocs/op
+BenchmarkBone_ParseAll 29328 41459 ns/op 16208 B/op 147 allocs/op
+BenchmarkChi_ParseAll 48531 25053 ns/op 11232 B/op 78 allocs/op
+BenchmarkDenco_ParseAll 325532 4284 ns/op 928 B/op 16 allocs/op
+BenchmarkEcho_ParseAll 433771 2759 ns/op 0 B/op 0 allocs/op
+BenchmarkGin_ParseAll 576316 2082 ns/op 0 B/op 0 allocs/op
+BenchmarkGocraftWeb_ParseAll 41500 29692 ns/op 13728 B/op 181 allocs/op
+BenchmarkGoji_ParseAll 80833 15563 ns/op 5376 B/op 32 allocs/op
+BenchmarkGojiv2_ParseAll 19836 60335 ns/op 34448 B/op 277 allocs/op
+BenchmarkGoJsonRest_ParseAll 32210 38027 ns/op 13866 B/op 321 allocs/op
+BenchmarkGoRestful_ParseAll 6644 190842 ns/op 117600 B/op 354 allocs/op
+BenchmarkGorillaMux_ParseAll 12634 95894 ns/op 30288 B/op 250 allocs/op
+BenchmarkGowwwRouter_ParseAll 98152 12159 ns/op 6912 B/op 48 allocs/op
+BenchmarkHttpRouter_ParseAll 933208 1273 ns/op 0 B/op 0 allocs/op
+BenchmarkHttpTreeMux_ParseAll 107191 11554 ns/op 5728 B/op 51 allocs/op
+BenchmarkKocha_ParseAll 184862 6225 ns/op 1112 B/op 54 allocs/op
+BenchmarkLARS_ParseAll 644546 1858 ns/op 0 B/op 0 allocs/op
+BenchmarkMacaron_ParseAll 26145 46484 ns/op 19136 B/op 208 allocs/op
+BenchmarkMartini_ParseAll 10000 121838 ns/op 25072 B/op 253 allocs/op
+BenchmarkPat_ParseAll 25417 47196 ns/op 15216 B/op 308 allocs/op
+BenchmarkPossum_ParseAll 58550 20735 ns/op 10816 B/op 78 allocs/op
+BenchmarkR2router_ParseAll 72732 16584 ns/op 8352 B/op 120 allocs/op
+BenchmarkRivet_ParseAll 281365 4968 ns/op 912 B/op 16 allocs/op
+BenchmarkTango_ParseAll 42831 28668 ns/op 7168 B/op 208 allocs/op
+BenchmarkTigerTonic_ParseAll 23774 49972 ns/op 16048 B/op 332 allocs/op
+BenchmarkTraffic_ParseAll 10000 104679 ns/op 45520 B/op 605 allocs/op
+BenchmarkVulcan_ParseAll 64810 18108 ns/op 2548 B/op 78 allocs/op
```
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0bb90f22..44d62514 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,107 @@
-### Gin v1.5.0
+# Gin ChangeLog
+
+## Gin v1.7.0
+
+### BUGFIXES
+
+* fix compile error from [#2572](https://github.com/gin-gonic/gin/pull/2572) ([#2600](https://github.com/gin-gonic/gin/pull/2600))
+* fix: print headers without Authorization header on broken pipe ([#2528](https://github.com/gin-gonic/gin/pull/2528))
+* fix(tree): reassign fullpath when register new node ([#2366](https://github.com/gin-gonic/gin/pull/2366))
+
+### ENHANCEMENTS
+
+* Support params and exact routes without creating conflicts ([#2663](https://github.com/gin-gonic/gin/pull/2663))
+* chore: improve render string performance ([#2365](https://github.com/gin-gonic/gin/pull/2365))
+* Sync route tree to httprouter latest code ([#2368](https://github.com/gin-gonic/gin/pull/2368))
+* chore: rename getQueryCache/getFormCache to initQueryCache/initFormCa ([#2375](https://github.com/gin-gonic/gin/pull/2375))
+* chore(performance): improve countParams ([#2378](https://github.com/gin-gonic/gin/pull/2378))
+* Remove some functions that have the same effect as the bytes package ([#2387](https://github.com/gin-gonic/gin/pull/2387))
+* update:SetMode function ([#2321](https://github.com/gin-gonic/gin/pull/2321))
+* remove a unused type SecureJSONPrefix ([#2391](https://github.com/gin-gonic/gin/pull/2391))
+* Add a redirect sample for POST method ([#2389](https://github.com/gin-gonic/gin/pull/2389))
+* Add CustomRecovery builtin middleware ([#2322](https://github.com/gin-gonic/gin/pull/2322))
+* binding: avoid 2038 problem on 32-bit architectures ([#2450](https://github.com/gin-gonic/gin/pull/2450))
+* Prevent panic in Context.GetQuery() when there is no Request ([#2412](https://github.com/gin-gonic/gin/pull/2412))
+* Add GetUint and GetUint64 method on gin.context ([#2487](https://github.com/gin-gonic/gin/pull/2487))
+* update content-disposition header to MIME-style ([#2512](https://github.com/gin-gonic/gin/pull/2512))
+* reduce allocs and improve the render `WriteString` ([#2508](https://github.com/gin-gonic/gin/pull/2508))
+* implement ".Unwrap() error" on Error type ([#2525](https://github.com/gin-gonic/gin/pull/2525)) ([#2526](https://github.com/gin-gonic/gin/pull/2526))
+* Allow bind with a map[string]string ([#2484](https://github.com/gin-gonic/gin/pull/2484))
+* chore: update tree ([#2371](https://github.com/gin-gonic/gin/pull/2371))
+* Support binding for slice/array obj [Rewrite] ([#2302](https://github.com/gin-gonic/gin/pull/2302))
+* basic auth: fix timing oracle ([#2609](https://github.com/gin-gonic/gin/pull/2609))
+* Add mixed param and non-param paths (port of httprouter[#329](https://github.com/gin-gonic/gin/pull/329)) ([#2663](https://github.com/gin-gonic/gin/pull/2663))
+* feat(engine): add trustedproxies and remoteIP ([#2632](https://github.com/gin-gonic/gin/pull/2632))
+
+## Gin v1.6.3
+
+### ENHANCEMENTS
+
+ * Improve performance: Change `*sync.RWMutex` to `sync.RWMutex` in context. [#2351](https://github.com/gin-gonic/gin/pull/2351)
+
+## Gin v1.6.2
+
+### BUGFIXES
+ * fix missing initial sync.RWMutex [#2305](https://github.com/gin-gonic/gin/pull/2305)
+### ENHANCEMENTS
+ * Add set samesite in cookie. [#2306](https://github.com/gin-gonic/gin/pull/2306)
+
+## Gin v1.6.1
+
+### BUGFIXES
+ * Revert "fix accept incoming network connections" [#2294](https://github.com/gin-gonic/gin/pull/2294)
+
+## Gin v1.6.0
+
+### BREAKING
+ * chore(performance): Improve performance for adding RemoveExtraSlash flag [#2159](https://github.com/gin-gonic/gin/pull/2159)
+ * drop support govendor [#2148](https://github.com/gin-gonic/gin/pull/2148)
+ * Added support for SameSite cookie flag [#1615](https://github.com/gin-gonic/gin/pull/1615)
+### FEATURES
+ * add yaml negotiation [#2220](https://github.com/gin-gonic/gin/pull/2220)
+ * FileFromFS [#2112](https://github.com/gin-gonic/gin/pull/2112)
+### BUGFIXES
+ * Unix Socket Handling [#2280](https://github.com/gin-gonic/gin/pull/2280)
+ * Use json marshall in context json to fix breaking new line issue. Fixes #2209 [#2228](https://github.com/gin-gonic/gin/pull/2228)
+ * fix accept incoming network connections [#2216](https://github.com/gin-gonic/gin/pull/2216)
+ * Fixed a bug in the calculation of the maximum number of parameters [#2166](https://github.com/gin-gonic/gin/pull/2166)
+ * [FIX] allow empty headers on DataFromReader [#2121](https://github.com/gin-gonic/gin/pull/2121)
+ * Add mutex for protect Context.Keys map [#1391](https://github.com/gin-gonic/gin/pull/1391)
+### ENHANCEMENTS
+ * Add mitigation for log injection [#2277](https://github.com/gin-gonic/gin/pull/2277)
+ * tree: range over nodes values [#2229](https://github.com/gin-gonic/gin/pull/2229)
+ * tree: remove duplicate assignment [#2222](https://github.com/gin-gonic/gin/pull/2222)
+ * chore: upgrade go-isatty and json-iterator/go [#2215](https://github.com/gin-gonic/gin/pull/2215)
+ * path: sync code with httprouter [#2212](https://github.com/gin-gonic/gin/pull/2212)
+ * Use zero-copy approach to convert types between string and byte slice [#2206](https://github.com/gin-gonic/gin/pull/2206)
+ * Reuse bytes when cleaning the URL paths [#2179](https://github.com/gin-gonic/gin/pull/2179)
+ * tree: remove one else statement [#2177](https://github.com/gin-gonic/gin/pull/2177)
+ * tree: sync httprouter update (#2173) (#2172) [#2171](https://github.com/gin-gonic/gin/pull/2171)
+ * tree: sync part httprouter codes and reduce if/else [#2163](https://github.com/gin-gonic/gin/pull/2163)
+ * use http method constant [#2155](https://github.com/gin-gonic/gin/pull/2155)
+ * upgrade go-validator to v10 [#2149](https://github.com/gin-gonic/gin/pull/2149)
+ * Refactor redirect request in gin.go [#1970](https://github.com/gin-gonic/gin/pull/1970)
+ * Add build tag nomsgpack [#1852](https://github.com/gin-gonic/gin/pull/1852)
+### DOCS
+ * docs(path): improve comments [#2223](https://github.com/gin-gonic/gin/pull/2223)
+ * Renew README to fit the modification of SetCookie method [#2217](https://github.com/gin-gonic/gin/pull/2217)
+ * Fix spelling [#2202](https://github.com/gin-gonic/gin/pull/2202)
+ * Remove broken link from README. [#2198](https://github.com/gin-gonic/gin/pull/2198)
+ * Update docs on Context.Done(), Context.Deadline() and Context.Err() [#2196](https://github.com/gin-gonic/gin/pull/2196)
+ * Update validator to v10 [#2190](https://github.com/gin-gonic/gin/pull/2190)
+ * upgrade go-validator to v10 for README [#2189](https://github.com/gin-gonic/gin/pull/2189)
+ * Update to currently output [#2188](https://github.com/gin-gonic/gin/pull/2188)
+ * Fix "Custom Validators" example [#2186](https://github.com/gin-gonic/gin/pull/2186)
+ * Add project to README [#2165](https://github.com/gin-gonic/gin/pull/2165)
+ * docs(benchmarks): for gin v1.5 [#2153](https://github.com/gin-gonic/gin/pull/2153)
+ * Changed wording for clarity in README.md [#2122](https://github.com/gin-gonic/gin/pull/2122)
+### MISC
+ * ci support go1.14 [#2262](https://github.com/gin-gonic/gin/pull/2262)
+ * chore: upgrade depend version [#2231](https://github.com/gin-gonic/gin/pull/2231)
+ * Drop support go1.10 [#2147](https://github.com/gin-gonic/gin/pull/2147)
+ * fix comment in `mode.go` [#2129](https://github.com/gin-gonic/gin/pull/2129)
+
+## Gin v1.5.0
- [FIX] Use DefaultWriter and DefaultErrorWriter for debug messages [#1891](https://github.com/gin-gonic/gin/pull/1891)
- [NEW] Now you can parse the inline lowercase start structure [#1893](https://github.com/gin-gonic/gin/pull/1893)
@@ -90,7 +193,7 @@
- [NEW] Upgrade dependency libraries [#1491](https://github.com/gin-gonic/gin/pull/1491)
-### Gin v1.3.0
+## Gin v1.3.0
- [NEW] Add [`func (*Context) QueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.QueryMap), [`func (*Context) GetQueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetQueryMap), [`func (*Context) PostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.PostFormMap) and [`func (*Context) GetPostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetPostFormMap) to support `type map[string]string` as query string or form parameters, see [#1383](https://github.com/gin-gonic/gin/pull/1383)
- [NEW] Add [`func (*Context) AsciiJSON`](https://godoc.org/github.com/gin-gonic/gin#Context.AsciiJSON), see [#1358](https://github.com/gin-gonic/gin/pull/1358)
@@ -112,7 +215,7 @@
- [FIX] Gin Mode `""` when calling [`func Mode`](https://godoc.org/github.com/gin-gonic/gin#Mode) now returns `const DebugMode`, see [#1250](https://github.com/gin-gonic/gin/pull/1250)
- [FIX] `Flush()` now doesn't overwrite `responseWriter` status code, see [#1460](https://github.com/gin-gonic/gin/pull/1460)
-### Gin 1.2.0
+## Gin 1.2.0
- [NEW] Switch from godeps to govendor
- [NEW] Add support for Let's Encrypt via gin-gonic/autotls
@@ -135,25 +238,25 @@
- [FIX] Use X-Forwarded-For before X-Real-Ip
- [FIX] time.Time binding (#904)
-### Gin 1.1.4
+## Gin 1.1.4
- [NEW] Support google appengine for IsTerminal func
-### Gin 1.1.3
+## Gin 1.1.3
- [FIX] Reverted Logger: skip ANSI color commands
-### Gin 1.1
+## Gin 1.1
-- [NEW] Implement QueryArray and PostArray methods
-- [NEW] Refactor GetQuery and GetPostForm
-- [NEW] Add contribution guide
+- [NEW] Implement QueryArray and PostArray methods
+- [NEW] Refactor GetQuery and GetPostForm
+- [NEW] Add contribution guide
- [FIX] Corrected typos in README
-- [FIX] Removed additional Iota
-- [FIX] Changed imports to gopkg instead of github in README (#733)
+- [FIX] Removed additional Iota
+- [FIX] Changed imports to gopkg instead of github in README (#733)
- [FIX] Logger: skip ANSI color commands if output is not a tty
-### Gin 1.0rc2 (...)
+## Gin 1.0rc2 (...)
- [PERFORMANCE] Fast path for writing Content-Type.
- [PERFORMANCE] Much faster 404 routing
@@ -188,7 +291,7 @@
- [FIX] MIT license in every file
-### Gin 1.0rc1 (May 22, 2015)
+## Gin 1.0rc1 (May 22, 2015)
- [PERFORMANCE] Zero allocation router
- [PERFORMANCE] Faster JSON, XML and text rendering
@@ -196,7 +299,7 @@
- [PERFORMANCE] Misc code optimizations. Inlining, tail call optimizations
- [NEW] Built-in support for golang.org/x/net/context
- [NEW] Any(path, handler). Create a route that matches any path
-- [NEW] Refactored rendering pipeline (faster and static typeded)
+- [NEW] Refactored rendering pipeline (faster and static typed)
- [NEW] Refactored errors API
- [NEW] IndentedJSON() prints pretty JSON
- [NEW] Added gin.DefaultWriter
@@ -232,7 +335,7 @@
- [FIX] Better support for Google App Engine (using log instead of fmt)
-### Gin 0.6 (Mar 9, 2015)
+## Gin 0.6 (Mar 9, 2015)
- [NEW] Support multipart/form-data
- [NEW] NoMethod handler
@@ -242,14 +345,14 @@
- [FIX] Improve color logger
-### Gin 0.5 (Feb 7, 2015)
+## Gin 0.5 (Feb 7, 2015)
- [NEW] Content Negotiation
- [FIX] Solved security bug that allow a client to spoof ip
- [FIX] Fix unexported/ignored fields in binding
-### Gin 0.4 (Aug 21, 2014)
+## Gin 0.4 (Aug 21, 2014)
- [NEW] Development mode
- [NEW] Unit tests
@@ -258,7 +361,7 @@
- [FIX] Improved documentation for model binding
-### Gin 0.3 (Jul 18, 2014)
+## Gin 0.3 (Jul 18, 2014)
- [PERFORMANCE] Normal log and error log are printed in the same call.
- [PERFORMANCE] Improve performance of NoRouter()
@@ -276,7 +379,7 @@
- [FIX] Check application/x-www-form-urlencoded when parsing form
-### Gin 0.2b (Jul 08, 2014)
+## Gin 0.2b (Jul 08, 2014)
- [PERFORMANCE] Using sync.Pool to allocatio/gc overhead
- [NEW] Travis CI integration
- [NEW] Completely new logger
@@ -295,7 +398,7 @@
- [FIX] Recovery() middleware only prints panics
- [FIX] Context.Get() does not panic anymore. Use MustGet() instead.
- [FIX] Multiple http.WriteHeader() in NotFound handlers
-- [FIX] Engine.Run() panics if http server can't be setted up
+- [FIX] Engine.Run() panics if http server can't be set up
- [FIX] Crash when route path doesn't start with '/'
- [FIX] Do not update header when status code is negative
- [FIX] Setting response headers before calling WriteHeader in context.String()
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 547b777a..97daa808 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,4 +1,4 @@
-## Contributing
+## Contributing
- With issues:
- Use the search tool before opening a new issue.
@@ -8,6 +8,6 @@
- With pull requests:
- Open your pull request against `master`
- Your pull request should have no more than two commits, if not you should squash them.
- - It should pass all tests in the available continuous integrations systems such as TravisCI.
+ - It should pass all tests in the available continuous integration systems such as TravisCI.
- You should add/modify tests to cover your proposed code changes.
- If your pull request contains a new feature, please document it on the README.
diff --git a/Makefile b/Makefile
index 51a6b916..1a991939 100644
--- a/Makefile
+++ b/Makefile
@@ -1,20 +1,16 @@
GO ?= go
GOFMT ?= gofmt "-s"
-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/*")
+PACKAGES ?= $(shell $(GO) list ./...)
+VETPACKAGES ?= $(shell $(GO) list ./... | grep -v /examples/)
+GOFILES := $(shell find . -name "*.go")
TESTFOLDER := $(shell $(GO) list ./... | grep -E 'gin$$|binding$$|render$$' | grep -v examples)
-
-all: install
-
-install: deps
- govendor sync
+TESTTAGS ?= ""
.PHONY: test
test:
echo "mode: count" > coverage.out
for d in $(TESTFOLDER); do \
- $(GO) test -v -covermode=count -coverprofile=profile.out $$d > tmp.out; \
+ $(GO) test -tags $(TESTTAGS) -v -covermode=count -coverprofile=profile.out $$d > tmp.out; \
cat tmp.out; \
if grep -q "^--- FAIL" tmp.out; then \
rm tmp.out; \
@@ -48,11 +44,6 @@ fmt-check:
vet:
$(GO) vet $(VETPACKAGES)
-deps:
- @hash govendor > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
- $(GO) get -u github.com/kardianos/govendor; \
- fi
-
.PHONY: lint
lint:
@hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
diff --git a/README.md b/README.md
index 3f2d3c2c..d4772d76 100644
--- a/README.md
+++ b/README.md
@@ -5,36 +5,41 @@
[](https://travis-ci.org/gin-gonic/gin)
[](https://codecov.io/gh/gin-gonic/gin)
[](https://goreportcard.com/report/github.com/gin-gonic/gin)
-[](https://godoc.org/github.com/gin-gonic/gin)
+[](https://pkg.go.dev/github.com/gin-gonic/gin?tab=doc)
[](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[](https://sourcegraph.com/github.com/gin-gonic/gin?badge)
[](https://www.codetriage.com/gin-gonic/gin)
[](https://github.com/gin-gonic/gin/releases)
+[](https://www.tickgit.com/browse?repo=github.com/gin-gonic/gin)
Gin is a web framework written in Go (Golang). It features a martini-like API with performance that is up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin.
## Contents
-- [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)
+- [Gin Web Framework](#gin-web-framework)
+ - [Contents](#contents)
+ - [Installation](#installation)
+ - [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)
+ - [Single file](#single-file)
+ - [Multiple files](#multiple-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)
+ - [Controlling Log output coloring](#controlling-log-output-coloring)
- [Model binding and validation](#model-binding-and-validation)
- [Custom Validators](#custom-validators)
- [Only Bind Query String](#only-bind-query-string)
@@ -44,10 +49,17 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [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)
+ - [SecureJSON](#securejson)
+ - [JSONP](#jsonp)
+ - [AsciiJSON](#asciijson)
+ - [PureJSON](#purejson)
- [Serving static files](#serving-static-files)
+ - [Serving data from file](#serving-data-from-file)
- [Serving data from reader](#serving-data-from-reader)
- [HTML rendering](#html-rendering)
+ - [Custom Template renderer](#custom-template-renderer)
+ - [Custom Delimiters](#custom-delimiters)
+ - [Custom Template Funcs](#custom-template-funcs)
- [Multitemplate](#multitemplate)
- [Redirects](#redirects)
- [Custom Middleware](#custom-middleware)
@@ -56,21 +68,23 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [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)
+ - [Graceful shutdown or restart](#graceful-shutdown-or-restart)
+ - [Third-party packages](#third-party-packages)
+ - [Manually](#manually)
- [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)
+ - [Testing](#testing)
+ - [Users](#users)
## Installation
To install Gin package, you need to install Go and set your Go workspace first.
-1. The first need [Go](https://golang.org/) installed (**version 1.11+ is required**), then you can use the below Go command to install Gin.
+1. The first need [Go](https://golang.org/) installed (**version 1.12+ is required**), then you can use the below Go command to install Gin.
```sh
$ go get -u github.com/gin-gonic/gin
@@ -88,46 +102,8 @@ import "github.com/gin-gonic/gin"
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 "$_"
-```
-
-If you are on a Mac and you're installing Go 1.8 (released: Feb 2017) or later, GOPATH is automatically determined by the Go toolchain for you. It defaults to $HOME/go on macOS so you can create your project like this
-
-```sh
-$ mkdir -p $HOME/go/src/github.com/myusername/project && cd "$_"
-```
-
-3. Vendor init your project and add gin
-
-```sh
-$ govendor init
-$ govendor fetch github.com/gin-gonic/gin@v1.3
-```
-
-4. Copy a starting template inside your project
-
-```sh
-$ curl https://raw.githubusercontent.com/gin-gonic/examples/master/basic/main.go > main.go
-```
-
-5. Run your project
-
-```sh
-$ go run main.go
-```
-
## Quick start
-
+
```sh
# assume the following codes in example.go file
$ cat example.go
@@ -160,35 +136,38 @@ Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httpr
[See all benchmarks](/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
+| Benchmark name | (1) | (2) | (3) | (4) |
+| ------------------------------ | ---------:| ---------------:| ------------:| ---------------:|
+| BenchmarkGin_GithubAll | **43550** | **27364 ns/op** | **0 B/op** | **0 allocs/op** |
+| BenchmarkAce_GithubAll | 40543 | 29670 ns/op | 0 B/op | 0 allocs/op |
+| BenchmarkAero_GithubAll | 57632 | 20648 ns/op | 0 B/op | 0 allocs/op |
+| BenchmarkBear_GithubAll | 9234 | 216179 ns/op | 86448 B/op | 943 allocs/op |
+| BenchmarkBeego_GithubAll | 7407 | 243496 ns/op | 71456 B/op | 609 allocs/op |
+| BenchmarkBone_GithubAll | 420 | 2922835 ns/op | 720160 B/op | 8620 allocs/op |
+| BenchmarkChi_GithubAll | 7620 | 238331 ns/op | 87696 B/op | 609 allocs/op |
+| BenchmarkDenco_GithubAll | 18355 | 64494 ns/op | 20224 B/op | 167 allocs/op |
+| BenchmarkEcho_GithubAll | 31251 | 38479 ns/op | 0 B/op | 0 allocs/op |
+| BenchmarkGocraftWeb_GithubAll | 4117 | 300062 ns/op | 131656 B/op | 1686 allocs/op |
+| BenchmarkGoji_GithubAll | 3274 | 416158 ns/op | 56112 B/op | 334 allocs/op |
+| BenchmarkGojiv2_GithubAll | 1402 | 870518 ns/op | 352720 B/op | 4321 allocs/op |
+| BenchmarkGoJsonRest_GithubAll | 2976 | 401507 ns/op | 134371 B/op | 2737 allocs/op |
+| BenchmarkGoRestful_GithubAll | 410 | 2913158 ns/op | 910144 B/op | 2938 allocs/op |
+| BenchmarkGorillaMux_GithubAll | 346 | 3384987 ns/op | 251650 B/op | 1994 allocs/op |
+| BenchmarkGowwwRouter_GithubAll | 10000 | 143025 ns/op | 72144 B/op | 501 allocs/op |
+| BenchmarkHttpRouter_GithubAll | 55938 | 21360 ns/op | 0 B/op | 0 allocs/op |
+| BenchmarkHttpTreeMux_GithubAll | 10000 | 153944 ns/op | 65856 B/op | 671 allocs/op |
+| BenchmarkKocha_GithubAll | 10000 | 106315 ns/op | 23304 B/op | 843 allocs/op |
+| BenchmarkLARS_GithubAll | 47779 | 25084 ns/op | 0 B/op | 0 allocs/op |
+| BenchmarkMacaron_GithubAll | 3266 | 371907 ns/op | 149409 B/op | 1624 allocs/op |
+| BenchmarkMartini_GithubAll | 331 | 3444706 ns/op | 226551 B/op | 2325 allocs/op |
+| BenchmarkPat_GithubAll | 273 | 4381818 ns/op | 1483152 B/op | 26963 allocs/op |
+| BenchmarkPossum_GithubAll | 10000 | 164367 ns/op | 84448 B/op | 609 allocs/op |
+| BenchmarkR2router_GithubAll | 10000 | 160220 ns/op | 77328 B/op | 979 allocs/op |
+| BenchmarkRivet_GithubAll | 14625 | 82453 ns/op | 16272 B/op | 167 allocs/op |
+| BenchmarkTango_GithubAll | 6255 | 279611 ns/op | 63826 B/op | 1618 allocs/op |
+| BenchmarkTigerTonic_GithubAll | 2008 | 687874 ns/op | 193856 B/op | 4474 allocs/op |
+| BenchmarkTraffic_GithubAll | 355 | 3478508 ns/op | 820744 B/op | 14114 allocs/op |
+| BenchmarkVulcan_GithubAll | 6885 | 193333 ns/op | 19894 B/op | 609 allocs/op |
- (1): Total Repetitions achieved in constant time, higher means more confident result
- (2): Single Repetition Duration (ns/op), lower is better
@@ -199,8 +178,8 @@ BenchmarkVulcan_GithubAll | 5000 | 394253 | 19894
- [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] Complete suite of unit tests.
+- [x] Battle tested.
- [x] API frozen, new releases will not break your code.
## Build with [jsoniter](https://github.com/json-iterator/go)
@@ -264,6 +243,13 @@ func main() {
c.FullPath() == "/user/:name/*action" // true
})
+ // This handler will add a new router for /user/groups.
+ // Exact routes are resolved before param routes, regardless of the order they were defined.
+ // Routes starting with /user/groups are never interpreted as /user/:name/... routes
+ router.GET("/user/groups", func(c *gin.Context) {
+ c.String(http.StatusOK, "The available groups are [...]", name)
+ })
+
router.Run(":8080")
}
```
@@ -361,7 +347,7 @@ func main() {
```
```
-ids: map[b:hello a:1234], names: map[second:tianou first:thinkerou]
+ids: map[b:hello a:1234]; names: map[second:tianou first:thinkerou]
```
### Upload files
@@ -378,14 +364,14 @@ References issue [#774](https://github.com/gin-gonic/gin/issues/774) and detail
func main() {
router := gin.Default()
// Set a lower memory limit for multipart forms (default is 32 MiB)
- // router.MaxMultipartMemory = 8 << 20 // 8 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.SaveUploadedFile(file, dst)
c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
})
@@ -409,7 +395,7 @@ See the detail [example code](https://github.com/gin-gonic/examples/tree/master/
func main() {
router := gin.Default()
// Set a lower memory limit for multipart forms (default is 32 MiB)
- // router.MaxMultipartMemory = 8 << 20 // 8 MiB
+ router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) {
// Multipart form
form, _ := c.MultipartForm()
@@ -419,7 +405,7 @@ func main() {
log.Println(file.Filename)
// Upload the file to specific dst.
- // c.SaveUploadedFile(file, dst)
+ c.SaveUploadedFile(file, dst)
}
c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files)))
})
@@ -517,6 +503,39 @@ func main() {
}
```
+### Custom Recovery behavior
+```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.CustomRecovery(func(c *gin.Context, recovered interface{}) {
+ if err, ok := recovered.(string); ok {
+ c.String(http.StatusInternalServerError, fmt.Sprintf("error: %s", err))
+ }
+ c.AbortWithStatus(http.StatusInternalServerError)
+ }))
+
+ r.GET("/panic", func(c *gin.Context) {
+ // panic with a string -- the custom middleware could save this to a database or report it to the user
+ panic("foo")
+ })
+
+ r.GET("/", func(c *gin.Context) {
+ c.String(http.StatusOK, "ohai")
+ })
+
+ // Listen and serve on 0.0.0.0:8080
+ r.Run(":8080")
+}
+```
+
### How to write log file
```go
func main() {
@@ -576,44 +595,44 @@ 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
+### Controlling Log output coloring
By default, logs output on console should be colorized depending on the detected TTY.
-Never colorize logs:
+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:
+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")
}
```
@@ -622,7 +641,7 @@ func main() {
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).
+Gin uses [**go-playground/validator/v10**](https://github.com/go-playground/validator) for validation. Check the full docs on tags usage [here](https://godoc.org/github.com/go-playground/validator#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"`.
@@ -655,12 +674,12 @@ func main() {
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"})
})
@@ -676,12 +695,12 @@ func main() {
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"})
})
@@ -693,12 +712,12 @@ func main() {
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"})
})
@@ -742,12 +761,11 @@ package main
import (
"net/http"
- "reflect"
"time"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
- "gopkg.in/go-playground/validator.v8"
+ "github.com/go-playground/validator/v10"
)
// Booking contains binded and validated data.
@@ -756,11 +774,9 @@ type Booking struct {
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 {
+var bookableDate validator.Func = func(fl validator.FieldLevel) bool {
+ date, ok := fl.Field().Interface().(time.Time)
+ if ok {
today := time.Now()
if today.After(date) {
return false
@@ -791,11 +807,14 @@ func getBookable(c *gin.Context) {
```
```console
-$ curl "localhost:8085/bookable?check_in=2018-04-16&check_out=2018-04-17"
+$ curl "localhost:8085/bookable?check_in=2030-04-16&check_out=2030-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"}
+$ curl "localhost:8085/bookable?check_in=2030-03-10&check_out=2030-03-09"
+{"error":"Key: 'Booking.CheckOut' Error:Field validation for 'CheckOut' failed on the 'gtfield' tag"}
+
+$ curl "localhost:8085/bookable?check_in=2000-03-09&check_out=2000-03-10"
+{"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.
@@ -1133,7 +1152,7 @@ func main() {
data := gin.H{
"foo": "bar",
}
-
+
//callback is x
// Will output : x({\"foo\":\"bar\"})
c.JSONP(http.StatusOK, data)
@@ -1178,21 +1197,21 @@ 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")
}
@@ -1212,6 +1231,24 @@ func main() {
}
```
+### Serving data from file
+
+```go
+func main() {
+ router := gin.Default()
+
+ router.GET("/local/file", func(c *gin.Context) {
+ c.File("local/file.go")
+ })
+
+ var fs http.FileSystem = // ...
+ router.GET("/fs/file", func(c *gin.Context) {
+ c.FileFromFS("fs/file.go", fs)
+ })
+}
+
+```
+
### Serving data from reader
```go
@@ -1225,6 +1262,7 @@ func main() {
}
reader := response.Body
+ defer reader.Close()
contentLength := response.ContentLength
contentType := response.Header.Get("Content-Type")
@@ -1400,6 +1438,12 @@ r.GET("/test", func(c *gin.Context) {
})
```
+Issuing a HTTP redirect from POST. Refer to issue: [#444](https://github.com/gin-gonic/gin/issues/444)
+```go
+r.POST("/test", func(c *gin.Context) {
+ c.Redirect(http.StatusFound, "/foo")
+})
+```
Issuing a Router redirect, use `HandleContext` like below.
@@ -1699,12 +1743,13 @@ func main() {
}
```
-### Graceful restart or stop
+### Graceful shutdown or restart
-Do you want to graceful restart or stop your web server?
-There are some ways this can be done.
+There are a few approaches you can use to perform a graceful shutdown or restart. You can make use of third-party packages specifically built for that, or you can manually do the same with the functions and methods from the built-in packages.
-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.
+#### Third-party packages
+
+We can use [fvbock/endless](https://github.com/fvbock/endless) to replace the default `ListenAndServe`. Refer to issue [#296](https://github.com/gin-gonic/gin/issues/296) for more details.
```go
router := gin.Default()
@@ -1713,13 +1758,15 @@ router.GET("/", handler)
endless.ListenAndServe(":4242", router)
```
-An alternative to endless:
+Alternatives:
* [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.
+#### Manually
+
+In case you are using Go 1.8 or a later version, you may not need to use those libraries. Consider using `http.Server`'s built-in [Shutdown()](https://golang.org/pkg/net/http/#Server.Shutdown) method for graceful shutdowns. The example below describes its usage, and we've got more examples using gin [here](https://github.com/gin-gonic/examples/tree/master/graceful-shutdown).
```go
// +build go1.8
@@ -1750,10 +1797,11 @@ func main() {
Handler: router,
}
+ // Initializing the server in a goroutine so that
+ // it won't block the graceful shutdown handling below
go func() {
- // service connections
- if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
- log.Fatalf("listen: %s\n", err)
+ if err := srv.ListenAndServe(); err != nil && errors.Is(err, http.ErrServerClosed) {
+ log.Printf("listen: %s\n", err)
}
}()
@@ -1765,18 +1813,17 @@ func main() {
// 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 ...")
+ log.Println("Shutting down server...")
+ // The context is used to inform the server it has 5 seconds to finish
+ // the request it is currently handling
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.Fatal("Server forced to shutdown:", err)
}
+
log.Println("Server exiting")
}
```
@@ -2077,6 +2124,39 @@ func main() {
}
```
+## Don't trust all proxies
+
+Gin lets you specify which headers to hold the real client IP (if any),
+as well as specifying which proxies (or direct clients) you trust to
+specify one of these headers.
+
+The `TrustedProxies` slice on your `gin.Engine` specifes network addresses or
+network CIDRs from where clients which their request headers related to client
+IP can be trusted. They can be IPv4 addresses, IPv4 CIDRs, IPv6 addresses or
+IPv6 CIDRs.
+
+```go
+import (
+ "fmt"
+
+ "github.com/gin-gonic/gin"
+)
+
+func main() {
+
+ router := gin.Default()
+ router.TrustedProxies = []string{"192.168.1.2"}
+
+ router.GET("/", func(c *gin.Context) {
+ // If the client is 192.168.1.2, use the X-Forwarded-For
+ // header to deduce the original client IP from the trust-
+ // worthy parts of that header.
+ // Otherwise, simply return the direct client IP
+ fmt.Printf("ClientIP: %s\n", c.ClientIP())
+ })
+ router.Run()
+}
+```
## Testing
@@ -2134,3 +2214,4 @@ Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framewor
* [krakend](https://github.com/devopsfaith/krakend): Ultra performant API Gateway with middlewares.
* [picfit](https://github.com/thoas/picfit): An image resizing server written in Go.
* [brigade](https://github.com/brigadecore/brigade): Event-based Scripting for Kubernetes.
+* [dkron](https://github.com/distribworks/dkron): Distributed, fault tolerant job scheduling system.
diff --git a/auth.go b/auth.go
index c96b1e29..4d8a6ce4 100644
--- a/auth.go
+++ b/auth.go
@@ -5,9 +5,12 @@
package gin
import (
+ "crypto/subtle"
"encoding/base64"
"net/http"
"strconv"
+
+ "github.com/gin-gonic/gin/internal/bytesconv"
)
// AuthUserKey is the cookie name for user credential in basic auth.
@@ -28,7 +31,7 @@ func (a authPairs) searchCredential(authValue string) (string, bool) {
return "", false
}
for _, pair := range a {
- if pair.value == authValue {
+ if subtle.ConstantTimeCompare([]byte(pair.value), []byte(authValue)) == 1 {
return pair.user, true
}
}
@@ -68,8 +71,9 @@ func BasicAuth(accounts Accounts) HandlerFunc {
}
func processAccounts(accounts Accounts) authPairs {
- assert1(len(accounts) > 0, "Empty list of authorized credentials")
- pairs := make(authPairs, 0, len(accounts))
+ length := len(accounts)
+ assert1(length > 0, "Empty list of authorized credentials")
+ pairs := make(authPairs, 0, length)
for user, password := range accounts {
assert1(user != "", "User can not be empty")
value := authorizationHeader(user, password)
@@ -83,5 +87,5 @@ func processAccounts(accounts Accounts) authPairs {
func authorizationHeader(user, password string) string {
base := user + ":" + password
- return "Basic " + base64.StdEncoding.EncodeToString([]byte(base))
+ return "Basic " + base64.StdEncoding.EncodeToString(bytesconv.StringToBytes(base))
}
diff --git a/binding/binding.go b/binding/binding.go
index 6d58c3cd..5caeb581 100644
--- a/binding/binding.go
+++ b/binding/binding.go
@@ -2,6 +2,9 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
+//go:build !nomsgpack
+// +build !nomsgpack
+
package binding
import "net/http"
@@ -49,7 +52,8 @@ type BindingUri interface {
// https://github.com/go-playground/validator/tree/v8.18.2.
type StructValidator interface {
// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
- // If the received type is not a struct, any validation should be skipped and nil must be returned.
+ // If the received type is a slice|array, the validation should be performed travel on every element.
+ // If the received type is not a struct or slice|array, any validation should be skipped and nil must be returned.
// If the received type is a struct or pointer to a struct, the validation should be performed.
// If the struct is not valid or the validation itself fails, a descriptive error should be returned.
// Otherwise nil must be returned.
@@ -84,7 +88,7 @@ var (
// Default returns the appropriate Binding instance based on the HTTP method
// and the content type.
func Default(method, contentType string) Binding {
- if method == "GET" {
+ if method == http.MethodGet {
return Form
}
diff --git a/binding/binding_msgpack_test.go b/binding/binding_msgpack_test.go
new file mode 100644
index 00000000..04d94079
--- /dev/null
+++ b/binding/binding_msgpack_test.go
@@ -0,0 +1,58 @@
+// Copyright 2020 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.
+
+//go:build !nomsgpack
+// +build !nomsgpack
+
+package binding
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/ugorji/go/codec"
+)
+
+func TestBindingMsgPack(t *testing.T) {
+ test := FooStruct{
+ Foo: "bar",
+ }
+
+ h := new(codec.MsgpackHandle)
+ assert.NotNil(t, h)
+ buf := bytes.NewBuffer([]byte{})
+ assert.NotNil(t, buf)
+ err := codec.NewEncoder(buf, h).Encode(test)
+ assert.NoError(t, err)
+
+ data := buf.Bytes()
+
+ testMsgPackBodyBinding(t,
+ MsgPack, "msgpack",
+ "/", "/",
+ string(data), string(data[1:]))
+}
+
+func testMsgPackBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
+ assert.Equal(t, name, b.Name())
+
+ obj := FooStruct{}
+ req := requestWithBody("POST", path, body)
+ req.Header.Add("Content-Type", MIMEMSGPACK)
+ err := b.Bind(req, &obj)
+ assert.NoError(t, err)
+ assert.Equal(t, "bar", obj.Foo)
+
+ obj = FooStruct{}
+ req = requestWithBody("POST", badPath, badBody)
+ req.Header.Add("Content-Type", MIMEMSGPACK)
+ err = MsgPack.Bind(req, &obj)
+ assert.Error(t, err)
+}
+
+func TestBindingDefaultMsgPack(t *testing.T) {
+ assert.Equal(t, MsgPack, Default("POST", MIMEMSGPACK))
+ assert.Equal(t, MsgPack, Default("PUT", MIMEMSGPACK2))
+}
diff --git a/binding/binding_nomsgpack.go b/binding/binding_nomsgpack.go
new file mode 100644
index 00000000..9afa3dcf
--- /dev/null
+++ b/binding/binding_nomsgpack.go
@@ -0,0 +1,112 @@
+// Copyright 2020 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.
+
+//go:build nomsgpack
+// +build nomsgpack
+
+package binding
+
+import "net/http"
+
+// Content-Type MIME of the most common data formats.
+const (
+ MIMEJSON = "application/json"
+ MIMEHTML = "text/html"
+ MIMEXML = "application/xml"
+ MIMEXML2 = "text/xml"
+ MIMEPlain = "text/plain"
+ MIMEPOSTForm = "application/x-www-form-urlencoded"
+ MIMEMultipartPOSTForm = "multipart/form-data"
+ MIMEPROTOBUF = "application/x-protobuf"
+ MIMEYAML = "application/x-yaml"
+)
+
+// Binding describes the interface which needs to be implemented for binding the
+// data present in the request such as JSON request body, query parameters or
+// the form POST.
+type Binding interface {
+ Name() string
+ Bind(*http.Request, interface{}) error
+}
+
+// BindingBody adds BindBody method to Binding. BindBody is similar with Bind,
+// but it reads the body from supplied bytes instead of req.Body.
+type BindingBody interface {
+ Binding
+ BindBody([]byte, interface{}) error
+}
+
+// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
+// but it read the Params.
+type BindingUri interface {
+ Name() string
+ BindUri(map[string][]string, interface{}) error
+}
+
+// StructValidator is the minimal interface which needs to be implemented in
+// order for it to be used as the validator engine for ensuring the correctness
+// of the request. Gin provides a default implementation for this using
+// https://github.com/go-playground/validator/tree/v8.18.2.
+type StructValidator interface {
+ // ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
+ // If the received type is not a struct, any validation should be skipped and nil must be returned.
+ // If the received type is a struct or pointer to a struct, the validation should be performed.
+ // If the struct is not valid or the validation itself fails, a descriptive error should be returned.
+ // Otherwise nil must be returned.
+ ValidateStruct(interface{}) error
+
+ // Engine returns the underlying validator engine which powers the
+ // StructValidator implementation.
+ Engine() interface{}
+}
+
+// Validator is the default validator which implements the StructValidator
+// interface. It uses https://github.com/go-playground/validator/tree/v8.18.2
+// under the hood.
+var Validator StructValidator = &defaultValidator{}
+
+// These implement the Binding interface and can be used to bind the data
+// present in the request to struct instances.
+var (
+ JSON = jsonBinding{}
+ XML = xmlBinding{}
+ Form = formBinding{}
+ Query = queryBinding{}
+ FormPost = formPostBinding{}
+ FormMultipart = formMultipartBinding{}
+ ProtoBuf = protobufBinding{}
+ YAML = yamlBinding{}
+ Uri = uriBinding{}
+ Header = headerBinding{}
+)
+
+// Default returns the appropriate Binding instance based on the HTTP method
+// and the content type.
+func Default(method, contentType string) Binding {
+ if method == "GET" {
+ return Form
+ }
+
+ switch contentType {
+ case MIMEJSON:
+ return JSON
+ case MIMEXML, MIMEXML2:
+ return XML
+ case MIMEPROTOBUF:
+ return ProtoBuf
+ case MIMEYAML:
+ return YAML
+ case MIMEMultipartPOSTForm:
+ return FormMultipart
+ default: // case MIMEPOSTForm:
+ return Form
+ }
+}
+
+func validate(obj interface{}) error {
+ if Validator == nil {
+ return nil
+ }
+ return Validator.ValidateStruct(obj)
+}
diff --git a/binding/binding_test.go b/binding/binding_test.go
index f0b6f795..17336177 100644
--- a/binding/binding_test.go
+++ b/binding/binding_test.go
@@ -13,6 +13,7 @@ import (
"mime/multipart"
"net/http"
"os"
+ "reflect"
"strconv"
"strings"
"testing"
@@ -21,7 +22,6 @@ import (
"github.com/gin-gonic/gin/testdata/protoexample"
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert"
- "github.com/ugorji/go/codec"
)
type appkey struct {
@@ -35,7 +35,7 @@ type QueryTest struct {
}
type FooStruct struct {
- Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required"`
+ Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required,max=32"`
}
type FooBarStruct struct {
@@ -163,9 +163,6 @@ func TestBindingDefault(t *testing.T) {
assert.Equal(t, ProtoBuf, Default("POST", MIMEPROTOBUF))
assert.Equal(t, ProtoBuf, Default("PUT", MIMEPROTOBUF))
- assert.Equal(t, MsgPack, Default("POST", MIMEMSGPACK))
- assert.Equal(t, MsgPack, Default("PUT", MIMEMSGPACK2))
-
assert.Equal(t, YAML, Default("POST", MIMEYAML))
assert.Equal(t, YAML, Default("PUT", MIMEYAML))
}
@@ -184,6 +181,20 @@ func TestBindingJSON(t *testing.T) {
`{"foo": "bar"}`, `{"bar": "foo"}`)
}
+func TestBindingJSONSlice(t *testing.T) {
+ EnableDecoderDisallowUnknownFields = true
+ defer func() {
+ EnableDecoderDisallowUnknownFields = false
+ }()
+
+ testBodyBindingSlice(t, JSON, "json", "/", "/", `[]`, ``)
+ testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{}]`)
+ testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": ""}]`)
+ testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": 123}]`)
+ testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"bar": 123}]`)
+ testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": "123456789012345678901234567890123"}]`)
+}
+
func TestBindingJSONUseNumber(t *testing.T) {
testBodyBindingUseNumber(t,
JSON, "json",
@@ -204,6 +215,12 @@ func TestBindingJSONDisallowUnknownFields(t *testing.T) {
`{"foo": "bar"}`, `{"foo": "bar", "what": "this"}`)
}
+func TestBindingJSONStringMap(t *testing.T) {
+ testBodyBindingStringMap(t, JSON,
+ "/", "/",
+ `{"foo": "bar", "hello": "world"}`, `{"num": 2}`)
+}
+
func TestBindingForm(t *testing.T) {
testFormBinding(t, "POST",
"/", "/",
@@ -340,6 +357,37 @@ func TestBindingFormForType(t *testing.T) {
"", "", "StructPointer")
}
+func TestBindingFormStringMap(t *testing.T) {
+ testBodyBindingStringMap(t, Form,
+ "/", "",
+ `foo=bar&hello=world`, "")
+ // Should pick the last value
+ testBodyBindingStringMap(t, Form,
+ "/", "",
+ `foo=something&foo=bar&hello=world`, "")
+}
+
+func TestBindingFormStringSliceMap(t *testing.T) {
+ obj := make(map[string][]string)
+ req := requestWithBody("POST", "/", "foo=something&foo=bar&hello=world")
+ req.Header.Add("Content-Type", MIMEPOSTForm)
+ err := Form.Bind(req, &obj)
+ assert.NoError(t, err)
+ assert.NotNil(t, obj)
+ assert.Len(t, obj, 2)
+ target := map[string][]string{
+ "foo": {"something", "bar"},
+ "hello": {"world"},
+ }
+ assert.True(t, reflect.DeepEqual(obj, target))
+
+ objInvalid := make(map[string][]int)
+ req = requestWithBody("POST", "/", "foo=something&foo=bar&hello=world")
+ req.Header.Add("Content-Type", MIMEPOSTForm)
+ err = Form.Bind(req, &objInvalid)
+ assert.Error(t, err)
+}
+
func TestBindingQuery(t *testing.T) {
testQueryBinding(t, "POST",
"/?foo=bar&bar=foo", "/",
@@ -370,6 +418,28 @@ func TestBindingQueryBoolFail(t *testing.T) {
"bool_foo=unused", "")
}
+func TestBindingQueryStringMap(t *testing.T) {
+ b := Query
+
+ obj := make(map[string]string)
+ req := requestWithBody("GET", "/?foo=bar&hello=world", "")
+ err := b.Bind(req, &obj)
+ assert.NoError(t, err)
+ assert.NotNil(t, obj)
+ assert.Len(t, obj, 2)
+ assert.Equal(t, "bar", obj["foo"])
+ assert.Equal(t, "world", obj["hello"])
+
+ obj = make(map[string]string)
+ req = requestWithBody("GET", "/?foo=bar&foo=2&hello=world", "") // should pick last
+ err = b.Bind(req, &obj)
+ assert.NoError(t, err)
+ assert.NotNil(t, obj)
+ assert.Len(t, obj, 2)
+ assert.Equal(t, "2", obj["foo"])
+ assert.Equal(t, "world", obj["hello"])
+}
+
func TestBindingXML(t *testing.T) {
testBodyBinding(t,
XML, "xml",
@@ -391,6 +461,13 @@ func TestBindingYAML(t *testing.T) {
`foo: bar`, `bar: foo`)
}
+func TestBindingYAMLStringMap(t *testing.T) {
+ // YAML is a superset of JSON, so the test below is JSON (to avoid newlines)
+ testBodyBindingStringMap(t, YAML,
+ "/", "/",
+ `{"foo": "bar", "hello": "world"}`, `{"nested": {"foo": "bar"}}`)
+}
+
func TestBindingYAMLFail(t *testing.T) {
testBodyBindingFail(t,
YAML, "yaml",
@@ -633,26 +710,6 @@ func TestBindingProtoBufFail(t *testing.T) {
string(data), string(data[1:]))
}
-func TestBindingMsgPack(t *testing.T) {
- test := FooStruct{
- Foo: "bar",
- }
-
- h := new(codec.MsgpackHandle)
- assert.NotNil(t, h)
- buf := bytes.NewBuffer([]byte{})
- assert.NotNil(t, buf)
- err := codec.NewEncoder(buf, h).Encode(test)
- assert.NoError(t, err)
-
- data := buf.Bytes()
-
- testMsgPackBodyBinding(t,
- MsgPack, "msgpack",
- "/", "/",
- string(data), string(data[1:]))
-}
-
func TestValidationFails(t *testing.T) {
var obj FooStruct
req := requestWithBody("POST", "/", `{"bar": "foo"}`)
@@ -1138,6 +1195,46 @@ func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody
assert.Error(t, err)
}
+func testBodyBindingSlice(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
+ assert.Equal(t, name, b.Name())
+
+ var obj1 []FooStruct
+ req := requestWithBody("POST", path, body)
+ err := b.Bind(req, &obj1)
+ assert.NoError(t, err)
+
+ var obj2 []FooStruct
+ req = requestWithBody("POST", badPath, badBody)
+ err = JSON.Bind(req, &obj2)
+ assert.Error(t, err)
+}
+
+func testBodyBindingStringMap(t *testing.T, b Binding, path, badPath, body, badBody string) {
+ obj := make(map[string]string)
+ req := requestWithBody("POST", path, body)
+ if b.Name() == "form" {
+ req.Header.Add("Content-Type", MIMEPOSTForm)
+ }
+ err := b.Bind(req, &obj)
+ assert.NoError(t, err)
+ assert.NotNil(t, obj)
+ assert.Len(t, obj, 2)
+ assert.Equal(t, "bar", obj["foo"])
+ assert.Equal(t, "world", obj["hello"])
+
+ if badPath != "" && badBody != "" {
+ obj = make(map[string]string)
+ req = requestWithBody("POST", badPath, badBody)
+ err = b.Bind(req, &obj)
+ assert.Error(t, err)
+ }
+
+ objInt := make(map[string]int)
+ req = requestWithBody("POST", path, body)
+ err = b.Bind(req, &objInt)
+ assert.Error(t, err)
+}
+
func testBodyBindingUseNumber(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
assert.Equal(t, name, b.Name())
@@ -1250,23 +1347,6 @@ func testProtoBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body
assert.Error(t, err)
}
-func testMsgPackBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
- assert.Equal(t, name, b.Name())
-
- obj := FooStruct{}
- req := requestWithBody("POST", path, body)
- req.Header.Add("Content-Type", MIMEMSGPACK)
- err := b.Bind(req, &obj)
- assert.NoError(t, err)
- assert.Equal(t, "bar", obj.Foo)
-
- obj = FooStruct{}
- req = requestWithBody("POST", badPath, badBody)
- req.Header.Add("Content-Type", MIMEMSGPACK)
- err = MsgPack.Bind(req, &obj)
- assert.Error(t, err)
-}
-
func requestWithBody(method, path, body string) (req *http.Request) {
req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
return
diff --git a/binding/default_validator.go b/binding/default_validator.go
index a4c1a7f6..c57a120f 100644
--- a/binding/default_validator.go
+++ b/binding/default_validator.go
@@ -5,7 +5,9 @@
package binding
import (
+ "fmt"
"reflect"
+ "strings"
"sync"
"github.com/go-playground/validator/v10"
@@ -16,22 +18,54 @@ type defaultValidator struct {
validate *validator.Validate
}
+type sliceValidateError []error
+
+func (err sliceValidateError) Error() string {
+ var errMsgs []string
+ for i, e := range err {
+ if e == nil {
+ continue
+ }
+ errMsgs = append(errMsgs, fmt.Sprintf("[%d]: %s", i, e.Error()))
+ }
+ return strings.Join(errMsgs, "\n")
+}
+
var _ StructValidator = &defaultValidator{}
// ValidateStruct receives any kind of type, but only performed struct or pointer to struct type.
func (v *defaultValidator) ValidateStruct(obj interface{}) error {
+ if obj == nil {
+ return nil
+ }
+
value := reflect.ValueOf(obj)
- valueType := value.Kind()
- if valueType == reflect.Ptr {
- valueType = value.Elem().Kind()
- }
- if valueType == reflect.Struct {
- v.lazyinit()
- if err := v.validate.Struct(obj); err != nil {
- return err
+ switch value.Kind() {
+ case reflect.Ptr:
+ return v.ValidateStruct(value.Elem().Interface())
+ case reflect.Struct:
+ return v.validateStruct(obj)
+ case reflect.Slice, reflect.Array:
+ count := value.Len()
+ validateRet := make(sliceValidateError, 0)
+ for i := 0; i < count; i++ {
+ if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
+ validateRet = append(validateRet, err)
+ }
}
+ if len(validateRet) == 0 {
+ return nil
+ }
+ return validateRet
+ default:
+ return nil
}
- return nil
+}
+
+// validateStruct receives struct type
+func (v *defaultValidator) validateStruct(obj interface{}) error {
+ v.lazyinit()
+ return v.validate.Struct(obj)
}
// Engine returns the underlying validator engine which powers the default
diff --git a/binding/default_validator_test.go b/binding/default_validator_test.go
new file mode 100644
index 00000000..e9c6de44
--- /dev/null
+++ b/binding/default_validator_test.go
@@ -0,0 +1,68 @@
+// Copyright 2020 Gin Core Team. All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package binding
+
+import (
+ "errors"
+ "testing"
+)
+
+func TestSliceValidateError(t *testing.T) {
+ tests := []struct {
+ name string
+ err sliceValidateError
+ want string
+ }{
+ {"has nil elements", sliceValidateError{errors.New("test error"), nil}, "[0]: test error"},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := tt.err.Error(); got != tt.want {
+ t.Errorf("sliceValidateError.Error() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestDefaultValidator(t *testing.T) {
+ type exampleStruct struct {
+ A string `binding:"max=8"`
+ B int `binding:"gt=0"`
+ }
+ tests := []struct {
+ name string
+ v *defaultValidator
+ obj interface{}
+ wantErr bool
+ }{
+ {"validate nil obj", &defaultValidator{}, nil, false},
+ {"validate int obj", &defaultValidator{}, 3, false},
+ {"validate struct failed-1", &defaultValidator{}, exampleStruct{A: "123456789", B: 1}, true},
+ {"validate struct failed-2", &defaultValidator{}, exampleStruct{A: "12345678", B: 0}, true},
+ {"validate struct passed", &defaultValidator{}, exampleStruct{A: "12345678", B: 1}, false},
+ {"validate *struct failed-1", &defaultValidator{}, &exampleStruct{A: "123456789", B: 1}, true},
+ {"validate *struct failed-2", &defaultValidator{}, &exampleStruct{A: "12345678", B: 0}, true},
+ {"validate *struct passed", &defaultValidator{}, &exampleStruct{A: "12345678", B: 1}, false},
+ {"validate []struct failed-1", &defaultValidator{}, []exampleStruct{{A: "123456789", B: 1}}, true},
+ {"validate []struct failed-2", &defaultValidator{}, []exampleStruct{{A: "12345678", B: 0}}, true},
+ {"validate []struct passed", &defaultValidator{}, []exampleStruct{{A: "12345678", B: 1}}, false},
+ {"validate []*struct failed-1", &defaultValidator{}, []*exampleStruct{{A: "123456789", B: 1}}, true},
+ {"validate []*struct failed-2", &defaultValidator{}, []*exampleStruct{{A: "12345678", B: 0}}, true},
+ {"validate []*struct passed", &defaultValidator{}, []*exampleStruct{{A: "12345678", B: 1}}, false},
+ {"validate *[]struct failed-1", &defaultValidator{}, &[]exampleStruct{{A: "123456789", B: 1}}, true},
+ {"validate *[]struct failed-2", &defaultValidator{}, &[]exampleStruct{{A: "12345678", B: 0}}, true},
+ {"validate *[]struct passed", &defaultValidator{}, &[]exampleStruct{{A: "12345678", B: 1}}, false},
+ {"validate *[]*struct failed-1", &defaultValidator{}, &[]*exampleStruct{{A: "123456789", B: 1}}, true},
+ {"validate *[]*struct failed-2", &defaultValidator{}, &[]*exampleStruct{{A: "12345678", B: 0}}, true},
+ {"validate *[]*struct passed", &defaultValidator{}, &[]*exampleStruct{{A: "12345678", B: 1}}, false},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if err := tt.v.ValidateStruct(tt.obj); (err != nil) != tt.wantErr {
+ t.Errorf("defaultValidator.Validate() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
diff --git a/binding/form.go b/binding/form.go
index 9e9fc3de..b93c34cf 100644
--- a/binding/form.go
+++ b/binding/form.go
@@ -8,7 +8,7 @@ import (
"net/http"
)
-const defaultMemory = 32 * 1024 * 1024
+const defaultMemory = 32 << 20
type formBinding struct{}
type formPostBinding struct{}
diff --git a/binding/form_mapping.go b/binding/form_mapping.go
index d6199c4f..2f4e45b4 100644
--- a/binding/form_mapping.go
+++ b/binding/form_mapping.go
@@ -12,6 +12,7 @@ import (
"strings"
"time"
+ "github.com/gin-gonic/gin/internal/bytesconv"
"github.com/gin-gonic/gin/internal/json"
)
@@ -28,6 +29,21 @@ func mapForm(ptr interface{}, form map[string][]string) error {
var emptyField = reflect.StructField{}
func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error {
+ // Check if ptr is a map
+ ptrVal := reflect.ValueOf(ptr)
+ var pointed interface{}
+ if ptrVal.Kind() == reflect.Ptr {
+ ptrVal = ptrVal.Elem()
+ pointed = ptrVal.Interface()
+ }
+ if ptrVal.Kind() == reflect.Map &&
+ ptrVal.Type().Key().Kind() == reflect.String {
+ if pointed != nil {
+ ptr = pointed
+ }
+ return setFormMap(ptr, form)
+ }
+
return mappingByPtr(ptr, formSource(form), tag)
}
@@ -208,9 +224,9 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
case time.Time:
return setTimeField(val, field, value)
}
- return json.Unmarshal([]byte(val), value.Addr().Interface())
+ return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
case reflect.Map:
- return json.Unmarshal([]byte(val), value.Addr().Interface())
+ return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
default:
return errUnknownType
}
@@ -269,7 +285,7 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
switch tf := strings.ToLower(timeFormat); tf {
case "unix", "unixnano":
- tv, err := strconv.ParseInt(val, 10, 0)
+ tv, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return err
}
@@ -348,3 +364,29 @@ func head(str, sep string) (head string, tail string) {
}
return str[:idx], str[idx+len(sep):]
}
+
+func setFormMap(ptr interface{}, form map[string][]string) error {
+ el := reflect.TypeOf(ptr).Elem()
+
+ if el.Kind() == reflect.Slice {
+ ptrMap, ok := ptr.(map[string][]string)
+ if !ok {
+ return errors.New("cannot convert to map slices of strings")
+ }
+ for k, v := range form {
+ ptrMap[k] = v
+ }
+
+ return nil
+ }
+
+ ptrMap, ok := ptr.(map[string]string)
+ if !ok {
+ return errors.New("cannot convert to map of strings")
+ }
+ for k, v := range form {
+ ptrMap[k] = v[len(v)-1] // pick last
+ }
+
+ return nil
+}
diff --git a/binding/form_mapping_test.go b/binding/form_mapping_test.go
index 2a560371..2675d46b 100644
--- a/binding/form_mapping_test.go
+++ b/binding/form_mapping_test.go
@@ -190,7 +190,7 @@ func TestMappingTime(t *testing.T) {
assert.Error(t, err)
}
-func TestMapiingTimeDuration(t *testing.T) {
+func TestMappingTimeDuration(t *testing.T) {
var s struct {
D time.Duration
}
diff --git a/binding/json_test.go b/binding/json_test.go
index cae4cccc..fbd5c527 100644
--- a/binding/json_test.go
+++ b/binding/json_test.go
@@ -19,3 +19,12 @@ func TestJSONBindingBindBody(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}
+
+func TestJSONBindingBindBodyMap(t *testing.T) {
+ s := make(map[string]string)
+ err := jsonBinding{}.BindBody([]byte(`{"foo": "FOO","hello":"world"}`), &s)
+ require.NoError(t, err)
+ assert.Len(t, s, 2)
+ assert.Equal(t, "FOO", s["foo"])
+ assert.Equal(t, "world", s["hello"])
+}
diff --git a/binding/msgpack.go b/binding/msgpack.go
index b7f73197..2a442996 100644
--- a/binding/msgpack.go
+++ b/binding/msgpack.go
@@ -2,6 +2,9 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
+//go:build !nomsgpack
+// +build !nomsgpack
+
package binding
import (
diff --git a/binding/msgpack_test.go b/binding/msgpack_test.go
index 6baa6739..75600ba8 100644
--- a/binding/msgpack_test.go
+++ b/binding/msgpack_test.go
@@ -2,6 +2,9 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
+//go:build !nomsgpack
+// +build !nomsgpack
+
package binding
import (
diff --git a/context.go b/context.go
index 046f284e..1ba0fa2b 100644
--- a/context.go
+++ b/context.go
@@ -16,6 +16,7 @@ import (
"net/url"
"os"
"strings"
+ "sync"
"time"
"github.com/gin-contrib/sse"
@@ -33,9 +34,11 @@ const (
MIMEPOSTForm = binding.MIMEPOSTForm
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
MIMEYAML = binding.MIMEYAML
- BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
)
+// BodyBytesKey indicates a default body bytes key.
+const BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
+
const abortIndex int8 = math.MaxInt8 / 2
// Context is the most important part of gin. It allows us to pass variables between middleware,
@@ -51,6 +54,10 @@ type Context struct {
fullPath string
engine *Engine
+ params *Params
+
+ // This mutex protect Keys map
+ mu sync.RWMutex
// Keys is a key/value pair exclusively for the context of each request.
Keys map[string]interface{}
@@ -67,6 +74,10 @@ type Context struct {
// formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH,
// or PUT body parameters.
formCache url.Values
+
+ // SameSite allows a server to define a cookie attribute making it impossible for
+ // the browser to send this cookie along with cross-site requests.
+ sameSite http.SameSite
}
/************************************/
@@ -78,18 +89,25 @@ func (c *Context) reset() {
c.Params = c.Params[0:0]
c.handlers = nil
c.index = -1
+
c.fullPath = ""
c.Keys = nil
c.Errors = c.Errors[0:0]
c.Accepted = nil
c.queryCache = nil
c.formCache = nil
+ *c.params = (*c.params)[0:0]
}
// Copy returns a copy of the current context that can be safely used outside the request's scope.
// This has to be used when the context has to be passed to a goroutine.
func (c *Context) Copy() *Context {
- var cp = *c
+ cp := Context{
+ writermem: c.writermem,
+ Request: c.Request,
+ Params: c.Params,
+ engine: c.engine,
+ }
cp.writermem.ResponseWriter = nil
cp.Writer = &cp.writermem
cp.index = abortIndex
@@ -219,16 +237,21 @@ func (c *Context) Error(err error) *Error {
// Set is used to store a new key/value pair exclusively for this context.
// It also lazy initializes c.Keys if it was not used previously.
func (c *Context) Set(key string, value interface{}) {
+ c.mu.Lock()
if c.Keys == nil {
c.Keys = make(map[string]interface{})
}
+
c.Keys[key] = value
+ c.mu.Unlock()
}
// Get returns the value for the given key, ie: (value, true).
// If the value does not exists it returns (nil, false)
func (c *Context) Get(key string) (value interface{}, exists bool) {
+ c.mu.RLock()
value, exists = c.Keys[key]
+ c.mu.RUnlock()
return
}
@@ -272,6 +295,22 @@ func (c *Context) GetInt64(key string) (i64 int64) {
return
}
+// GetUint returns the value associated with the key as an unsigned integer.
+func (c *Context) GetUint(key string) (ui uint) {
+ if val, ok := c.Get(key); ok && val != nil {
+ ui, _ = val.(uint)
+ }
+ return
+}
+
+// GetUint64 returns the value associated with the key as an unsigned integer.
+func (c *Context) GetUint64(key string) (ui64 uint64) {
+ if val, ok := c.Get(key); ok && val != nil {
+ ui64, _ = val.(uint64)
+ }
+ return
+}
+
// GetFloat64 returns the value associated with the key as a float64.
func (c *Context) GetFloat64(key string) (f64 float64) {
if val, ok := c.Get(key); ok && val != nil {
@@ -391,16 +430,20 @@ func (c *Context) QueryArray(key string) []string {
return values
}
-func (c *Context) getQueryCache() {
+func (c *Context) initQueryCache() {
if c.queryCache == nil {
- c.queryCache = c.Request.URL.Query()
+ if c.Request != nil {
+ c.queryCache = c.Request.URL.Query()
+ } else {
+ c.queryCache = url.Values{}
+ }
}
}
// GetQueryArray returns a slice of strings for a given query key, plus
// a boolean value whether at least one value exists for the given key.
func (c *Context) GetQueryArray(key string) ([]string, bool) {
- c.getQueryCache()
+ c.initQueryCache()
if values, ok := c.queryCache[key]; ok && len(values) > 0 {
return values, true
}
@@ -416,7 +459,7 @@ func (c *Context) QueryMap(key string) map[string]string {
// GetQueryMap returns a map for a given query key, plus a boolean value
// whether at least one value exists for the given key.
func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
- c.getQueryCache()
+ c.initQueryCache()
return c.get(c.queryCache, key)
}
@@ -458,7 +501,7 @@ func (c *Context) PostFormArray(key string) []string {
return values
}
-func (c *Context) getFormCache() {
+func (c *Context) initFormCache() {
if c.formCache == nil {
c.formCache = make(url.Values)
req := c.Request
@@ -474,7 +517,7 @@ func (c *Context) getFormCache() {
// GetPostFormArray returns a slice of strings for a given form key, plus
// a boolean value whether at least one value exists for the given key.
func (c *Context) GetPostFormArray(key string) ([]string, bool) {
- c.getFormCache()
+ c.initFormCache()
if values := c.formCache[key]; len(values) > 0 {
return values, true
}
@@ -490,7 +533,7 @@ func (c *Context) PostFormMap(key string) map[string]string {
// GetPostFormMap returns a map for a given form key, plus a boolean value
// whether at least one value exists for the given key.
func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
- c.getFormCache()
+ c.initFormCache()
return c.get(c.formCache, key)
}
@@ -682,32 +725,82 @@ func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (e
return bb.BindBody(body, obj)
}
-// ClientIP implements a best effort algorithm to return the real client IP, it parses
-// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
-// Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP.
+// ClientIP implements a best effort algorithm to return the real client IP.
+// It called c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not.
+// If it's it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-Ip]).
+// If the headers are nots syntactically valid OR the remote IP does not correspong to a trusted proxy,
+// the remote IP (coming form Request.RemoteAddr) is returned.
func (c *Context) ClientIP() string {
- if c.engine.ForwardedByClientIP {
- clientIP := c.requestHeader("X-Forwarded-For")
- clientIP = strings.TrimSpace(strings.Split(clientIP, ",")[0])
- if clientIP == "" {
- clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))
- }
- if clientIP != "" {
- return clientIP
- }
- }
-
if c.engine.AppEngine {
if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" {
return addr
}
}
- if ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)); err == nil {
- return ip
+ remoteIP, trusted := c.RemoteIP()
+ if remoteIP == nil {
+ return ""
}
- return ""
+ if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {
+ for _, headerName := range c.engine.RemoteIPHeaders {
+ ip, valid := validateHeader(c.requestHeader(headerName))
+ if valid {
+ return ip
+ }
+ }
+ }
+ return remoteIP.String()
+}
+
+// RemoteIP parses the IP from Request.RemoteAddr, normalizes and returns the IP (without the port).
+// It also checks if the remoteIP is a trusted proxy or not.
+// In order to perform this validation, it will see if the IP is contained within at least one of the CIDR blocks
+// defined in Engine.TrustedProxies
+func (c *Context) RemoteIP() (net.IP, bool) {
+ ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr))
+ if err != nil {
+ return nil, false
+ }
+ remoteIP := net.ParseIP(ip)
+ if remoteIP == nil {
+ return nil, false
+ }
+
+ trustedCIDRs, _ := c.engine.prepareTrustedCIDRs()
+ c.engine.trustedCIDRs = trustedCIDRs
+ if c.engine.trustedCIDRs != nil {
+ for _, cidr := range c.engine.trustedCIDRs {
+ if cidr.Contains(remoteIP) {
+ return remoteIP, true
+ }
+ }
+ }
+
+ return remoteIP, false
+}
+
+func validateHeader(header string) (clientIP string, valid bool) {
+ if header == "" {
+ return "", false
+ }
+ items := strings.Split(header, ",")
+ for i, ipStr := range items {
+ ipStr = strings.TrimSpace(ipStr)
+ ip := net.ParseIP(ipStr)
+ if ip == nil {
+ return "", false
+ }
+
+ // We need to return the first IP in the list, but,
+ // we should not early return since we need to validate that
+ // the rest of the header is syntactically valid
+ if i == 0 {
+ clientIP = ipStr
+ valid = true
+ }
+ }
+ return
}
// ContentType returns the Content-Type header of the request.
@@ -772,6 +865,11 @@ func (c *Context) GetRawData() ([]byte, error) {
return ioutil.ReadAll(c.Request.Body)
}
+// SetSameSite with cookie
+func (c *Context) SetSameSite(samesite http.SameSite) {
+ c.sameSite = samesite
+}
+
// SetCookie adds a Set-Cookie header to the ResponseWriter's headers.
// The provided cookie must have a valid Name. Invalid cookies may be
// silently dropped.
@@ -785,6 +883,7 @@ func (c *Context) SetCookie(name, value string, maxAge int, path, domain string,
MaxAge: maxAge,
Path: path,
Domain: domain,
+ SameSite: c.sameSite,
Secure: secure,
HttpOnly: httpOnly,
})
@@ -838,11 +937,11 @@ func (c *Context) IndentedJSON(code int, obj interface{}) {
// Default prepends "while(1)," to response body if the given struct is array values.
// It also sets the Content-Type as "application/json".
func (c *Context) SecureJSON(code int, obj interface{}) {
- c.Render(code, render.SecureJSON{Prefix: c.engine.secureJsonPrefix, Data: obj})
+ c.Render(code, render.SecureJSON{Prefix: c.engine.secureJSONPrefix, Data: obj})
}
// JSONP serializes the given struct as JSON into the response body.
-// It add padding to response body to request data from a server residing in a different domain than the client.
+// It adds padding to response body to request data from a server residing in a different domain than the client.
// It also sets the Content-Type as "application/javascript".
func (c *Context) JSONP(code int, obj interface{}) {
callback := c.DefaultQuery("callback", "")
@@ -919,15 +1018,26 @@ func (c *Context) DataFromReader(code int, contentLength int64, contentType stri
})
}
-// File writes the specified file into the body stream in a efficient way.
+// File writes the specified file into the body stream in an efficient way.
func (c *Context) File(filepath string) {
http.ServeFile(c.Writer, c.Request, filepath)
}
+// FileFromFS writes the specified file from http.FileSystem into the body stream in an efficient way.
+func (c *Context) FileFromFS(filepath string, fs http.FileSystem) {
+ defer func(old string) {
+ c.Request.URL.Path = old
+ }(c.Request.URL.Path)
+
+ c.Request.URL.Path = filepath
+
+ http.FileServer(fs).ServeHTTP(c.Writer, c.Request)
+}
+
// 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))
+ c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
http.ServeFile(c.Writer, c.Request, filepath)
}
@@ -969,6 +1079,7 @@ type Negotiate struct {
HTMLData interface{}
JSONData interface{}
XMLData interface{}
+ YAMLData interface{}
Data interface{}
}
@@ -987,6 +1098,10 @@ func (c *Context) Negotiate(code int, config Negotiate) {
data := chooseData(config.XMLData, config.Data)
c.XML(code, data)
+ case binding.MIMEYAML:
+ data := chooseData(config.YAMLData, config.Data)
+ c.YAML(code, data)
+
default:
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) // nolint: errcheck
}
@@ -1003,20 +1118,20 @@ func (c *Context) NegotiateFormat(offered ...string) string {
return offered[0]
}
for _, accepted := range c.Accepted {
- for _, offert := range offered {
+ for _, offer := range offered {
// 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] == '*' || offer[i] == '*' {
+ return offer
}
- if accepted[i] != offert[i] {
+ if accepted[i] != offer[i] {
break
}
}
if i == len(accepted) {
- return offert
+ return offer
}
}
}
@@ -1032,26 +1147,20 @@ func (c *Context) SetAccepted(formats ...string) {
/***** GOLANG.ORG/X/NET/CONTEXT *****/
/************************************/
-// Deadline returns the time when work done on behalf of this context
-// should be canceled. Deadline returns ok==false when no deadline is
-// set. Successive calls to Deadline return the same results.
+// Deadline always returns that there is no deadline (ok==false),
+// maybe you want to use Request.Context().Deadline() instead.
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.
+// Done always returns nil (chan which will wait forever),
+// if you want to abort your work when the connection was closed
+// you should use Request.Context().Done() instead.
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.
+// Err always returns nil, maybe you want to use Request.Context().Err() instead.
func (c *Context) Err() error {
return nil
}
diff --git a/context_appengine.go b/context_appengine.go
index 38c189a0..d5658434 100644
--- a/context_appengine.go
+++ b/context_appengine.go
@@ -1,9 +1,10 @@
-// +build appengine
-
// Copyright 2017 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
+//go:build appengine
+// +build appengine
+
package gin
func init() {
diff --git a/context_test.go b/context_test.go
index 18709d3d..8fe47615 100644
--- a/context_test.go
+++ b/context_test.go
@@ -261,6 +261,18 @@ func TestContextGetInt64(t *testing.T) {
assert.Equal(t, int64(42424242424242), c.GetInt64("int64"))
}
+func TestContextGetUint(t *testing.T) {
+ c, _ := CreateTestContext(httptest.NewRecorder())
+ c.Set("uint", uint(1))
+ assert.Equal(t, uint(1), c.GetUint("uint"))
+}
+
+func TestContextGetUint64(t *testing.T) {
+ c, _ := CreateTestContext(httptest.NewRecorder())
+ c.Set("uint64", uint64(18446744073709551615))
+ assert.Equal(t, uint64(18446744073709551615), c.GetUint64("uint64"))
+}
+
func TestContextGetFloat64(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("float64", 4.2)
@@ -410,6 +422,21 @@ func TestContextQuery(t *testing.T) {
assert.Empty(t, c.PostForm("foo"))
}
+func TestContextDefaultQueryOnEmptyRequest(t *testing.T) {
+ c, _ := CreateTestContext(httptest.NewRecorder()) // here c.Request == nil
+ assert.NotPanics(t, func() {
+ value, ok := c.GetQuery("NoKey")
+ assert.False(t, ok)
+ assert.Empty(t, value)
+ })
+ assert.NotPanics(t, func() {
+ assert.Equal(t, "nada", c.DefaultQuery("NoKey", "nada"))
+ })
+ assert.NotPanics(t, func() {
+ assert.Empty(t, c.Query("NoKey"))
+ })
+}
+
func TestContextQueryAndPostForm(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
body := bytes.NewBufferString("foo=bar&page=11&both=&foo=second")
@@ -602,14 +629,16 @@ func TestContextPostFormMultipart(t *testing.T) {
func TestContextSetCookie(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
+ c.SetSameSite(http.SameSiteLaxMode)
c.SetCookie("user", "gin", 1, "/", "localhost", true, true)
- assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure", c.Writer.Header().Get("Set-Cookie"))
+ assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure; SameSite=Lax", c.Writer.Header().Get("Set-Cookie"))
}
func TestContextSetCookiePathEmpty(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
+ c.SetSameSite(http.SameSiteLaxMode)
c.SetCookie("user", "gin", 1, "", "localhost", true, true)
- assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure", c.Writer.Header().Get("Set-Cookie"))
+ assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure; SameSite=Lax", c.Writer.Header().Get("Set-Cookie"))
}
func TestContextGetCookie(t *testing.T) {
@@ -662,7 +691,7 @@ func TestContextRenderJSON(t *testing.T) {
c.JSON(http.StatusCreated, H{"foo": "bar", "html": ""})
assert.Equal(t, http.StatusCreated, w.Code)
- assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}\n", w.Body.String())
+ assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
@@ -690,7 +719,7 @@ func TestContextRenderJSONPWithoutCallback(t *testing.T) {
c.JSONP(http.StatusCreated, H{"foo": "bar"})
assert.Equal(t, http.StatusCreated, w.Code)
- assert.Equal(t, "{\"foo\":\"bar\"}\n", w.Body.String())
+ assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
@@ -716,7 +745,7 @@ func TestContextRenderAPIJSON(t *testing.T) {
c.JSON(http.StatusCreated, H{"foo": "bar"})
assert.Equal(t, http.StatusCreated, w.Code)
- assert.Equal(t, "{\"foo\":\"bar\"}\n", w.Body.String())
+ assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, "application/vnd.api+json", w.Header().Get("Content-Type"))
}
@@ -938,7 +967,7 @@ func TestContextRenderNoContentHTMLString(t *testing.T) {
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
}
-// TestContextData tests that the response can be written from `bytesting`
+// TestContextData tests that the response can be written from `bytestring`
// with specified MIME type
func TestContextRenderData(t *testing.T) {
w := httptest.NewRecorder()
@@ -992,6 +1021,19 @@ func TestContextRenderFile(t *testing.T) {
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
}
+func TestContextRenderFileFromFS(t *testing.T) {
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
+ c.Request, _ = http.NewRequest("GET", "/some/path", nil)
+ c.FileFromFS("./gin.go", Dir(".", false))
+
+ 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.Header().Get("Content-Type"))
+ assert.Equal(t, "/some/path", c.Request.URL.Path)
+}
+
func TestContextRenderAttachment(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
@@ -1114,12 +1156,12 @@ func TestContextNegotiationWithJSON(t *testing.T) {
c.Request, _ = http.NewRequest("POST", "", nil)
c.Negotiate(http.StatusOK, Negotiate{
- Offered: []string{MIMEJSON, MIMEXML},
+ Offered: []string{MIMEJSON, MIMEXML, MIMEYAML},
Data: H{"foo": "bar"},
})
assert.Equal(t, http.StatusOK, w.Code)
- assert.Equal(t, "{\"foo\":\"bar\"}\n", w.Body.String())
+ assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
@@ -1129,7 +1171,7 @@ func TestContextNegotiationWithXML(t *testing.T) {
c.Request, _ = http.NewRequest("POST", "", nil)
c.Negotiate(http.StatusOK, Negotiate{
- Offered: []string{MIMEXML, MIMEJSON},
+ Offered: []string{MIMEXML, MIMEJSON, MIMEYAML},
Data: H{"foo": "bar"},
})
@@ -1240,7 +1282,7 @@ func TestContextIsAborted(t *testing.T) {
assert.True(t, c.IsAborted())
}
-// TestContextData tests that the response can be written from `bytesting`
+// TestContextData tests that the response can be written from `bytestring`
// with specified MIME type
func TestContextAbortWithStatus(t *testing.T) {
w := httptest.NewRecorder()
@@ -1283,7 +1325,7 @@ func TestContextAbortWithStatusJSON(t *testing.T) {
_, err := buf.ReadFrom(w.Body)
assert.NoError(t, err)
jsonStringBody := buf.String()
- assert.Equal(t, fmt.Sprint("{\"foo\":\"fooValue\",\"bar\":\"barValue\"}\n"), jsonStringBody)
+ assert.Equal(t, fmt.Sprint("{\"foo\":\"fooValue\",\"bar\":\"barValue\"}"), jsonStringBody)
}
func TestContextError(t *testing.T) {
@@ -1350,11 +1392,10 @@ func TestContextClientIP(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
- c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ")
- c.Request.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30")
- c.Request.Header.Set("X-Appengine-Remote-Addr", "50.50.50.50")
- c.Request.RemoteAddr = " 40.40.40.40:42123 "
+ resetContextForClientIPTests(c)
+ // Legacy tests (validating that the defaults don't break the
+ // (insecure!) old behaviour)
assert.Equal(t, "20.20.20.20", c.ClientIP())
c.Request.Header.Del("X-Forwarded-For")
@@ -1374,6 +1415,74 @@ func TestContextClientIP(t *testing.T) {
// no port
c.Request.RemoteAddr = "50.50.50.50"
assert.Empty(t, c.ClientIP())
+
+ // Tests exercising the TrustedProxies functionality
+ resetContextForClientIPTests(c)
+
+ // No trusted proxies
+ c.engine.TrustedProxies = []string{}
+ c.engine.RemoteIPHeaders = []string{"X-Forwarded-For"}
+ assert.Equal(t, "40.40.40.40", c.ClientIP())
+
+ // Last proxy is trusted, but the RemoteAddr is not
+ c.engine.TrustedProxies = []string{"30.30.30.30"}
+ assert.Equal(t, "40.40.40.40", c.ClientIP())
+
+ // Only trust RemoteAddr
+ c.engine.TrustedProxies = []string{"40.40.40.40"}
+ assert.Equal(t, "20.20.20.20", c.ClientIP())
+
+ // All steps are trusted
+ c.engine.TrustedProxies = []string{"40.40.40.40", "30.30.30.30", "20.20.20.20"}
+ assert.Equal(t, "20.20.20.20", c.ClientIP())
+
+ // Use CIDR
+ c.engine.TrustedProxies = []string{"40.40.25.25/16", "30.30.30.30"}
+ assert.Equal(t, "20.20.20.20", c.ClientIP())
+
+ // Use hostname that resolves to all the proxies
+ c.engine.TrustedProxies = []string{"foo"}
+ assert.Equal(t, "40.40.40.40", c.ClientIP())
+
+ // Use hostname that returns an error
+ c.engine.TrustedProxies = []string{"bar"}
+ assert.Equal(t, "40.40.40.40", c.ClientIP())
+
+ // X-Forwarded-For has a non-IP element
+ c.engine.TrustedProxies = []string{"40.40.40.40"}
+ c.Request.Header.Set("X-Forwarded-For", " blah ")
+ assert.Equal(t, "40.40.40.40", c.ClientIP())
+
+ // Result from LookupHost has non-IP element. This should never
+ // happen, but we should test it to make sure we handle it
+ // gracefully.
+ c.engine.TrustedProxies = []string{"baz"}
+ c.Request.Header.Set("X-Forwarded-For", " 30.30.30.30 ")
+ assert.Equal(t, "40.40.40.40", c.ClientIP())
+
+ c.engine.TrustedProxies = []string{"40.40.40.40"}
+ c.Request.Header.Del("X-Forwarded-For")
+ c.engine.RemoteIPHeaders = []string{"X-Forwarded-For", "X-Real-IP"}
+ assert.Equal(t, "10.10.10.10", c.ClientIP())
+
+ c.engine.RemoteIPHeaders = []string{}
+ c.engine.AppEngine = true
+ assert.Equal(t, "50.50.50.50", c.ClientIP())
+
+ c.Request.Header.Del("X-Appengine-Remote-Addr")
+ assert.Equal(t, "40.40.40.40", c.ClientIP())
+
+ // no port
+ c.Request.RemoteAddr = "50.50.50.50"
+ assert.Empty(t, c.ClientIP())
+}
+
+func resetContextForClientIPTests(c *Context) {
+ c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ")
+ c.Request.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30")
+ c.Request.Header.Set("X-Appengine-Remote-Addr", "50.50.50.50")
+ c.Request.RemoteAddr = " 40.40.40.40:42123 "
+ c.engine.AppEngine = false
}
func TestContextContentType(t *testing.T) {
@@ -1905,3 +2014,25 @@ func TestRaceParamsContextCopy(t *testing.T) {
performRequest(router, "GET", "/name2/api")
wg.Wait()
}
+
+func TestContextWithKeysMutex(t *testing.T) {
+ c := &Context{}
+ c.Set("foo", "bar")
+
+ value, err := c.Get("foo")
+ assert.Equal(t, "bar", value)
+ assert.True(t, err)
+
+ value, err = c.Get("foo2")
+ assert.Nil(t, value)
+ assert.False(t, err)
+}
+
+func TestRemoteIPFail(t *testing.T) {
+ c, _ := CreateTestContext(httptest.NewRecorder())
+ c.Request, _ = http.NewRequest("POST", "/", nil)
+ c.Request.RemoteAddr = "[:::]:80"
+ ip, trust := c.RemoteIP()
+ assert.Nil(t, ip)
+ assert.False(t, trust)
+}
diff --git a/debug.go b/debug.go
index c66ca440..4c7cd0c3 100644
--- a/debug.go
+++ b/debug.go
@@ -12,7 +12,7 @@ import (
"strings"
)
-const ginSupportMinGoVer = 10
+const ginSupportMinGoVer = 12
// IsDebugging returns true if the framework is running in debug mode.
// Use SetMode(gin.ReleaseMode) to disable debug mode.
@@ -67,7 +67,7 @@ func getMinVer(v string) (uint64, error) {
func debugPrintWARNINGDefault() {
if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer {
- debugPrint(`[WARNING] Now Gin requires Go 1.11 or later and Go 1.12 will be required soon.
+ debugPrint(`[WARNING] Now Gin requires Go 1.12+.
`)
}
diff --git a/debug_test.go b/debug_test.go
index d707b4bf..c2272d0f 100644
--- a/debug_test.go
+++ b/debug_test.go
@@ -7,6 +7,7 @@ package gin
import (
"bytes"
"errors"
+ "fmt"
"html/template"
"io"
"log"
@@ -64,6 +65,18 @@ func TestDebugPrintRoutes(t *testing.T) {
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re)
}
+func TestDebugPrintRouteFunc(t *testing.T) {
+ DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
+ fmt.Fprintf(DefaultWriter, "[GIN-debug] %-6s %-40s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers)
+ }
+ re := captureOutput(t, func() {
+ SetMode(DebugMode)
+ debugPrintRoute("GET", "/path/to/route/:param1/:param2", HandlersChain{func(c *Context) {}, handlerNameTest})
+ SetMode(TestMode)
+ })
+ assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param1/:param2 --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re)
+}
+
func TestDebugPrintLoadTemplate(t *testing.T) {
re := captureOutput(t, func() {
SetMode(DebugMode)
@@ -91,7 +104,7 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
})
m, e := getMinVer(runtime.Version())
if e == nil && m <= ginSupportMinGoVer {
- assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.11 or later and Go 1.12 will be required soon.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
+ assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.12+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
} else {
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
}
diff --git a/errors.go b/errors.go
index 25e8ff60..0f276c13 100644
--- a/errors.go
+++ b/errors.go
@@ -55,7 +55,7 @@ func (msg *Error) SetMeta(data interface{}) *Error {
// JSON creates a properly formatted JSON
func (msg *Error) JSON() interface{} {
- json := H{}
+ jsonData := H{}
if msg.Meta != nil {
value := reflect.ValueOf(msg.Meta)
switch value.Kind() {
@@ -63,16 +63,16 @@ func (msg *Error) JSON() interface{} {
return msg.Meta
case reflect.Map:
for _, key := range value.MapKeys() {
- json[key.String()] = value.MapIndex(key).Interface()
+ jsonData[key.String()] = value.MapIndex(key).Interface()
}
default:
- json["meta"] = msg.Meta
+ jsonData["meta"] = msg.Meta
}
}
- if _, ok := json["error"]; !ok {
- json["error"] = msg.Error()
+ if _, ok := jsonData["error"]; !ok {
+ jsonData["error"] = msg.Error()
}
- return json
+ return jsonData
}
// MarshalJSON implements the json.Marshaller interface.
@@ -90,6 +90,11 @@ func (msg *Error) IsType(flags ErrorType) bool {
return (msg.Type & flags) > 0
}
+// Unwrap returns the wrapped error, to allow interoperability with errors.Is(), errors.As() and errors.Unwrap()
+func (msg *Error) Unwrap() error {
+ return msg.Err
+}
+
// ByType returns a readonly copy filtered the byte.
// ie ByType(gin.ErrorTypePublic) returns a slice of errors with type=ErrorTypePublic.
func (a errorMsgs) ByType(typ ErrorType) errorMsgs {
@@ -135,17 +140,17 @@ func (a errorMsgs) Errors() []string {
}
func (a errorMsgs) JSON() interface{} {
- switch len(a) {
+ switch length := len(a); length {
case 0:
return nil
case 1:
return a.Last().JSON()
default:
- json := make([]interface{}, len(a))
+ jsonData := make([]interface{}, length)
for i, err := range a {
- json[i] = err.JSON()
+ jsonData[i] = err.JSON()
}
- return json
+ return jsonData
}
}
diff --git a/errors_1.13_test.go b/errors_1.13_test.go
new file mode 100644
index 00000000..5fb6057b
--- /dev/null
+++ b/errors_1.13_test.go
@@ -0,0 +1,34 @@
+//go:build go1.13
+// +build go1.13
+
+package gin
+
+import (
+ "errors"
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+type TestErr string
+
+func (e TestErr) Error() string { return string(e) }
+
+// TestErrorUnwrap tests the behavior of gin.Error with "errors.Is()" and "errors.As()".
+// "errors.Is()" and "errors.As()" have been added to the standard library in go 1.13,
+// hence the "// +build go1.13" directive at the beginning of this file.
+func TestErrorUnwrap(t *testing.T) {
+ innerErr := TestErr("somme error")
+
+ // 2 layers of wrapping : use 'fmt.Errorf("%w")' to wrap a gin.Error{}, which itself wraps innerErr
+ err := fmt.Errorf("wrapped: %w", &Error{
+ Err: innerErr,
+ Type: ErrorTypeAny,
+ })
+
+ // check that 'errors.Is()' and 'errors.As()' behave as expected :
+ assert.True(t, errors.Is(err, innerErr))
+ var testErr TestErr
+ assert.True(t, errors.As(err, &testErr))
+}
diff --git a/fs.go b/fs.go
index 7a6738a6..007d9b75 100644
--- a/fs.go
+++ b/fs.go
@@ -9,7 +9,7 @@ import (
"os"
)
-type onlyfilesFS struct {
+type onlyFilesFS struct {
fs http.FileSystem
}
@@ -26,11 +26,11 @@ func Dir(root string, listDirectory bool) http.FileSystem {
if listDirectory {
return fs
}
- return &onlyfilesFS{fs}
+ return &onlyFilesFS{fs}
}
// Open conforms to http.Filesystem.
-func (fs onlyfilesFS) Open(name string) (http.File, error) {
+func (fs onlyFilesFS) Open(name string) (http.File, error) {
f, err := fs.fs.Open(name)
if err != nil {
return nil, err
diff --git a/gin.go b/gin.go
index caf4cf2c..03a0e127 100644
--- a/gin.go
+++ b/gin.go
@@ -11,19 +11,22 @@ import (
"net/http"
"os"
"path"
+ "strings"
"sync"
+ "github.com/gin-gonic/gin/internal/bytesconv"
"github.com/gin-gonic/gin/render"
)
const defaultMultipartMemory = 32 << 20 // 32 MB
var (
- default404Body = []byte("404 page not found")
- default405Body = []byte("405 method not allowed")
- defaultAppEngine bool
+ default404Body = []byte("404 page not found")
+ default405Body = []byte("405 method not allowed")
)
+var defaultAppEngine bool
+
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
@@ -79,9 +82,26 @@ type Engine struct {
// If no other Method is allowed, the request is delegated to the NotFound
// handler.
HandleMethodNotAllowed bool
- ForwardedByClientIP bool
- // #726 #755 If enabled, it will thrust some headers starting with
+ // If enabled, client IP will be parsed from the request's headers that
+ // match those stored at `(*gin.Engine).RemoteIPHeaders`. If no IP was
+ // fetched, it falls back to the IP obtained from
+ // `(*gin.Context).Request.RemoteAddr`.
+ ForwardedByClientIP bool
+
+ // List of headers used to obtain the client IP when
+ // `(*gin.Engine).ForwardedByClientIP` is `true` and
+ // `(*gin.Context).Request.RemoteAddr` is matched by at least one of the
+ // network origins of `(*gin.Engine).TrustedProxies`.
+ RemoteIPHeaders []string
+
+ // List of network origins (IPv4 addresses, IPv4 CIDRs, IPv6 addresses or
+ // IPv6 CIDRs) from which to trust request's headers that contain
+ // alternative client IP when `(*gin.Engine).ForwardedByClientIP` is
+ // `true`.
+ TrustedProxies []string
+
+ // #726 #755 If enabled, it will trust some headers starting with
// 'X-AppEngine...' for better integration with that PaaS.
AppEngine bool
@@ -97,8 +117,12 @@ type Engine struct {
// method call.
MaxMultipartMemory int64
+ // RemoveExtraSlash a parameter can be parsed from the URL even with extra slashes.
+ // See the PR #1817 and issue #1644
+ RemoveExtraSlash bool
+
delims render.Delims
- secureJsonPrefix string
+ secureJSONPrefix string
HTMLRender render.HTMLRender
FuncMap template.FuncMap
allNoRoute HandlersChain
@@ -107,6 +131,8 @@ type Engine struct {
noMethod HandlersChain
pool sync.Pool
trees methodTrees
+ maxParams uint16
+ trustedCIDRs []*net.IPNet
}
var _ IRouter = &Engine{}
@@ -132,13 +158,16 @@ func New() *Engine {
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
+ RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
+ TrustedProxies: []string{"0.0.0.0/0"},
AppEngine: defaultAppEngine,
UseRawPath: false,
+ RemoveExtraSlash: false,
UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory,
trees: make(methodTrees, 0, 9),
delims: render.Delims{Left: "{{", Right: "}}"},
- secureJsonPrefix: "while(1);",
+ secureJSONPrefix: "while(1);",
}
engine.RouterGroup.engine = engine
engine.pool.New = func() interface{} {
@@ -156,7 +185,8 @@ func Default() *Engine {
}
func (engine *Engine) allocateContext() *Context {
- return &Context{engine: engine}
+ v := make(Params, 0, engine.maxParams)
+ return &Context{engine: engine, params: &v}
}
// Delims sets template left and right delims and returns a Engine instance.
@@ -165,9 +195,9 @@ func (engine *Engine) Delims(left, right string) *Engine {
return engine
}
-// SecureJsonPrefix sets the secureJsonPrefix used in Context.SecureJSON.
+// SecureJsonPrefix sets the secureJSONPrefix used in Context.SecureJSON.
func (engine *Engine) SecureJsonPrefix(prefix string) *Engine {
- engine.secureJsonPrefix = prefix
+ engine.secureJSONPrefix = prefix
return engine
}
@@ -249,6 +279,7 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
assert1(len(handlers) > 0, "there must be at least one handler")
debugPrintRoute(method, path, handlers)
+
root := engine.trees.get(method)
if root == nil {
root = new(node)
@@ -256,6 +287,11 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
root.addRoute(path, handlers)
+
+ // Update maxParams
+ if paramsCount := countParams(path); paramsCount > engine.maxParams {
+ engine.maxParams = paramsCount
+ }
}
// Routes returns a slice of registered routes, including some useful information, such as:
@@ -290,12 +326,60 @@ func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo {
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
+ trustedCIDRs, err := engine.prepareTrustedCIDRs()
+ if err != nil {
+ return err
+ }
+ engine.trustedCIDRs = trustedCIDRs
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine)
return
}
+func (engine *Engine) prepareTrustedCIDRs() ([]*net.IPNet, error) {
+ if engine.TrustedProxies == nil {
+ return nil, nil
+ }
+
+ cidr := make([]*net.IPNet, 0, len(engine.TrustedProxies))
+ for _, trustedProxy := range engine.TrustedProxies {
+ if !strings.Contains(trustedProxy, "/") {
+ ip := parseIP(trustedProxy)
+ if ip == nil {
+ return cidr, &net.ParseError{Type: "IP address", Text: trustedProxy}
+ }
+
+ switch len(ip) {
+ case net.IPv4len:
+ trustedProxy += "/32"
+ case net.IPv6len:
+ trustedProxy += "/128"
+ }
+ }
+ _, cidrNet, err := net.ParseCIDR(trustedProxy)
+ if err != nil {
+ return cidr, err
+ }
+ cidr = append(cidr, cidrNet)
+ }
+ return cidr, nil
+}
+
+// parseIP parse a string representation of an IP and returns a net.IP with the
+// minimum byte representation or nil if input is invalid.
+func parseIP(ip string) net.IP {
+ parsedIP := net.ParseIP(ip)
+
+ if ipv4 := parsedIP.To4(); ipv4 != nil {
+ // return ip in a 4-byte representation
+ return ipv4
+ }
+
+ // return ip in a 16-byte representation or nil
+ return parsedIP
+}
+
// RunTLS attaches the router to a http.Server and starts listening and serving HTTPS (secure) 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.
@@ -314,16 +398,13 @@ func (engine *Engine) RunUnix(file string) (err error) {
debugPrint("Listening and serving HTTP on unix:/%s", file)
defer func() { debugPrintError(err) }()
- os.Remove(file)
listener, err := net.Listen("unix", file)
if err != nil {
return
}
defer listener.Close()
- err = os.Chmod(file, 0777)
- if err != nil {
- return
- }
+ defer os.Remove(file)
+
err = http.Serve(listener, engine)
return
}
@@ -385,7 +466,10 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
rPath = c.Request.URL.RawPath
unescape = engine.UnescapePathValues
}
- rPath = cleanPath(rPath)
+
+ if engine.RemoveExtraSlash {
+ rPath = cleanPath(rPath)
+ }
// Find root of the tree for the given HTTP method
t := engine.trees
@@ -395,10 +479,12 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
}
root := t[i].root
// Find route in tree
- value := root.getValue(rPath, c.Params, unescape)
+ value := root.getValue(rPath, c.params, unescape)
+ if value.params != nil {
+ c.Params = *value.params
+ }
if value.handlers != nil {
c.handlers = value.handlers
- c.Params = value.params
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
@@ -457,18 +543,11 @@ func redirectTrailingSlash(c *Context) {
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 = p + "/"
if length := len(p); length > 1 && p[length-1] == '/' {
req.URL.Path = p[:length-1]
}
- debugPrint("redirecting request %d: %s --> %s", code, p, req.URL.String())
- http.Redirect(c.Writer, req, req.URL.String(), code)
- c.writermem.WriteHeaderNow()
+ redirectRequest(c)
}
func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
@@ -476,15 +555,23 @@ func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
rPath := req.URL.Path
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, rPath, req.URL.String())
- http.Redirect(c.Writer, req, req.URL.String(), code)
- c.writermem.WriteHeaderNow()
+ req.URL.Path = bytesconv.BytesToString(fixedPath)
+ redirectRequest(c)
return true
}
return false
}
+
+func redirectRequest(c *Context) {
+ req := c.Request
+ rPath := req.URL.Path
+ rURL := req.URL.String()
+
+ code := http.StatusMovedPermanently // Permanent redirect, request with GET method
+ if req.Method != http.MethodGet {
+ code = http.StatusTemporaryRedirect
+ }
+ debugPrint("redirecting request %d: %s --> %s", code, rPath, rURL)
+ http.Redirect(c.Writer, req, rURL, code)
+ c.writermem.WriteHeaderNow()
+}
diff --git a/gin_integration_test.go b/gin_integration_test.go
index d86f610b..fd972657 100644
--- a/gin_integration_test.go
+++ b/gin_integration_test.go
@@ -14,6 +14,7 @@ import (
"net/http"
"net/http/httptest"
"os"
+ "path/filepath"
"sync"
"testing"
"time"
@@ -54,6 +55,13 @@ func TestRunEmpty(t *testing.T) {
testRequest(t, "http://localhost:8080/example")
}
+func TestTrustedCIDRsForRun(t *testing.T) {
+ os.Setenv("PORT", "")
+ router := New()
+ router.TrustedProxies = []string{"hello/world"}
+ assert.Error(t, router.Run(":8080"))
+}
+
func TestRunTLS(t *testing.T) {
router := New()
go func() {
@@ -146,15 +154,19 @@ func TestRunWithPort(t *testing.T) {
func TestUnixSocket(t *testing.T) {
router := New()
+ unixTestSocket := filepath.Join(os.TempDir(), "unix_unit_test")
+
+ defer os.Remove(unixTestSocket)
+
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
- assert.NoError(t, router.RunUnix("/tmp/unix_unit_test"))
+ assert.NoError(t, router.RunUnix(unixTestSocket))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
- c, err := net.Dial("unix", "/tmp/unix_unit_test")
+ c, err := net.Dial("unix", unixTestSocket)
assert.NoError(t, err)
fmt.Fprint(c, "GET /example HTTP/1.0\r\n\r\n")
@@ -291,7 +303,7 @@ func TestConcurrentHandleContext(t *testing.T) {
// }
func testGetRequestHandler(t *testing.T, h http.Handler, url string) {
- req, err := http.NewRequest("GET", url, nil)
+ req, err := http.NewRequest(http.MethodGet, url, nil)
assert.NoError(t, err)
w := httptest.NewRecorder()
diff --git a/gin_test.go b/gin_test.go
index 11bdd79c..678d95f2 100644
--- a/gin_test.go
+++ b/gin_test.go
@@ -9,6 +9,7 @@ import (
"fmt"
"html/template"
"io/ioutil"
+ "net"
"net/http"
"net/http/httptest"
"reflect"
@@ -532,6 +533,139 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
assert.Equal(t, int64(expectValue), middlewareCounter)
}
+func TestPrepareTrustedCIRDsWith(t *testing.T) {
+ r := New()
+
+ // valid ipv4 cidr
+ {
+ expectedTrustedCIDRs := []*net.IPNet{parseCIDR("0.0.0.0/0")}
+ r.TrustedProxies = []string{"0.0.0.0/0"}
+
+ trustedCIDRs, err := r.prepareTrustedCIDRs()
+
+ assert.NoError(t, err)
+ assert.Equal(t, expectedTrustedCIDRs, trustedCIDRs)
+ }
+
+ // invalid ipv4 cidr
+ {
+ r.TrustedProxies = []string{"192.168.1.33/33"}
+
+ _, err := r.prepareTrustedCIDRs()
+
+ assert.Error(t, err)
+ }
+
+ // valid ipv4 address
+ {
+ expectedTrustedCIDRs := []*net.IPNet{parseCIDR("192.168.1.33/32")}
+ r.TrustedProxies = []string{"192.168.1.33"}
+
+ trustedCIDRs, err := r.prepareTrustedCIDRs()
+
+ assert.NoError(t, err)
+ assert.Equal(t, expectedTrustedCIDRs, trustedCIDRs)
+ }
+
+ // invalid ipv4 address
+ {
+ r.TrustedProxies = []string{"192.168.1.256"}
+
+ _, err := r.prepareTrustedCIDRs()
+
+ assert.Error(t, err)
+ }
+
+ // valid ipv6 address
+ {
+ expectedTrustedCIDRs := []*net.IPNet{parseCIDR("2002:0000:0000:1234:abcd:ffff:c0a8:0101/128")}
+ r.TrustedProxies = []string{"2002:0000:0000:1234:abcd:ffff:c0a8:0101"}
+
+ trustedCIDRs, err := r.prepareTrustedCIDRs()
+
+ assert.NoError(t, err)
+ assert.Equal(t, expectedTrustedCIDRs, trustedCIDRs)
+ }
+
+ // invalid ipv6 address
+ {
+ r.TrustedProxies = []string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101"}
+
+ _, err := r.prepareTrustedCIDRs()
+
+ assert.Error(t, err)
+ }
+
+ // valid ipv6 cidr
+ {
+ expectedTrustedCIDRs := []*net.IPNet{parseCIDR("::/0")}
+ r.TrustedProxies = []string{"::/0"}
+
+ trustedCIDRs, err := r.prepareTrustedCIDRs()
+
+ assert.NoError(t, err)
+ assert.Equal(t, expectedTrustedCIDRs, trustedCIDRs)
+ }
+
+ // invalid ipv6 cidr
+ {
+ r.TrustedProxies = []string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101/129"}
+
+ _, err := r.prepareTrustedCIDRs()
+
+ assert.Error(t, err)
+ }
+
+ // valid combination
+ {
+ expectedTrustedCIDRs := []*net.IPNet{
+ parseCIDR("::/0"),
+ parseCIDR("192.168.0.0/16"),
+ parseCIDR("172.16.0.1/32"),
+ }
+ r.TrustedProxies = []string{
+ "::/0",
+ "192.168.0.0/16",
+ "172.16.0.1",
+ }
+
+ trustedCIDRs, err := r.prepareTrustedCIDRs()
+
+ assert.NoError(t, err)
+ assert.Equal(t, expectedTrustedCIDRs, trustedCIDRs)
+ }
+
+ // invalid combination
+ {
+ r.TrustedProxies = []string{
+ "::/0",
+ "192.168.0.0/16",
+ "172.16.0.256",
+ }
+ _, err := r.prepareTrustedCIDRs()
+
+ assert.Error(t, err)
+ }
+
+ // nil value
+ {
+ r.TrustedProxies = nil
+ trustedCIDRs, err := r.prepareTrustedCIDRs()
+
+ assert.Nil(t, trustedCIDRs)
+ assert.Nil(t, err)
+ }
+
+}
+
+func parseCIDR(cidr string) *net.IPNet {
+ _, parsedCIDR, err := net.ParseCIDR(cidr)
+ if err != nil {
+ fmt.Println(err)
+ }
+ return parsedCIDR
+}
+
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/githubapi_test.go b/githubapi_test.go
index fb74d659..3f440bce 100644
--- a/githubapi_test.go
+++ b/githubapi_test.go
@@ -24,265 +24,265 @@ type route struct {
// http://developer.github.com/v3/
var githubAPI = []route{
// OAuth Authorizations
- {"GET", "/authorizations"},
- {"GET", "/authorizations/:id"},
- {"POST", "/authorizations"},
- //{"PUT", "/authorizations/clients/:client_id"},
- //{"PATCH", "/authorizations/:id"},
- {"DELETE", "/authorizations/:id"},
- {"GET", "/applications/:client_id/tokens/:access_token"},
- {"DELETE", "/applications/:client_id/tokens"},
- {"DELETE", "/applications/:client_id/tokens/:access_token"},
+ {http.MethodGet, "/authorizations"},
+ {http.MethodGet, "/authorizations/:id"},
+ {http.MethodPost, "/authorizations"},
+ //{http.MethodPut, "/authorizations/clients/:client_id"},
+ //{http.MethodPatch, "/authorizations/:id"},
+ {http.MethodDelete, "/authorizations/:id"},
+ {http.MethodGet, "/applications/:client_id/tokens/:access_token"},
+ {http.MethodDelete, "/applications/:client_id/tokens"},
+ {http.MethodDelete, "/applications/:client_id/tokens/:access_token"},
// Activity
- {"GET", "/events"},
- {"GET", "/repos/:owner/:repo/events"},
- {"GET", "/networks/:owner/:repo/events"},
- {"GET", "/orgs/:org/events"},
- {"GET", "/users/:user/received_events"},
- {"GET", "/users/:user/received_events/public"},
- {"GET", "/users/:user/events"},
- {"GET", "/users/:user/events/public"},
- {"GET", "/users/:user/events/orgs/:org"},
- {"GET", "/feeds"},
- {"GET", "/notifications"},
- {"GET", "/repos/:owner/:repo/notifications"},
- {"PUT", "/notifications"},
- {"PUT", "/repos/:owner/:repo/notifications"},
- {"GET", "/notifications/threads/:id"},
- //{"PATCH", "/notifications/threads/:id"},
- {"GET", "/notifications/threads/:id/subscription"},
- {"PUT", "/notifications/threads/:id/subscription"},
- {"DELETE", "/notifications/threads/:id/subscription"},
- {"GET", "/repos/:owner/:repo/stargazers"},
- {"GET", "/users/:user/starred"},
- {"GET", "/user/starred"},
- {"GET", "/user/starred/:owner/:repo"},
- {"PUT", "/user/starred/:owner/:repo"},
- {"DELETE", "/user/starred/:owner/:repo"},
- {"GET", "/repos/:owner/:repo/subscribers"},
- {"GET", "/users/:user/subscriptions"},
- {"GET", "/user/subscriptions"},
- {"GET", "/repos/:owner/:repo/subscription"},
- {"PUT", "/repos/:owner/:repo/subscription"},
- {"DELETE", "/repos/:owner/:repo/subscription"},
- {"GET", "/user/subscriptions/:owner/:repo"},
- {"PUT", "/user/subscriptions/:owner/:repo"},
- {"DELETE", "/user/subscriptions/:owner/:repo"},
+ {http.MethodGet, "/events"},
+ {http.MethodGet, "/repos/:owner/:repo/events"},
+ {http.MethodGet, "/networks/:owner/:repo/events"},
+ {http.MethodGet, "/orgs/:org/events"},
+ {http.MethodGet, "/users/:user/received_events"},
+ {http.MethodGet, "/users/:user/received_events/public"},
+ {http.MethodGet, "/users/:user/events"},
+ {http.MethodGet, "/users/:user/events/public"},
+ {http.MethodGet, "/users/:user/events/orgs/:org"},
+ {http.MethodGet, "/feeds"},
+ {http.MethodGet, "/notifications"},
+ {http.MethodGet, "/repos/:owner/:repo/notifications"},
+ {http.MethodPut, "/notifications"},
+ {http.MethodPut, "/repos/:owner/:repo/notifications"},
+ {http.MethodGet, "/notifications/threads/:id"},
+ //{http.MethodPatch, "/notifications/threads/:id"},
+ {http.MethodGet, "/notifications/threads/:id/subscription"},
+ {http.MethodPut, "/notifications/threads/:id/subscription"},
+ {http.MethodDelete, "/notifications/threads/:id/subscription"},
+ {http.MethodGet, "/repos/:owner/:repo/stargazers"},
+ {http.MethodGet, "/users/:user/starred"},
+ {http.MethodGet, "/user/starred"},
+ {http.MethodGet, "/user/starred/:owner/:repo"},
+ {http.MethodPut, "/user/starred/:owner/:repo"},
+ {http.MethodDelete, "/user/starred/:owner/:repo"},
+ {http.MethodGet, "/repos/:owner/:repo/subscribers"},
+ {http.MethodGet, "/users/:user/subscriptions"},
+ {http.MethodGet, "/user/subscriptions"},
+ {http.MethodGet, "/repos/:owner/:repo/subscription"},
+ {http.MethodPut, "/repos/:owner/:repo/subscription"},
+ {http.MethodDelete, "/repos/:owner/:repo/subscription"},
+ {http.MethodGet, "/user/subscriptions/:owner/:repo"},
+ {http.MethodPut, "/user/subscriptions/:owner/:repo"},
+ {http.MethodDelete, "/user/subscriptions/:owner/:repo"},
// Gists
- {"GET", "/users/:user/gists"},
- {"GET", "/gists"},
- //{"GET", "/gists/public"},
- //{"GET", "/gists/starred"},
- {"GET", "/gists/:id"},
- {"POST", "/gists"},
- //{"PATCH", "/gists/:id"},
- {"PUT", "/gists/:id/star"},
- {"DELETE", "/gists/:id/star"},
- {"GET", "/gists/:id/star"},
- {"POST", "/gists/:id/forks"},
- {"DELETE", "/gists/:id"},
+ {http.MethodGet, "/users/:user/gists"},
+ {http.MethodGet, "/gists"},
+ //{http.MethodGet, "/gists/public"},
+ //{http.MethodGet, "/gists/starred"},
+ {http.MethodGet, "/gists/:id"},
+ {http.MethodPost, "/gists"},
+ //{http.MethodPatch, "/gists/:id"},
+ {http.MethodPut, "/gists/:id/star"},
+ {http.MethodDelete, "/gists/:id/star"},
+ {http.MethodGet, "/gists/:id/star"},
+ {http.MethodPost, "/gists/:id/forks"},
+ {http.MethodDelete, "/gists/:id"},
// Git Data
- {"GET", "/repos/:owner/:repo/git/blobs/:sha"},
- {"POST", "/repos/:owner/:repo/git/blobs"},
- {"GET", "/repos/:owner/:repo/git/commits/:sha"},
- {"POST", "/repos/:owner/:repo/git/commits"},
- //{"GET", "/repos/:owner/:repo/git/refs/*ref"},
- {"GET", "/repos/:owner/:repo/git/refs"},
- {"POST", "/repos/:owner/:repo/git/refs"},
- //{"PATCH", "/repos/:owner/:repo/git/refs/*ref"},
- //{"DELETE", "/repos/:owner/:repo/git/refs/*ref"},
- {"GET", "/repos/:owner/:repo/git/tags/:sha"},
- {"POST", "/repos/:owner/:repo/git/tags"},
- {"GET", "/repos/:owner/:repo/git/trees/:sha"},
- {"POST", "/repos/:owner/:repo/git/trees"},
+ {http.MethodGet, "/repos/:owner/:repo/git/blobs/:sha"},
+ {http.MethodPost, "/repos/:owner/:repo/git/blobs"},
+ {http.MethodGet, "/repos/:owner/:repo/git/commits/:sha"},
+ {http.MethodPost, "/repos/:owner/:repo/git/commits"},
+ //{http.MethodGet, "/repos/:owner/:repo/git/refs/*ref"},
+ {http.MethodGet, "/repos/:owner/:repo/git/refs"},
+ {http.MethodPost, "/repos/:owner/:repo/git/refs"},
+ //{http.MethodPatch, "/repos/:owner/:repo/git/refs/*ref"},
+ //{http.MethodDelete, "/repos/:owner/:repo/git/refs/*ref"},
+ {http.MethodGet, "/repos/:owner/:repo/git/tags/:sha"},
+ {http.MethodPost, "/repos/:owner/:repo/git/tags"},
+ {http.MethodGet, "/repos/:owner/:repo/git/trees/:sha"},
+ {http.MethodPost, "/repos/:owner/:repo/git/trees"},
// Issues
- {"GET", "/issues"},
- {"GET", "/user/issues"},
- {"GET", "/orgs/:org/issues"},
- {"GET", "/repos/:owner/:repo/issues"},
- {"GET", "/repos/:owner/:repo/issues/:number"},
- {"POST", "/repos/:owner/:repo/issues"},
- //{"PATCH", "/repos/:owner/:repo/issues/:number"},
- {"GET", "/repos/:owner/:repo/assignees"},
- {"GET", "/repos/:owner/:repo/assignees/:assignee"},
- {"GET", "/repos/:owner/:repo/issues/:number/comments"},
- //{"GET", "/repos/:owner/:repo/issues/comments"},
- //{"GET", "/repos/:owner/:repo/issues/comments/:id"},
- {"POST", "/repos/:owner/:repo/issues/:number/comments"},
- //{"PATCH", "/repos/:owner/:repo/issues/comments/:id"},
- //{"DELETE", "/repos/:owner/:repo/issues/comments/:id"},
- {"GET", "/repos/:owner/:repo/issues/:number/events"},
- //{"GET", "/repos/:owner/:repo/issues/events"},
- //{"GET", "/repos/:owner/:repo/issues/events/:id"},
- {"GET", "/repos/:owner/:repo/labels"},
- {"GET", "/repos/:owner/:repo/labels/:name"},
- {"POST", "/repos/:owner/:repo/labels"},
- //{"PATCH", "/repos/:owner/:repo/labels/:name"},
- {"DELETE", "/repos/:owner/:repo/labels/:name"},
- {"GET", "/repos/:owner/:repo/issues/:number/labels"},
- {"POST", "/repos/:owner/:repo/issues/:number/labels"},
- {"DELETE", "/repos/:owner/:repo/issues/:number/labels/:name"},
- {"PUT", "/repos/:owner/:repo/issues/:number/labels"},
- {"DELETE", "/repos/:owner/:repo/issues/:number/labels"},
- {"GET", "/repos/:owner/:repo/milestones/:number/labels"},
- {"GET", "/repos/:owner/:repo/milestones"},
- {"GET", "/repos/:owner/:repo/milestones/:number"},
- {"POST", "/repos/:owner/:repo/milestones"},
- //{"PATCH", "/repos/:owner/:repo/milestones/:number"},
- {"DELETE", "/repos/:owner/:repo/milestones/:number"},
+ {http.MethodGet, "/issues"},
+ {http.MethodGet, "/user/issues"},
+ {http.MethodGet, "/orgs/:org/issues"},
+ {http.MethodGet, "/repos/:owner/:repo/issues"},
+ {http.MethodGet, "/repos/:owner/:repo/issues/:number"},
+ {http.MethodPost, "/repos/:owner/:repo/issues"},
+ //{http.MethodPatch, "/repos/:owner/:repo/issues/:number"},
+ {http.MethodGet, "/repos/:owner/:repo/assignees"},
+ {http.MethodGet, "/repos/:owner/:repo/assignees/:assignee"},
+ {http.MethodGet, "/repos/:owner/:repo/issues/:number/comments"},
+ //{http.MethodGet, "/repos/:owner/:repo/issues/comments"},
+ //{http.MethodGet, "/repos/:owner/:repo/issues/comments/:id"},
+ {http.MethodPost, "/repos/:owner/:repo/issues/:number/comments"},
+ //{http.MethodPatch, "/repos/:owner/:repo/issues/comments/:id"},
+ //{http.MethodDelete, "/repos/:owner/:repo/issues/comments/:id"},
+ {http.MethodGet, "/repos/:owner/:repo/issues/:number/events"},
+ //{http.MethodGet, "/repos/:owner/:repo/issues/events"},
+ //{http.MethodGet, "/repos/:owner/:repo/issues/events/:id"},
+ {http.MethodGet, "/repos/:owner/:repo/labels"},
+ {http.MethodGet, "/repos/:owner/:repo/labels/:name"},
+ {http.MethodPost, "/repos/:owner/:repo/labels"},
+ //{http.MethodPatch, "/repos/:owner/:repo/labels/:name"},
+ {http.MethodDelete, "/repos/:owner/:repo/labels/:name"},
+ {http.MethodGet, "/repos/:owner/:repo/issues/:number/labels"},
+ {http.MethodPost, "/repos/:owner/:repo/issues/:number/labels"},
+ {http.MethodDelete, "/repos/:owner/:repo/issues/:number/labels/:name"},
+ {http.MethodPut, "/repos/:owner/:repo/issues/:number/labels"},
+ {http.MethodDelete, "/repos/:owner/:repo/issues/:number/labels"},
+ {http.MethodGet, "/repos/:owner/:repo/milestones/:number/labels"},
+ {http.MethodGet, "/repos/:owner/:repo/milestones"},
+ {http.MethodGet, "/repos/:owner/:repo/milestones/:number"},
+ {http.MethodPost, "/repos/:owner/:repo/milestones"},
+ //{http.MethodPatch, "/repos/:owner/:repo/milestones/:number"},
+ {http.MethodDelete, "/repos/:owner/:repo/milestones/:number"},
// Miscellaneous
- {"GET", "/emojis"},
- {"GET", "/gitignore/templates"},
- {"GET", "/gitignore/templates/:name"},
- {"POST", "/markdown"},
- {"POST", "/markdown/raw"},
- {"GET", "/meta"},
- {"GET", "/rate_limit"},
+ {http.MethodGet, "/emojis"},
+ {http.MethodGet, "/gitignore/templates"},
+ {http.MethodGet, "/gitignore/templates/:name"},
+ {http.MethodPost, "/markdown"},
+ {http.MethodPost, "/markdown/raw"},
+ {http.MethodGet, "/meta"},
+ {http.MethodGet, "/rate_limit"},
// Organizations
- {"GET", "/users/:user/orgs"},
- {"GET", "/user/orgs"},
- {"GET", "/orgs/:org"},
- //{"PATCH", "/orgs/:org"},
- {"GET", "/orgs/:org/members"},
- {"GET", "/orgs/:org/members/:user"},
- {"DELETE", "/orgs/:org/members/:user"},
- {"GET", "/orgs/:org/public_members"},
- {"GET", "/orgs/:org/public_members/:user"},
- {"PUT", "/orgs/:org/public_members/:user"},
- {"DELETE", "/orgs/:org/public_members/:user"},
- {"GET", "/orgs/:org/teams"},
- {"GET", "/teams/:id"},
- {"POST", "/orgs/:org/teams"},
- //{"PATCH", "/teams/:id"},
- {"DELETE", "/teams/:id"},
- {"GET", "/teams/:id/members"},
- {"GET", "/teams/:id/members/:user"},
- {"PUT", "/teams/:id/members/:user"},
- {"DELETE", "/teams/:id/members/:user"},
- {"GET", "/teams/:id/repos"},
- {"GET", "/teams/:id/repos/:owner/:repo"},
- {"PUT", "/teams/:id/repos/:owner/:repo"},
- {"DELETE", "/teams/:id/repos/:owner/:repo"},
- {"GET", "/user/teams"},
+ {http.MethodGet, "/users/:user/orgs"},
+ {http.MethodGet, "/user/orgs"},
+ {http.MethodGet, "/orgs/:org"},
+ //{http.MethodPatch, "/orgs/:org"},
+ {http.MethodGet, "/orgs/:org/members"},
+ {http.MethodGet, "/orgs/:org/members/:user"},
+ {http.MethodDelete, "/orgs/:org/members/:user"},
+ {http.MethodGet, "/orgs/:org/public_members"},
+ {http.MethodGet, "/orgs/:org/public_members/:user"},
+ {http.MethodPut, "/orgs/:org/public_members/:user"},
+ {http.MethodDelete, "/orgs/:org/public_members/:user"},
+ {http.MethodGet, "/orgs/:org/teams"},
+ {http.MethodGet, "/teams/:id"},
+ {http.MethodPost, "/orgs/:org/teams"},
+ //{http.MethodPatch, "/teams/:id"},
+ {http.MethodDelete, "/teams/:id"},
+ {http.MethodGet, "/teams/:id/members"},
+ {http.MethodGet, "/teams/:id/members/:user"},
+ {http.MethodPut, "/teams/:id/members/:user"},
+ {http.MethodDelete, "/teams/:id/members/:user"},
+ {http.MethodGet, "/teams/:id/repos"},
+ {http.MethodGet, "/teams/:id/repos/:owner/:repo"},
+ {http.MethodPut, "/teams/:id/repos/:owner/:repo"},
+ {http.MethodDelete, "/teams/:id/repos/:owner/:repo"},
+ {http.MethodGet, "/user/teams"},
// Pull Requests
- {"GET", "/repos/:owner/:repo/pulls"},
- {"GET", "/repos/:owner/:repo/pulls/:number"},
- {"POST", "/repos/:owner/:repo/pulls"},
- //{"PATCH", "/repos/:owner/:repo/pulls/:number"},
- {"GET", "/repos/:owner/:repo/pulls/:number/commits"},
- {"GET", "/repos/:owner/:repo/pulls/:number/files"},
- {"GET", "/repos/:owner/:repo/pulls/:number/merge"},
- {"PUT", "/repos/:owner/:repo/pulls/:number/merge"},
- {"GET", "/repos/:owner/:repo/pulls/:number/comments"},
- //{"GET", "/repos/:owner/:repo/pulls/comments"},
- //{"GET", "/repos/:owner/:repo/pulls/comments/:number"},
- {"PUT", "/repos/:owner/:repo/pulls/:number/comments"},
- //{"PATCH", "/repos/:owner/:repo/pulls/comments/:number"},
- //{"DELETE", "/repos/:owner/:repo/pulls/comments/:number"},
+ {http.MethodGet, "/repos/:owner/:repo/pulls"},
+ {http.MethodGet, "/repos/:owner/:repo/pulls/:number"},
+ {http.MethodPost, "/repos/:owner/:repo/pulls"},
+ //{http.MethodPatch, "/repos/:owner/:repo/pulls/:number"},
+ {http.MethodGet, "/repos/:owner/:repo/pulls/:number/commits"},
+ {http.MethodGet, "/repos/:owner/:repo/pulls/:number/files"},
+ {http.MethodGet, "/repos/:owner/:repo/pulls/:number/merge"},
+ {http.MethodPut, "/repos/:owner/:repo/pulls/:number/merge"},
+ {http.MethodGet, "/repos/:owner/:repo/pulls/:number/comments"},
+ //{http.MethodGet, "/repos/:owner/:repo/pulls/comments"},
+ //{http.MethodGet, "/repos/:owner/:repo/pulls/comments/:number"},
+ {http.MethodPut, "/repos/:owner/:repo/pulls/:number/comments"},
+ //{http.MethodPatch, "/repos/:owner/:repo/pulls/comments/:number"},
+ //{http.MethodDelete, "/repos/:owner/:repo/pulls/comments/:number"},
// Repositories
- {"GET", "/user/repos"},
- {"GET", "/users/:user/repos"},
- {"GET", "/orgs/:org/repos"},
- {"GET", "/repositories"},
- {"POST", "/user/repos"},
- {"POST", "/orgs/:org/repos"},
- {"GET", "/repos/:owner/:repo"},
- //{"PATCH", "/repos/:owner/:repo"},
- {"GET", "/repos/:owner/:repo/contributors"},
- {"GET", "/repos/:owner/:repo/languages"},
- {"GET", "/repos/:owner/:repo/teams"},
- {"GET", "/repos/:owner/:repo/tags"},
- {"GET", "/repos/:owner/:repo/branches"},
- {"GET", "/repos/:owner/:repo/branches/:branch"},
- {"DELETE", "/repos/:owner/:repo"},
- {"GET", "/repos/:owner/:repo/collaborators"},
- {"GET", "/repos/:owner/:repo/collaborators/:user"},
- {"PUT", "/repos/:owner/:repo/collaborators/:user"},
- {"DELETE", "/repos/:owner/:repo/collaborators/:user"},
- {"GET", "/repos/:owner/:repo/comments"},
- {"GET", "/repos/:owner/:repo/commits/:sha/comments"},
- {"POST", "/repos/:owner/:repo/commits/:sha/comments"},
- {"GET", "/repos/:owner/:repo/comments/:id"},
- //{"PATCH", "/repos/:owner/:repo/comments/:id"},
- {"DELETE", "/repos/:owner/:repo/comments/:id"},
- {"GET", "/repos/:owner/:repo/commits"},
- {"GET", "/repos/:owner/:repo/commits/:sha"},
- {"GET", "/repos/:owner/:repo/readme"},
- //{"GET", "/repos/:owner/:repo/contents/*path"},
- //{"PUT", "/repos/:owner/:repo/contents/*path"},
- //{"DELETE", "/repos/:owner/:repo/contents/*path"},
- //{"GET", "/repos/:owner/:repo/:archive_format/:ref"},
- {"GET", "/repos/:owner/:repo/keys"},
- {"GET", "/repos/:owner/:repo/keys/:id"},
- {"POST", "/repos/:owner/:repo/keys"},
- //{"PATCH", "/repos/:owner/:repo/keys/:id"},
- {"DELETE", "/repos/:owner/:repo/keys/:id"},
- {"GET", "/repos/:owner/:repo/downloads"},
- {"GET", "/repos/:owner/:repo/downloads/:id"},
- {"DELETE", "/repos/:owner/:repo/downloads/:id"},
- {"GET", "/repos/:owner/:repo/forks"},
- {"POST", "/repos/:owner/:repo/forks"},
- {"GET", "/repos/:owner/:repo/hooks"},
- {"GET", "/repos/:owner/:repo/hooks/:id"},
- {"POST", "/repos/:owner/:repo/hooks"},
- //{"PATCH", "/repos/:owner/:repo/hooks/:id"},
- {"POST", "/repos/:owner/:repo/hooks/:id/tests"},
- {"DELETE", "/repos/:owner/:repo/hooks/:id"},
- {"POST", "/repos/:owner/:repo/merges"},
- {"GET", "/repos/:owner/:repo/releases"},
- {"GET", "/repos/:owner/:repo/releases/:id"},
- {"POST", "/repos/:owner/:repo/releases"},
- //{"PATCH", "/repos/:owner/:repo/releases/:id"},
- {"DELETE", "/repos/:owner/:repo/releases/:id"},
- {"GET", "/repos/:owner/:repo/releases/:id/assets"},
- {"GET", "/repos/:owner/:repo/stats/contributors"},
- {"GET", "/repos/:owner/:repo/stats/commit_activity"},
- {"GET", "/repos/:owner/:repo/stats/code_frequency"},
- {"GET", "/repos/:owner/:repo/stats/participation"},
- {"GET", "/repos/:owner/:repo/stats/punch_card"},
- {"GET", "/repos/:owner/:repo/statuses/:ref"},
- {"POST", "/repos/:owner/:repo/statuses/:ref"},
+ {http.MethodGet, "/user/repos"},
+ {http.MethodGet, "/users/:user/repos"},
+ {http.MethodGet, "/orgs/:org/repos"},
+ {http.MethodGet, "/repositories"},
+ {http.MethodPost, "/user/repos"},
+ {http.MethodPost, "/orgs/:org/repos"},
+ {http.MethodGet, "/repos/:owner/:repo"},
+ //{http.MethodPatch, "/repos/:owner/:repo"},
+ {http.MethodGet, "/repos/:owner/:repo/contributors"},
+ {http.MethodGet, "/repos/:owner/:repo/languages"},
+ {http.MethodGet, "/repos/:owner/:repo/teams"},
+ {http.MethodGet, "/repos/:owner/:repo/tags"},
+ {http.MethodGet, "/repos/:owner/:repo/branches"},
+ {http.MethodGet, "/repos/:owner/:repo/branches/:branch"},
+ {http.MethodDelete, "/repos/:owner/:repo"},
+ {http.MethodGet, "/repos/:owner/:repo/collaborators"},
+ {http.MethodGet, "/repos/:owner/:repo/collaborators/:user"},
+ {http.MethodPut, "/repos/:owner/:repo/collaborators/:user"},
+ {http.MethodDelete, "/repos/:owner/:repo/collaborators/:user"},
+ {http.MethodGet, "/repos/:owner/:repo/comments"},
+ {http.MethodGet, "/repos/:owner/:repo/commits/:sha/comments"},
+ {http.MethodPost, "/repos/:owner/:repo/commits/:sha/comments"},
+ {http.MethodGet, "/repos/:owner/:repo/comments/:id"},
+ //{http.MethodPatch, "/repos/:owner/:repo/comments/:id"},
+ {http.MethodDelete, "/repos/:owner/:repo/comments/:id"},
+ {http.MethodGet, "/repos/:owner/:repo/commits"},
+ {http.MethodGet, "/repos/:owner/:repo/commits/:sha"},
+ {http.MethodGet, "/repos/:owner/:repo/readme"},
+ //{http.MethodGet, "/repos/:owner/:repo/contents/*path"},
+ //{http.MethodPut, "/repos/:owner/:repo/contents/*path"},
+ //{http.MethodDelete, "/repos/:owner/:repo/contents/*path"},
+ //{http.MethodGet, "/repos/:owner/:repo/:archive_format/:ref"},
+ {http.MethodGet, "/repos/:owner/:repo/keys"},
+ {http.MethodGet, "/repos/:owner/:repo/keys/:id"},
+ {http.MethodPost, "/repos/:owner/:repo/keys"},
+ //{http.MethodPatch, "/repos/:owner/:repo/keys/:id"},
+ {http.MethodDelete, "/repos/:owner/:repo/keys/:id"},
+ {http.MethodGet, "/repos/:owner/:repo/downloads"},
+ {http.MethodGet, "/repos/:owner/:repo/downloads/:id"},
+ {http.MethodDelete, "/repos/:owner/:repo/downloads/:id"},
+ {http.MethodGet, "/repos/:owner/:repo/forks"},
+ {http.MethodPost, "/repos/:owner/:repo/forks"},
+ {http.MethodGet, "/repos/:owner/:repo/hooks"},
+ {http.MethodGet, "/repos/:owner/:repo/hooks/:id"},
+ {http.MethodPost, "/repos/:owner/:repo/hooks"},
+ //{http.MethodPatch, "/repos/:owner/:repo/hooks/:id"},
+ {http.MethodPost, "/repos/:owner/:repo/hooks/:id/tests"},
+ {http.MethodDelete, "/repos/:owner/:repo/hooks/:id"},
+ {http.MethodPost, "/repos/:owner/:repo/merges"},
+ {http.MethodGet, "/repos/:owner/:repo/releases"},
+ {http.MethodGet, "/repos/:owner/:repo/releases/:id"},
+ {http.MethodPost, "/repos/:owner/:repo/releases"},
+ //{http.MethodPatch, "/repos/:owner/:repo/releases/:id"},
+ {http.MethodDelete, "/repos/:owner/:repo/releases/:id"},
+ {http.MethodGet, "/repos/:owner/:repo/releases/:id/assets"},
+ {http.MethodGet, "/repos/:owner/:repo/stats/contributors"},
+ {http.MethodGet, "/repos/:owner/:repo/stats/commit_activity"},
+ {http.MethodGet, "/repos/:owner/:repo/stats/code_frequency"},
+ {http.MethodGet, "/repos/:owner/:repo/stats/participation"},
+ {http.MethodGet, "/repos/:owner/:repo/stats/punch_card"},
+ {http.MethodGet, "/repos/:owner/:repo/statuses/:ref"},
+ {http.MethodPost, "/repos/:owner/:repo/statuses/:ref"},
// Search
- {"GET", "/search/repositories"},
- {"GET", "/search/code"},
- {"GET", "/search/issues"},
- {"GET", "/search/users"},
- {"GET", "/legacy/issues/search/:owner/:repository/:state/:keyword"},
- {"GET", "/legacy/repos/search/:keyword"},
- {"GET", "/legacy/user/search/:keyword"},
- {"GET", "/legacy/user/email/:email"},
+ {http.MethodGet, "/search/repositories"},
+ {http.MethodGet, "/search/code"},
+ {http.MethodGet, "/search/issues"},
+ {http.MethodGet, "/search/users"},
+ {http.MethodGet, "/legacy/issues/search/:owner/:repository/:state/:keyword"},
+ {http.MethodGet, "/legacy/repos/search/:keyword"},
+ {http.MethodGet, "/legacy/user/search/:keyword"},
+ {http.MethodGet, "/legacy/user/email/:email"},
// Users
- {"GET", "/users/:user"},
- {"GET", "/user"},
- //{"PATCH", "/user"},
- {"GET", "/users"},
- {"GET", "/user/emails"},
- {"POST", "/user/emails"},
- {"DELETE", "/user/emails"},
- {"GET", "/users/:user/followers"},
- {"GET", "/user/followers"},
- {"GET", "/users/:user/following"},
- {"GET", "/user/following"},
- {"GET", "/user/following/:user"},
- {"GET", "/users/:user/following/:target_user"},
- {"PUT", "/user/following/:user"},
- {"DELETE", "/user/following/:user"},
- {"GET", "/users/:user/keys"},
- {"GET", "/user/keys"},
- {"GET", "/user/keys/:id"},
- {"POST", "/user/keys"},
- //{"PATCH", "/user/keys/:id"},
- {"DELETE", "/user/keys/:id"},
+ {http.MethodGet, "/users/:user"},
+ {http.MethodGet, "/user"},
+ //{http.MethodPatch, "/user"},
+ {http.MethodGet, "/users"},
+ {http.MethodGet, "/user/emails"},
+ {http.MethodPost, "/user/emails"},
+ {http.MethodDelete, "/user/emails"},
+ {http.MethodGet, "/users/:user/followers"},
+ {http.MethodGet, "/user/followers"},
+ {http.MethodGet, "/users/:user/following"},
+ {http.MethodGet, "/user/following"},
+ {http.MethodGet, "/user/following/:user"},
+ {http.MethodGet, "/users/:user/following/:target_user"},
+ {http.MethodPut, "/user/following/:user"},
+ {http.MethodDelete, "/user/following/:user"},
+ {http.MethodGet, "/users/:user/keys"},
+ {http.MethodGet, "/user/keys"},
+ {http.MethodGet, "/user/keys/:id"},
+ {http.MethodPost, "/user/keys"},
+ //{http.MethodPatch, "/user/keys/:id"},
+ {http.MethodDelete, "/user/keys/:id"},
}
func TestShouldBindUri(t *testing.T) {
@@ -291,18 +291,18 @@ func TestShouldBindUri(t *testing.T) {
type Person struct {
Name string `uri:"name" binding:"required"`
- Id string `uri:"id" binding:"required"`
+ ID string `uri:"id" binding:"required"`
}
- router.Handle("GET", "/rest/:name/:id", func(c *Context) {
+ router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
var person Person
assert.NoError(t, c.ShouldBindUri(&person))
assert.True(t, "" != person.Name)
- assert.True(t, "" != person.Id)
+ assert.True(t, "" != person.ID)
c.String(http.StatusOK, "ShouldBindUri test OK")
})
path, _ := exampleFromPath("/rest/:name/:id")
- w := performRequest(router, "GET", path)
+ w := performRequest(router, http.MethodGet, path)
assert.Equal(t, "ShouldBindUri test OK", w.Body.String())
assert.Equal(t, http.StatusOK, w.Code)
}
@@ -313,18 +313,18 @@ func TestBindUri(t *testing.T) {
type Person struct {
Name string `uri:"name" binding:"required"`
- Id string `uri:"id" binding:"required"`
+ ID string `uri:"id" binding:"required"`
}
- router.Handle("GET", "/rest/:name/:id", func(c *Context) {
+ router.Handle(http.MethodGet, "/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)
+ assert.True(t, "" != person.ID)
c.String(http.StatusOK, "BindUri test OK")
})
path, _ := exampleFromPath("/rest/:name/:id")
- w := performRequest(router, "GET", path)
+ w := performRequest(router, http.MethodGet, path)
assert.Equal(t, "BindUri test OK", w.Body.String())
assert.Equal(t, http.StatusOK, w.Code)
}
@@ -336,13 +336,13 @@ func TestBindUriError(t *testing.T) {
type Member struct {
Number string `uri:"num" binding:"required,uuid"`
}
- router.Handle("GET", "/new/rest/:num", func(c *Context) {
+ router.Handle(http.MethodGet, "/new/rest/:num", func(c *Context) {
var m Member
assert.Error(t, c.BindUri(&m))
})
path1, _ := exampleFromPath("/new/rest/:num")
- w1 := performRequest(router, "GET", path1)
+ w1 := performRequest(router, http.MethodGet, path1)
assert.Equal(t, http.StatusBadRequest, w1.Code)
}
@@ -358,7 +358,7 @@ func TestRaceContextCopy(t *testing.T) {
go readWriteKeys(c.Copy())
c.String(http.StatusOK, "run OK, no panics")
})
- w := performRequest(router, "GET", "/test/copy/race")
+ w := performRequest(router, http.MethodGet, "/test/copy/race")
assert.Equal(t, "run OK, no panics", w.Body.String())
}
@@ -438,7 +438,7 @@ func exampleFromPath(path string) (string, Params) {
func BenchmarkGithub(b *testing.B) {
router := New()
githubConfigRouter(router)
- runRequest(b, router, "GET", "/legacy/issues/search/:owner/:repository/:state/:keyword")
+ runRequest(b, router, http.MethodGet, "/legacy/issues/search/:owner/:repository/:state/:keyword")
}
func BenchmarkParallelGithub(b *testing.B) {
@@ -446,7 +446,7 @@ func BenchmarkParallelGithub(b *testing.B) {
router := New()
githubConfigRouter(router)
- req, _ := http.NewRequest("POST", "/repos/manucorporat/sse/git/blobs", nil)
+ req, _ := http.NewRequest(http.MethodPost, "/repos/manucorporat/sse/git/blobs", nil)
b.RunParallel(func(pb *testing.PB) {
// Each goroutine has its own bytes.Buffer.
@@ -462,7 +462,7 @@ func BenchmarkParallelGithubDefault(b *testing.B) {
router := New()
githubConfigRouter(router)
- req, _ := http.NewRequest("POST", "/repos/manucorporat/sse/git/blobs", nil)
+ req, _ := http.NewRequest(http.MethodPost, "/repos/manucorporat/sse/git/blobs", nil)
b.RunParallel(func(pb *testing.PB) {
// Each goroutine has its own bytes.Buffer.
diff --git a/go.mod b/go.mod
index 1213bd23..884ff851 100644
--- a/go.mod
+++ b/go.mod
@@ -1,14 +1,14 @@
module github.com/gin-gonic/gin
-go 1.12
+go 1.13
require (
github.com/gin-contrib/sse v0.1.0
- github.com/go-playground/validator/v10 v10.0.1
- github.com/golang/protobuf v1.3.2
- github.com/json-iterator/go v1.1.7
- github.com/mattn/go-isatty v0.0.9
+ github.com/go-playground/validator/v10 v10.4.1
+ github.com/golang/protobuf v1.3.3
+ github.com/json-iterator/go v1.1.9
+ github.com/mattn/go-isatty v0.0.12
github.com/stretchr/testify v1.4.0
github.com/ugorji/go/codec v1.1.7
- gopkg.in/yaml.v2 v2.2.2
+ gopkg.in/yaml.v2 v2.2.8
)
diff --git a/go.sum b/go.sum
index 9815f2f4..a64b3319 100644
--- a/go.sum
+++ b/go.sum
@@ -9,17 +9,17 @@ github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8c
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
-github.com/go-playground/validator/v10 v10.0.1 h1:QgDDZpXlR/L3atIL2PbFt0TpazbtN7N6PxTGcgcyEUg=
-github.com/go-playground/validator/v10 v10.0.1/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
-github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
-github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
+github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
+github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo=
-github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
-github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
-github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
+github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
@@ -34,11 +34,19 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
-golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
-golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/internal/bytesconv/bytesconv.go b/internal/bytesconv/bytesconv.go
new file mode 100644
index 00000000..86e4c4d4
--- /dev/null
+++ b/internal/bytesconv/bytesconv.go
@@ -0,0 +1,24 @@
+// Copyright 2020 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 bytesconv
+
+import (
+ "unsafe"
+)
+
+// StringToBytes converts string to byte slice without a memory allocation.
+func StringToBytes(s string) []byte {
+ return *(*[]byte)(unsafe.Pointer(
+ &struct {
+ string
+ Cap int
+ }{s, len(s)},
+ ))
+}
+
+// BytesToString converts byte slice to string without a memory allocation.
+func BytesToString(b []byte) string {
+ return *(*string)(unsafe.Pointer(&b))
+}
diff --git a/internal/bytesconv/bytesconv_test.go b/internal/bytesconv/bytesconv_test.go
new file mode 100644
index 00000000..eeaad5ee
--- /dev/null
+++ b/internal/bytesconv/bytesconv_test.go
@@ -0,0 +1,99 @@
+// Copyright 2020 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 bytesconv
+
+import (
+ "bytes"
+ "math/rand"
+ "strings"
+ "testing"
+ "time"
+)
+
+var testString = "Albert Einstein: Logic will get you from A to B. Imagination will take you everywhere."
+var testBytes = []byte(testString)
+
+func rawBytesToStr(b []byte) string {
+ return string(b)
+}
+
+func rawStrToBytes(s string) []byte {
+ return []byte(s)
+}
+
+// go test -v
+
+func TestBytesToString(t *testing.T) {
+ data := make([]byte, 1024)
+ for i := 0; i < 100; i++ {
+ rand.Read(data)
+ if rawBytesToStr(data) != BytesToString(data) {
+ t.Fatal("don't match")
+ }
+ }
+}
+
+const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+const (
+ letterIdxBits = 6 // 6 bits to represent a letter index
+ letterIdxMask = 1<= 0; {
+ if remain == 0 {
+ cache, remain = src.Int63(), letterIdxMax
+ }
+ if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
+ sb.WriteByte(letterBytes[idx])
+ i--
+ }
+ cache >>= letterIdxBits
+ remain--
+ }
+
+ return sb.String()
+}
+
+func TestStringToBytes(t *testing.T) {
+ for i := 0; i < 100; i++ {
+ s := RandStringBytesMaskImprSrcSB(64)
+ if !bytes.Equal(rawStrToBytes(s), StringToBytes(s)) {
+ t.Fatal("don't match")
+ }
+ }
+}
+
+// go test -v -run=none -bench=^BenchmarkBytesConv -benchmem=true
+
+func BenchmarkBytesConvBytesToStrRaw(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ rawBytesToStr(testBytes)
+ }
+}
+
+func BenchmarkBytesConvBytesToStr(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ BytesToString(testBytes)
+ }
+}
+
+func BenchmarkBytesConvStrToBytesRaw(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ rawStrToBytes(testString)
+ }
+}
+
+func BenchmarkBytesConvStrToBytes(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ StringToBytes(testString)
+ }
+}
diff --git a/internal/json/json.go b/internal/json/json.go
index 480e8bff..172aeb24 100644
--- a/internal/json/json.go
+++ b/internal/json/json.go
@@ -2,6 +2,7 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
+//go:build !jsoniter
// +build !jsoniter
package json
diff --git a/internal/json/jsoniter.go b/internal/json/jsoniter.go
index fabd7b84..232f8dca 100644
--- a/internal/json/jsoniter.go
+++ b/internal/json/jsoniter.go
@@ -2,11 +2,12 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
+//go:build jsoniter
// +build jsoniter
package json
-import "github.com/json-iterator/go"
+import jsoniter "github.com/json-iterator/go"
var (
json = jsoniter.ConfigCompatibleWithStandardLibrary
diff --git a/logger.go b/logger.go
index fcf90c25..d361b74d 100644
--- a/logger.go
+++ b/logger.go
@@ -99,19 +99,19 @@ func (p *LogFormatterParams) MethodColor() string {
method := p.Method
switch method {
- case "GET":
+ case http.MethodGet:
return blue
- case "POST":
+ case http.MethodPost:
return cyan
- case "PUT":
+ case http.MethodPut:
return yellow
- case "DELETE":
+ case http.MethodDelete:
return red
- case "PATCH":
+ case http.MethodPatch:
return green
- case "HEAD":
+ case http.MethodHead:
return magenta
- case "OPTIONS":
+ case http.MethodOptions:
return white
default:
return reset
@@ -141,7 +141,7 @@ var defaultLogFormatter = func(param LogFormatterParams) string {
// 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",
+ return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %#v\n%s",
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
statusColor, param.StatusCode, resetColor,
param.Latency,
diff --git a/logger_test.go b/logger_test.go
index fc53f356..0d40666e 100644
--- a/logger_test.go
+++ b/logger_test.go
@@ -158,7 +158,7 @@ func TestLoggerWithFormatter(t *testing.T) {
router := New()
router.Use(LoggerWithFormatter(func(param LogFormatterParams) string {
- return fmt.Sprintf("[FORMATTER TEST] %v | %3d | %13v | %15s | %-7s %s\n%s",
+ return fmt.Sprintf("[FORMATTER TEST] %v | %3d | %13v | %15s | %-7s %#v\n%s",
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
param.StatusCode,
param.Latency,
@@ -275,11 +275,11 @@ func TestDefaultLogFormatter(t *testing.T) {
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 | 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))
+ 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))
}
@@ -369,15 +369,15 @@ func TestErrorLogger(t *testing.T) {
w := performRequest(router, "GET", "/error")
assert.Equal(t, http.StatusOK, w.Code)
- assert.Equal(t, "{\"error\":\"this is an error\"}\n", w.Body.String())
+ assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String())
w = performRequest(router, "GET", "/abort")
assert.Equal(t, http.StatusUnauthorized, w.Code)
- assert.Equal(t, "{\"error\":\"no authorized\"}\n", w.Body.String())
+ assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String())
w = performRequest(router, "GET", "/print")
assert.Equal(t, http.StatusInternalServerError, w.Code)
- assert.Equal(t, "hola!{\"error\":\"this is an error\"}\n", w.Body.String())
+ assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
}
func TestLoggerWithWriterSkippingPaths(t *testing.T) {
diff --git a/middleware_test.go b/middleware_test.go
index 2ae9e889..fca1c530 100644
--- a/middleware_test.go
+++ b/middleware_test.go
@@ -246,5 +246,5 @@ func TestMiddlewareWrite(t *testing.T) {
w := performRequest(router, "GET", "/")
assert.Equal(t, http.StatusBadRequest, w.Code)
- assert.Equal(t, strings.Replace("hola\n{\"foo\":\"bar\"}\n{\"foo\":\"bar\"}\nevent:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))
+ assert.Equal(t, strings.Replace("hola\n{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))
}
diff --git a/mode.go b/mode.go
index edfc2940..c8813aff 100644
--- a/mode.go
+++ b/mode.go
@@ -22,6 +22,7 @@ const (
// TestMode indicates gin mode is test.
TestMode = "test"
)
+
const (
debugCode = iota
releaseCode
@@ -50,19 +51,21 @@ func init() {
// SetMode sets gin mode according to input string.
func SetMode(value string) {
+ if value == "" {
+ value = DebugMode
+ }
+
switch value {
- case DebugMode, "":
+ case DebugMode:
ginMode = debugCode
case ReleaseMode:
ginMode = releaseCode
case TestMode:
ginMode = testCode
default:
- panic("gin mode unknown: " + value)
- }
- if value == "" {
- value = DebugMode
+ panic("gin mode unknown: " + value + " (available mode: debug release test)")
}
+
modeName = value
}
diff --git a/path.go b/path.go
index d1f59622..d42d6b9d 100644
--- a/path.go
+++ b/path.go
@@ -19,13 +19,17 @@ package gin
//
// If the result of this process is an empty string, "/" is returned.
func cleanPath(p string) string {
+ const stackBufSize = 128
// Turn empty string into "/"
if p == "" {
return "/"
}
+ // Reasonably sized buffer on stack to avoid allocations in the common case.
+ // If a larger buffer is required, it gets allocated dynamically.
+ buf := make([]byte, 0, stackBufSize)
+
n := len(p)
- var buf []byte
// Invariants:
// reading from path; r is index of next byte to process.
@@ -37,15 +41,21 @@ func cleanPath(p string) string {
if p[0] != '/' {
r = 0
- buf = make([]byte, n+1)
+
+ if n+1 > stackBufSize {
+ buf = make([]byte, n+1)
+ } else {
+ buf = buf[:n+1]
+ }
buf[0] = '/'
}
trailing := n > 1 && p[n-1] == '/'
// A bit more clunky without a 'lazybuf' like the path package, but the loop
- // gets completely inlined (bufApp). So in contrast to the path package this
- // loop has no expensive function calls (except 1x make)
+ // gets completely inlined (bufApp calls).
+ // loop has no expensive function calls (except 1x make) // So in contrast to the path package this loop has no expensive function
+ // calls (except make, if needed).
for r < n {
switch {
@@ -69,7 +79,7 @@ func cleanPath(p string) string {
// can backtrack
w--
- if buf == nil {
+ if len(buf) == 0 {
for w > 1 && p[w] != '/' {
w--
}
@@ -81,14 +91,14 @@ func cleanPath(p string) string {
}
default:
- // real path element.
- // add slash if needed
+ // Real path element.
+ // Add slash if needed
if w > 1 {
bufApp(&buf, p, w, '/')
w++
}
- // copy element
+ // Copy element
for r < n && p[r] != '/' {
bufApp(&buf, p, w, p[r])
w++
@@ -97,27 +107,44 @@ func cleanPath(p string) string {
}
}
- // re-append trailing slash
+ // Re-append trailing slash
if trailing && w > 1 {
bufApp(&buf, p, w, '/')
w++
}
- if buf == nil {
+ // If the original string was not modified (or only shortened at the end),
+ // return the respective substring of the original string.
+ // Otherwise return a new string from the buffer.
+ if len(buf) == 0 {
return p[:w]
}
return string(buf[:w])
}
-// internal helper to lazily create a buffer if necessary.
+// Internal helper to lazily create a buffer if necessary.
+// Calls to this function get inlined.
func bufApp(buf *[]byte, s string, w int, c byte) {
- if *buf == nil {
+ b := *buf
+ if len(b) == 0 {
+ // No modification of the original string so far.
+ // If the next character is the same as in the original string, we do
+ // not yet have to allocate a buffer.
if s[w] == c {
return
}
- *buf = make([]byte, len(s))
- copy(*buf, s[:w])
+ // Otherwise use either the stack buffer, if it is large enough, or
+ // allocate a new buffer on the heap, and copy all previous characters.
+ length := len(s)
+ if length > cap(b) {
+ *buf = make([]byte, length)
+ } else {
+ *buf = (*buf)[:length]
+ }
+ b = *buf
+
+ copy(b, s[:w])
}
- (*buf)[w] = c
+ b[w] = c
}
diff --git a/path_test.go b/path_test.go
index c1e6ed4f..caefd63a 100644
--- a/path_test.go
+++ b/path_test.go
@@ -6,15 +6,17 @@
package gin
import (
- "runtime"
+ "strings"
"testing"
"github.com/stretchr/testify/assert"
)
-var cleanTests = []struct {
+type cleanPathTest struct {
path, result string
-}{
+}
+
+var cleanTests = []cleanPathTest{
// Already clean
{"/", "/"},
{"/abc", "/abc"},
@@ -77,13 +79,62 @@ func TestPathCleanMallocs(t *testing.T) {
if testing.Short() {
t.Skip("skipping malloc count in short mode")
}
- if runtime.GOMAXPROCS(0) > 1 {
- t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1")
- return
- }
for _, test := range cleanTests {
allocs := testing.AllocsPerRun(100, func() { cleanPath(test.result) })
assert.EqualValues(t, allocs, 0)
}
}
+
+func BenchmarkPathClean(b *testing.B) {
+ b.ReportAllocs()
+
+ for i := 0; i < b.N; i++ {
+ for _, test := range cleanTests {
+ cleanPath(test.path)
+ }
+ }
+}
+
+func genLongPaths() (testPaths []cleanPathTest) {
+ for i := 1; i <= 1234; i++ {
+ ss := strings.Repeat("a", i)
+
+ correctPath := "/" + ss
+ testPaths = append(testPaths, cleanPathTest{
+ path: correctPath,
+ result: correctPath,
+ }, cleanPathTest{
+ path: ss,
+ result: correctPath,
+ }, cleanPathTest{
+ path: "//" + ss,
+ result: correctPath,
+ }, cleanPathTest{
+ path: "/" + ss + "/b/..",
+ result: correctPath,
+ })
+ }
+ return
+}
+
+func TestPathCleanLong(t *testing.T) {
+ cleanTests := genLongPaths()
+
+ for _, test := range cleanTests {
+ assert.Equal(t, test.result, cleanPath(test.path))
+ assert.Equal(t, test.result, cleanPath(test.result))
+ }
+}
+
+func BenchmarkPathCleanLong(b *testing.B) {
+ cleanTests := genLongPaths()
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for i := 0; i < b.N; i++ {
+ for _, test := range cleanTests {
+ cleanPath(test.path)
+ }
+ }
+}
diff --git a/recovery.go b/recovery.go
index bc946c03..563f5aaa 100644
--- a/recovery.go
+++ b/recovery.go
@@ -26,13 +26,29 @@ var (
slash = []byte("/")
)
+// RecoveryFunc defines the function passable to CustomRecovery.
+type RecoveryFunc func(c *Context, err interface{})
+
// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
func Recovery() HandlerFunc {
return RecoveryWithWriter(DefaultErrorWriter)
}
+//CustomRecovery returns a middleware that recovers from any panics and calls the provided handle func to handle it.
+func CustomRecovery(handle RecoveryFunc) HandlerFunc {
+ return RecoveryWithWriter(DefaultErrorWriter, handle)
+}
+
// RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one.
-func RecoveryWithWriter(out io.Writer) HandlerFunc {
+func RecoveryWithWriter(out io.Writer, recovery ...RecoveryFunc) HandlerFunc {
+ if len(recovery) > 0 {
+ return CustomRecoveryWithWriter(out, recovery[0])
+ }
+ return CustomRecoveryWithWriter(out, defaultHandleRecovery)
+}
+
+// CustomRecoveryWithWriter returns a middleware for a given writer that recovers from any panics and calls the provided handle func to handle it.
+func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
var logger *log.Logger
if out != nil {
logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags)
@@ -60,23 +76,23 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc {
headers[idx] = current[0] + ": *"
}
}
+ headersToStr := strings.Join(headers, "\r\n")
if brokenPipe {
- logger.Printf("%s\n%s%s", err, string(httpRequest), reset)
+ logger.Printf("%s\n%s%s", err, headersToStr, reset)
} else if IsDebugging() {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
- timeFormat(time.Now()), strings.Join(headers, "\r\n"), err, stack, reset)
+ timeFormat(time.Now()), headersToStr, err, stack, reset)
} else {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
timeFormat(time.Now()), err, stack, reset)
}
}
-
- // If the connection is dead, we can't write a status to it.
if brokenPipe {
+ // If the connection is dead, we can't write a status to it.
c.Error(err.(error)) // nolint: errcheck
c.Abort()
} else {
- c.AbortWithStatus(http.StatusInternalServerError)
+ handle(c, err)
}
}
}()
@@ -84,6 +100,10 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc {
}
}
+func defaultHandleRecovery(c *Context, err interface{}) {
+ c.AbortWithStatus(http.StatusInternalServerError)
+}
+
// stack returns a nicely formatted stack frame, skipping skip frames.
func stack(skip int) []byte {
buf := new(bytes.Buffer) // the returned data
@@ -146,6 +166,6 @@ func function(pc uintptr) []byte {
}
func timeFormat(t time.Time) string {
- var timeString = t.Format("2006/01/02 - 15:04:05")
+ timeString := t.Format("2006/01/02 - 15:04:05")
return timeString
}
diff --git a/recovery_test.go b/recovery_test.go
index 21a0a480..6cc2a47a 100644
--- a/recovery_test.go
+++ b/recovery_test.go
@@ -62,7 +62,7 @@ func TestPanicInHandler(t *testing.T) {
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, buffer.String(), "panic recovered")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
- assert.Contains(t, buffer.String(), "TestPanicInHandler")
+ assert.Contains(t, buffer.String(), t.Name())
assert.NotContains(t, buffer.String(), "GET /recovery")
// Debug mode prints the request
@@ -144,3 +144,107 @@ func TestPanicWithBrokenPipe(t *testing.T) {
})
}
}
+
+func TestCustomRecoveryWithWriter(t *testing.T) {
+ errBuffer := new(bytes.Buffer)
+ buffer := new(bytes.Buffer)
+ router := New()
+ handleRecovery := func(c *Context, err interface{}) {
+ errBuffer.WriteString(err.(string))
+ c.AbortWithStatus(http.StatusBadRequest)
+ }
+ router.Use(CustomRecoveryWithWriter(buffer, handleRecovery))
+ router.GET("/recovery", func(_ *Context) {
+ panic("Oupps, Houston, we have a problem")
+ })
+ // RUN
+ w := performRequest(router, "GET", "/recovery")
+ // TEST
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+ assert.Contains(t, buffer.String(), "panic recovered")
+ assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
+ assert.Contains(t, buffer.String(), t.Name())
+ assert.NotContains(t, buffer.String(), "GET /recovery")
+
+ // Debug mode prints the request
+ SetMode(DebugMode)
+ // RUN
+ w = performRequest(router, "GET", "/recovery")
+ // TEST
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+ assert.Contains(t, buffer.String(), "GET /recovery")
+
+ assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String())
+
+ SetMode(TestMode)
+}
+
+func TestCustomRecovery(t *testing.T) {
+ errBuffer := new(bytes.Buffer)
+ buffer := new(bytes.Buffer)
+ router := New()
+ DefaultErrorWriter = buffer
+ handleRecovery := func(c *Context, err interface{}) {
+ errBuffer.WriteString(err.(string))
+ c.AbortWithStatus(http.StatusBadRequest)
+ }
+ router.Use(CustomRecovery(handleRecovery))
+ router.GET("/recovery", func(_ *Context) {
+ panic("Oupps, Houston, we have a problem")
+ })
+ // RUN
+ w := performRequest(router, "GET", "/recovery")
+ // TEST
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+ assert.Contains(t, buffer.String(), "panic recovered")
+ assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
+ assert.Contains(t, buffer.String(), t.Name())
+ assert.NotContains(t, buffer.String(), "GET /recovery")
+
+ // Debug mode prints the request
+ SetMode(DebugMode)
+ // RUN
+ w = performRequest(router, "GET", "/recovery")
+ // TEST
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+ assert.Contains(t, buffer.String(), "GET /recovery")
+
+ assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String())
+
+ SetMode(TestMode)
+}
+
+func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
+ errBuffer := new(bytes.Buffer)
+ buffer := new(bytes.Buffer)
+ router := New()
+ DefaultErrorWriter = buffer
+ handleRecovery := func(c *Context, err interface{}) {
+ errBuffer.WriteString(err.(string))
+ c.AbortWithStatus(http.StatusBadRequest)
+ }
+ router.Use(RecoveryWithWriter(DefaultErrorWriter, handleRecovery))
+ router.GET("/recovery", func(_ *Context) {
+ panic("Oupps, Houston, we have a problem")
+ })
+ // RUN
+ w := performRequest(router, "GET", "/recovery")
+ // TEST
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+ assert.Contains(t, buffer.String(), "panic recovered")
+ assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
+ assert.Contains(t, buffer.String(), t.Name())
+ assert.NotContains(t, buffer.String(), "GET /recovery")
+
+ // Debug mode prints the request
+ SetMode(DebugMode)
+ // RUN
+ w = performRequest(router, "GET", "/recovery")
+ // TEST
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+ assert.Contains(t, buffer.String(), "GET /recovery")
+
+ assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String())
+
+ SetMode(TestMode)
+}
diff --git a/render/json.go b/render/json.go
index 70506f78..41863093 100644
--- a/render/json.go
+++ b/render/json.go
@@ -10,6 +10,7 @@ import (
"html/template"
"net/http"
+ "github.com/gin-gonic/gin/internal/bytesconv"
"github.com/gin-gonic/gin/internal/json"
)
@@ -40,9 +41,6 @@ type AsciiJSON struct {
Data interface{}
}
-// SecureJSONPrefix is a string which represents SecureJSON prefix.
-type SecureJSONPrefix string
-
// PureJSON contains the given interface object.
type PureJSON struct {
Data interface{}
@@ -68,8 +66,11 @@ func (r JSON) WriteContentType(w http.ResponseWriter) {
// WriteJSON marshals the given interface object and writes it with custom ContentType.
func WriteJSON(w http.ResponseWriter, obj interface{}) error {
writeContentType(w, jsonContentType)
- encoder := json.NewEncoder(w)
- err := encoder.Encode(&obj)
+ jsonBytes, err := json.Marshal(obj)
+ if err != nil {
+ return err
+ }
+ _, err = w.Write(jsonBytes)
return err
}
@@ -97,8 +98,9 @@ func (r SecureJSON) Render(w http.ResponseWriter) error {
return err
}
// if the jsonBytes is array values
- if bytes.HasPrefix(jsonBytes, []byte("[")) && bytes.HasSuffix(jsonBytes, []byte("]")) {
- _, err = w.Write([]byte(r.Prefix))
+ if bytes.HasPrefix(jsonBytes, bytesconv.StringToBytes("[")) && bytes.HasSuffix(jsonBytes,
+ bytesconv.StringToBytes("]")) {
+ _, err = w.Write(bytesconv.StringToBytes(r.Prefix))
if err != nil {
return err
}
@@ -126,11 +128,11 @@ func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
}
callback := template.JSEscapeString(r.Callback)
- _, err = w.Write([]byte(callback))
+ _, err = w.Write(bytesconv.StringToBytes(callback))
if err != nil {
return err
}
- _, err = w.Write([]byte("("))
+ _, err = w.Write(bytesconv.StringToBytes("("))
if err != nil {
return err
}
@@ -138,7 +140,7 @@ func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
if err != nil {
return err
}
- _, err = w.Write([]byte(");"))
+ _, err = w.Write(bytesconv.StringToBytes(");"))
if err != nil {
return err
}
@@ -160,7 +162,7 @@ func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
}
var buffer bytes.Buffer
- for _, r := range string(ret) {
+ for _, r := range bytesconv.BytesToString(ret) {
cvt := string(r)
if r >= 128 {
cvt = fmt.Sprintf("\\u%04x", int64(r))
diff --git a/render/msgpack.go b/render/msgpack.go
index dc681fcf..6ef5b6e5 100644
--- a/render/msgpack.go
+++ b/render/msgpack.go
@@ -2,6 +2,9 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
+//go:build !nomsgpack
+// +build !nomsgpack
+
package render
import (
@@ -10,6 +13,10 @@ import (
"github.com/ugorji/go/codec"
)
+var (
+ _ Render = MsgPack{}
+)
+
// MsgPack contains the given interface object.
type MsgPack struct {
Data interface{}
diff --git a/render/render.go b/render/render.go
index abfc79fc..bcd568bf 100644
--- a/render/render.go
+++ b/render/render.go
@@ -27,7 +27,6 @@ var (
_ HTMLRender = HTMLDebug{}
_ HTMLRender = HTMLProduction{}
_ Render = YAML{}
- _ Render = MsgPack{}
_ Render = Reader{}
_ Render = AsciiJSON{}
_ Render = ProtoBuf{}
diff --git a/render/render_msgpack_test.go b/render/render_msgpack_test.go
new file mode 100644
index 00000000..8170fbe8
--- /dev/null
+++ b/render/render_msgpack_test.go
@@ -0,0 +1,44 @@
+// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+//go:build !nomsgpack
+// +build !nomsgpack
+
+package render
+
+import (
+ "bytes"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/ugorji/go/codec"
+)
+
+// TODO unit tests
+// test errors
+
+func TestRenderMsgPack(t *testing.T) {
+ w := httptest.NewRecorder()
+ data := map[string]interface{}{
+ "foo": "bar",
+ }
+
+ (MsgPack{data}).WriteContentType(w)
+ assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
+
+ err := (MsgPack{data}).Render(w)
+
+ assert.NoError(t, err)
+
+ h := new(codec.MsgpackHandle)
+ assert.NotNil(t, h)
+ buf := bytes.NewBuffer([]byte{})
+ assert.NotNil(t, buf)
+ err = codec.NewEncoder(buf, h).Encode(data)
+
+ assert.NoError(t, err)
+ assert.Equal(t, w.Body.String(), string(buf.Bytes()))
+ assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
+}
diff --git a/render/render_test.go b/render/render_test.go
index 376733df..353c82bb 100644
--- a/render/render_test.go
+++ b/render/render_test.go
@@ -5,7 +5,6 @@
package render
import (
- "bytes"
"encoding/xml"
"errors"
"html/template"
@@ -17,7 +16,6 @@ import (
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert"
- "github.com/ugorji/go/codec"
testdata "github.com/gin-gonic/gin/testdata/protoexample"
)
@@ -25,30 +23,6 @@ import (
// TODO unit tests
// test errors
-func TestRenderMsgPack(t *testing.T) {
- w := httptest.NewRecorder()
- data := map[string]interface{}{
- "foo": "bar",
- }
-
- (MsgPack{data}).WriteContentType(w)
- assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
-
- err := (MsgPack{data}).Render(w)
-
- assert.NoError(t, err)
-
- h := new(codec.MsgpackHandle)
- assert.NotNil(t, h)
- buf := bytes.NewBuffer([]byte{})
- assert.NotNil(t, buf)
- err = codec.NewEncoder(buf, h).Encode(data)
-
- assert.NoError(t, err)
- assert.Equal(t, w.Body.String(), buf.String())
- assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
-}
-
func TestRenderJSON(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]interface{}{
@@ -62,7 +36,7 @@ func TestRenderJSON(t *testing.T) {
err := (JSON{data}).Render(w)
assert.NoError(t, err)
- assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}\n", w.Body.String())
+ assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
diff --git a/render/text.go b/render/text.go
index 4e52d4c5..461b720a 100644
--- a/render/text.go
+++ b/render/text.go
@@ -6,8 +6,9 @@ package render
import (
"fmt"
- "io"
"net/http"
+
+ "github.com/gin-gonic/gin/internal/bytesconv"
)
// String contains the given interface object slice and its format.
@@ -35,6 +36,6 @@ func WriteString(w http.ResponseWriter, format string, data []interface{}) (err
_, err = fmt.Fprintf(w, format, data...)
return
}
- _, err = io.WriteString(w, format)
+ _, err = w.Write(bytesconv.StringToBytes(format))
return
}
diff --git a/routergroup.go b/routergroup.go
index 2e7a5b90..15d9930d 100644
--- a/routergroup.go
+++ b/routergroup.go
@@ -95,51 +95,51 @@ func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...Ha
// POST is a shortcut for router.Handle("POST", path, handle).
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
- return group.handle("POST", relativePath, handlers)
+ return group.handle(http.MethodPost, relativePath, handlers)
}
// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
- return group.handle("GET", relativePath, handlers)
+ return group.handle(http.MethodGet, relativePath, handlers)
}
// DELETE is a shortcut for router.Handle("DELETE", path, handle).
func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes {
- return group.handle("DELETE", relativePath, handlers)
+ return group.handle(http.MethodDelete, relativePath, handlers)
}
// PATCH is a shortcut for router.Handle("PATCH", path, handle).
func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes {
- return group.handle("PATCH", relativePath, handlers)
+ return group.handle(http.MethodPatch, relativePath, handlers)
}
// PUT is a shortcut for router.Handle("PUT", path, handle).
func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes {
- return group.handle("PUT", relativePath, handlers)
+ return group.handle(http.MethodPut, relativePath, handlers)
}
// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle).
func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes {
- return group.handle("OPTIONS", relativePath, handlers)
+ return group.handle(http.MethodOptions, relativePath, handlers)
}
// HEAD is a shortcut for router.Handle("HEAD", path, handle).
func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes {
- return group.handle("HEAD", relativePath, handlers)
+ return group.handle(http.MethodHead, relativePath, handlers)
}
// Any registers a route that matches all the HTTP methods.
// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE.
func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes {
- group.handle("GET", relativePath, handlers)
- group.handle("POST", relativePath, handlers)
- group.handle("PUT", relativePath, handlers)
- group.handle("PATCH", relativePath, handlers)
- group.handle("HEAD", relativePath, handlers)
- group.handle("OPTIONS", relativePath, handlers)
- group.handle("DELETE", relativePath, handlers)
- group.handle("CONNECT", relativePath, handlers)
- group.handle("TRACE", relativePath, handlers)
+ group.handle(http.MethodGet, relativePath, handlers)
+ group.handle(http.MethodPost, relativePath, handlers)
+ group.handle(http.MethodPut, relativePath, handlers)
+ group.handle(http.MethodPatch, relativePath, handlers)
+ group.handle(http.MethodHead, relativePath, handlers)
+ group.handle(http.MethodOptions, relativePath, handlers)
+ group.handle(http.MethodDelete, relativePath, handlers)
+ group.handle(http.MethodConnect, relativePath, handlers)
+ group.handle(http.MethodTrace, relativePath, handlers)
return group.returnObj()
}
@@ -187,7 +187,7 @@ func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileS
fileServer := http.StripPrefix(absolutePath, http.FileServer(fs))
return func(c *Context) {
- if _, nolisting := fs.(*onlyfilesFS); nolisting {
+ if _, noListing := fs.(*onlyFilesFS); noListing {
c.Writer.WriteHeader(http.StatusNotFound)
}
diff --git a/routergroup_test.go b/routergroup_test.go
index ce3d54a2..0e49d65b 100644
--- a/routergroup_test.go
+++ b/routergroup_test.go
@@ -33,13 +33,13 @@ func TestRouterGroupBasic(t *testing.T) {
}
func TestRouterGroupBasicHandle(t *testing.T) {
- performRequestInGroup(t, "GET")
- performRequestInGroup(t, "POST")
- performRequestInGroup(t, "PUT")
- performRequestInGroup(t, "PATCH")
- performRequestInGroup(t, "DELETE")
- performRequestInGroup(t, "HEAD")
- performRequestInGroup(t, "OPTIONS")
+ performRequestInGroup(t, http.MethodGet)
+ performRequestInGroup(t, http.MethodPost)
+ performRequestInGroup(t, http.MethodPut)
+ performRequestInGroup(t, http.MethodPatch)
+ performRequestInGroup(t, http.MethodDelete)
+ performRequestInGroup(t, http.MethodHead)
+ performRequestInGroup(t, http.MethodOptions)
}
func performRequestInGroup(t *testing.T, method string) {
@@ -55,25 +55,25 @@ func performRequestInGroup(t *testing.T, method string) {
}
switch method {
- case "GET":
+ case http.MethodGet:
v1.GET("/test", handler)
login.GET("/test", handler)
- case "POST":
+ case http.MethodPost:
v1.POST("/test", handler)
login.POST("/test", handler)
- case "PUT":
+ case http.MethodPut:
v1.PUT("/test", handler)
login.PUT("/test", handler)
- case "PATCH":
+ case http.MethodPatch:
v1.PATCH("/test", handler)
login.PATCH("/test", handler)
- case "DELETE":
+ case http.MethodDelete:
v1.DELETE("/test", handler)
login.DELETE("/test", handler)
- case "HEAD":
+ case http.MethodHead:
v1.HEAD("/test", handler)
login.HEAD("/test", handler)
- case "OPTIONS":
+ case http.MethodOptions:
v1.OPTIONS("/test", handler)
login.OPTIONS("/test", handler)
default:
@@ -128,7 +128,7 @@ func TestRouterGroupTooManyHandlers(t *testing.T) {
func TestRouterGroupBadMethod(t *testing.T) {
router := New()
assert.Panics(t, func() {
- router.Handle("get", "/")
+ router.Handle(http.MethodGet, "/")
})
assert.Panics(t, func() {
router.Handle(" GET", "/")
@@ -162,7 +162,7 @@ func testRoutesInterface(t *testing.T, r IRoutes) {
handler := func(c *Context) {}
assert.Equal(t, r, r.Use(handler))
- assert.Equal(t, r, r.Handle("GET", "/handler", handler))
+ assert.Equal(t, r, r.Handle(http.MethodGet, "/handler", handler))
assert.Equal(t, r, r.Any("/any", handler))
assert.Equal(t, r, r.GET("/", handler))
assert.Equal(t, r, r.POST("/", handler))
diff --git a/routes_test.go b/routes_test.go
index 0c2f9a0c..485f0eea 100644
--- a/routes_test.go
+++ b/routes_test.go
@@ -70,10 +70,10 @@ func testRouteNotOK2(method string, t *testing.T) {
router := New()
router.HandleMethodNotAllowed = true
var methodRoute string
- if method == "POST" {
- methodRoute = "GET"
+ if method == http.MethodPost {
+ methodRoute = http.MethodGet
} else {
- methodRoute = "POST"
+ methodRoute = http.MethodPost
}
router.Handle(methodRoute, "/test", func(c *Context) {
passed = true
@@ -99,46 +99,46 @@ func TestRouterMethod(t *testing.T) {
c.String(http.StatusOK, "sup3")
})
- w := performRequest(router, "PUT", "/hey")
+ w := performRequest(router, http.MethodPut, "/hey")
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "called", w.Body.String())
}
func TestRouterGroupRouteOK(t *testing.T) {
- testRouteOK("GET", t)
- testRouteOK("POST", t)
- testRouteOK("PUT", t)
- testRouteOK("PATCH", t)
- testRouteOK("HEAD", t)
- testRouteOK("OPTIONS", t)
- testRouteOK("DELETE", t)
- testRouteOK("CONNECT", t)
- testRouteOK("TRACE", t)
+ testRouteOK(http.MethodGet, t)
+ testRouteOK(http.MethodPost, t)
+ testRouteOK(http.MethodPut, t)
+ testRouteOK(http.MethodPatch, t)
+ testRouteOK(http.MethodHead, t)
+ testRouteOK(http.MethodOptions, t)
+ testRouteOK(http.MethodDelete, t)
+ testRouteOK(http.MethodConnect, t)
+ testRouteOK(http.MethodTrace, t)
}
func TestRouteNotOK(t *testing.T) {
- testRouteNotOK("GET", t)
- testRouteNotOK("POST", t)
- testRouteNotOK("PUT", t)
- testRouteNotOK("PATCH", t)
- testRouteNotOK("HEAD", t)
- testRouteNotOK("OPTIONS", t)
- testRouteNotOK("DELETE", t)
- testRouteNotOK("CONNECT", t)
- testRouteNotOK("TRACE", t)
+ testRouteNotOK(http.MethodGet, t)
+ testRouteNotOK(http.MethodPost, t)
+ testRouteNotOK(http.MethodPut, t)
+ testRouteNotOK(http.MethodPatch, t)
+ testRouteNotOK(http.MethodHead, t)
+ testRouteNotOK(http.MethodOptions, t)
+ testRouteNotOK(http.MethodDelete, t)
+ testRouteNotOK(http.MethodConnect, t)
+ testRouteNotOK(http.MethodTrace, t)
}
func TestRouteNotOK2(t *testing.T) {
- testRouteNotOK2("GET", t)
- testRouteNotOK2("POST", t)
- testRouteNotOK2("PUT", t)
- testRouteNotOK2("PATCH", t)
- testRouteNotOK2("HEAD", t)
- testRouteNotOK2("OPTIONS", t)
- testRouteNotOK2("DELETE", t)
- testRouteNotOK2("CONNECT", t)
- testRouteNotOK2("TRACE", t)
+ testRouteNotOK2(http.MethodGet, t)
+ testRouteNotOK2(http.MethodPost, t)
+ testRouteNotOK2(http.MethodPut, t)
+ testRouteNotOK2(http.MethodPatch, t)
+ testRouteNotOK2(http.MethodHead, t)
+ testRouteNotOK2(http.MethodOptions, t)
+ testRouteNotOK2(http.MethodDelete, t)
+ testRouteNotOK2(http.MethodConnect, t)
+ testRouteNotOK2(http.MethodTrace, t)
}
func TestRouteRedirectTrailingSlash(t *testing.T) {
@@ -150,50 +150,50 @@ func TestRouteRedirectTrailingSlash(t *testing.T) {
router.POST("/path3", func(c *Context) {})
router.PUT("/path4/", func(c *Context) {})
- w := performRequest(router, "GET", "/path/")
+ w := performRequest(router, http.MethodGet, "/path/")
assert.Equal(t, "/path", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code)
- w = performRequest(router, "GET", "/path2")
+ w = performRequest(router, http.MethodGet, "/path2")
assert.Equal(t, "/path2/", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code)
- w = performRequest(router, "POST", "/path3/")
+ w = performRequest(router, http.MethodPost, "/path3/")
assert.Equal(t, "/path3", w.Header().Get("Location"))
assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
- w = performRequest(router, "PUT", "/path4")
+ w = performRequest(router, http.MethodPut, "/path4")
assert.Equal(t, "/path4/", w.Header().Get("Location"))
assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
- w = performRequest(router, "GET", "/path")
+ w = performRequest(router, http.MethodGet, "/path")
assert.Equal(t, http.StatusOK, w.Code)
- w = performRequest(router, "GET", "/path2/")
+ w = performRequest(router, http.MethodGet, "/path2/")
assert.Equal(t, http.StatusOK, w.Code)
- w = performRequest(router, "POST", "/path3")
+ w = performRequest(router, http.MethodPost, "/path3")
assert.Equal(t, http.StatusOK, w.Code)
- w = performRequest(router, "PUT", "/path4/")
+ w = performRequest(router, http.MethodPut, "/path4/")
assert.Equal(t, http.StatusOK, w.Code)
- w = performRequest(router, "GET", "/path2", header{Key: "X-Forwarded-Prefix", Value: "/api"})
+ w = performRequest(router, http.MethodGet, "/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/"})
+ w = performRequest(router, http.MethodGet, "/path2/", header{Key: "X-Forwarded-Prefix", Value: "/api/"})
assert.Equal(t, 200, w.Code)
router.RedirectTrailingSlash = false
- w = performRequest(router, "GET", "/path/")
+ w = performRequest(router, http.MethodGet, "/path/")
assert.Equal(t, http.StatusNotFound, w.Code)
- w = performRequest(router, "GET", "/path2")
+ w = performRequest(router, http.MethodGet, "/path2")
assert.Equal(t, http.StatusNotFound, w.Code)
- w = performRequest(router, "POST", "/path3/")
+ w = performRequest(router, http.MethodPost, "/path3/")
assert.Equal(t, http.StatusNotFound, w.Code)
- w = performRequest(router, "PUT", "/path4")
+ w = performRequest(router, http.MethodPut, "/path4")
assert.Equal(t, http.StatusNotFound, w.Code)
}
@@ -207,19 +207,19 @@ func TestRouteRedirectFixedPath(t *testing.T) {
router.POST("/PATH3", func(c *Context) {})
router.POST("/Path4/", func(c *Context) {})
- w := performRequest(router, "GET", "/PATH")
+ w := performRequest(router, http.MethodGet, "/PATH")
assert.Equal(t, "/path", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code)
- w = performRequest(router, "GET", "/path2")
+ w = performRequest(router, http.MethodGet, "/path2")
assert.Equal(t, "/Path2", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code)
- w = performRequest(router, "POST", "/path3")
+ w = performRequest(router, http.MethodPost, "/path3")
assert.Equal(t, "/PATH3", w.Header().Get("Location"))
assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
- w = performRequest(router, "POST", "/path4")
+ w = performRequest(router, http.MethodPost, "/path4")
assert.Equal(t, "/Path4/", w.Header().Get("Location"))
assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
}
@@ -238,7 +238,6 @@ func TestRouteParamsByName(t *testing.T) {
assert.True(t, ok)
assert.Equal(t, name, c.Param("name"))
- assert.Equal(t, name, c.Param("name"))
assert.Equal(t, lastName, c.Param("last_name"))
assert.Empty(t, c.Param("wtf"))
@@ -249,7 +248,7 @@ func TestRouteParamsByName(t *testing.T) {
assert.False(t, ok)
})
- w := performRequest(router, "GET", "/test/john/smith/is/super/great")
+ w := performRequest(router, http.MethodGet, "/test/john/smith/is/super/great")
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "john", name)
@@ -263,6 +262,7 @@ func TestRouteParamsByNameWithExtraSlash(t *testing.T) {
lastName := ""
wild := ""
router := New()
+ router.RemoveExtraSlash = true
router.GET("/test/:name/:last_name/*wild", func(c *Context) {
name = c.Params.ByName("name")
lastName = c.Params.ByName("last_name")
@@ -271,7 +271,6 @@ func TestRouteParamsByNameWithExtraSlash(t *testing.T) {
assert.True(t, ok)
assert.Equal(t, name, c.Param("name"))
- assert.Equal(t, name, c.Param("name"))
assert.Equal(t, lastName, c.Param("last_name"))
assert.Empty(t, c.Param("wtf"))
@@ -282,7 +281,7 @@ func TestRouteParamsByNameWithExtraSlash(t *testing.T) {
assert.False(t, ok)
})
- w := performRequest(router, "GET", "//test//john//smith//is//super//great")
+ w := performRequest(router, http.MethodGet, "//test//john//smith//is//super//great")
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "john", name)
@@ -310,16 +309,16 @@ func TestRouteStaticFile(t *testing.T) {
router.Static("/using_static", dir)
router.StaticFile("/result", f.Name())
- w := performRequest(router, "GET", "/using_static/"+filename)
- w2 := performRequest(router, "GET", "/result")
+ w := performRequest(router, http.MethodGet, "/using_static/"+filename)
+ w2 := performRequest(router, http.MethodGet, "/result")
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.Header().Get("Content-Type"))
- w3 := performRequest(router, "HEAD", "/using_static/"+filename)
- w4 := performRequest(router, "HEAD", "/result")
+ w3 := performRequest(router, http.MethodHead, "/using_static/"+filename)
+ w4 := performRequest(router, http.MethodHead, "/result")
assert.Equal(t, w3, w4)
assert.Equal(t, http.StatusOK, w3.Code)
@@ -330,7 +329,7 @@ func TestRouteStaticListingDir(t *testing.T) {
router := New()
router.StaticFS("/", Dir("./", true))
- w := performRequest(router, "GET", "/")
+ w := performRequest(router, http.MethodGet, "/")
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "gin.go")
@@ -342,7 +341,7 @@ func TestRouteStaticNoListing(t *testing.T) {
router := New()
router.Static("/", "./")
- w := performRequest(router, "GET", "/")
+ w := performRequest(router, http.MethodGet, "/")
assert.Equal(t, http.StatusNotFound, w.Code)
assert.NotContains(t, w.Body.String(), "gin.go")
@@ -357,7 +356,7 @@ func TestRouterMiddlewareAndStatic(t *testing.T) {
})
static.Static("/", "./")
- w := performRequest(router, "GET", "/gin.go")
+ w := performRequest(router, http.MethodGet, "/gin.go")
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "package gin")
@@ -371,13 +370,13 @@ func TestRouteNotAllowedEnabled(t *testing.T) {
router := New()
router.HandleMethodNotAllowed = true
router.POST("/path", func(c *Context) {})
- w := performRequest(router, "GET", "/path")
+ w := performRequest(router, http.MethodGet, "/path")
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
router.NoMethod(func(c *Context) {
c.String(http.StatusTeapot, "responseText")
})
- w = performRequest(router, "GET", "/path")
+ w = performRequest(router, http.MethodGet, "/path")
assert.Equal(t, "responseText", w.Body.String())
assert.Equal(t, http.StatusTeapot, w.Code)
}
@@ -386,9 +385,9 @@ func TestRouteNotAllowedEnabled2(t *testing.T) {
router := New()
router.HandleMethodNotAllowed = true
// add one methodTree to trees
- router.addRoute("POST", "/", HandlersChain{func(_ *Context) {}})
+ router.addRoute(http.MethodPost, "/", HandlersChain{func(_ *Context) {}})
router.GET("/path2", func(c *Context) {})
- w := performRequest(router, "POST", "/path2")
+ w := performRequest(router, http.MethodPost, "/path2")
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
}
@@ -396,17 +395,40 @@ func TestRouteNotAllowedDisabled(t *testing.T) {
router := New()
router.HandleMethodNotAllowed = false
router.POST("/path", func(c *Context) {})
- w := performRequest(router, "GET", "/path")
+ w := performRequest(router, http.MethodGet, "/path")
assert.Equal(t, http.StatusNotFound, w.Code)
router.NoMethod(func(c *Context) {
c.String(http.StatusTeapot, "responseText")
})
- w = performRequest(router, "GET", "/path")
+ w = performRequest(router, http.MethodGet, "/path")
assert.Equal(t, "404 page not found", w.Body.String())
assert.Equal(t, http.StatusNotFound, w.Code)
}
+func TestRouterNotFoundWithRemoveExtraSlash(t *testing.T) {
+ router := New()
+ router.RemoveExtraSlash = true
+ router.GET("/path", func(c *Context) {})
+ router.GET("/", func(c *Context) {})
+
+ testRoutes := []struct {
+ route string
+ code int
+ location string
+ }{
+ {"/../path", http.StatusOK, ""}, // CleanPath
+ {"/nope", http.StatusNotFound, ""}, // NotFound
+ }
+ for _, tr := range testRoutes {
+ w := performRequest(router, "GET", tr.route)
+ assert.Equal(t, tr.code, w.Code)
+ if w.Code != http.StatusNotFound {
+ assert.Equal(t, tr.location, fmt.Sprint(w.Header().Get("Location")))
+ }
+ }
+}
+
func TestRouterNotFound(t *testing.T) {
router := New()
router.RedirectFixedPath = true
@@ -419,17 +441,17 @@ func TestRouterNotFound(t *testing.T) {
code int
location string
}{
- {"/path/", http.StatusMovedPermanently, "/path"}, // TSR -/
- {"/dir", http.StatusMovedPermanently, "/dir/"}, // TSR +/
- {"/PATH", http.StatusMovedPermanently, "/path"}, // Fixed Case
- {"/DIR/", http.StatusMovedPermanently, "/dir/"}, // Fixed Case
- {"/PATH/", http.StatusMovedPermanently, "/path"}, // Fixed Case -/
- {"/DIR", http.StatusMovedPermanently, "/dir/"}, // Fixed Case +/
- {"/../path", http.StatusOK, ""}, // CleanPath
- {"/nope", http.StatusNotFound, ""}, // NotFound
+ {"/path/", http.StatusMovedPermanently, "/path"}, // TSR -/
+ {"/dir", http.StatusMovedPermanently, "/dir/"}, // TSR +/
+ {"/PATH", http.StatusMovedPermanently, "/path"}, // Fixed Case
+ {"/DIR/", http.StatusMovedPermanently, "/dir/"}, // Fixed Case
+ {"/PATH/", http.StatusMovedPermanently, "/path"}, // Fixed Case -/
+ {"/DIR", http.StatusMovedPermanently, "/dir/"}, // Fixed Case +/
+ {"/../path", http.StatusMovedPermanently, "/path"}, // Without CleanPath
+ {"/nope", http.StatusNotFound, ""}, // NotFound
}
for _, tr := range testRoutes {
- w := performRequest(router, "GET", tr.route)
+ w := performRequest(router, http.MethodGet, tr.route)
assert.Equal(t, tr.code, w.Code)
if w.Code != http.StatusNotFound {
assert.Equal(t, tr.location, fmt.Sprint(w.Header().Get("Location")))
@@ -442,20 +464,20 @@ func TestRouterNotFound(t *testing.T) {
c.AbortWithStatus(http.StatusNotFound)
notFound = true
})
- w := performRequest(router, "GET", "/nope")
+ w := performRequest(router, http.MethodGet, "/nope")
assert.Equal(t, http.StatusNotFound, w.Code)
assert.True(t, notFound)
// Test other method than GET (want 307 instead of 301)
router.PATCH("/path", func(c *Context) {})
- w = performRequest(router, "PATCH", "/path/")
+ w = performRequest(router, http.MethodPatch, "/path/")
assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
assert.Equal(t, "map[Location:[/path]]", fmt.Sprint(w.Header()))
// Test special case where no node for the prefix "/" exists
router = New()
router.GET("/a", func(c *Context) {})
- w = performRequest(router, "GET", "/")
+ w = performRequest(router, http.MethodGet, "/")
assert.Equal(t, http.StatusNotFound, w.Code)
}
@@ -466,10 +488,10 @@ func TestRouterStaticFSNotFound(t *testing.T) {
c.String(404, "non existent")
})
- w := performRequest(router, "GET", "/nonexistent")
+ w := performRequest(router, http.MethodGet, "/nonexistent")
assert.Equal(t, "non existent", w.Body.String())
- w = performRequest(router, "HEAD", "/nonexistent")
+ w = performRequest(router, http.MethodHead, "/nonexistent")
assert.Equal(t, "non existent", w.Body.String())
}
@@ -479,7 +501,7 @@ func TestRouterStaticFSFileNotFound(t *testing.T) {
router.StaticFS("/", http.FileSystem(http.Dir(".")))
assert.NotPanics(t, func() {
- performRequest(router, "GET", "/nonexistent")
+ performRequest(router, http.MethodGet, "/nonexistent")
})
}
@@ -490,17 +512,17 @@ func TestMiddlewareCalledOnceByRouterStaticFSNotFound(t *testing.T) {
// Middleware must be called just only once by per request.
middlewareCalledNum := 0
router.Use(func(c *Context) {
- middlewareCalledNum += 1
+ middlewareCalledNum++
})
router.StaticFS("/", http.FileSystem(http.Dir("/thisreallydoesntexist/")))
// First access
- performRequest(router, "GET", "/nonexistent")
+ performRequest(router, http.MethodGet, "/nonexistent")
assert.Equal(t, 1, middlewareCalledNum)
// Second access
- performRequest(router, "HEAD", "/nonexistent")
+ performRequest(router, http.MethodHead, "/nonexistent")
assert.Equal(t, 2, middlewareCalledNum)
}
@@ -519,7 +541,7 @@ func TestRouteRawPath(t *testing.T) {
assert.Equal(t, "222", num)
})
- w := performRequest(route, "POST", "/project/Some%2FOther%2FProject/build/222")
+ w := performRequest(route, http.MethodPost, "/project/Some%2FOther%2FProject/build/222")
assert.Equal(t, http.StatusOK, w.Code)
}
@@ -539,7 +561,7 @@ func TestRouteRawPathNoUnescape(t *testing.T) {
assert.Equal(t, "333", num)
})
- w := performRequest(route, "POST", "/project/Some%2FOther%2FProject/build/333")
+ w := performRequest(route, http.MethodPost, "/project/Some%2FOther%2FProject/build/333")
assert.Equal(t, http.StatusOK, w.Code)
}
@@ -550,7 +572,7 @@ func TestRouteServeErrorWithWriteHeader(t *testing.T) {
c.Next()
})
- w := performRequest(route, "GET", "/NotFound")
+ w := performRequest(route, http.MethodGet, "/NotFound")
assert.Equal(t, 421, w.Code)
assert.Equal(t, 0, w.Body.Len())
}
@@ -569,6 +591,9 @@ func TestRouteContextHoldsFullPath(t *testing.T) {
"/simple-two/one-two",
"/project/:name/build/*params",
"/project/:name/bui",
+ "/user/:id/status",
+ "/user/:id",
+ "/user/:id/profile",
}
for _, route := range routes {
@@ -581,7 +606,7 @@ func TestRouteContextHoldsFullPath(t *testing.T) {
}
for _, route := range routes {
- w := performRequest(router, "GET", route)
+ w := performRequest(router, http.MethodGet, route)
assert.Equal(t, http.StatusOK, w.Code)
}
@@ -591,6 +616,6 @@ func TestRouteContextHoldsFullPath(t *testing.T) {
assert.Equal(t, "", c.FullPath())
})
- w := performRequest(router, "GET", "/not-found")
+ w := performRequest(router, http.MethodGet, "/not-found")
assert.Equal(t, http.StatusNotFound, w.Code)
}
diff --git a/tree.go b/tree.go
index 41947570..ca753e6d 100644
--- a/tree.go
+++ b/tree.go
@@ -5,9 +5,18 @@
package gin
import (
+ "bytes"
"net/url"
"strings"
"unicode"
+ "unicode/utf8"
+
+ "github.com/gin-gonic/gin/internal/bytesconv"
+)
+
+var (
+ strColon = []byte(":")
+ strStar = []byte("*")
)
// Param is a single URL parameter, consisting of a key and a value.
@@ -62,17 +71,31 @@ func min(a, b int) int {
return b
}
-func countParams(path string) uint8 {
- var n uint
- for i := 0; i < len(path); i++ {
- if path[i] == ':' || path[i] == '*' {
- n++
- }
+func longestCommonPrefix(a, b string) int {
+ i := 0
+ max := min(len(a), len(b))
+ for i < max && a[i] == b[i] {
+ i++
}
- if n >= 255 {
- return 255
+ return i
+}
+
+// addChild will add a child node, keeping wildcards at the end
+func (n *node) addChild(child *node) {
+ if n.wildChild && len(n.children) > 0 {
+ wildcardChild := n.children[len(n.children)-1]
+ n.children = append(n.children[:len(n.children)-1], child, wildcardChild)
+ } else {
+ n.children = append(n.children, child)
}
- return uint8(n)
+}
+
+func countParams(path string) uint16 {
+ var n uint16
+ s := bytesconv.StringToBytes(path)
+ n += uint16(bytes.Count(s, strColon))
+ n += uint16(bytes.Count(s, strStar))
+ return n
}
type nodeType uint8
@@ -87,34 +110,32 @@ const (
type node struct {
path string
indices string
- children []*node
- handlers HandlersChain
- priority uint32
- nType nodeType
- maxParams uint8
wildChild bool
+ nType nodeType
+ priority uint32
+ children []*node // child nodes, at most 1 :param style node at the end of the array
+ handlers HandlersChain
fullPath string
}
-// increments priority of the given child and reorders if necessary.
+// Increments priority of the given child and reorders if necessary
func (n *node) incrementChildPrio(pos int) int {
- n.children[pos].priority++
- prio := n.children[pos].priority
+ cs := n.children
+ cs[pos].priority++
+ prio := cs[pos].priority
- // adjust position (move to front)
+ // Adjust position (move to front)
newPos := pos
- for newPos > 0 && n.children[newPos-1].priority < prio {
- // swap node positions
- n.children[newPos-1], n.children[newPos] = n.children[newPos], n.children[newPos-1]
-
- newPos--
+ for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- {
+ // Swap node positions
+ cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1]
}
- // build new index char string
+ // Build new index char string
if newPos != pos {
- n.indices = n.indices[:newPos] + // unchanged prefix, might be empty
- n.indices[pos:pos+1] + // the index char we move
- n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos'
+ n.indices = n.indices[:newPos] + // Unchanged prefix, might be empty
+ n.indices[pos:pos+1] + // The index char we move
+ n.indices[newPos:pos] + n.indices[pos+1:] // Rest without char at 'pos'
}
return newPos
@@ -125,254 +146,242 @@ func (n *node) incrementChildPrio(pos int) int {
func (n *node) addRoute(path string, handlers HandlersChain) {
fullPath := path
n.priority++
- numParams := countParams(path)
+
+ // Empty tree
+ if len(n.path) == 0 && len(n.children) == 0 {
+ n.insertChild(path, fullPath, handlers)
+ n.nType = root
+ return
+ }
parentFullPathIndex := 0
- // non-empty tree
- if len(n.path) > 0 || len(n.children) > 0 {
- walk:
- for {
- // Update maxParams of the current node
- if numParams > n.maxParams {
- n.maxParams = numParams
+walk:
+ for {
+ // Find the longest common prefix.
+ // This also implies that the common prefix contains no ':' or '*'
+ // since the existing key can't contain those chars.
+ i := longestCommonPrefix(path, n.path)
+
+ // Split edge
+ if i < len(n.path) {
+ child := node{
+ path: n.path[i:],
+ wildChild: n.wildChild,
+ indices: n.indices,
+ children: n.children,
+ handlers: n.handlers,
+ priority: n.priority - 1,
+ fullPath: n.fullPath,
}
- // Find the longest common prefix.
- // This also implies that the common prefix contains no ':' or '*'
- // since the existing key can't contain those chars.
- i := 0
- max := min(len(path), len(n.path))
- for i < max && path[i] == n.path[i] {
- i++
+ n.children = []*node{&child}
+ // []byte for proper unicode char conversion, see #65
+ n.indices = bytesconv.BytesToString([]byte{n.path[i]})
+ n.path = path[:i]
+ n.handlers = nil
+ n.wildChild = false
+ n.fullPath = fullPath[:parentFullPathIndex+i]
+ }
+
+ // Make new node a child of this node
+ if i < len(path) {
+ path = path[i:]
+ c := path[0]
+
+ // '/' after param
+ if n.nType == param && c == '/' && len(n.children) == 1 {
+ parentFullPathIndex += len(n.path)
+ n = n.children[0]
+ n.priority++
+ continue walk
}
- // Split edge
- if i < len(n.path) {
- child := node{
- path: n.path[i:],
- wildChild: n.wildChild,
- indices: n.indices,
- children: n.children,
- handlers: n.handlers,
- priority: n.priority - 1,
- fullPath: n.fullPath,
+ // Check if a child with the next path byte exists
+ for i, max := 0, len(n.indices); i < max; i++ {
+ if c == n.indices[i] {
+ parentFullPathIndex += len(n.path)
+ i = n.incrementChildPrio(i)
+ n = n.children[i]
+ continue walk
}
+ }
- // Update maxParams (max of all children)
- for i := range child.children {
- if child.children[i].maxParams > child.maxParams {
- child.maxParams = child.children[i].maxParams
- }
- }
-
- n.children = []*node{&child}
+ // Otherwise insert it
+ if c != ':' && c != '*' && n.nType != catchAll {
// []byte for proper unicode char conversion, see #65
- n.indices = string([]byte{n.path[i]})
- n.path = path[:i]
- n.handlers = nil
- n.wildChild = false
- n.fullPath = fullPath[:parentFullPathIndex+i]
- }
-
- // Make new node a child of this node
- if i < len(path) {
- path = path[i:]
-
- if n.wildChild {
- parentFullPathIndex += len(n.path)
- n = n.children[0]
- n.priority++
-
- // Update maxParams of the child node
- if numParams > n.maxParams {
- n.maxParams = numParams
- }
- numParams--
-
- // Check if the wildcard matches
- if len(path) >= len(n.path) && n.path == path[:len(n.path)] {
- // check for longer wildcard, e.g. :name and :names
- if len(n.path) >= len(path) || path[len(n.path)] == '/' {
- continue walk
- }
- }
-
- pathSeg := path
- if n.nType != catchAll {
- pathSeg = strings.SplitN(path, "/", 2)[0]
- }
- prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
- panic("'" + pathSeg +
- "' in new path '" + fullPath +
- "' conflicts with existing wildcard '" + n.path +
- "' in existing prefix '" + prefix +
- "'")
+ n.indices += bytesconv.BytesToString([]byte{c})
+ child := &node{
+ fullPath: fullPath,
}
+ n.addChild(child)
+ n.incrementChildPrio(len(n.indices) - 1)
+ n = child
+ } else if n.wildChild {
+ // inserting a wildcard node, need to check if it conflicts with the existing wildcard
+ n = n.children[len(n.children)-1]
+ n.priority++
- c := path[0]
-
- // slash after param
- if n.nType == param && c == '/' && len(n.children) == 1 {
- parentFullPathIndex += len(n.path)
- n = n.children[0]
- n.priority++
+ // Check if the wildcard matches
+ if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
+ // Adding a child to a catchAll is not possible
+ n.nType != catchAll &&
+ // Check for longer wildcard, e.g. :name and :names
+ (len(n.path) >= len(path) || path[len(n.path)] == '/') {
continue walk
}
- // Check if a child with the next path byte exists
- for i := 0; i < len(n.indices); i++ {
- if c == n.indices[i] {
- parentFullPathIndex += len(n.path)
- i = n.incrementChildPrio(i)
- n = n.children[i]
- continue walk
- }
+ // Wildcard conflict
+ pathSeg := path
+ if n.nType != catchAll {
+ pathSeg = strings.SplitN(pathSeg, "/", 2)[0]
}
-
- // Otherwise insert it
- if c != ':' && c != '*' {
- // []byte for proper unicode char conversion, see #65
- n.indices += string([]byte{c})
- child := &node{
- maxParams: numParams,
- fullPath: fullPath,
- }
- n.children = append(n.children, child)
- n.incrementChildPrio(len(n.indices) - 1)
- n = child
- }
- n.insertChild(numParams, path, fullPath, handlers)
- return
-
- } else if i == len(path) { // Make node a (in-path) leaf
- if n.handlers != nil {
- panic("handlers are already registered for path '" + fullPath + "'")
- }
- n.handlers = handlers
+ prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
+ panic("'" + pathSeg +
+ "' in new path '" + fullPath +
+ "' conflicts with existing wildcard '" + n.path +
+ "' in existing prefix '" + prefix +
+ "'")
}
+
+ n.insertChild(path, fullPath, handlers)
return
}
- } else { // Empty tree
- n.insertChild(numParams, path, fullPath, handlers)
- n.nType = root
+
+ // Otherwise add handle to current node
+ if n.handlers != nil {
+ panic("handlers are already registered for path '" + fullPath + "'")
+ }
+ n.handlers = handlers
+ n.fullPath = fullPath
+ return
}
}
-func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) {
- var offset int // already handled bytes of the path
-
- // find prefix until first wildcard (beginning with ':' or '*')
- for i, max := 0, len(path); numParams > 0; i++ {
- c := path[i]
+// Search for a wildcard segment and check the name for invalid characters.
+// Returns -1 as index, if no wildcard was found.
+func findWildcard(path string) (wildcard string, i int, valid bool) {
+ // Find start
+ for start, c := range []byte(path) {
+ // A wildcard starts with ':' (param) or '*' (catch-all)
if c != ':' && c != '*' {
continue
}
- // find wildcard end (either '/' or path end)
- end := i + 1
- for end < max && path[end] != '/' {
- switch path[end] {
- // the wildcard name must not contain ':' and '*'
+ // Find end and check for invalid characters
+ valid = true
+ for end, c := range []byte(path[start+1:]) {
+ switch c {
+ case '/':
+ return path[start : start+1+end], start, valid
case ':', '*':
- panic("only one wildcard per path segment is allowed, has: '" +
- path[i:] + "' in path '" + fullPath + "'")
- default:
- end++
+ valid = false
}
}
+ return path[start:], start, valid
+ }
+ return "", -1, false
+}
- // check if this Node existing children which would be
- // unreachable if we insert the wildcard here
- if len(n.children) > 0 {
- panic("wildcard route '" + path[i:end] +
- "' conflicts with existing children in path '" + fullPath + "'")
+func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) {
+ for {
+ // Find prefix until first wildcard
+ wildcard, i, valid := findWildcard(path)
+ if i < 0 { // No wildcard found
+ break
+ }
+
+ // The wildcard name must not contain ':' and '*'
+ if !valid {
+ panic("only one wildcard per path segment is allowed, has: '" +
+ wildcard + "' in path '" + fullPath + "'")
}
// check if the wildcard has a name
- if end-i < 2 {
+ if len(wildcard) < 2 {
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
}
- if c == ':' { // param
- // split path at the beginning of the wildcard
+ if wildcard[0] == ':' { // param
if i > 0 {
- n.path = path[offset:i]
- offset = i
+ // Insert prefix before the current wildcard
+ n.path = path[:i]
+ path = path[i:]
}
child := &node{
- nType: param,
- maxParams: numParams,
- fullPath: fullPath,
+ nType: param,
+ path: wildcard,
+ fullPath: fullPath,
}
- n.children = []*node{child}
+ n.addChild(child)
n.wildChild = true
n = child
n.priority++
- numParams--
// if the path doesn't end with the wildcard, then there
// will be another non-wildcard subpath starting with '/'
- if end < max {
- n.path = path[offset:end]
- offset = end
+ if len(wildcard) < len(path) {
+ path = path[len(wildcard):]
child := &node{
- maxParams: numParams,
- priority: 1,
- fullPath: fullPath,
+ priority: 1,
+ fullPath: fullPath,
}
- n.children = []*node{child}
+ n.addChild(child)
n = child
+ continue
}
- } else { // catchAll
- if end != max || numParams > 1 {
- panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
- }
-
- if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
- panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
- }
-
- // currently fixed width 1 for '/'
- i--
- if path[i] != '/' {
- panic("no / before catch-all in path '" + fullPath + "'")
- }
-
- n.path = path[offset:i]
-
- // first node: catchAll node with empty path
- child := &node{
- wildChild: true,
- nType: catchAll,
- maxParams: 1,
- fullPath: fullPath,
- }
- n.children = []*node{child}
- n.indices = string(path[i])
- n = child
- n.priority++
-
- // second node: node holding the variable
- child = &node{
- path: path[i:],
- nType: catchAll,
- maxParams: 1,
- handlers: handlers,
- priority: 1,
- fullPath: fullPath,
- }
- n.children = []*node{child}
-
+ // Otherwise we're done. Insert the handle in the new leaf
+ n.handlers = handlers
return
}
+
+ // catchAll
+ if i+len(wildcard) != len(path) {
+ panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
+ }
+
+ if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
+ panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
+ }
+
+ // currently fixed width 1 for '/'
+ i--
+ if path[i] != '/' {
+ panic("no / before catch-all in path '" + fullPath + "'")
+ }
+
+ n.path = path[:i]
+
+ // First node: catchAll node with empty path
+ child := &node{
+ wildChild: true,
+ nType: catchAll,
+ fullPath: fullPath,
+ }
+
+ n.addChild(child)
+ n.indices = string('/')
+ n = child
+ n.priority++
+
+ // second node: node holding the variable
+ child = &node{
+ path: path[i:],
+ nType: catchAll,
+ handlers: handlers,
+ priority: 1,
+ fullPath: fullPath,
+ }
+ n.children = []*node{child}
+
+ return
}
- // insert remaining path part and handle to the leaf
- n.path = path[offset:]
+ // If no wildcard was found, simply insert the path and handle
+ n.path = path
n.handlers = handlers
n.fullPath = fullPath
}
@@ -380,67 +389,71 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
// nodeValue holds return values of (*Node).getValue method
type nodeValue struct {
handlers HandlersChain
- params Params
+ params *Params
tsr bool
fullPath string
}
-// getValue returns the handle registered with the given path (key). The values of
+// Returns the handle registered with the given path (key). The values of
// wildcards are saved to a map.
// If no handle can be found, a TSR (trailing slash redirect) recommendation is
// made if a handle exists with an extra (without the) trailing slash for the
// given path.
-func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) {
- value.params = po
+func (n *node) getValue(path string, params *Params, unescape bool) (value nodeValue) {
walk: // Outer loop for walking the tree
for {
- if len(path) > len(n.path) {
- if path[:len(n.path)] == n.path {
- path = path[len(n.path):]
- // If this node does not have a wildcard (param or catchAll)
- // child, we can just look up the next child node and continue
- // to walk down the tree
- if !n.wildChild {
- c := path[0]
- for i := 0; i < len(n.indices); i++ {
- if c == n.indices[i] {
- n = n.children[i]
- continue walk
- }
- }
+ prefix := n.path
+ if len(path) > len(prefix) {
+ if path[:len(prefix)] == prefix {
+ path = path[len(prefix):]
+ // Try all the non-wildcard children first by matching the indices
+ idxc := path[0]
+ for i, c := range []byte(n.indices) {
+ if c == idxc {
+ n = n.children[i]
+ continue walk
+ }
+ }
+
+ // If there is no wildcard pattern, recommend a redirection
+ if !n.wildChild {
// Nothing found.
// We can recommend to redirect to the same URL without a
// trailing slash if a leaf exists for that path.
- value.tsr = path == "/" && n.handlers != nil
+ value.tsr = (path == "/" && n.handlers != nil)
return
}
- // handle wildcard child
- n = n.children[0]
+ // Handle wildcard child, which is always at the end of the array
+ n = n.children[len(n.children)-1]
+
switch n.nType {
case param:
- // find param end (either '/' or path end)
+ // Find param end (either '/' or path end)
end := 0
for end < len(path) && path[end] != '/' {
end++
}
- // save param value
- if cap(value.params) < int(n.maxParams) {
- value.params = make(Params, 0, n.maxParams)
- }
- i := len(value.params)
- value.params = value.params[:i+1] // expand slice within preallocated capacity
- value.params[i].Key = n.path[1:]
- val := path[:end]
- if unescape {
- var err error
- if value.params[i].Value, err = url.QueryUnescape(val); err != nil {
- value.params[i].Value = val // fallback, in case of error
+ // Save param value
+ if params != nil {
+ if value.params == nil {
+ value.params = params
+ }
+ // Expand slice within preallocated capacity
+ i := len(*value.params)
+ *value.params = (*value.params)[:i+1]
+ val := path[:end]
+ if unescape {
+ if v, err := url.QueryUnescape(val); err == nil {
+ val = v
+ }
+ }
+ (*value.params)[i] = Param{
+ Key: n.path[1:],
+ Value: val,
}
- } else {
- value.params[i].Value = val
}
// we need to go deeper!
@@ -452,7 +465,7 @@ walk: // Outer loop for walking the tree
}
// ... but we can't
- value.tsr = len(path) == end+1
+ value.tsr = (len(path) == end+1)
return
}
@@ -464,26 +477,29 @@ walk: // Outer loop for walking the tree
// No handle found. Check if a handle for this path + a
// trailing slash exists for TSR recommendation
n = n.children[0]
- value.tsr = n.path == "/" && n.handlers != nil
+ value.tsr = (n.path == "/" && n.handlers != nil)
}
-
return
case catchAll:
- // save param value
- if cap(value.params) < int(n.maxParams) {
- value.params = make(Params, 0, n.maxParams)
- }
- i := len(value.params)
- value.params = value.params[:i+1] // expand slice within preallocated capacity
- value.params[i].Key = n.path[2:]
- if unescape {
- var err error
- if value.params[i].Value, err = url.QueryUnescape(path); err != nil {
- value.params[i].Value = path // fallback, in case of error
+ // Save param value
+ if params != nil {
+ if value.params == nil {
+ value.params = params
+ }
+ // Expand slice within preallocated capacity
+ i := len(*value.params)
+ *value.params = (*value.params)[:i+1]
+ val := path
+ if unescape {
+ if v, err := url.QueryUnescape(path); err == nil {
+ val = v
+ }
+ }
+ (*value.params)[i] = Param{
+ Key: n.path[2:],
+ Value: val,
}
- } else {
- value.params[i].Value = path
}
value.handlers = n.handlers
@@ -494,7 +510,9 @@ walk: // Outer loop for walking the tree
panic("invalid node type")
}
}
- } else if path == n.path {
+ }
+
+ if path == prefix {
// We should have reached the node containing the handle.
// Check if this node has a handle registered.
if value.handlers = n.handlers; value.handlers != nil {
@@ -502,6 +520,9 @@ walk: // Outer loop for walking the tree
return
}
+ // If there is no handle for this route, but this route has a
+ // wildcard child, there must be a handle for this path with an
+ // additional trailing slash
if path == "/" && n.wildChild && n.nType != root {
value.tsr = true
return
@@ -509,8 +530,8 @@ walk: // Outer loop for walking the tree
// No handle found. Check if a handle for this path + a
// trailing slash exists for trailing slash recommendation
- for i := 0; i < len(n.indices); i++ {
- if n.indices[i] == '/' {
+ for i, c := range []byte(n.indices) {
+ if c == '/' {
n = n.children[i]
value.tsr = (len(n.path) == 1 && n.handlers != nil) ||
(n.nType == catchAll && n.children[0].handlers != nil)
@@ -524,114 +545,220 @@ walk: // Outer loop for walking the tree
// Nothing found. We can recommend to redirect to the same URL with an
// extra trailing slash if a leaf exists for that path
value.tsr = (path == "/") ||
- (len(n.path) == len(path)+1 && n.path[len(path)] == '/' &&
- path == n.path[:len(n.path)-1] && n.handlers != nil)
+ (len(prefix) == len(path)+1 && prefix[len(path)] == '/' &&
+ path == prefix[:len(prefix)-1] && n.handlers != nil)
return
}
}
-// findCaseInsensitivePath makes a case-insensitive lookup of the given path and tries to find a handler.
+// Makes a case-insensitive lookup of the given path and tries to find a handler.
// It can optionally also fix trailing slashes.
// It returns the case-corrected path and a bool indicating whether the lookup
// was successful.
-func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPath []byte, found bool) {
- ciPath = make([]byte, 0, len(path)+1) // preallocate enough memory
+func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]byte, bool) {
+ const stackBufSize = 128
- // Outer loop for walking the tree
- for len(path) >= len(n.path) && strings.EqualFold(path[:len(n.path)], n.path) {
- path = path[len(n.path):]
+ // Use a static sized buffer on the stack in the common case.
+ // If the path is too long, allocate a buffer on the heap instead.
+ buf := make([]byte, 0, stackBufSize)
+ if length := len(path) + 1; length > stackBufSize {
+ buf = make([]byte, 0, length)
+ }
+
+ ciPath := n.findCaseInsensitivePathRec(
+ path,
+ buf, // Preallocate enough memory for new path
+ [4]byte{}, // Empty rune buffer
+ fixTrailingSlash,
+ )
+
+ return ciPath, ciPath != nil
+}
+
+// Shift bytes in array by n bytes left
+func shiftNRuneBytes(rb [4]byte, n int) [4]byte {
+ switch n {
+ case 0:
+ return rb
+ case 1:
+ return [4]byte{rb[1], rb[2], rb[3], 0}
+ case 2:
+ return [4]byte{rb[2], rb[3]}
+ case 3:
+ return [4]byte{rb[3]}
+ default:
+ return [4]byte{}
+ }
+}
+
+// Recursive case-insensitive lookup function used by n.findCaseInsensitivePath
+func (n *node) findCaseInsensitivePathRec(path string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) []byte {
+ npLen := len(n.path)
+
+walk: // Outer loop for walking the tree
+ for len(path) >= npLen && (npLen == 0 || strings.EqualFold(path[1:npLen], n.path[1:])) {
+ // Add common prefix to result
+ oldPath := path
+ path = path[npLen:]
ciPath = append(ciPath, n.path...)
- if len(path) > 0 {
- // If this node does not have a wildcard (param or catchAll) child,
- // we can just look up the next child node and continue to walk down
- // the tree
- if !n.wildChild {
- r := unicode.ToLower(rune(path[0]))
- for i, index := range n.indices {
- // must use recursive approach since both index and
- // ToLower(index) could exist. We must check both.
- if r == unicode.ToLower(index) {
- out, found := n.children[i].findCaseInsensitivePath(path, fixTrailingSlash)
- if found {
- return append(ciPath, out...), true
- }
- }
- }
-
- // Nothing found. We can recommend to redirect to the same URL
- // without a trailing slash if a leaf exists for that path
- found = fixTrailingSlash && path == "/" && n.handlers != nil
- return
- }
-
- n = n.children[0]
- switch n.nType {
- case param:
- // find param end (either '/' or path end)
- k := 0
- for k < len(path) && path[k] != '/' {
- k++
- }
-
- // add param value to case insensitive path
- ciPath = append(ciPath, path[:k]...)
-
- // we need to go deeper!
- if k < len(path) {
- if len(n.children) > 0 {
- path = path[k:]
- n = n.children[0]
- continue
- }
-
- // ... but we can't
- if fixTrailingSlash && len(path) == k+1 {
- return ciPath, true
- }
- return
- }
-
- if n.handlers != nil {
- return ciPath, true
- } else if fixTrailingSlash && len(n.children) == 1 {
- // No handle found. Check if a handle for this path + a
- // trailing slash exists
- n = n.children[0]
- if n.path == "/" && n.handlers != nil {
- return append(ciPath, '/'), true
- }
- }
- return
-
- case catchAll:
- return append(ciPath, path...), true
-
- default:
- panic("invalid node type")
- }
- } else {
+ if len(path) == 0 {
// We should have reached the node containing the handle.
// Check if this node has a handle registered.
if n.handlers != nil {
- return ciPath, true
+ return ciPath
}
// No handle found.
// Try to fix the path by adding a trailing slash
if fixTrailingSlash {
- for i := 0; i < len(n.indices); i++ {
- if n.indices[i] == '/' {
+ for i, c := range []byte(n.indices) {
+ if c == '/' {
n = n.children[i]
if (len(n.path) == 1 && n.handlers != nil) ||
(n.nType == catchAll && n.children[0].handlers != nil) {
- return append(ciPath, '/'), true
+ return append(ciPath, '/')
}
- return
+ return nil
}
}
}
- return
+ return nil
+ }
+
+ // If this node does not have a wildcard (param or catchAll) child,
+ // we can just look up the next child node and continue to walk down
+ // the tree
+ if !n.wildChild {
+ // Skip rune bytes already processed
+ rb = shiftNRuneBytes(rb, npLen)
+
+ if rb[0] != 0 {
+ // Old rune not finished
+ idxc := rb[0]
+ for i, c := range []byte(n.indices) {
+ if c == idxc {
+ // continue with child node
+ n = n.children[i]
+ npLen = len(n.path)
+ continue walk
+ }
+ }
+ } else {
+ // Process a new rune
+ var rv rune
+
+ // Find rune start.
+ // Runes are up to 4 byte long,
+ // -4 would definitely be another rune.
+ var off int
+ for max := min(npLen, 3); off < max; off++ {
+ if i := npLen - off; utf8.RuneStart(oldPath[i]) {
+ // read rune from cached path
+ rv, _ = utf8.DecodeRuneInString(oldPath[i:])
+ break
+ }
+ }
+
+ // Calculate lowercase bytes of current rune
+ lo := unicode.ToLower(rv)
+ utf8.EncodeRune(rb[:], lo)
+
+ // Skip already processed bytes
+ rb = shiftNRuneBytes(rb, off)
+
+ idxc := rb[0]
+ for i, c := range []byte(n.indices) {
+ // Lowercase matches
+ if c == idxc {
+ // must use a recursive approach since both the
+ // uppercase byte and the lowercase byte might exist
+ // as an index
+ if out := n.children[i].findCaseInsensitivePathRec(
+ path, ciPath, rb, fixTrailingSlash,
+ ); out != nil {
+ return out
+ }
+ break
+ }
+ }
+
+ // If we found no match, the same for the uppercase rune,
+ // if it differs
+ if up := unicode.ToUpper(rv); up != lo {
+ utf8.EncodeRune(rb[:], up)
+ rb = shiftNRuneBytes(rb, off)
+
+ idxc := rb[0]
+ for i, c := range []byte(n.indices) {
+ // Uppercase matches
+ if c == idxc {
+ // Continue with child node
+ n = n.children[i]
+ npLen = len(n.path)
+ continue walk
+ }
+ }
+ }
+ }
+
+ // Nothing found. We can recommend to redirect to the same URL
+ // without a trailing slash if a leaf exists for that path
+ if fixTrailingSlash && path == "/" && n.handlers != nil {
+ return ciPath
+ }
+ return nil
+ }
+
+ n = n.children[0]
+ switch n.nType {
+ case param:
+ // Find param end (either '/' or path end)
+ end := 0
+ for end < len(path) && path[end] != '/' {
+ end++
+ }
+
+ // Add param value to case insensitive path
+ ciPath = append(ciPath, path[:end]...)
+
+ // We need to go deeper!
+ if end < len(path) {
+ if len(n.children) > 0 {
+ // Continue with child node
+ n = n.children[0]
+ npLen = len(n.path)
+ path = path[end:]
+ continue
+ }
+
+ // ... but we can't
+ if fixTrailingSlash && len(path) == end+1 {
+ return ciPath
+ }
+ return nil
+ }
+
+ if n.handlers != nil {
+ return ciPath
+ }
+
+ if fixTrailingSlash && len(n.children) == 1 {
+ // No handle found. Check if a handle for this path + a
+ // trailing slash exists
+ n = n.children[0]
+ if n.path == "/" && n.handlers != nil {
+ return append(ciPath, '/')
+ }
+ }
+
+ return nil
+
+ case catchAll:
+ return append(ciPath, path...)
+
+ default:
+ panic("invalid node type")
}
}
@@ -639,13 +766,12 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa
// Try to fix the path by adding / removing a trailing slash
if fixTrailingSlash {
if path == "/" {
- return ciPath, true
+ return ciPath
}
- if len(path)+1 == len(n.path) && n.path[len(path)] == '/' &&
- strings.EqualFold(path, n.path[:len(path)]) &&
- n.handlers != nil {
- return append(ciPath, n.path...), true
+ if len(path)+1 == npLen && n.path[len(path)] == '/' &&
+ strings.EqualFold(path[1:], n.path[1:len(path)]) && n.handlers != nil {
+ return append(ciPath, n.path...)
}
}
- return
+ return nil
}
diff --git a/tree_test.go b/tree_test.go
index e6e28865..d7c4fb0b 100644
--- a/tree_test.go
+++ b/tree_test.go
@@ -28,6 +28,11 @@ type testRequests []struct {
ps Params
}
+func getParams() *Params {
+ ps := make(Params, 0, 20)
+ return &ps
+}
+
func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ...bool) {
unescape := false
if len(unescapes) >= 1 {
@@ -35,7 +40,7 @@ func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ..
}
for _, request := range requests {
- value := tree.getValue(request.path, nil, unescape)
+ value := tree.getValue(request.path, getParams(), unescape)
if value.handlers == nil {
if !request.nilHandler {
@@ -50,9 +55,12 @@ func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ..
}
}
- if !reflect.DeepEqual(value.params, request.ps) {
- t.Errorf("Params mismatch for route '%s'", request.path)
+ if value.params != nil {
+ if !reflect.DeepEqual(*value.params, request.ps) {
+ t.Errorf("Params mismatch for route '%s'", request.path)
+ }
}
+
}
}
@@ -76,33 +84,11 @@ func checkPriorities(t *testing.T, n *node) uint32 {
return prio
}
-func checkMaxParams(t *testing.T, n *node) uint8 {
- var maxParams uint8
- for i := range n.children {
- params := checkMaxParams(t, n.children[i])
- if params > maxParams {
- maxParams = params
- }
- }
- if n.nType > root && !n.wildChild {
- maxParams++
- }
-
- if n.maxParams != maxParams {
- t.Errorf(
- "maxParams mismatch for node '%s': is %d, should be %d",
- n.path, n.maxParams, maxParams,
- )
- }
-
- return maxParams
-}
-
func TestCountParams(t *testing.T) {
if countParams("/path/:param1/static/*catch-all") != 2 {
t.Fail()
}
- if countParams(strings.Repeat("/:param", 256)) != 255 {
+ if countParams(strings.Repeat("/:param", 256)) != 256 {
t.Fail()
}
}
@@ -142,7 +128,6 @@ func TestTreeAddAndGet(t *testing.T) {
})
checkPriorities(t, tree)
- checkMaxParams(t, tree)
}
func TestTreeWildcard(t *testing.T) {
@@ -152,6 +137,8 @@ func TestTreeWildcard(t *testing.T) {
"/",
"/cmd/:tool/:sub",
"/cmd/:tool/",
+ "/cmd/whoami",
+ "/cmd/whoami/root/",
"/src/*filepath",
"/search/",
"/search/:query",
@@ -170,8 +157,12 @@ func TestTreeWildcard(t *testing.T) {
checkRequests(t, tree, testRequests{
{"/", false, "/", nil},
- {"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}},
- {"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}},
+ {"/cmd/test", true, "/cmd/:tool/", Params{Param{"tool", "test"}}},
+ {"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
+ {"/cmd/whoami", false, "/cmd/whoami", nil},
+ {"/cmd/whoami/", true, "/cmd/whoami", nil},
+ {"/cmd/whoami/root/", false, "/cmd/whoami/root/", nil},
+ {"/cmd/whoami/root", true, "/cmd/whoami/root/", nil},
{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "test"}, Param{Key: "sub", Value: "3"}}},
{"/src/", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/"}}},
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
@@ -186,7 +177,6 @@ func TestTreeWildcard(t *testing.T) {
})
checkPriorities(t, tree)
- checkMaxParams(t, tree)
}
func TestUnescapeParameters(t *testing.T) {
@@ -224,7 +214,6 @@ func TestUnescapeParameters(t *testing.T) {
}, unescape)
checkPriorities(t, tree)
- checkMaxParams(t, tree)
}
func catchPanic(testFunc func()) (recv interface{}) {
@@ -262,20 +251,38 @@ func testRoutes(t *testing.T, routes []testRoute) {
func TestTreeWildcardConflict(t *testing.T) {
routes := []testRoute{
{"/cmd/:tool/:sub", false},
- {"/cmd/vet", true},
+ {"/cmd/vet", false},
+ {"/foo/bar", false},
+ {"/foo/:name", false},
+ {"/foo/:names", true},
+ {"/cmd/*path", true},
+ {"/cmd/:badvar", true},
+ {"/cmd/:tool/names", false},
+ {"/cmd/:tool/:badsub/details", true},
{"/src/*filepath", false},
+ {"/src/:file", true},
+ {"/src/static.json", true},
{"/src/*filepathx", true},
{"/src/", true},
+ {"/src/foo/bar", true},
{"/src1/", false},
{"/src1/*filepath", true},
{"/src2*filepath", true},
+ {"/src2/*filepath", false},
{"/search/:query", false},
- {"/search/invalid", true},
+ {"/search/valid", false},
{"/user_:name", false},
- {"/user_x", true},
+ {"/user_x", false},
{"/user_:name", false},
{"/id:id", false},
- {"/id/:id", true},
+ {"/id/:id", false},
+ }
+ testRoutes(t, routes)
+}
+
+func TestCatchAllAfterSlash(t *testing.T) {
+ routes := []testRoute{
+ {"/non-leading-*catchall", true},
}
testRoutes(t, routes)
}
@@ -283,14 +290,17 @@ func TestTreeWildcardConflict(t *testing.T) {
func TestTreeChildConflict(t *testing.T) {
routes := []testRoute{
{"/cmd/vet", false},
- {"/cmd/:tool/:sub", true},
+ {"/cmd/:tool", false},
+ {"/cmd/:tool/:sub", false},
+ {"/cmd/:tool/misc", false},
+ {"/cmd/:tool/:othersub", true},
{"/src/AUTHORS", false},
{"/src/*filepath", true},
{"/user_x", false},
- {"/user_:name", true},
+ {"/user_:name", false},
{"/id/:id", false},
- {"/id:id", true},
- {"/:id", true},
+ {"/id:id", false},
+ {"/:id", false},
{"/*filepath", true},
}
testRoutes(t, routes)
@@ -323,12 +333,14 @@ func TestTreeDupliatePath(t *testing.T) {
}
}
+ //printChildren(tree, "")
+
checkRequests(t, tree, testRequests{
{"/", false, "/", nil},
{"/doc/", false, "/doc/", nil},
- {"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
- {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
- {"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}},
+ {"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
+ {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
+ {"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}},
})
}
@@ -356,6 +368,8 @@ func TestTreeCatchAllConflict(t *testing.T) {
{"/src/*filepath/x", true},
{"/src2/", false},
{"/src2/*filepath/x", true},
+ {"/src3/*filepath", false},
+ {"/src3/*filepath/x", true},
}
testRoutes(t, routes)
}
@@ -368,6 +382,12 @@ func TestTreeCatchAllConflictRoot(t *testing.T) {
testRoutes(t, routes)
}
+func TestTreeCatchMaxParams(t *testing.T) {
+ tree := &node{}
+ var route = "/cmd/*filepath"
+ tree.addRoute(route, fakeHandler(route))
+}
+
func TestTreeDoubleWildcard(t *testing.T) {
const panicMsg = "only one wildcard per path segment is allowed"
@@ -501,6 +521,9 @@ func TestTreeRootTrailingSlashRedirect(t *testing.T) {
func TestTreeFindCaseInsensitivePath(t *testing.T) {
tree := &node{}
+ longPath := "/l" + strings.Repeat("o", 128) + "ng"
+ lOngPath := "/l" + strings.Repeat("O", 128) + "ng/"
+
routes := [...]string{
"/hi",
"/b/",
@@ -524,6 +547,17 @@ func TestTreeFindCaseInsensitivePath(t *testing.T) {
"/doc/go/away",
"/no/a",
"/no/b",
+ "/Π",
+ "/u/apfêl/",
+ "/u/äpfêl/",
+ "/u/öpfêl",
+ "/v/Äpfêl/",
+ "/v/Öpfêl",
+ "/w/♬", // 3 byte
+ "/w/♭/", // 3 byte, last byte differs
+ "/w/𠜎", // 4 byte
+ "/w/𠜏/", // 4 byte
+ longPath,
}
for _, route := range routes {
@@ -602,6 +636,21 @@ func TestTreeFindCaseInsensitivePath(t *testing.T) {
{"/DOC/", "/doc", true, true},
{"/NO", "", false, true},
{"/DOC/GO", "", false, true},
+ {"/π", "/Π", true, false},
+ {"/π/", "/Π", true, true},
+ {"/u/ÄPFÊL/", "/u/äpfêl/", true, false},
+ {"/u/ÄPFÊL", "/u/äpfêl/", true, true},
+ {"/u/ÖPFÊL/", "/u/öpfêl", true, true},
+ {"/u/ÖPFÊL", "/u/öpfêl", true, false},
+ {"/v/äpfêL/", "/v/Äpfêl/", true, false},
+ {"/v/äpfêL", "/v/Äpfêl/", true, true},
+ {"/v/öpfêL/", "/v/Öpfêl", true, true},
+ {"/v/öpfêL", "/v/Öpfêl", true, false},
+ {"/w/♬/", "/w/♬", true, true},
+ {"/w/♭", "/w/♭/", true, true},
+ {"/w/𠜎/", "/w/𠜎", true, true},
+ {"/w/𠜏", "/w/𠜏/", true, true},
+ {lOngPath, longPath, true, true},
}
// With fixTrailingSlash = true
for _, test := range tests {
@@ -666,8 +715,7 @@ func TestTreeWildcardConflictEx(t *testing.T) {
{"/who/are/foo", "/foo", `/who/are/\*you`, `/\*you`},
{"/who/are/foo/", "/foo/", `/who/are/\*you`, `/\*you`},
{"/who/are/foo/bar", "/foo/bar", `/who/are/\*you`, `/\*you`},
- {"/conxxx", "xxx", `/con:tact`, `:tact`},
- {"/conooo/xxx", "ooo", `/con:tact`, `:tact`},
+ {"/con:nection", ":nection", `/con:tact`, `:tact`},
}
for _, conflict := range conflicts {
@@ -689,8 +737,7 @@ func TestTreeWildcardConflictEx(t *testing.T) {
tree.addRoute(conflict.route, fakeHandler(conflict.route))
})
- if !regexp.MustCompile(fmt.Sprintf("'%s' in new path .* conflicts with existing wildcard '%s' in existing prefix '%s'",
- conflict.segPath, conflict.existSegPath, conflict.existPath)).MatchString(fmt.Sprint(recv)) {
+ if !regexp.MustCompile(fmt.Sprintf("'%s' in new path .* conflicts with existing wildcard '%s' in existing prefix '%s'", conflict.segPath, conflict.existSegPath, conflict.existPath)).MatchString(fmt.Sprint(recv)) {
t.Fatalf("invalid wildcard conflict error (%v)", recv)
}
}
diff --git a/utils.go b/utils.go
index 71b80de7..c32f0eeb 100644
--- a/utils.go
+++ b/utils.go
@@ -90,20 +90,23 @@ func filterFlags(content string) string {
}
func chooseData(custom, wildcard interface{}) interface{} {
- if custom == nil {
- if wildcard == nil {
- panic("negotiation config is invalid")
- }
+ if custom != nil {
+ return custom
+ }
+ if wildcard != nil {
return wildcard
}
- return custom
+ panic("negotiation config is invalid")
}
func parseAccept(acceptHeader string) []string {
parts := strings.Split(acceptHeader, ",")
out := make([]string, 0, len(parts))
for _, part := range parts {
- if part = strings.TrimSpace(strings.Split(part, ";")[0]); part != "" {
+ if i := strings.IndexByte(part, ';'); i > 0 {
+ part = part[:i]
+ }
+ if part = strings.TrimSpace(part); part != "" {
out = append(out, part)
}
}
@@ -127,8 +130,7 @@ func joinPaths(absolutePath, relativePath string) string {
}
finalPath := path.Join(absolutePath, relativePath)
- appendSlash := lastChar(relativePath) == '/' && lastChar(finalPath) != '/'
- if appendSlash {
+ if lastChar(relativePath) == '/' && lastChar(finalPath) != '/' {
return finalPath + "/"
}
return finalPath
diff --git a/utils_test.go b/utils_test.go
index 9b57c57b..cc486c35 100644
--- a/utils_test.go
+++ b/utils_test.go
@@ -18,6 +18,12 @@ func init() {
SetMode(TestMode)
}
+func BenchmarkParseAccept(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ parseAccept("text/html , application/xhtml+xml,application/xml;q=0.9, */* ;q=0.8")
+ }
+}
+
type testStruct struct {
T *testing.T
}
diff --git a/vendor/vendor.json b/vendor/vendor.json
deleted file mode 100644
index 70b2d9eb..00000000
--- a/vendor/vendor.json
+++ /dev/null
@@ -1,153 +0,0 @@
-{
- "comment": "v1.4.0",
- "ignore": "test",
- "package": [
- {
- "checksumSHA1": "CSPbwbyzqA6sfORicn4HFtIhF/c=",
- "path": "github.com/davecgh/go-spew/spew",
- "revision": "8991bc29aa16c548c550c7ff78260e27b9ab7c73",
- "revisionTime": "2018-02-21T22:46:20Z",
- "version": "v1.1",
- "versionExact": "v1.1.1"
- },
- {
- "checksumSHA1": "qlEzrgKgIkh7y0ePm9BNo1cNdXo=",
- "path": "github.com/gin-contrib/sse",
- "revision": "54d8467d122d380a14768b6b4e5cd7ca4755938f",
- "revisionTime": "2019-06-02T15:02:53Z",
- "version": "v0.1",
- "versionExact": "v0.1.0"
- },
- {
- "checksumSHA1": "b4DmyMT9bicTRVJw1hJXHLhIH+0=",
- "path": "github.com/go-playground/locales",
- "revision": "f63010822830b6fe52288ee52d5a1151088ce039",
- "revisionTime": "2018-03-23T16:04:04Z",
- "version": "v0.12",
- "versionExact": "v0.12.1"
- },
- {
- "checksumSHA1": "JgF260rC9YpWyY5WEljjimWLUXs=",
- "path": "github.com/go-playground/locales/currency",
- "revision": "630ebbb602847eba93e75ae38bbc7bb7abcf1ff3",
- "revisionTime": "2019-04-30T15:33:29Z"
- },
- {
- "checksumSHA1": "9pKcUHBaVS+360X6h4IowhmOPjk=",
- "path": "github.com/go-playground/universal-translator",
- "revision": "b32fa301c9fe55953584134cb6853a13c87ec0a1",
- "revisionTime": "2017-02-09T16:11:52Z",
- "version": "v0.16",
- "versionExact": "v0.16.0"
- },
- {
- "checksumSHA1": "Y2MOwzNZfl4NRNDbLCZa6sgx7O0=",
- "path": "github.com/golang/protobuf/proto",
- "revision": "c823c79ea1570fb5ff454033735a8e68575d1d0f",
- "revisionTime": "2019-02-05T22:20:52Z",
- "version": "v1.3",
- "versionExact": "v1.3.0"
- },
- {
- "checksumSHA1": "zNo6yGy/bCJuzkEcP70oEBtOB2M=",
- "path": "github.com/leodido/go-urn",
- "revision": "70078a794e8ea4b497ba7c19a78cd60f90ccf0f4",
- "revisionTime": "2018-05-24T03:26:21Z",
- "version": "v1.1",
- "versionExact": "v1.1.0"
- },
- {
- "checksumSHA1": "TB2vxux9xQbvsTHOVt4aRTuvSn4=",
- "path": "github.com/json-iterator/go",
- "revision": "0ff49de124c6f76f8494e194af75bde0f1a49a29",
- "revisionTime": "2019-03-06T14:29:09Z",
- "version": "v1.1",
- "versionExact": "v1.1.6"
- },
- {
- "checksumSHA1": "Ya+baVBU/RkXXUWD3LGFmGJiiIg=",
- "path": "github.com/mattn/go-isatty",
- "revision": "c2a7a6ca930a4cd0bc33a3f298eb71960732a3a7",
- "revisionTime": "2019-03-12T13:58:54Z",
- "version": "v0.0",
- "versionExact": "v0.0.7"
- },
- {
- "checksumSHA1": "ZTcgWKWHsrX0RXYVXn5Xeb8Q0go=",
- "path": "github.com/modern-go/concurrent",
- "revision": "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94",
- "revisionTime": "2018-03-06T01:26:44Z"
- },
- {
- "checksumSHA1": "qvH48wzTIV3QKSDqI0dLFtVjaDI=",
- "path": "github.com/modern-go/reflect2",
- "revision": "94122c33edd36123c84d5368cfb2b69df93a0ec8",
- "revisionTime": "2018-07-18T01:23:57Z"
- },
- {
- "checksumSHA1": "LuFv4/jlrmFNnDb/5SCSEPAM9vU=",
- "path": "github.com/pmezard/go-difflib/difflib",
- "revision": "5d4384ee4fb2527b0a1256a821ebfc92f91efefc",
- "revisionTime": "2018-12-26T10:54:42Z"
- },
- {
- "checksumSHA1": "cpNsoLqBprpKh+VZTBOZNVXzBEk=",
- "path": "github.com/stretchr/objx",
- "revision": "c61a9dfcced1815e7d40e214d00d1a8669a9f58c",
- "revisionTime": "2019-02-11T16:23:28Z"
- },
- {
- "checksumSHA1": "DBdcVxnvaINHhWyyGgih/Mel6gE=",
- "path": "github.com/stretchr/testify",
- "revision": "ffdc059bfe9ce6a4e144ba849dbedead332c6053",
- "revisionTime": "2018-12-05T02:12:43Z",
- "version": "v1.3",
- "versionExact": "v1.3.0"
- },
- {
- "checksumSHA1": "c6pbpF7eowwO59phRTpF8cQ80Z0=",
- "path": "github.com/stretchr/testify/assert",
- "revision": "f35b8ab0b5a2cef36673838d662e249dd9c94686",
- "revisionTime": "2018-05-06T18:05:49Z",
- "version": "v1.2",
- "versionExact": "v1.2.2"
- },
- {
- "checksumSHA1": "wnEANt4k5X/KGwoFyfSSnpxULm4=",
- "path": "github.com/stretchr/testify/require",
- "revision": "f35b8ab0b5a2cef36673838d662e249dd9c94686",
- "revisionTime": "2018-05-06T18:05:49Z"
- },
- {
- "checksumSHA1": "S4ei9eSqVThDio0Jn2sav6yUbvg=",
- "path": "github.com/ugorji/go/codec",
- "revision": "82dbfaf494e3b01d2d481376f11f6a5c8cf9599f",
- "revisionTime": "2019-07-02T14:15:27Z",
- "version": "v1.1",
- "versionExact": "v1.1.6"
- },
- {
- "checksumSHA1": "2gaep1KNRDNyDA3O+KgPTQsGWvs=",
- "path": "golang.org/x/sys/unix",
- "revision": "a43fa875dd822b81eb6d2ad538bc1f4caba169bd",
- "revisionTime": "2019-05-02T15:41:39Z"
- },
- {
- "checksumSHA1": "ACzc7AkwLtNgKhqtj8V7SGUJgnw=",
- "path": "gopkg.in/go-playground/validator.v9",
- "revision": "46b4b1e301c24cac870ffcb4ba5c8a703d1ef475",
- "revisionTime": "2019-03-31T13:31:25Z",
- "version": "v9.28",
- "versionExact": "v9.28.0"
- },
- {
- "checksumSHA1": "QqDq2x8XOU7IoOR98Cx1eiV5QY8=",
- "path": "gopkg.in/yaml.v2",
- "revision": "51d6538a90f86fe93ac480b35f37b2be17fef232",
- "revisionTime": "2018-11-15T11:05:04Z",
- "version": "v2.2",
- "versionExact": "v2.2.2"
- }
- ],
- "rootPath": "github.com/gin-gonic/gin"
-}
diff --git a/version.go b/version.go
index 6f8235f9..95b4ed14 100644
--- a/version.go
+++ b/version.go
@@ -5,4 +5,4 @@
package gin
// Version is the current gin framework's version.
-const Version = "v1.5.0"
+const Version = "v1.7.0"