diff --git a/.gitignore b/.gitignore index f3b636df..14dc8f20 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ vendor/* !vendor/vendor.json coverage.out count.out +test diff --git a/.travis.yml b/.travis.yml index 821ce8df..63b5d113 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,8 @@ go: - 1.6.x - 1.7.x - 1.8.x + - 1.9.x + - 1.10.x - master git: diff --git a/BENCHMARKS.md b/BENCHMARKS.md index 6efe3ca4..9a7df86a 100644 --- a/BENCHMARKS.md +++ b/BENCHMARKS.md @@ -1,298 +1,604 @@ -**Machine:** intel i7 ivy bridge quad-core. 8GB RAM. -**Date:** June 4th, 2015 -[https://github.com/gin-gonic/go-http-routing-benchmark](https://github.com/gin-gonic/go-http-routing-benchmark) + +## 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) + +## Static Routes: 157 ``` -BenchmarkAce_Param 5000000 372 ns/op 32 B/op 1 allocs/op -BenchmarkBear_Param 1000000 1165 ns/op 424 B/op 5 allocs/op -BenchmarkBeego_Param 1000000 2440 ns/op 720 B/op 10 allocs/op -BenchmarkBone_Param 1000000 1067 ns/op 384 B/op 3 allocs/op -BenchmarkDenco_Param 5000000 240 ns/op 32 B/op 1 allocs/op -BenchmarkEcho_Param 10000000 130 ns/op 0 B/op 0 allocs/op -BenchmarkGin_Param 10000000 133 ns/op 0 B/op 0 allocs/op -BenchmarkGocraftWeb_Param 1000000 1826 ns/op 656 B/op 9 allocs/op -BenchmarkGoji_Param 2000000 957 ns/op 336 B/op 2 allocs/op -BenchmarkGoJsonRest_Param 1000000 2021 ns/op 657 B/op 14 allocs/op -BenchmarkGoRestful_Param 200000 8825 ns/op 2496 B/op 31 allocs/op -BenchmarkGorillaMux_Param 500000 3340 ns/op 784 B/op 9 allocs/op -BenchmarkHttpRouter_Param 10000000 152 ns/op 32 B/op 1 allocs/op -BenchmarkHttpTreeMux_Param 2000000 717 ns/op 336 B/op 2 allocs/op -BenchmarkKocha_Param 3000000 423 ns/op 56 B/op 3 allocs/op -BenchmarkMacaron_Param 1000000 3410 ns/op 1104 B/op 11 allocs/op -BenchmarkMartini_Param 200000 7101 ns/op 1152 B/op 12 allocs/op -BenchmarkPat_Param 1000000 2040 ns/op 656 B/op 14 allocs/op -BenchmarkPossum_Param 1000000 2048 ns/op 624 B/op 7 allocs/op -BenchmarkR2router_Param 1000000 1144 ns/op 432 B/op 6 allocs/op -BenchmarkRevel_Param 200000 6725 ns/op 1672 B/op 28 allocs/op -BenchmarkRivet_Param 1000000 1121 ns/op 464 B/op 5 allocs/op -BenchmarkTango_Param 1000000 1479 ns/op 256 B/op 10 allocs/op -BenchmarkTigerTonic_Param 1000000 3393 ns/op 992 B/op 19 allocs/op -BenchmarkTraffic_Param 300000 5525 ns/op 1984 B/op 23 allocs/op -BenchmarkVulcan_Param 2000000 924 ns/op 98 B/op 3 allocs/op -BenchmarkZeus_Param 1000000 1084 ns/op 368 B/op 3 allocs/op -BenchmarkAce_Param5 3000000 614 ns/op 160 B/op 1 allocs/op -BenchmarkBear_Param5 1000000 1617 ns/op 469 B/op 5 allocs/op -BenchmarkBeego_Param5 1000000 3373 ns/op 992 B/op 13 allocs/op -BenchmarkBone_Param5 1000000 1478 ns/op 432 B/op 3 allocs/op -BenchmarkDenco_Param5 3000000 570 ns/op 160 B/op 1 allocs/op -BenchmarkEcho_Param5 5000000 256 ns/op 0 B/op 0 allocs/op -BenchmarkGin_Param5 10000000 222 ns/op 0 B/op 0 allocs/op -BenchmarkGocraftWeb_Param5 1000000 2789 ns/op 928 B/op 12 allocs/op -BenchmarkGoji_Param5 1000000 1287 ns/op 336 B/op 2 allocs/op -BenchmarkGoJsonRest_Param5 1000000 3670 ns/op 1105 B/op 17 allocs/op -BenchmarkGoRestful_Param5 200000 10756 ns/op 2672 B/op 31 allocs/op -BenchmarkGorillaMux_Param5 300000 5543 ns/op 912 B/op 9 allocs/op -BenchmarkHttpRouter_Param5 5000000 403 ns/op 160 B/op 1 allocs/op -BenchmarkHttpTreeMux_Param5 1000000 1089 ns/op 336 B/op 2 allocs/op -BenchmarkKocha_Param5 1000000 1682 ns/op 440 B/op 10 allocs/op -BenchmarkMacaron_Param5 300000 4596 ns/op 1376 B/op 14 allocs/op -BenchmarkMartini_Param5 100000 15703 ns/op 1280 B/op 12 allocs/op -BenchmarkPat_Param5 300000 5320 ns/op 1008 B/op 42 allocs/op -BenchmarkPossum_Param5 1000000 2155 ns/op 624 B/op 7 allocs/op -BenchmarkR2router_Param5 1000000 1559 ns/op 432 B/op 6 allocs/op -BenchmarkRevel_Param5 200000 8184 ns/op 2024 B/op 35 allocs/op -BenchmarkRivet_Param5 1000000 1914 ns/op 528 B/op 9 allocs/op -BenchmarkTango_Param5 1000000 3280 ns/op 944 B/op 18 allocs/op -BenchmarkTigerTonic_Param5 200000 11638 ns/op 2519 B/op 53 allocs/op -BenchmarkTraffic_Param5 200000 8941 ns/op 2280 B/op 31 allocs/op -BenchmarkVulcan_Param5 1000000 1279 ns/op 98 B/op 3 allocs/op -BenchmarkZeus_Param5 1000000 1574 ns/op 416 B/op 3 allocs/op -BenchmarkAce_Param20 1000000 1528 ns/op 640 B/op 1 allocs/op -BenchmarkBear_Param20 300000 4906 ns/op 1633 B/op 5 allocs/op -BenchmarkBeego_Param20 200000 10529 ns/op 3868 B/op 17 allocs/op -BenchmarkBone_Param20 300000 7362 ns/op 2539 B/op 5 allocs/op -BenchmarkDenco_Param20 1000000 1884 ns/op 640 B/op 1 allocs/op -BenchmarkEcho_Param20 2000000 689 ns/op 0 B/op 0 allocs/op -BenchmarkGin_Param20 3000000 545 ns/op 0 B/op 0 allocs/op -BenchmarkGocraftWeb_Param20 200000 9437 ns/op 3804 B/op 16 allocs/op -BenchmarkGoji_Param20 500000 3987 ns/op 1246 B/op 2 allocs/op -BenchmarkGoJsonRest_Param20 100000 12799 ns/op 4492 B/op 21 allocs/op -BenchmarkGoRestful_Param20 100000 19451 ns/op 5244 B/op 33 allocs/op -BenchmarkGorillaMux_Param20 100000 12456 ns/op 3275 B/op 11 allocs/op -BenchmarkHttpRouter_Param20 1000000 1333 ns/op 640 B/op 1 allocs/op -BenchmarkHttpTreeMux_Param20 300000 6490 ns/op 2187 B/op 4 allocs/op -BenchmarkKocha_Param20 300000 5335 ns/op 1808 B/op 27 allocs/op -BenchmarkMacaron_Param20 200000 11325 ns/op 4252 B/op 18 allocs/op -BenchmarkMartini_Param20 20000 64419 ns/op 3644 B/op 14 allocs/op -BenchmarkPat_Param20 50000 24672 ns/op 4888 B/op 151 allocs/op -BenchmarkPossum_Param20 1000000 2085 ns/op 624 B/op 7 allocs/op -BenchmarkR2router_Param20 300000 6809 ns/op 2283 B/op 8 allocs/op -BenchmarkRevel_Param20 100000 16600 ns/op 5551 B/op 54 allocs/op -BenchmarkRivet_Param20 200000 8428 ns/op 2620 B/op 26 allocs/op -BenchmarkTango_Param20 100000 16302 ns/op 8224 B/op 48 allocs/op -BenchmarkTigerTonic_Param20 30000 46828 ns/op 10538 B/op 178 allocs/op -BenchmarkTraffic_Param20 50000 28871 ns/op 7998 B/op 66 allocs/op -BenchmarkVulcan_Param20 1000000 2267 ns/op 98 B/op 3 allocs/op -BenchmarkZeus_Param20 300000 6828 ns/op 2507 B/op 5 allocs/op -BenchmarkAce_ParamWrite 3000000 502 ns/op 40 B/op 2 allocs/op -BenchmarkBear_ParamWrite 1000000 1303 ns/op 424 B/op 5 allocs/op -BenchmarkBeego_ParamWrite 1000000 2489 ns/op 728 B/op 11 allocs/op -BenchmarkBone_ParamWrite 1000000 1181 ns/op 384 B/op 3 allocs/op -BenchmarkDenco_ParamWrite 5000000 315 ns/op 32 B/op 1 allocs/op -BenchmarkEcho_ParamWrite 10000000 237 ns/op 8 B/op 1 allocs/op -BenchmarkGin_ParamWrite 5000000 336 ns/op 0 B/op 0 allocs/op -BenchmarkGocraftWeb_ParamWrite 1000000 2079 ns/op 664 B/op 10 allocs/op -BenchmarkGoji_ParamWrite 1000000 1092 ns/op 336 B/op 2 allocs/op -BenchmarkGoJsonRest_ParamWrite 1000000 3329 ns/op 1136 B/op 19 allocs/op -BenchmarkGoRestful_ParamWrite 200000 9273 ns/op 2504 B/op 32 allocs/op -BenchmarkGorillaMux_ParamWrite 500000 3919 ns/op 792 B/op 10 allocs/op -BenchmarkHttpRouter_ParamWrite 10000000 223 ns/op 32 B/op 1 allocs/op -BenchmarkHttpTreeMux_ParamWrite 2000000 788 ns/op 336 B/op 2 allocs/op -BenchmarkKocha_ParamWrite 3000000 549 ns/op 56 B/op 3 allocs/op -BenchmarkMacaron_ParamWrite 500000 4558 ns/op 1216 B/op 16 allocs/op -BenchmarkMartini_ParamWrite 200000 8850 ns/op 1256 B/op 16 allocs/op -BenchmarkPat_ParamWrite 500000 3679 ns/op 1088 B/op 19 allocs/op -BenchmarkPossum_ParamWrite 1000000 2114 ns/op 624 B/op 7 allocs/op -BenchmarkR2router_ParamWrite 1000000 1320 ns/op 432 B/op 6 allocs/op -BenchmarkRevel_ParamWrite 200000 8048 ns/op 2128 B/op 33 allocs/op -BenchmarkRivet_ParamWrite 1000000 1393 ns/op 472 B/op 6 allocs/op -BenchmarkTango_ParamWrite 2000000 819 ns/op 136 B/op 5 allocs/op -BenchmarkTigerTonic_ParamWrite 300000 5860 ns/op 1440 B/op 25 allocs/op -BenchmarkTraffic_ParamWrite 200000 7429 ns/op 2400 B/op 27 allocs/op -BenchmarkVulcan_ParamWrite 2000000 972 ns/op 98 B/op 3 allocs/op -BenchmarkZeus_ParamWrite 1000000 1226 ns/op 368 B/op 3 allocs/op -BenchmarkAce_GithubStatic 5000000 294 ns/op 0 B/op 0 allocs/op -BenchmarkBear_GithubStatic 3000000 575 ns/op 88 B/op 3 allocs/op -BenchmarkBeego_GithubStatic 1000000 1561 ns/op 368 B/op 7 allocs/op -BenchmarkBone_GithubStatic 200000 12301 ns/op 2880 B/op 60 allocs/op -BenchmarkDenco_GithubStatic 20000000 74.6 ns/op 0 B/op 0 allocs/op -BenchmarkEcho_GithubStatic 10000000 176 ns/op 0 B/op 0 allocs/op -BenchmarkGin_GithubStatic 10000000 159 ns/op 0 B/op 0 allocs/op -BenchmarkGocraftWeb_GithubStatic 1000000 1116 ns/op 304 B/op 6 allocs/op -BenchmarkGoji_GithubStatic 5000000 413 ns/op 0 B/op 0 allocs/op -BenchmarkGoRestful_GithubStatic 30000 55200 ns/op 3520 B/op 36 allocs/op -BenchmarkGoJsonRest_GithubStatic 1000000 1504 ns/op 337 B/op 12 allocs/op -BenchmarkGorillaMux_GithubStatic 100000 23620 ns/op 464 B/op 8 allocs/op -BenchmarkHttpRouter_GithubStatic 20000000 78.3 ns/op 0 B/op 0 allocs/op -BenchmarkHttpTreeMux_GithubStatic 20000000 84.9 ns/op 0 B/op 0 allocs/op -BenchmarkKocha_GithubStatic 20000000 111 ns/op 0 B/op 0 allocs/op -BenchmarkMacaron_GithubStatic 1000000 2686 ns/op 752 B/op 8 allocs/op -BenchmarkMartini_GithubStatic 100000 22244 ns/op 832 B/op 11 allocs/op -BenchmarkPat_GithubStatic 100000 13278 ns/op 3648 B/op 76 allocs/op -BenchmarkPossum_GithubStatic 1000000 1429 ns/op 480 B/op 4 allocs/op -BenchmarkR2router_GithubStatic 2000000 726 ns/op 144 B/op 5 allocs/op -BenchmarkRevel_GithubStatic 300000 6271 ns/op 1288 B/op 25 allocs/op -BenchmarkRivet_GithubStatic 3000000 474 ns/op 112 B/op 2 allocs/op -BenchmarkTango_GithubStatic 1000000 1842 ns/op 256 B/op 10 allocs/op -BenchmarkTigerTonic_GithubStatic 5000000 361 ns/op 48 B/op 1 allocs/op -BenchmarkTraffic_GithubStatic 30000 47197 ns/op 18920 B/op 149 allocs/op -BenchmarkVulcan_GithubStatic 1000000 1415 ns/op 98 B/op 3 allocs/op -BenchmarkZeus_GithubStatic 1000000 2522 ns/op 512 B/op 11 allocs/op -BenchmarkAce_GithubParam 3000000 578 ns/op 96 B/op 1 allocs/op -BenchmarkBear_GithubParam 1000000 1592 ns/op 464 B/op 5 allocs/op -BenchmarkBeego_GithubParam 1000000 2891 ns/op 784 B/op 11 allocs/op -BenchmarkBone_GithubParam 300000 6440 ns/op 1456 B/op 16 allocs/op -BenchmarkDenco_GithubParam 3000000 514 ns/op 128 B/op 1 allocs/op -BenchmarkEcho_GithubParam 5000000 292 ns/op 0 B/op 0 allocs/op -BenchmarkGin_GithubParam 10000000 242 ns/op 0 B/op 0 allocs/op -BenchmarkGocraftWeb_GithubParam 1000000 2343 ns/op 720 B/op 10 allocs/op -BenchmarkGoji_GithubParam 1000000 1566 ns/op 336 B/op 2 allocs/op -BenchmarkGoJsonRest_GithubParam 1000000 2828 ns/op 721 B/op 15 allocs/op -BenchmarkGoRestful_GithubParam 10000 177711 ns/op 2816 B/op 35 allocs/op -BenchmarkGorillaMux_GithubParam 100000 13591 ns/op 816 B/op 9 allocs/op -BenchmarkHttpRouter_GithubParam 5000000 352 ns/op 96 B/op 1 allocs/op -BenchmarkHttpTreeMux_GithubParam 2000000 973 ns/op 336 B/op 2 allocs/op -BenchmarkKocha_GithubParam 2000000 889 ns/op 128 B/op 5 allocs/op -BenchmarkMacaron_GithubParam 500000 4047 ns/op 1168 B/op 12 allocs/op -BenchmarkMartini_GithubParam 50000 28982 ns/op 1184 B/op 12 allocs/op -BenchmarkPat_GithubParam 200000 8747 ns/op 2480 B/op 56 allocs/op -BenchmarkPossum_GithubParam 1000000 2158 ns/op 624 B/op 7 allocs/op -BenchmarkR2router_GithubParam 1000000 1352 ns/op 432 B/op 6 allocs/op -BenchmarkRevel_GithubParam 200000 7673 ns/op 1784 B/op 30 allocs/op -BenchmarkRivet_GithubParam 1000000 1573 ns/op 480 B/op 6 allocs/op -BenchmarkTango_GithubParam 1000000 2418 ns/op 480 B/op 13 allocs/op -BenchmarkTigerTonic_GithubParam 300000 6048 ns/op 1440 B/op 28 allocs/op -BenchmarkTraffic_GithubParam 100000 20143 ns/op 6024 B/op 55 allocs/op -BenchmarkVulcan_GithubParam 1000000 2224 ns/op 98 B/op 3 allocs/op -BenchmarkZeus_GithubParam 500000 4156 ns/op 1312 B/op 12 allocs/op -BenchmarkAce_GithubAll 10000 109482 ns/op 13792 B/op 167 allocs/op -BenchmarkBear_GithubAll 10000 287490 ns/op 79952 B/op 943 allocs/op -BenchmarkBeego_GithubAll 3000 562184 ns/op 146272 B/op 2092 allocs/op -BenchmarkBone_GithubAll 500 2578716 ns/op 648016 B/op 8119 allocs/op -BenchmarkDenco_GithubAll 20000 94955 ns/op 20224 B/op 167 allocs/op -BenchmarkEcho_GithubAll 30000 58705 ns/op 0 B/op 0 allocs/op -BenchmarkGin_GithubAll 30000 50991 ns/op 0 B/op 0 allocs/op -BenchmarkGocraftWeb_GithubAll 5000 449648 ns/op 133280 B/op 1889 allocs/op -BenchmarkGoji_GithubAll 2000 689748 ns/op 56113 B/op 334 allocs/op -BenchmarkGoJsonRest_GithubAll 5000 537769 ns/op 135995 B/op 2940 allocs/op -BenchmarkGoRestful_GithubAll 100 18410628 ns/op 797236 B/op 7725 allocs/op -BenchmarkGorillaMux_GithubAll 200 8036360 ns/op 153137 B/op 1791 allocs/op -BenchmarkHttpRouter_GithubAll 20000 63506 ns/op 13792 B/op 167 allocs/op -BenchmarkHttpTreeMux_GithubAll 10000 165927 ns/op 56112 B/op 334 allocs/op -BenchmarkKocha_GithubAll 10000 171362 ns/op 23304 B/op 843 allocs/op -BenchmarkMacaron_GithubAll 2000 817008 ns/op 224960 B/op 2315 allocs/op -BenchmarkMartini_GithubAll 100 12609209 ns/op 237952 B/op 2686 allocs/op -BenchmarkPat_GithubAll 300 4830398 ns/op 1504101 B/op 32222 allocs/op -BenchmarkPossum_GithubAll 10000 301716 ns/op 97440 B/op 812 allocs/op -BenchmarkR2router_GithubAll 10000 270691 ns/op 77328 B/op 1182 allocs/op -BenchmarkRevel_GithubAll 1000 1491919 ns/op 345553 B/op 5918 allocs/op -BenchmarkRivet_GithubAll 10000 283860 ns/op 84272 B/op 1079 allocs/op -BenchmarkTango_GithubAll 5000 473821 ns/op 87078 B/op 2470 allocs/op -BenchmarkTigerTonic_GithubAll 2000 1120131 ns/op 241088 B/op 6052 allocs/op -BenchmarkTraffic_GithubAll 200 8708979 ns/op 2664762 B/op 22390 allocs/op -BenchmarkVulcan_GithubAll 5000 353392 ns/op 19894 B/op 609 allocs/op -BenchmarkZeus_GithubAll 2000 944234 ns/op 300688 B/op 2648 allocs/op -BenchmarkAce_GPlusStatic 5000000 251 ns/op 0 B/op 0 allocs/op -BenchmarkBear_GPlusStatic 3000000 415 ns/op 72 B/op 3 allocs/op -BenchmarkBeego_GPlusStatic 1000000 1416 ns/op 352 B/op 7 allocs/op -BenchmarkBone_GPlusStatic 10000000 192 ns/op 32 B/op 1 allocs/op -BenchmarkDenco_GPlusStatic 30000000 47.6 ns/op 0 B/op 0 allocs/op -BenchmarkEcho_GPlusStatic 10000000 131 ns/op 0 B/op 0 allocs/op -BenchmarkGin_GPlusStatic 10000000 131 ns/op 0 B/op 0 allocs/op -BenchmarkGocraftWeb_GPlusStatic 1000000 1035 ns/op 288 B/op 6 allocs/op -BenchmarkGoji_GPlusStatic 5000000 304 ns/op 0 B/op 0 allocs/op -BenchmarkGoJsonRest_GPlusStatic 1000000 1286 ns/op 337 B/op 12 allocs/op -BenchmarkGoRestful_GPlusStatic 200000 9649 ns/op 2160 B/op 30 allocs/op -BenchmarkGorillaMux_GPlusStatic 1000000 2346 ns/op 464 B/op 8 allocs/op -BenchmarkHttpRouter_GPlusStatic 30000000 42.7 ns/op 0 B/op 0 allocs/op -BenchmarkHttpTreeMux_GPlusStatic 30000000 49.5 ns/op 0 B/op 0 allocs/op -BenchmarkKocha_GPlusStatic 20000000 74.8 ns/op 0 B/op 0 allocs/op -BenchmarkMacaron_GPlusStatic 1000000 2520 ns/op 736 B/op 8 allocs/op -BenchmarkMartini_GPlusStatic 300000 5310 ns/op 832 B/op 11 allocs/op -BenchmarkPat_GPlusStatic 5000000 398 ns/op 96 B/op 2 allocs/op -BenchmarkPossum_GPlusStatic 1000000 1434 ns/op 480 B/op 4 allocs/op -BenchmarkR2router_GPlusStatic 2000000 646 ns/op 144 B/op 5 allocs/op -BenchmarkRevel_GPlusStatic 300000 6172 ns/op 1272 B/op 25 allocs/op -BenchmarkRivet_GPlusStatic 3000000 444 ns/op 112 B/op 2 allocs/op -BenchmarkTango_GPlusStatic 1000000 1400 ns/op 208 B/op 10 allocs/op -BenchmarkTigerTonic_GPlusStatic 10000000 213 ns/op 32 B/op 1 allocs/op -BenchmarkTraffic_GPlusStatic 1000000 3091 ns/op 1208 B/op 16 allocs/op -BenchmarkVulcan_GPlusStatic 2000000 863 ns/op 98 B/op 3 allocs/op -BenchmarkZeus_GPlusStatic 10000000 237 ns/op 16 B/op 1 allocs/op -BenchmarkAce_GPlusParam 3000000 435 ns/op 64 B/op 1 allocs/op -BenchmarkBear_GPlusParam 1000000 1205 ns/op 448 B/op 5 allocs/op -BenchmarkBeego_GPlusParam 1000000 2494 ns/op 720 B/op 10 allocs/op -BenchmarkBone_GPlusParam 1000000 1126 ns/op 384 B/op 3 allocs/op -BenchmarkDenco_GPlusParam 5000000 325 ns/op 64 B/op 1 allocs/op -BenchmarkEcho_GPlusParam 10000000 168 ns/op 0 B/op 0 allocs/op -BenchmarkGin_GPlusParam 10000000 170 ns/op 0 B/op 0 allocs/op -BenchmarkGocraftWeb_GPlusParam 1000000 1895 ns/op 656 B/op 9 allocs/op -BenchmarkGoji_GPlusParam 1000000 1071 ns/op 336 B/op 2 allocs/op -BenchmarkGoJsonRest_GPlusParam 1000000 2282 ns/op 657 B/op 14 allocs/op -BenchmarkGoRestful_GPlusParam 100000 19400 ns/op 2560 B/op 33 allocs/op -BenchmarkGorillaMux_GPlusParam 500000 5001 ns/op 784 B/op 9 allocs/op -BenchmarkHttpRouter_GPlusParam 10000000 240 ns/op 64 B/op 1 allocs/op -BenchmarkHttpTreeMux_GPlusParam 2000000 797 ns/op 336 B/op 2 allocs/op -BenchmarkKocha_GPlusParam 3000000 505 ns/op 56 B/op 3 allocs/op -BenchmarkMacaron_GPlusParam 1000000 3668 ns/op 1104 B/op 11 allocs/op -BenchmarkMartini_GPlusParam 200000 10672 ns/op 1152 B/op 12 allocs/op -BenchmarkPat_GPlusParam 1000000 2376 ns/op 704 B/op 14 allocs/op -BenchmarkPossum_GPlusParam 1000000 2090 ns/op 624 B/op 7 allocs/op -BenchmarkR2router_GPlusParam 1000000 1233 ns/op 432 B/op 6 allocs/op -BenchmarkRevel_GPlusParam 200000 6778 ns/op 1704 B/op 28 allocs/op -BenchmarkRivet_GPlusParam 1000000 1279 ns/op 464 B/op 5 allocs/op -BenchmarkTango_GPlusParam 1000000 1981 ns/op 272 B/op 10 allocs/op -BenchmarkTigerTonic_GPlusParam 500000 3893 ns/op 1064 B/op 19 allocs/op -BenchmarkTraffic_GPlusParam 200000 6585 ns/op 2000 B/op 23 allocs/op -BenchmarkVulcan_GPlusParam 1000000 1233 ns/op 98 B/op 3 allocs/op -BenchmarkZeus_GPlusParam 1000000 1350 ns/op 368 B/op 3 allocs/op -BenchmarkAce_GPlus2Params 3000000 512 ns/op 64 B/op 1 allocs/op -BenchmarkBear_GPlus2Params 1000000 1564 ns/op 464 B/op 5 allocs/op -BenchmarkBeego_GPlus2Params 1000000 3043 ns/op 784 B/op 11 allocs/op -BenchmarkBone_GPlus2Params 1000000 3152 ns/op 736 B/op 7 allocs/op -BenchmarkDenco_GPlus2Params 3000000 431 ns/op 64 B/op 1 allocs/op -BenchmarkEcho_GPlus2Params 5000000 247 ns/op 0 B/op 0 allocs/op -BenchmarkGin_GPlus2Params 10000000 219 ns/op 0 B/op 0 allocs/op -BenchmarkGocraftWeb_GPlus2Params 1000000 2363 ns/op 720 B/op 10 allocs/op -BenchmarkGoji_GPlus2Params 1000000 1540 ns/op 336 B/op 2 allocs/op -BenchmarkGoJsonRest_GPlus2Params 1000000 2872 ns/op 721 B/op 15 allocs/op -BenchmarkGoRestful_GPlus2Params 100000 23030 ns/op 2720 B/op 35 allocs/op -BenchmarkGorillaMux_GPlus2Params 200000 10516 ns/op 816 B/op 9 allocs/op -BenchmarkHttpRouter_GPlus2Params 5000000 273 ns/op 64 B/op 1 allocs/op -BenchmarkHttpTreeMux_GPlus2Params 2000000 939 ns/op 336 B/op 2 allocs/op -BenchmarkKocha_GPlus2Params 2000000 844 ns/op 128 B/op 5 allocs/op -BenchmarkMacaron_GPlus2Params 500000 3914 ns/op 1168 B/op 12 allocs/op -BenchmarkMartini_GPlus2Params 50000 35759 ns/op 1280 B/op 16 allocs/op -BenchmarkPat_GPlus2Params 200000 7089 ns/op 2304 B/op 41 allocs/op -BenchmarkPossum_GPlus2Params 1000000 2093 ns/op 624 B/op 7 allocs/op -BenchmarkR2router_GPlus2Params 1000000 1320 ns/op 432 B/op 6 allocs/op -BenchmarkRevel_GPlus2Params 200000 7351 ns/op 1800 B/op 30 allocs/op -BenchmarkRivet_GPlus2Params 1000000 1485 ns/op 480 B/op 6 allocs/op -BenchmarkTango_GPlus2Params 1000000 2111 ns/op 448 B/op 12 allocs/op -BenchmarkTigerTonic_GPlus2Params 300000 6271 ns/op 1528 B/op 28 allocs/op -BenchmarkTraffic_GPlus2Params 100000 14886 ns/op 3312 B/op 34 allocs/op -BenchmarkVulcan_GPlus2Params 1000000 1883 ns/op 98 B/op 3 allocs/op -BenchmarkZeus_GPlus2Params 1000000 2686 ns/op 784 B/op 6 allocs/op -BenchmarkAce_GPlusAll 300000 5912 ns/op 640 B/op 11 allocs/op -BenchmarkBear_GPlusAll 100000 16448 ns/op 5072 B/op 61 allocs/op -BenchmarkBeego_GPlusAll 50000 32916 ns/op 8976 B/op 129 allocs/op -BenchmarkBone_GPlusAll 50000 25836 ns/op 6992 B/op 76 allocs/op -BenchmarkDenco_GPlusAll 500000 4462 ns/op 672 B/op 11 allocs/op -BenchmarkEcho_GPlusAll 500000 2806 ns/op 0 B/op 0 allocs/op -BenchmarkGin_GPlusAll 500000 2579 ns/op 0 B/op 0 allocs/op -BenchmarkGocraftWeb_GPlusAll 50000 25223 ns/op 8144 B/op 116 allocs/op -BenchmarkGoji_GPlusAll 100000 14237 ns/op 3696 B/op 22 allocs/op -BenchmarkGoJsonRest_GPlusAll 50000 29227 ns/op 8221 B/op 183 allocs/op -BenchmarkGoRestful_GPlusAll 10000 203144 ns/op 36064 B/op 441 allocs/op -BenchmarkGorillaMux_GPlusAll 20000 80906 ns/op 9712 B/op 115 allocs/op -BenchmarkHttpRouter_GPlusAll 500000 3040 ns/op 640 B/op 11 allocs/op -BenchmarkHttpTreeMux_GPlusAll 200000 9627 ns/op 3696 B/op 22 allocs/op -BenchmarkKocha_GPlusAll 200000 8108 ns/op 976 B/op 43 allocs/op -BenchmarkMacaron_GPlusAll 30000 48083 ns/op 13968 B/op 142 allocs/op -BenchmarkMartini_GPlusAll 10000 196978 ns/op 15072 B/op 178 allocs/op -BenchmarkPat_GPlusAll 30000 58865 ns/op 16880 B/op 343 allocs/op -BenchmarkPossum_GPlusAll 100000 19685 ns/op 6240 B/op 52 allocs/op -BenchmarkR2router_GPlusAll 100000 16251 ns/op 5040 B/op 76 allocs/op -BenchmarkRevel_GPlusAll 20000 93489 ns/op 21656 B/op 368 allocs/op -BenchmarkRivet_GPlusAll 100000 16907 ns/op 5408 B/op 64 allocs/op +Gin: 30512 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 +``` + +## GithubAPI Routes: 203 + +``` +Gin: 52672 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 +``` + +## GPlusAPI Routes: 13 + +``` +Gin: 3968 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 +``` + +## ParseAPI Routes: 26 + +``` +Gin: 6928 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 +``` + +## Static Routes + +``` +BenchmarkGin_StaticAll 50000 34506 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 +``` + +## Micro Benchmarks + +``` +BenchmarkGin_Param 20000000 113 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 +``` + +## GitHub + +``` +BenchmarkGin_GithubStatic 10000000 156 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 +``` + +## Google+ + +``` +BenchmarkGin_GPlusStatic 10000000 183 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 +``` + +## Parse.com + +``` +BenchmarkGin_ParseStatic 10000000 133 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 ``` diff --git a/Makefile b/Makefile index 9ba475a4..5468563a 100644 --- a/Makefile +++ b/Makefile @@ -2,14 +2,14 @@ GOFMT ?= gofmt "-s" PACKAGES ?= $(shell go list ./... | grep -v /vendor/) GOFILES := $(shell find . -name "*.go" -type f -not -path "./vendor/*") -all: build +all: install install: deps govendor sync .PHONY: test test: - go test -v -covermode=count -coverprofile=coverage.out + sh coverage.sh .PHONY: fmt fmt: diff --git a/README.md b/README.md index 05b696df..7dd7f734 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ [](https://goreportcard.com/report/github.com/gin-gonic/gin) [](https://godoc.org/github.com/gin-gonic/gin) [](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[](https://www.codetriage.com/gin-gonic/gin) Gin is a web framework written in Go (Golang). It features a martini-like API with much better performance, up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin. @@ -40,45 +41,44 @@ $ go run example.go ## Benchmarks -Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httprouter) +Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httprouter) [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) ---------------------------------|----------:|----------:|----------:|------: -BenchmarkAce_GithubAll | 10000 | 109482 | 13792 | 167 -BenchmarkBear_GithubAll | 10000 | 287490 | 79952 | 943 -BenchmarkBeego_GithubAll | 3000 | 562184 | 146272 | 2092 -BenchmarkBone_GithubAll | 500 | 2578716 | 648016 | 8119 -BenchmarkDenco_GithubAll | 20000 | 94955 | 20224 | 167 -BenchmarkEcho_GithubAll | 30000 | 58705 | 0 | 0 -**BenchmarkGin_GithubAll** | **30000** | **50991** | **0** | **0** -BenchmarkGocraftWeb_GithubAll | 5000 | 449648 | 133280 | 1889 -BenchmarkGoji_GithubAll | 2000 | 689748 | 56113 | 334 -BenchmarkGoJsonRest_GithubAll | 5000 | 537769 | 135995 | 2940 -BenchmarkGoRestful_GithubAll | 100 | 18410628 | 797236 | 7725 -BenchmarkGorillaMux_GithubAll | 200 | 8036360 | 153137 | 1791 -BenchmarkHttpRouter_GithubAll | 20000 | 63506 | 13792 | 167 -BenchmarkHttpTreeMux_GithubAll | 10000 | 165927 | 56112 | 334 -BenchmarkKocha_GithubAll | 10000 | 171362 | 23304 | 843 -BenchmarkMacaron_GithubAll | 2000 | 817008 | 224960 | 2315 -BenchmarkMartini_GithubAll | 100 | 12609209 | 237952 | 2686 -BenchmarkPat_GithubAll | 300 | 4830398 | 1504101 | 32222 -BenchmarkPossum_GithubAll | 10000 | 301716 | 97440 | 812 -BenchmarkR2router_GithubAll | 10000 | 270691 | 77328 | 1182 -BenchmarkRevel_GithubAll | 1000 | 1491919 | 345553 | 5918 -BenchmarkRivet_GithubAll | 10000 | 283860 | 84272 | 1079 -BenchmarkTango_GithubAll | 5000 | 473821 | 87078 | 2470 -BenchmarkTigerTonic_GithubAll | 2000 | 1120131 | 241088 | 6052 -BenchmarkTraffic_GithubAll | 200 | 8708979 | 2664762 | 22390 -BenchmarkVulcan_GithubAll | 5000 | 353392 | 19894 | 609 -BenchmarkZeus_GithubAll | 2000 | 944234 | 300688 | 2648 - -(1): Total Repetitions -(2): Single Repetition Duration (ns/op) -(3): Heap Memory (B/op) -(4): Average Allocations per Repetition (allocs/op) +- (1): Total Repetitions achieved in constant time, higher means more confident result +- (2): Single Repetition Duration (ns/op), lower is better +- (3): Heap Memory (B/op), lower is better +- (4): Average Allocations per Repetition (allocs/op), lower is better ## Gin v1. stable @@ -88,7 +88,6 @@ BenchmarkZeus_GithubAll | 2000 | 944234 | 300688 | 2648 - [x] Battle tested - [x] API frozen, new releases will not break your code. - ## Start using it 1. Download and install it: @@ -119,7 +118,7 @@ $ go get github.com/kardianos/govendor 2. Create your project folder and `cd` inside ```sh -$ mkdir -p ~/go/src/github.com/myusername/project && cd "$_" +$ mkdir -p $GOPATH/src/github.com/myusername/project && cd "$_" ``` 3. Vendor init your project and add gin @@ -141,6 +140,14 @@ $ curl https://raw.githubusercontent.com/gin-gonic/gin/master/examples/basic/mai $ go run main.go ``` +## Build with [jsoniter](https://github.com/json-iterator/go) + +Gin use `encoding/json` as default json package but you can change to [jsoniter](https://github.com/json-iterator/go) by build from other tags. + +```sh +$ go build -tags=jsoniter . +``` + ## API Examples ### Using GET, POST, PUT, PATCH, DELETE and OPTIONS @@ -271,12 +278,17 @@ References issue [#774](https://github.com/gin-gonic/gin/issues/774) and detail ```go func main() { router := gin.Default() + // Set a lower memory limit for multipart forms (default is 32 MiB) + // router.MaxMultipartMemory = 8 << 20 // 8 MiB router.POST("/upload", func(c *gin.Context) { // single file file, _ := c.FormFile("file") log.Println(file.Filename) - c.String(http.StatusOK, fmt.Printf("'%s' uploaded!", file.Filename)) + // Upload the file to specific dst. + // c.SaveUploadedFile(file, dst) + + c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename)) }) router.Run(":8080") } @@ -297,6 +309,8 @@ See the detail [example code](examples/upload-file/multiple). ```go func main() { router := gin.Default() + // Set a lower memory limit for multipart forms (default is 32 MiB) + // router.MaxMultipartMemory = 8 << 20 // 8 MiB router.POST("/upload", func(c *gin.Context) { // Multipart form form, _ := c.MultipartForm() @@ -304,8 +318,11 @@ func main() { for _, file := range files { log.Println(file.Filename) + + // Upload the file to specific dst. + // c.SaveUploadedFile(file, dst) } - c.String(http.StatusOK, fmt.Printf("%d files uploaded!", len(files))) + c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files))) }) router.Run(":8080") } @@ -357,6 +374,7 @@ r := gin.New() instead of ```go +// Default With the Logger and Recovery middleware already attached r := gin.Default() ``` @@ -368,7 +386,11 @@ func main() { r := gin.New() // Global middleware + // Logger middleware will write the logs to gin.DefaultWriter even if you set with GIN_MODE=release. + // By default gin.DefaultWriter = os.Stdout r.Use(gin.Logger()) + + // Recovery middleware recovers from any panics and writes a 500 if there was one. r.Use(gin.Recovery()) // Per route middleware, you can add as many as you desire. @@ -396,15 +418,47 @@ func main() { } ``` +### How to write log file +```go +func main() { + // Disable Console Color, you don't need console color when writing the logs to file. + gin.DisableConsoleColor() + + // Logging to a file. + f, _ := os.Create("gin.log") + gin.DefaultWriter = io.MultiWriter(f) + + // Use the following code if you need to write the logs to file and console at the same time. + // gin.DefaultWriter = io.MultiWriter(f, os.Stdout) + + router := gin.Default() + router.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + + Â Â router.Run(":8080") +} +``` + ### Model binding and validation To bind a request body into a type, use model binding. We currently support binding of JSON, XML and standard form values (foo=bar&boo=baz). +Gin uses [**go-playground/validator.v8**](https://github.com/go-playground/validator) for validation. Check the full docs on tags usage [here](http://godoc.org/gopkg.in/go-playground/validator.v8#hdr-Baked_In_Validators_and_Tags). + Note that you need to set the corresponding binding tag on all fields you want to bind. For example, when binding from JSON, set `json:"fieldname"`. -When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use BindWith. +Also, Gin provides two sets of methods for binding: +- **Type** - Must bind + - **Methods** - `Bind`, `BindJSON`, `BindQuery` + - **Behavior** - These methods use `MustBindWith` under the hood. If there is a binding error, the request is aborted with `c.AbortWithError(400, err).SetType(ErrorTypeBind)`. This sets the response status code to 400 and the `Content-Type` header is set to `text/plain; charset=utf-8`. Note that if you try to set the response code after this, it will result in a warning `[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422`. If you wish to have greater control over the behavior, consider using the `ShouldBind` equivalent method. +- **Type** - Should bind + - **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindQuery` + - **Behavior** - These methods use `ShouldBindWith` under the hood. If there is a binding error, the error is returned and it is the developer's responsibility to handle the request and error appropriately. -You can also specify that specific fields are required. If a field is decorated with `binding:"required"` and has a empty value when binding, the current request will fail with an error. +When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use `MustBindWith` or `ShouldBindWith`. + +You can also specify that specific fields are required. If a field is decorated with `binding:"required"` and has a empty value when binding, an error will be returned. ```go // Binding from JSON @@ -419,12 +473,14 @@ func main() { // Example for binding JSON ({"user": "manu", "password": "123"}) router.POST("/loginJSON", func(c *gin.Context) { var json Login - if c.BindJSON(&json) == nil { + if err := c.ShouldBindJSON(&json); err == nil { if json.User == "manu" && json.Password == "123" { c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) } else { c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) } + } else { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) } }) @@ -432,12 +488,14 @@ func main() { router.POST("/loginForm", func(c *gin.Context) { var form Login // This will infer what binder to use depending on the content-type header. - if c.Bind(&form) == nil { + if err := c.ShouldBind(&form); err == nil { if form.User == "manu" && form.Password == "123" { c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) } else { c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) } + } else { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) } }) @@ -446,7 +504,133 @@ func main() { } ``` -### Bind Query String +**Sample request** +```shell +$ curl -v -X POST \ + http://localhost:8080/loginJSON \ + -H 'content-type: application/json' \ + -d '{ "user": "manu" }' +> POST /loginJSON HTTP/1.1 +> Host: localhost:8080 +> User-Agent: curl/7.51.0 +> Accept: */* +> content-type: application/json +> Content-Length: 18 +> +* upload completely sent off: 18 out of 18 bytes +< HTTP/1.1 400 Bad Request +< Content-Type: application/json; charset=utf-8 +< Date: Fri, 04 Aug 2017 03:51:31 GMT +< Content-Length: 100 +< +{"error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'required' tag"} +``` + +### Custom Validators + +It is also possible to register custom validators. See the [example code](examples/custom-validation/server.go). + +[embedmd]:# (examples/custom-validation/server.go go) +```go +package main + +import ( + "net/http" + "reflect" + "time" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "gopkg.in/go-playground/validator.v8" +) + +type Booking struct { + CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"` + CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"` +} + +func bookableDate( + v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value, + field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string, +) bool { + if date, ok := field.Interface().(time.Time); ok { + today := time.Now() + if today.Year() > date.Year() || today.YearDay() > date.YearDay() { + return false + } + } + return true +} + +func main() { + route := gin.Default() + + if v, ok := binding.Validator.Engine().(*validator.Validate); ok { + v.RegisterValidation("bookabledate", bookableDate) + } + + route.GET("/bookable", getBookable) + route.Run(":8085") +} + +func getBookable(c *gin.Context) { + var b Booking + if err := c.ShouldBindWith(&b, binding.Query); err == nil { + c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"}) + } else { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } +} +``` + +```console +$ curl "localhost:8085/bookable?check_in=2018-04-16&check_out=2018-04-17" +{"message":"Booking dates are valid!"} + +$ curl "localhost:8085/bookable?check_in=2018-03-08&check_out=2018-03-09" +{"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag"} +``` + +[Struct level validations](https://github.com/go-playground/validator/releases/tag/v8.7) can also be registed this way. +See the [struct-lvl-validation example](examples/struct-lvl-validations) to learn more. + +### Only Bind Query String + +`ShouldBindQuery` function only binds the query params and not the post data. See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-315953017). + +```go +package main + +import ( + "log" + + "github.com/gin-gonic/gin" +) + +type Person struct { + Name string `form:"name"` + Address string `form:"address"` +} + +func main() { + route := gin.Default() + route.Any("/testing", startPage) + route.Run(":8085") +} + +func startPage(c *gin.Context) { + var person Person + if c.ShouldBindQuery(&person) == nil { + log.Println("====== Only Bind By Query String ======") + log.Println(person.Name) + log.Println(person.Address) + } + c.String(200, "Success") +} + +``` + +### Bind Query String or Post Data See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-264681292). @@ -455,10 +639,12 @@ package main import "log" import "github.com/gin-gonic/gin" +import "time" type Person struct { - Name string `form:"name"` - Address string `form:"address"` + Name string `form:"name"` + Address string `form:"address"` + Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"` } func main() { @@ -472,15 +658,21 @@ func startPage(c *gin.Context) { // If `GET`, only `Form` binding engine (`query`) used. // If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`). // See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48 - if c.Bind(&person) == nil { + if c.ShouldBind(&person) == nil { log.Println(person.Name) log.Println(person.Address) + log.Println(person.Birthday) } c.String(200, "Success") } ``` +Test it with: +```sh +$ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15" +``` + ### Bind HTML checkboxes See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092) @@ -498,7 +690,7 @@ type myForm struct { func formHandler(c *gin.Context) { var fakeForm myForm - c.Bind(&fakeForm) + c.ShouldBind(&fakeForm) c.JSON(200, gin.H{"color": fakeForm.Colors}) } @@ -545,11 +737,11 @@ func main() { router := gin.Default() router.POST("/login", func(c *gin.Context) { // you can bind multipart form with explicit binding declaration: - // c.MustBindWith(&form, binding.Form) - // or you can simply use autobinding with Bind method: + // c.ShouldBindWith(&form, binding.Form) + // or you can simply use autobinding with ShouldBind method: var form LoginForm // in this case proper binding will be automatically selected - if c.Bind(&form) == nil { + if c.ShouldBind(&form) == nil { if form.User == "user" && form.Password == "password" { c.JSON(200, gin.H{"status": "you are logged in"}) } else { @@ -626,7 +818,7 @@ func main() { // Listen and serve on 0.0.0.0:8080 r.Run(":8080") } -``` +``` ### Serving static files @@ -737,35 +929,46 @@ You may use custom delims r := gin.Default() r.Delims("{[{", "}]}") r.LoadHTMLGlob("/path/to/templates")) -``` +``` #### Custom Template Funcs +See the detail [example code](examples/template). + main.go ```go - ... - - func formatAsDate(t time.Time) string { - year, month, day := t.Date() - return fmt.Sprintf("%d/%02d/%02d", year, month, day) - } - - ... - - router.SetFuncMap(template.FuncMap{ - "formatAsDate": formatAsDate, - }) - - ... - - router.GET("/raw", func(c *Context) { - c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{ - "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), - }) - }) - - ... +import ( + "fmt" + "html/template" + "net/http" + "time" + + "github.com/gin-gonic/gin" +) + +func formatAsDate(t time.Time) string { + year, month, day := t.Date() + return fmt.Sprintf("%d%02d/%02d", year, month, day) +} + +func main() { + router := gin.Default() + router.Delims("{[{", "}]}") + router.SetFuncMap(template.FuncMap{ + "formatAsDate": formatAsDate, + }) + router.LoadHTMLFiles("./fixtures/basic/raw.tmpl") + + router.GET("/raw", func(c *gin.Context) { + c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{ + "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), + }) + }) + + router.Run(":8080") +} + ``` raw.tmpl @@ -876,7 +1079,7 @@ func main() { ### Goroutines inside a middleware -When starting inside a middleware or handler, you **SHOULD NOT** use the original context inside it, you have to use a read-only copy. +When starting new Goroutines inside a middleware or handler, you **SHOULD NOT** use the original context inside it, you have to use a read-only copy. ```go func main() { @@ -938,7 +1141,7 @@ func main() { example for 1-line LetsEncrypt HTTPS servers. -[embedmd]:# (examples/auto-tls/example1.go go) +[embedmd]:# (examples/auto-tls/example1/main.go go) ```go package main @@ -963,7 +1166,7 @@ func main() { example for custom autocert manager. -[embedmd]:# (examples/auto-tls/example2.go go) +[embedmd]:# (examples/auto-tls/example2/main.go go) ```go package main @@ -993,6 +1196,88 @@ func main() { } ``` +### Run multiple service using Gin + +See the [question](https://github.com/gin-gonic/gin/issues/346) and try the following example: + +[embedmd]:# (examples/multiple-service/main.go go) +```go +package main + +import ( + "log" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "golang.org/x/sync/errgroup" +) + +var ( + g errgroup.Group +) + +func router01() http.Handler { + e := gin.New() + e.Use(gin.Recovery()) + e.GET("/", func(c *gin.Context) { + c.JSON( + http.StatusOK, + gin.H{ + "code": http.StatusOK, + "error": "Welcome server 01", + }, + ) + }) + + return e +} + +func router02() http.Handler { + e := gin.New() + e.Use(gin.Recovery()) + e.GET("/", func(c *gin.Context) { + c.JSON( + http.StatusOK, + gin.H{ + "code": http.StatusOK, + "error": "Welcome server 02", + }, + ) + }) + + return e +} + +func main() { + server01 := &http.Server{ + Addr: ":8080", + Handler: router01(), + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + } + + server02 := &http.Server{ + Addr: ":8081", + Handler: router02(), + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + } + + g.Go(func() error { + return server01.ListenAndServe() + }) + + g.Go(func() error { + return server02.ListenAndServe() + }) + + if err := g.Wait(); err != nil { + log.Fatal(err) + } +} +``` + ### Graceful restart or stop Do you want to graceful restart or stop your web server? @@ -1046,8 +1331,8 @@ func main() { go func() { // service connections - if err := srv.ListenAndServe(); err != nil { - log.Printf("listen: %s\n", err) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("listen: %s\n", err) } }() @@ -1063,7 +1348,53 @@ func main() { if err := srv.Shutdown(ctx); err != nil { log.Fatal("Server Shutdown:", err) } - log.Println("Server exist") + log.Println("Server exiting") +} +``` + +## Testing + +The `net/http/httptest` package is preferable way for HTTP testing. + +```go +package main + +func setupRouter() *gin.Engine { + r := gin.Default() + r.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + return r +} + +func main() { + r := setupRouter() + r.Run(":8080") +} +``` + +Test for code example above: + +```go +package main + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPingRoute(t *testing.T) { + router := setupRouter() + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/ping", nil) + router.ServeHTTP(w, req) + + assert.Equal(t, 200, w.Code) + assert.Equal(t, "pong", w.Body.String()) } ``` diff --git a/auth.go b/auth.go index 7ebf5576..c2143091 100644 --- a/auth.go +++ b/auth.go @@ -10,26 +10,26 @@ import ( "strconv" ) -// AuthUserKey is the cookie name for user credential in basic auth +// AuthUserKey is the cookie name for user credential in basic auth. const AuthUserKey = "user" -// Accounts defines a key/value for user/pass list of authorized logins +// Accounts defines a key/value for user/pass list of authorized logins. type Accounts map[string]string type authPair struct { - Value string - User string + value string + user string } type authPairs []authPair func (a authPairs) searchCredential(authValue string) (string, bool) { - if len(authValue) == 0 { + if authValue == "" { return "", false } for _, pair := range a { - if pair.Value == authValue { - return pair.User, true + if pair.value == authValue { + return pair.user, true } } return "", false @@ -47,16 +47,17 @@ func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc { pairs := processAccounts(accounts) return func(c *Context) { // Search user in the slice of allowed credentials - user, found := pairs.searchCredential(c.Request.Header.Get("Authorization")) + user, found := pairs.searchCredential(c.requestHeader("Authorization")) if !found { // Credentials doesn't match, we return 401 and abort handlers chain. c.Header("WWW-Authenticate", realm) c.AbortWithStatus(401) - } else { - // The user credentials was found, set user's id to key AuthUserKey in this context, the userId can be read later using - // c.MustGet(gin.AuthUserKey) - c.Set(AuthUserKey, user) + return } + + // The user credentials was found, set user's id to key AuthUserKey in this context, the user's id can be read later using + // c.MustGet(gin.AuthUserKey). + c.Set(AuthUserKey, user) } } @@ -70,11 +71,11 @@ func processAccounts(accounts Accounts) authPairs { assert1(len(accounts) > 0, "Empty list of authorized credentials") pairs := make(authPairs, 0, len(accounts)) for user, password := range accounts { - assert1(len(user) > 0, "User can not be empty") + assert1(user != "", "User can not be empty") value := authorizationHeader(user, password) pairs = append(pairs, authPair{ - Value: value, - User: user, + value: value, + user: user, }) } return pairs @@ -89,6 +90,6 @@ func secureCompare(given, actual string) bool { if subtle.ConstantTimeEq(int32(len(given)), int32(len(actual))) == 1 { return subtle.ConstantTimeCompare([]byte(given), []byte(actual)) == 1 } - // Securely compare actual to itself to keep constant time, but always return false + // Securely compare actual to itself to keep constant time, but always return false. return subtle.ConstantTimeCompare([]byte(actual), []byte(actual)) == 1 && false } diff --git a/auth_test.go b/auth_test.go index b22d9ced..dc8523b0 100644 --- a/auth_test.go +++ b/auth_test.go @@ -22,16 +22,16 @@ func TestBasicAuth(t *testing.T) { assert.Len(t, pairs, 3) assert.Contains(t, pairs, authPair{ - User: "bar", - Value: "Basic YmFyOmZvbw==", + user: "bar", + value: "Basic YmFyOmZvbw==", }) assert.Contains(t, pairs, authPair{ - User: "foo", - Value: "Basic Zm9vOmJhcg==", + user: "foo", + value: "Basic Zm9vOmJhcg==", }) assert.Contains(t, pairs, authPair{ - User: "admin", - Value: "Basic YWRtaW46cGFzc3dvcmQ=", + user: "admin", + value: "Basic YWRtaW46cGFzc3dvcmQ=", }) } @@ -53,15 +53,15 @@ func TestBasicAuthSearchCredential(t *testing.T) { }) user, found := pairs.searchCredential(authorizationHeader("admin", "password")) - assert.Equal(t, user, "admin") + assert.Equal(t, "admin", user) assert.True(t, found) user, found = pairs.searchCredential(authorizationHeader("foo", "bar")) - assert.Equal(t, user, "foo") + assert.Equal(t, "foo", user) assert.True(t, found) user, found = pairs.searchCredential(authorizationHeader("bar", "foo")) - assert.Equal(t, user, "bar") + assert.Equal(t, "bar", user) assert.True(t, found) user, found = pairs.searchCredential(authorizationHeader("admins", "password")) @@ -78,7 +78,7 @@ func TestBasicAuthSearchCredential(t *testing.T) { } func TestBasicAuthAuthorizationHeader(t *testing.T) { - assert.Equal(t, authorizationHeader("admin", "password"), "Basic YWRtaW46cGFzc3dvcmQ=") + assert.Equal(t, "Basic YWRtaW46cGFzc3dvcmQ=", authorizationHeader("admin", "password")) } func TestBasicAuthSecureCompare(t *testing.T) { @@ -101,8 +101,8 @@ func TestBasicAuthSucceed(t *testing.T) { req.Header.Set("Authorization", authorizationHeader("admin", "password")) router.ServeHTTP(w, req) - assert.Equal(t, w.Code, 200) - assert.Equal(t, w.Body.String(), "admin") + assert.Equal(t, 200, w.Code) + assert.Equal(t, "admin", w.Body.String()) } func TestBasicAuth401(t *testing.T) { @@ -121,8 +121,8 @@ func TestBasicAuth401(t *testing.T) { router.ServeHTTP(w, req) assert.False(t, called) - assert.Equal(t, w.Code, 401) - assert.Equal(t, w.HeaderMap.Get("WWW-Authenticate"), "Basic realm=\"Authorization Required\"") + assert.Equal(t, 401, w.Code) + assert.Equal(t, "Basic realm=\"Authorization Required\"", w.HeaderMap.Get("WWW-Authenticate")) } func TestBasicAuth401WithCustomRealm(t *testing.T) { @@ -141,6 +141,6 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) { router.ServeHTTP(w, req) assert.False(t, called) - assert.Equal(t, w.Code, 401) - assert.Equal(t, w.HeaderMap.Get("WWW-Authenticate"), "Basic realm=\"My Custom \\\"Realm\\\"\"") + assert.Equal(t, 401, w.Code) + assert.Equal(t, "Basic realm=\"My Custom \\\"Realm\\\"\"", w.HeaderMap.Get("WWW-Authenticate")) } diff --git a/benchmarks_test.go b/benchmarks_test.go index a2c62ba3..e7970034 100644 --- a/benchmarks_test.go +++ b/benchmarks_test.go @@ -59,8 +59,6 @@ func BenchmarkOneRouteJSON(B *testing.B) { runRequest(B, router, "GET", "/json") } -var htmlContentType = []string{"text/html; charset=utf-8"} - func BenchmarkOneRouteHTML(B *testing.B) { router := New() t := template.Must(template.New("index").Parse(` diff --git a/binding/binding.go b/binding/binding.go index 1dbf2460..646eb80a 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -4,7 +4,9 @@ package binding -import "net/http" +import ( + "net/http" +) const ( MIMEJSON = "application/json" @@ -19,11 +21,18 @@ const ( MIMEMSGPACK2 = "application/msgpack" ) +// 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 } +// StructValidator is the minimal interface which needs to be implemented in +// order for it to be used as the validator engine for ensuring the correctness +// of the reqest. Gin provides a default implementation for this using +// 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. @@ -31,20 +40,32 @@ type StructValidator interface { // 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{} MsgPack = msgpackBinding{} ) +// 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 diff --git a/binding/binding_test.go b/binding/binding_test.go index d7cdf77a..0c0f9f81 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -6,9 +6,13 @@ package binding import ( "bytes" + "encoding/json" + "errors" + "io/ioutil" "mime/multipart" "net/http" "testing" + "time" "github.com/gin-gonic/gin/binding/example" "github.com/golang/protobuf/proto" @@ -25,6 +29,116 @@ type FooBarStruct struct { Bar string `msgpack:"bar" json:"bar" form:"bar" xml:"bar" binding:"required"` } +type FooStructUseNumber struct { + Foo interface{} `json:"foo" binding:"required"` +} + +type FooBarStructForTimeType struct { + TimeFoo time.Time `form:"time_foo" time_format:"2006-01-02" time_utc:"1" time_location:"Asia/Chongqing"` + TimeBar time.Time `form:"time_bar" time_format:"2006-01-02" time_utc:"1"` +} + +type FooStructForTimeTypeNotFormat struct { + TimeFoo time.Time `form:"time_foo"` +} + +type FooStructForTimeTypeFailFormat struct { + TimeFoo time.Time `form:"time_foo" time_format:"2017-11-15"` +} + +type FooStructForTimeTypeFailLocation struct { + TimeFoo time.Time `form:"time_foo" time_format:"2006-01-02" time_location:"/asia/chongqing"` +} + +type FooStructForMapType struct { + // Unknown type: not support map + MapFoo map[string]interface{} `form:"map_foo"` +} + +type InvalidNameType struct { + TestName string `invalid_name:"test_name"` +} + +type InvalidNameMapType struct { + TestName struct { + MapFoo map[string]interface{} `form:"map_foo"` + } +} + +type FooStructForSliceType struct { + SliceFoo []int `form:"slice_foo"` +} + +type FooStructForSliceMapType struct { + // Unknown type: not support map + SliceMapFoo []map[string]interface{} `form:"slice_map_foo"` +} + +type FooBarStructForIntType struct { + IntFoo int `form:"int_foo"` + IntBar int `form:"int_bar" binding:"required"` +} + +type FooBarStructForInt8Type struct { + Int8Foo int8 `form:"int8_foo"` + Int8Bar int8 `form:"int8_bar" binding:"required"` +} + +type FooBarStructForInt16Type struct { + Int16Foo int16 `form:"int16_foo"` + Int16Bar int16 `form:"int16_bar" binding:"required"` +} + +type FooBarStructForInt32Type struct { + Int32Foo int32 `form:"int32_foo"` + Int32Bar int32 `form:"int32_bar" binding:"required"` +} + +type FooBarStructForInt64Type struct { + Int64Foo int64 `form:"int64_foo"` + Int64Bar int64 `form:"int64_bar" binding:"required"` +} + +type FooBarStructForUintType struct { + UintFoo uint `form:"uint_foo"` + UintBar uint `form:"uint_bar" binding:"required"` +} + +type FooBarStructForUint8Type struct { + Uint8Foo uint8 `form:"uint8_foo"` + Uint8Bar uint8 `form:"uint8_bar" binding:"required"` +} + +type FooBarStructForUint16Type struct { + Uint16Foo uint16 `form:"uint16_foo"` + Uint16Bar uint16 `form:"uint16_bar" binding:"required"` +} + +type FooBarStructForUint32Type struct { + Uint32Foo uint32 `form:"uint32_foo"` + Uint32Bar uint32 `form:"uint32_bar" binding:"required"` +} + +type FooBarStructForUint64Type struct { + Uint64Foo uint64 `form:"uint64_foo"` + Uint64Bar uint64 `form:"uint64_bar" binding:"required"` +} + +type FooBarStructForBoolType struct { + BoolFoo bool `form:"bool_foo"` + BoolBar bool `form:"bool_bar" binding:"required"` +} + +type FooBarStructForFloat32Type struct { + Float32Foo float32 `form:"float32_foo"` + Float32Bar float32 `form:"float32_bar" binding:"required"` +} + +type FooBarStructForFloat64Type struct { + Float64Foo float64 `form:"float64_foo"` + Float64Bar float64 `form:"float64_bar" binding:"required"` +} + func TestBindingDefault(t *testing.T) { assert.Equal(t, Default("GET", ""), Form) assert.Equal(t, Default("GET", MIMEJSON), Form) @@ -55,6 +169,20 @@ func TestBindingJSON(t *testing.T) { `{"foo": "bar"}`, `{"bar": "foo"}`) } +func TestBindingJSONUseNumber(t *testing.T) { + testBodyBindingUseNumber(t, + JSON, "json", + "/", "/", + `{"foo": 123}`, `{"bar": "foo"}`) +} + +func TestBindingJSONUseNumber2(t *testing.T) { + testBodyBindingUseNumber2(t, + JSON, "json", + "/", "/", + `{"foo": 123}`, `{"bar": "foo"}`) +} + func TestBindingForm(t *testing.T) { testFormBinding(t, "POST", "/", "/", @@ -67,6 +195,198 @@ func TestBindingForm2(t *testing.T) { "", "") } +func TestBindingFormForTime(t *testing.T) { + testFormBindingForTime(t, "POST", + "/", "/", + "time_foo=2017-11-15&time_bar=", "bar2=foo") + testFormBindingForTimeNotFormat(t, "POST", + "/", "/", + "time_foo=2017-11-15", "bar2=foo") + testFormBindingForTimeFailFormat(t, "POST", + "/", "/", + "time_foo=2017-11-15", "bar2=foo") + testFormBindingForTimeFailLocation(t, "POST", + "/", "/", + "time_foo=2017-11-15", "bar2=foo") +} + +func TestBindingFormForTime2(t *testing.T) { + testFormBindingForTime(t, "GET", + "/?time_foo=2017-11-15&time_bar=", "/?bar2=foo", + "", "") + testFormBindingForTimeNotFormat(t, "GET", + "/?time_foo=2017-11-15", "/?bar2=foo", + "", "") + testFormBindingForTimeFailFormat(t, "GET", + "/?time_foo=2017-11-15", "/?bar2=foo", + "", "") + testFormBindingForTimeFailLocation(t, "GET", + "/?time_foo=2017-11-15", "/?bar2=foo", + "", "") +} + +func TestBindingFormInvalidName(t *testing.T) { + testFormBindingInvalidName(t, "POST", + "/", "/", + "test_name=bar", "bar2=foo") +} + +func TestBindingFormInvalidName2(t *testing.T) { + testFormBindingInvalidName2(t, "POST", + "/", "/", + "map_foo=bar", "bar2=foo") +} + +func TestBindingFormForType(t *testing.T) { + testFormBindingForType(t, "POST", + "/", "/", + "map_foo=", "bar2=1", "Map") + + testFormBindingForType(t, "POST", + "/", "/", + "slice_foo=1&slice_foo=2", "bar2=1&bar2=2", "Slice") + + testFormBindingForType(t, "GET", + "/?slice_foo=1&slice_foo=2", "/?bar2=1&bar2=2", + "", "", "Slice") + + testFormBindingForType(t, "POST", + "/", "/", + "slice_map_foo=1&slice_map_foo=2", "bar2=1&bar2=2", "SliceMap") + + testFormBindingForType(t, "GET", + "/?slice_map_foo=1&slice_map_foo=2", "/?bar2=1&bar2=2", + "", "", "SliceMap") + + testFormBindingForType(t, "POST", + "/", "/", + "int_foo=&int_bar=-12", "bar2=-123", "Int") + + testFormBindingForType(t, "GET", + "/?int_foo=&int_bar=-12", "/?bar2=-123", + "", "", "Int") + + testFormBindingForType(t, "POST", + "/", "/", + "int8_foo=&int8_bar=-12", "bar2=-123", "Int8") + + testFormBindingForType(t, "GET", + "/?int8_foo=&int8_bar=-12", "/?bar2=-123", + "", "", "Int8") + + testFormBindingForType(t, "POST", + "/", "/", + "int16_foo=&int16_bar=-12", "bar2=-123", "Int16") + + testFormBindingForType(t, "GET", + "/?int16_foo=&int16_bar=-12", "/?bar2=-123", + "", "", "Int16") + + testFormBindingForType(t, "POST", + "/", "/", + "int32_foo=&int32_bar=-12", "bar2=-123", "Int32") + + testFormBindingForType(t, "GET", + "/?int32_foo=&int32_bar=-12", "/?bar2=-123", + "", "", "Int32") + + testFormBindingForType(t, "POST", + "/", "/", + "int64_foo=&int64_bar=-12", "bar2=-123", "Int64") + + testFormBindingForType(t, "GET", + "/?int64_foo=&int64_bar=-12", "/?bar2=-123", + "", "", "Int64") + + testFormBindingForType(t, "POST", + "/", "/", + "uint_foo=&uint_bar=12", "bar2=123", "Uint") + + testFormBindingForType(t, "GET", + "/?uint_foo=&uint_bar=12", "/?bar2=123", + "", "", "Uint") + + testFormBindingForType(t, "POST", + "/", "/", + "uint8_foo=&uint8_bar=12", "bar2=123", "Uint8") + + testFormBindingForType(t, "GET", + "/?uint8_foo=&uint8_bar=12", "/?bar2=123", + "", "", "Uint8") + + testFormBindingForType(t, "POST", + "/", "/", + "uint16_foo=&uint16_bar=12", "bar2=123", "Uint16") + + testFormBindingForType(t, "GET", + "/?uint16_foo=&uint16_bar=12", "/?bar2=123", + "", "", "Uint16") + + testFormBindingForType(t, "POST", + "/", "/", + "uint32_foo=&uint32_bar=12", "bar2=123", "Uint32") + + testFormBindingForType(t, "GET", + "/?uint32_foo=&uint32_bar=12", "/?bar2=123", + "", "", "Uint32") + + testFormBindingForType(t, "POST", + "/", "/", + "uint64_foo=&uint64_bar=12", "bar2=123", "Uint64") + + testFormBindingForType(t, "GET", + "/?uint64_foo=&uint64_bar=12", "/?bar2=123", + "", "", "Uint64") + + testFormBindingForType(t, "POST", + "/", "/", + "bool_foo=&bool_bar=true", "bar2=true", "Bool") + + testFormBindingForType(t, "GET", + "/?bool_foo=&bool_bar=true", "/?bar2=true", + "", "", "Bool") + + testFormBindingForType(t, "POST", + "/", "/", + "float32_foo=&float32_bar=-12.34", "bar2=12.3", "Float32") + + testFormBindingForType(t, "GET", + "/?float32_foo=&float32_bar=-12.34", "/?bar2=12.3", + "", "", "Float32") + + testFormBindingForType(t, "POST", + "/", "/", + "float64_foo=&float64_bar=-12.34", "bar2=12.3", "Float64") + + testFormBindingForType(t, "GET", + "/?float64_foo=&float64_bar=-12.34", "/?bar2=12.3", + "", "", "Float64") +} + +func TestBindingQuery(t *testing.T) { + testQueryBinding(t, "POST", + "/?foo=bar&bar=foo", "/", + "foo=unused", "bar2=foo") +} + +func TestBindingQuery2(t *testing.T) { + testQueryBinding(t, "GET", + "/?foo=bar&bar=foo", "/?bar2=foo", + "foo=unused", "") +} + +func TestBindingQueryFail(t *testing.T) { + testQueryBindingFail(t, "POST", + "/?map_foo=", "/", + "map_foo=unused", "bar2=foo") +} + +func TestBindingQueryFail2(t *testing.T) { + testQueryBindingFail(t, "GET", + "/?map_foo=", "/?bar2=foo", + "map_foo=unused", "") +} + func TestBindingXML(t *testing.T) { testBodyBinding(t, XML, "xml", @@ -74,12 +394,25 @@ func TestBindingXML(t *testing.T) { "", "") } +func TestBindingXMLFail(t *testing.T) { + testBodyBindingFail(t, + XML, "xml", + "/", "/", + "", "") +} + func createFormPostRequest() *http.Request { req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar&bar=foo")) req.Header.Set("Content-Type", MIMEPOSTForm) return req } +func createFormPostRequestFail() *http.Request { + req, _ := http.NewRequest("POST", "/?map_foo=getfoo", bytes.NewBufferString("map_foo=bar")) + req.Header.Set("Content-Type", MIMEPOSTForm) + return req +} + func createFormMultipartRequest() *http.Request { boundary := "--testboundary" body := new(bytes.Buffer) @@ -94,24 +427,53 @@ func createFormMultipartRequest() *http.Request { return req } +func createFormMultipartRequestFail() *http.Request { + boundary := "--testboundary" + body := new(bytes.Buffer) + mw := multipart.NewWriter(body) + defer mw.Close() + + mw.SetBoundary(boundary) + mw.WriteField("map_foo", "bar") + req, _ := http.NewRequest("POST", "/?map_foo=getfoo", body) + req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary) + return req +} + func TestBindingFormPost(t *testing.T) { req := createFormPostRequest() var obj FooBarStruct FormPost.Bind(req, &obj) + assert.Equal(t, FormPost.Name(), "form-urlencoded") assert.Equal(t, obj.Foo, "bar") assert.Equal(t, obj.Bar, "foo") } +func TestBindingFormPostFail(t *testing.T) { + req := createFormPostRequestFail() + var obj FooStructForMapType + err := FormPost.Bind(req, &obj) + assert.Error(t, err) +} + func TestBindingFormMultipart(t *testing.T) { req := createFormMultipartRequest() var obj FooBarStruct FormMultipart.Bind(req, &obj) + assert.Equal(t, FormMultipart.Name(), "multipart/form-data") assert.Equal(t, obj.Foo, "bar") assert.Equal(t, obj.Bar, "foo") } +func TestBindingFormMultipartFail(t *testing.T) { + req := createFormMultipartRequestFail() + var obj FooStructForMapType + err := FormMultipart.Bind(req, &obj) + assert.Error(t, err) +} + func TestBindingProtoBuf(t *testing.T) { test := &example.Test{ Label: proto.String("yes"), @@ -124,6 +486,18 @@ func TestBindingProtoBuf(t *testing.T) { string(data), string(data[1:])) } +func TestBindingProtoBufFail(t *testing.T) { + test := &example.Test{ + Label: proto.String("yes"), + } + data, _ := proto.Marshal(test) + + testProtoBodyBindingFail(t, + ProtoBuf, "protobuf", + "/", "/", + string(data), string(data[1:])) +} + func TestBindingMsgPack(t *testing.T) { test := FooStruct{ Foo: "bar", @@ -204,6 +578,351 @@ func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) assert.Error(t, err) } +func TestFormBindingFail(t *testing.T) { + b := Form + assert.Equal(t, b.Name(), "form") + + obj := FooBarStruct{} + req, _ := http.NewRequest("POST", "/", nil) + err := b.Bind(req, &obj) + assert.Error(t, err) +} + +func TestFormPostBindingFail(t *testing.T) { + b := FormPost + assert.Equal(t, b.Name(), "form-urlencoded") + + obj := FooBarStruct{} + req, _ := http.NewRequest("POST", "/", nil) + err := b.Bind(req, &obj) + assert.Error(t, err) +} + +func TestFormMultipartBindingFail(t *testing.T) { + b := FormMultipart + assert.Equal(t, b.Name(), "multipart/form-data") + + obj := FooBarStruct{} + req, _ := http.NewRequest("POST", "/", nil) + err := b.Bind(req, &obj) + assert.Error(t, err) +} + +func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody string) { + b := Form + assert.Equal(t, b.Name(), "form") + + obj := FooBarStructForTimeType{} + req := requestWithBody(method, path, body) + if method == "POST" { + req.Header.Add("Content-Type", MIMEPOSTForm) + } + err := b.Bind(req, &obj) + + assert.NoError(t, err) + assert.Equal(t, obj.TimeFoo.Unix(), int64(1510675200)) + assert.Equal(t, obj.TimeFoo.Location().String(), "Asia/Chongqing") + assert.Equal(t, obj.TimeBar.Unix(), int64(-62135596800)) + assert.Equal(t, obj.TimeBar.Location().String(), "UTC") + + obj = FooBarStructForTimeType{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) +} + +func testFormBindingForTimeNotFormat(t *testing.T, method, path, badPath, body, badBody string) { + b := Form + assert.Equal(t, b.Name(), "form") + + obj := FooStructForTimeTypeNotFormat{} + req := requestWithBody(method, path, body) + if method == "POST" { + req.Header.Add("Content-Type", MIMEPOSTForm) + } + err := b.Bind(req, &obj) + assert.Error(t, err) + + obj = FooStructForTimeTypeNotFormat{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) +} + +func testFormBindingForTimeFailFormat(t *testing.T, method, path, badPath, body, badBody string) { + b := Form + assert.Equal(t, b.Name(), "form") + + obj := FooStructForTimeTypeFailFormat{} + req := requestWithBody(method, path, body) + if method == "POST" { + req.Header.Add("Content-Type", MIMEPOSTForm) + } + err := b.Bind(req, &obj) + assert.Error(t, err) + + obj = FooStructForTimeTypeFailFormat{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) +} + +func testFormBindingForTimeFailLocation(t *testing.T, method, path, badPath, body, badBody string) { + b := Form + assert.Equal(t, b.Name(), "form") + + obj := FooStructForTimeTypeFailLocation{} + req := requestWithBody(method, path, body) + if method == "POST" { + req.Header.Add("Content-Type", MIMEPOSTForm) + } + err := b.Bind(req, &obj) + assert.Error(t, err) + + obj = FooStructForTimeTypeFailLocation{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) +} + +func testFormBindingInvalidName(t *testing.T, method, path, badPath, body, badBody string) { + b := Form + assert.Equal(t, b.Name(), "form") + + obj := InvalidNameType{} + req := requestWithBody(method, path, body) + if method == "POST" { + req.Header.Add("Content-Type", MIMEPOSTForm) + } + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.TestName, "") + + obj = InvalidNameType{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) +} + +func testFormBindingInvalidName2(t *testing.T, method, path, badPath, body, badBody string) { + b := Form + assert.Equal(t, b.Name(), "form") + + obj := InvalidNameMapType{} + req := requestWithBody(method, path, body) + if method == "POST" { + req.Header.Add("Content-Type", MIMEPOSTForm) + } + err := b.Bind(req, &obj) + assert.Error(t, err) + + obj = InvalidNameMapType{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) +} + +func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody string, typ string) { + b := Form + assert.Equal(t, b.Name(), "form") + + req := requestWithBody(method, path, body) + if method == "POST" { + req.Header.Add("Content-Type", MIMEPOSTForm) + } + switch typ { + case "Int": + obj := FooBarStructForIntType{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.IntFoo, int(0)) + assert.Equal(t, obj.IntBar, int(-12)) + + obj = FooBarStructForIntType{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Int8": + obj := FooBarStructForInt8Type{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Int8Foo, int8(0)) + assert.Equal(t, obj.Int8Bar, int8(-12)) + + obj = FooBarStructForInt8Type{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Int16": + obj := FooBarStructForInt16Type{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Int16Foo, int16(0)) + assert.Equal(t, obj.Int16Bar, int16(-12)) + + obj = FooBarStructForInt16Type{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Int32": + obj := FooBarStructForInt32Type{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Int32Foo, int32(0)) + assert.Equal(t, obj.Int32Bar, int32(-12)) + + obj = FooBarStructForInt32Type{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Int64": + obj := FooBarStructForInt64Type{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Int64Foo, int64(0)) + assert.Equal(t, obj.Int64Bar, int64(-12)) + + obj = FooBarStructForInt64Type{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Uint": + obj := FooBarStructForUintType{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.UintFoo, uint(0x0)) + assert.Equal(t, obj.UintBar, uint(0xc)) + + obj = FooBarStructForUintType{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Uint8": + obj := FooBarStructForUint8Type{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Uint8Foo, uint8(0x0)) + assert.Equal(t, obj.Uint8Bar, uint8(0xc)) + + obj = FooBarStructForUint8Type{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Uint16": + obj := FooBarStructForUint16Type{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Uint16Foo, uint16(0x0)) + assert.Equal(t, obj.Uint16Bar, uint16(0xc)) + + obj = FooBarStructForUint16Type{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Uint32": + obj := FooBarStructForUint32Type{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Uint32Foo, uint32(0x0)) + assert.Equal(t, obj.Uint32Bar, uint32(0xc)) + + obj = FooBarStructForUint32Type{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Uint64": + obj := FooBarStructForUint64Type{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Uint64Foo, uint64(0x0)) + assert.Equal(t, obj.Uint64Bar, uint64(0xc)) + + obj = FooBarStructForUint64Type{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Float32": + obj := FooBarStructForFloat32Type{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Float32Foo, float32(0.0)) + assert.Equal(t, obj.Float32Bar, float32(-12.34)) + + obj = FooBarStructForFloat32Type{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Float64": + obj := FooBarStructForFloat64Type{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Float64Foo, float64(0.0)) + assert.Equal(t, obj.Float64Bar, float64(-12.34)) + + obj = FooBarStructForFloat64Type{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Bool": + obj := FooBarStructForBoolType{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.BoolFoo, false) + assert.Equal(t, obj.BoolBar, true) + + obj = FooBarStructForBoolType{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Slice": + obj := FooStructForSliceType{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.SliceFoo, []int{1, 2}) + + obj = FooStructForSliceType{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Map": + obj := FooStructForMapType{} + err := b.Bind(req, &obj) + assert.Error(t, err) + case "SliceMap": + obj := FooStructForSliceMapType{} + err := b.Bind(req, &obj) + assert.Error(t, err) + } +} + +func testQueryBinding(t *testing.T, method, path, badPath, body, badBody string) { + b := Query + assert.Equal(t, b.Name(), "query") + + obj := FooBarStruct{} + req := requestWithBody(method, path, body) + if method == "POST" { + req.Header.Add("Content-Type", MIMEPOSTForm) + } + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Foo, "bar") + assert.Equal(t, obj.Bar, "foo") +} + +func testQueryBindingFail(t *testing.T, method, path, badPath, body, badBody string) { + b := Query + assert.Equal(t, b.Name(), "query") + + obj := FooStructForMapType{} + req := requestWithBody(method, path, body) + if method == "POST" { + req.Header.Add("Content-Type", MIMEPOSTForm) + } + err := b.Bind(req, &obj) + assert.Error(t, err) +} + func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) { assert.Equal(t, b.Name(), name) @@ -219,6 +938,58 @@ func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody assert.Error(t, err) } +func testBodyBindingUseNumber(t *testing.T, b Binding, name, path, badPath, body, badBody string) { + assert.Equal(t, b.Name(), name) + + obj := FooStructUseNumber{} + req := requestWithBody("POST", path, body) + EnableDecoderUseNumber = true + err := b.Bind(req, &obj) + assert.NoError(t, err) + // we hope it is int64(123) + v, e := obj.Foo.(json.Number).Int64() + assert.NoError(t, e) + assert.Equal(t, v, int64(123)) + + obj = FooStructUseNumber{} + req = requestWithBody("POST", badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) +} + +func testBodyBindingUseNumber2(t *testing.T, b Binding, name, path, badPath, body, badBody string) { + assert.Equal(t, b.Name(), name) + + obj := FooStructUseNumber{} + req := requestWithBody("POST", path, body) + EnableDecoderUseNumber = false + err := b.Bind(req, &obj) + assert.NoError(t, err) + // it will return float64(123) if not use EnableDecoderUseNumber + // maybe it is not hoped + assert.Equal(t, obj.Foo, float64(123)) + + obj = FooStructUseNumber{} + req = requestWithBody("POST", badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) +} + +func testBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, badBody string) { + assert.Equal(t, b.Name(), name) + + obj := FooStruct{} + req := requestWithBody("POST", path, body) + err := b.Bind(req, &obj) + assert.Error(t, err) + assert.Equal(t, obj.Foo, "") + + obj = FooStruct{} + req = requestWithBody("POST", badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) +} + func testProtoBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) { assert.Equal(t, b.Name(), name) @@ -236,6 +1007,30 @@ func testProtoBodyBinding(t *testing.T, b Binding, name, path, badPath, body, ba assert.Error(t, err) } +type hook struct{} + +func (h hook) Read([]byte) (int, error) { + return 0, errors.New("error") +} + +func testProtoBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, badBody string) { + assert.Equal(t, b.Name(), name) + + obj := example.Test{} + req := requestWithBody("POST", path, body) + + req.Body = ioutil.NopCloser(&hook{}) + req.Header.Add("Content-Type", MIMEPROTOBUF) + err := b.Bind(req, &obj) + assert.Error(t, err) + + obj = example.Test{} + req = requestWithBody("POST", badPath, badBody) + req.Header.Add("Content-Type", MIMEPROTOBUF) + err = ProtoBuf.Bind(req, &obj) + assert.Error(t, err) +} + func testMsgPackBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) { assert.Equal(t, b.Name(), name) diff --git a/binding/default_validator.go b/binding/default_validator.go index fb0a7b8f..a38e72e0 100644 --- a/binding/default_validator.go +++ b/binding/default_validator.go @@ -28,6 +28,15 @@ func (v *defaultValidator) ValidateStruct(obj interface{}) error { return nil } +// Engine returns the underlying validator engine which powers the default +// Validator instance. This is useful if you want to register custom validations +// or struct level validations. See validator GoDoc for more info - +// https://godoc.org/gopkg.in/go-playground/validator.v8 +func (v *defaultValidator) Engine() interface{} { + v.lazyinit() + return v.validate +} + func (v *defaultValidator) lazyinit() { v.once.Do(func() { v.validate = validator.New() diff --git a/binding/form.go b/binding/form.go index 557333e6..0be59660 100644 --- a/binding/form.go +++ b/binding/form.go @@ -6,6 +6,8 @@ package binding import "net/http" +const defaultMemory = 32 * 1024 * 1024 + type formBinding struct{} type formPostBinding struct{} type formMultipartBinding struct{} @@ -18,7 +20,7 @@ func (formBinding) Bind(req *http.Request, obj interface{}) error { if err := req.ParseForm(); err != nil { return err } - req.ParseMultipartForm(32 << 10) // 32 MB + req.ParseMultipartForm(defaultMemory) if err := mapForm(obj, req.Form); err != nil { return err } @@ -44,7 +46,7 @@ func (formMultipartBinding) Name() string { } func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error { - if err := req.ParseMultipartForm(32 << 10); err != nil { + if err := req.ParseMultipartForm(defaultMemory); err != nil { return err } if err := mapForm(obj, req.MultipartForm.Value); err != nil { diff --git a/binding/form_mapping.go b/binding/form_mapping.go index 34f12678..dd8c6246 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -163,6 +163,14 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val l = time.UTC } + if locTag := structField.Tag.Get("time_location"); locTag != "" { + loc, err := time.LoadLocation(locTag) + if err != nil { + return err + } + l = loc + } + t, err := time.ParseInLocation(timeFormat, val, l) if err != nil { return err @@ -171,12 +179,3 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val value.Set(reflect.ValueOf(t)) return nil } - -// Don't pass in pointers to bind to. Can lead to bugs. See: -// https://github.com/codegangsta/martini-contrib/issues/40 -// https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659 -func ensureNotPointer(obj interface{}) { - if reflect.TypeOf(obj).Kind() == reflect.Ptr { - panic("Pointers are not accepted as binding models") - } -} diff --git a/binding/json.go b/binding/json.go index 215fa9bf..e928a8c1 100644 --- a/binding/json.go +++ b/binding/json.go @@ -7,13 +7,13 @@ package binding import ( "net/http" - "github.com/json-iterator/go" + "github.com/gin-gonic/gin/json" ) -var ( - json = jsoniter.ConfigCompatibleWithStandardLibrary - EnableDecoderUseNumber = false -) +// EnableDecoderUseNumber is used to call the UseNumber method on the JSON +// Decoder instance. UseNumber causes the Decoder to unmarshal a number into an +// interface{} as a Number instead of as a float64. +var EnableDecoderUseNumber = false type jsonBinding struct{} diff --git a/binding/msgpack.go b/binding/msgpack.go index 69367175..7faea4b5 100644 --- a/binding/msgpack.go +++ b/binding/msgpack.go @@ -17,12 +17,8 @@ func (msgpackBinding) Name() string { } func (msgpackBinding) Bind(req *http.Request, obj interface{}) error { - if err := codec.NewDecoder(req.Body, new(codec.MsgpackHandle)).Decode(&obj); err != nil { - //var decoder *codec.Decoder = codec.NewDecoder(req.Body, &codec.MsgpackHandle) - //if err := decoder.Decode(&obj); err != nil { return err } return validate(obj) - } diff --git a/binding/query.go b/binding/query.go new file mode 100644 index 00000000..219743f2 --- /dev/null +++ b/binding/query.go @@ -0,0 +1,21 @@ +// 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. + +package binding + +import "net/http" + +type queryBinding struct{} + +func (queryBinding) Name() string { + return "query" +} + +func (queryBinding) Bind(req *http.Request, obj interface{}) error { + values := req.URL.Query() + if err := mapForm(obj, values); err != nil { + return err + } + return validate(obj) +} diff --git a/binding/validate_test.go b/binding/validate_test.go index cbcb389d..cb76063c 100644 --- a/binding/validate_test.go +++ b/binding/validate_test.go @@ -6,10 +6,12 @@ package binding import ( "bytes" + "reflect" "testing" "time" "github.com/stretchr/testify/assert" + "gopkg.in/go-playground/validator.v8" ) type testInterface interface { @@ -190,3 +192,45 @@ func TestValidatePrimitives(t *testing.T) { assert.NoError(t, validate(&str)) assert.Equal(t, str, "value") } + +// structCustomValidation is a helper struct we use to check that +// custom validation can be registered on it. +// The `notone` binding directive is for custom validation and registered later. +type structCustomValidation struct { + Integer int `binding:"notone"` +} + +// notOne is a custom validator meant to be used with `validator.v8` library. +// The method signature for `v9` is significantly different and this function +// would need to be changed for tests to pass after upgrade. +// See https://github.com/gin-gonic/gin/pull/1015. +func notOne( + v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value, + field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string, +) bool { + if val, ok := field.Interface().(int); ok { + return val != 1 + } + return false +} + +func TestValidatorEngine(t *testing.T) { + // This validates that the function `notOne` matches + // the expected function signature by `defaultValidator` + // and by extension the validator library. + engine, ok := Validator.Engine().(*validator.Validate) + assert.True(t, ok) + + err := engine.RegisterValidation("notone", notOne) + // Check that we can register custom validation without error + assert.Nil(t, err) + + // Create an instance which will fail validation + withOne := structCustomValidation{Integer: 1} + errs := validate(withOne) + + // Check that we got back non-nil errs + assert.NotNil(t, errs) + // Check that the error matches expectation + assert.Error(t, errs, "", "", "notone") +} diff --git a/context.go b/context.go index 198dd3e3..90d4c6e5 100644 --- a/context.go +++ b/context.go @@ -13,6 +13,7 @@ import ( "net" "net/http" "net/url" + "os" "strings" "time" @@ -21,7 +22,7 @@ import ( "github.com/gin-gonic/gin/render" ) -// Content-Type MIME of the most common data formats +// Content-Type MIME of the most common data formats. const ( MIMEJSON = binding.MIMEJSON MIMEHTML = binding.MIMEHTML @@ -32,10 +33,7 @@ const ( MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm ) -const ( - defaultMemory = 32 << 20 // 32 MB - abortIndex int8 = math.MaxInt8 / 2 -) +const abortIndex int8 = math.MaxInt8 / 2 // Context is the most important part of gin. It allows us to pass variables between middleware, // manage the flow, validate the JSON of a request and render a JSON response for example. @@ -50,13 +48,13 @@ type Context struct { engine *Engine - // Keys is a key/value pair exclusively for the context of each request + // Keys is a key/value pair exclusively for the context of each request. Keys map[string]interface{} - // Errors is a list of errors attached to all the handlers/middlewares who used this context + // Errors is a list of errors attached to all the handlers/middlewares who used this context. Errors errorMsgs - // Accepted defines a list of manually accepted formats for content negotiation + // Accepted defines a list of manually accepted formats for content negotiation. Accepted []string } @@ -86,7 +84,7 @@ func (c *Context) Copy() *Context { } // HandlerName returns the main handler's name. For example if the handler is "handleGetUsers()", -// this function will return "main.handleGetUsers" +// this function will return "main.handleGetUsers". func (c *Context) HandlerName() string { return nameOfFunction(c.handlers.Last()) } @@ -105,8 +103,7 @@ func (c *Context) Handler() HandlerFunc { // See example in GitHub. func (c *Context) Next() { c.index++ - s := int8(len(c.handlers)) - for ; c.index < s; c.index++ { + for s := int8(len(c.handlers)); c.index < s; c.index++ { c.handlers[c.index](c) } } @@ -357,8 +354,7 @@ func (c *Context) QueryArray(key string) []string { // 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) { - req := c.Request - if values, ok := req.URL.Query()[key]; ok && len(values) > 0 { + if values, ok := c.Request.URL.Query()[key]; ok && len(values) > 0 { return values, true } return []string{}, false @@ -407,7 +403,7 @@ func (c *Context) PostFormArray(key string) []string { func (c *Context) GetPostFormArray(key string) ([]string, bool) { req := c.Request req.ParseForm() - req.ParseMultipartForm(defaultMemory) + req.ParseMultipartForm(c.engine.MaxMultipartMemory) if values := req.PostForm[key]; len(values) > 0 { return values, true } @@ -427,30 +423,53 @@ func (c *Context) FormFile(name string) (*multipart.FileHeader, error) { // MultipartForm is the parsed multipart form, including file uploads. func (c *Context) MultipartForm() (*multipart.Form, error) { - err := c.Request.ParseMultipartForm(defaultMemory) + err := c.Request.ParseMultipartForm(c.engine.MaxMultipartMemory) return c.Request.MultipartForm, err } +// SaveUploadedFile uploads the form file to specific dst. +func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error { + src, err := file.Open() + if err != nil { + return err + } + defer src.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + io.Copy(out, src) + return nil +} + // Bind checks the Content-Type to select a binding engine automatically, // Depending the "Content-Type" header different bindings are used: // "application/json" --> JSON binding // "application/xml" --> XML binding -// otherwise --> returns an error +// otherwise --> returns an error. // It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. // It decodes the json payload into the struct specified as a pointer. -// Like ParseBody() but this method also writes a 400 error if the json is not valid. +// It writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid. func (c *Context) Bind(obj interface{}) error { b := binding.Default(c.Request.Method, c.ContentType()) return c.MustBindWith(obj, b) } -// BindJSON is a shortcut for c.MustBindWith(obj, binding.JSON) +// BindJSON is a shortcut for c.MustBindWith(obj, binding.JSON). func (c *Context) BindJSON(obj interface{}) error { return c.MustBindWith(obj, binding.JSON) } -// MustBindWith binds the passed struct pointer using the specified binding -// engine. It will abort the request with HTTP 400 if any error ocurrs. +// BindQuery is a shortcut for c.MustBindWith(obj, binding.Query). +func (c *Context) BindQuery(obj interface{}) error { + return c.MustBindWith(obj, binding.Query) +} + +// MustBindWith binds the passed struct pointer using the specified binding engine. +// It will abort the request with HTTP 400 if any error ocurrs. // See the binding package. func (c *Context) MustBindWith(obj interface{}, b binding.Binding) (err error) { if err = c.ShouldBindWith(obj, b); err != nil { @@ -460,8 +479,30 @@ func (c *Context) MustBindWith(obj interface{}, b binding.Binding) (err error) { return } -// ShouldBindWith binds the passed struct pointer using the specified binding -// engine. +// ShouldBind checks the Content-Type to select a binding engine automatically, +// Depending the "Content-Type" header different bindings are used: +// "application/json" --> JSON binding +// "application/xml" --> XML binding +// otherwise --> returns an error +// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. +// It decodes the json payload into the struct specified as a pointer. +// Like c.Bind() but this method does not set the response status code to 400 and abort if the json is not valid. +func (c *Context) ShouldBind(obj interface{}) error { + b := binding.Default(c.Request.Method, c.ContentType()) + return c.ShouldBindWith(obj, b) +} + +// ShouldBindJSON is a shortcut for c.ShouldBindWith(obj, binding.JSON). +func (c *Context) ShouldBindJSON(obj interface{}) error { + return c.ShouldBindWith(obj, binding.JSON) +} + +// ShouldBindQuery is a shortcut for c.ShouldBindWith(obj, binding.Query). +func (c *Context) ShouldBindQuery(obj interface{}) error { + return c.ShouldBindWith(obj, binding.Query) +} + +// ShouldBindWith binds the passed struct pointer using the specified binding engine. // See the binding package. func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error { return b.Bind(c.Request, obj) @@ -477,17 +518,17 @@ func (c *Context) ClientIP() string { clientIP = clientIP[0:index] } clientIP = strings.TrimSpace(clientIP) - if len(clientIP) > 0 { + if clientIP != "" { return clientIP } clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip")) - if len(clientIP) > 0 { + if clientIP != "" { return clientIP } } if c.engine.AppEngine { - if addr := c.Request.Header.Get("X-Appengine-Remote-Addr"); addr != "" { + if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" { return addr } } @@ -515,17 +556,14 @@ func (c *Context) IsWebsocket() bool { } func (c *Context) requestHeader(key string) string { - if values, _ := c.Request.Header[key]; len(values) > 0 { - return values[0] - } - return "" + return c.Request.Header.Get(key) } /************************************/ /******** RESPONSE RENDERING ********/ /************************************/ -// bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function +// bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function. func bodyAllowedForStatus(status int) bool { switch { case status >= 100 && status <= 199: @@ -538,31 +576,35 @@ func bodyAllowedForStatus(status int) bool { return true } +// Status sets the HTTP response code. func (c *Context) Status(code int) { c.writermem.WriteHeader(code) } -// Header is a intelligent shortcut for c.Writer.Header().Set(key, value) +// Header is a intelligent shortcut for c.Writer.Header().Set(key, value). // It writes a header in the response. // If value == "", this method removes the header `c.Writer.Header().Del(key)` func (c *Context) Header(key, value string) { - if len(value) == 0 { + if value == "" { c.Writer.Header().Del(key) } else { c.Writer.Header().Set(key, value) } } -// GetHeader returns value from request headers +// GetHeader returns value from request headers. func (c *Context) GetHeader(key string) string { return c.requestHeader(key) } -// GetRawData return stream data +// GetRawData return stream data. func (c *Context) GetRawData() ([]byte, error) { return ioutil.ReadAll(c.Request.Body) } +// 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. func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool) { if path == "" { path = "/" @@ -578,6 +620,10 @@ func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, }) } +// Cookie returns the named cookie provided in the request or +// ErrNoCookie if not found. And return the named cookie is unescaped. +// If multiple cookies match the given name, only one cookie will +// be returned. func (c *Context) Cookie(name string) (string, error) { cookie, err := c.Request.Cookie(name) if err != nil { diff --git a/context_test.go b/context_test.go index 758fecdc..9024cfc1 100644 --- a/context_test.go +++ b/context_test.go @@ -45,6 +45,7 @@ func createMultipartRequest() *http.Request { must(mw.WriteField("id", "")) must(mw.WriteField("time_local", "31/12/2016 14:55")) must(mw.WriteField("time_utc", "31/12/2016 14:55")) + must(mw.WriteField("time_location", "31/12/2016 14:55")) req, err := http.NewRequest("POST", "/", body) must(err) req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary) @@ -72,12 +73,18 @@ func TestContextFormFile(t *testing.T) { if assert.NoError(t, err) { assert.Equal(t, "test", f.Filename) } + + assert.NoError(t, c.SaveUploadedFile(f, "test")) } func TestContextMultipartForm(t *testing.T) { buf := new(bytes.Buffer) mw := multipart.NewWriter(buf) mw.WriteField("foo", "bar") + w, err := mw.CreateFormFile("file", "test") + if assert.NoError(t, err) { + w.Write([]byte("test")) + } mw.Close() c, _ := CreateTestContext(httptest.NewRecorder()) c.Request, _ = http.NewRequest("POST", "/", buf) @@ -86,6 +93,42 @@ func TestContextMultipartForm(t *testing.T) { if assert.NoError(t, err) { assert.NotNil(t, f) } + + assert.NoError(t, c.SaveUploadedFile(f.File["file"][0], "test")) +} + +func TestSaveUploadedOpenFailed(t *testing.T) { + buf := new(bytes.Buffer) + mw := multipart.NewWriter(buf) + mw.Close() + + c, _ := CreateTestContext(httptest.NewRecorder()) + c.Request, _ = http.NewRequest("POST", "/", buf) + c.Request.Header.Set("Content-Type", mw.FormDataContentType()) + + f := &multipart.FileHeader{ + Filename: "file", + } + assert.Error(t, c.SaveUploadedFile(f, "test")) +} + +func TestSaveUploadedCreateFailed(t *testing.T) { + buf := new(bytes.Buffer) + mw := multipart.NewWriter(buf) + w, err := mw.CreateFormFile("file", "test") + if assert.NoError(t, err) { + w.Write([]byte("test")) + } + mw.Close() + c, _ := CreateTestContext(httptest.NewRecorder()) + c.Request, _ = http.NewRequest("POST", "/", buf) + c.Request.Header.Set("Content-Type", mw.FormDataContentType()) + f, err := c.FormFile("file") + if assert.NoError(t, err) { + assert.Equal(t, "test", f.Filename) + } + + assert.Error(t, c.SaveUploadedFile(f, "/")) } func TestContextReset(t *testing.T) { @@ -137,14 +180,14 @@ func TestContextSetGet(t *testing.T) { c.Set("foo", "bar") value, err := c.Get("foo") - assert.Equal(t, value, "bar") + assert.Equal(t, "bar", value) assert.True(t, err) value, err = c.Get("foo2") assert.Nil(t, value) assert.False(t, err) - assert.Equal(t, c.MustGet("foo"), "bar") + assert.Equal(t, "bar", c.MustGet("foo")) assert.Panics(t, func() { c.MustGet("no_exist") }) } @@ -178,7 +221,7 @@ func TestContextGetString(t *testing.T) { func TestContextSetGetBool(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) c.Set("bool", true) - assert.Equal(t, true, c.GetBool("bool")) + assert.True(t, c.GetBool("bool")) } func TestContextGetInt(t *testing.T) { @@ -295,26 +338,26 @@ func TestContextQuery(t *testing.T) { value, ok := c.GetQuery("foo") assert.True(t, ok) - assert.Equal(t, value, "bar") - assert.Equal(t, c.DefaultQuery("foo", "none"), "bar") - assert.Equal(t, c.Query("foo"), "bar") + assert.Equal(t, "bar", value) + assert.Equal(t, "bar", c.DefaultQuery("foo", "none")) + assert.Equal(t, "bar", c.Query("foo")) value, ok = c.GetQuery("page") assert.True(t, ok) - assert.Equal(t, value, "10") - assert.Equal(t, c.DefaultQuery("page", "0"), "10") - assert.Equal(t, c.Query("page"), "10") + assert.Equal(t, "10", value) + assert.Equal(t, "10", c.DefaultQuery("page", "0")) + assert.Equal(t, "10", c.Query("page")) value, ok = c.GetQuery("id") assert.True(t, ok) assert.Empty(t, value) - assert.Equal(t, c.DefaultQuery("id", "nada"), "") + assert.Empty(t, c.DefaultQuery("id", "nada")) assert.Empty(t, c.Query("id")) value, ok = c.GetQuery("NoKey") assert.False(t, ok) assert.Empty(t, value) - assert.Equal(t, c.DefaultQuery("NoKey", "nada"), "nada") + assert.Equal(t, "nada", c.DefaultQuery("NoKey", "nada")) assert.Empty(t, c.Query("NoKey")) // postform should not mess @@ -330,29 +373,29 @@ func TestContextQueryAndPostForm(t *testing.T) { c.Request, _ = http.NewRequest("POST", "/?both=GET&id=main&id=omit&array[]=first&array[]=second", body) c.Request.Header.Add("Content-Type", MIMEPOSTForm) - assert.Equal(t, c.DefaultPostForm("foo", "none"), "bar") - assert.Equal(t, c.PostForm("foo"), "bar") + assert.Equal(t, "bar", c.DefaultPostForm("foo", "none")) + assert.Equal(t, "bar", c.PostForm("foo")) assert.Empty(t, c.Query("foo")) value, ok := c.GetPostForm("page") assert.True(t, ok) - assert.Equal(t, value, "11") - assert.Equal(t, c.DefaultPostForm("page", "0"), "11") - assert.Equal(t, c.PostForm("page"), "11") - assert.Equal(t, c.Query("page"), "") + assert.Equal(t, "11", value) + assert.Equal(t, "11", c.DefaultPostForm("page", "0")) + assert.Equal(t, "11", c.PostForm("page")) + assert.Empty(t, c.Query("page")) value, ok = c.GetPostForm("both") assert.True(t, ok) assert.Empty(t, value) assert.Empty(t, c.PostForm("both")) - assert.Equal(t, c.DefaultPostForm("both", "nothing"), "") - assert.Equal(t, c.Query("both"), "GET") + assert.Empty(t, c.DefaultPostForm("both", "nothing")) + assert.Equal(t, "GET", c.Query("both"), "GET") value, ok = c.GetQuery("id") assert.True(t, ok) - assert.Equal(t, value, "main") - assert.Equal(t, c.DefaultPostForm("id", "000"), "000") - assert.Equal(t, c.Query("id"), "main") + assert.Equal(t, "main", value) + assert.Equal(t, "000", c.DefaultPostForm("id", "000")) + assert.Equal(t, "main", c.Query("id")) assert.Empty(t, c.PostForm("id")) value, ok = c.GetQuery("NoKey") @@ -361,8 +404,8 @@ func TestContextQueryAndPostForm(t *testing.T) { value, ok = c.GetPostForm("NoKey") assert.False(t, ok) assert.Empty(t, value) - assert.Equal(t, c.DefaultPostForm("NoKey", "nada"), "nada") - assert.Equal(t, c.DefaultQuery("NoKey", "nothing"), "nothing") + assert.Equal(t, "nada", c.DefaultPostForm("NoKey", "nada")) + assert.Equal(t, "nothing", c.DefaultQuery("NoKey", "nothing")) assert.Empty(t, c.PostForm("NoKey")) assert.Empty(t, c.Query("NoKey")) @@ -374,11 +417,11 @@ func TestContextQueryAndPostForm(t *testing.T) { Array []string `form:"array[]"` } assert.NoError(t, c.Bind(&obj)) - assert.Equal(t, obj.Foo, "bar") - assert.Equal(t, obj.ID, "main") - assert.Equal(t, obj.Page, 11) - assert.Equal(t, obj.Both, "") - assert.Equal(t, obj.Array, []string{"first", "second"}) + assert.Equal(t, "bar", obj.Foo, "bar") + assert.Equal(t, "main", obj.ID, "main") + assert.Equal(t, 11, obj.Page, 11) + assert.Empty(t, obj.Both) + assert.Equal(t, []string{"first", "second"}, obj.Array) values, ok := c.GetQueryArray("array[]") assert.True(t, ok) @@ -402,44 +445,48 @@ func TestContextPostFormMultipart(t *testing.T) { c.Request = createMultipartRequest() var obj struct { - Foo string `form:"foo"` - Bar string `form:"bar"` - BarAsInt int `form:"bar"` - Array []string `form:"array"` - ID string `form:"id"` - TimeLocal time.Time `form:"time_local" time_format:"02/01/2006 15:04"` - TimeUTC time.Time `form:"time_utc" time_format:"02/01/2006 15:04" time_utc:"1"` - BlankTime time.Time `form:"blank_time" time_format:"02/01/2006 15:04"` + Foo string `form:"foo"` + Bar string `form:"bar"` + BarAsInt int `form:"bar"` + Array []string `form:"array"` + ID string `form:"id"` + TimeLocal time.Time `form:"time_local" time_format:"02/01/2006 15:04"` + TimeUTC time.Time `form:"time_utc" time_format:"02/01/2006 15:04" time_utc:"1"` + TimeLocation time.Time `form:"time_location" time_format:"02/01/2006 15:04" time_location:"Asia/Tokyo"` + BlankTime time.Time `form:"blank_time" time_format:"02/01/2006 15:04"` } assert.NoError(t, c.Bind(&obj)) - assert.Equal(t, obj.Foo, "bar") - assert.Equal(t, obj.Bar, "10") - assert.Equal(t, obj.BarAsInt, 10) - assert.Equal(t, obj.Array, []string{"first", "second"}) - assert.Equal(t, obj.ID, "") - assert.Equal(t, obj.TimeLocal.Format("02/01/2006 15:04"), "31/12/2016 14:55") - assert.Equal(t, obj.TimeLocal.Location(), time.Local) - assert.Equal(t, obj.TimeUTC.Format("02/01/2006 15:04"), "31/12/2016 14:55") - assert.Equal(t, obj.TimeUTC.Location(), time.UTC) + assert.Equal(t, "bar", obj.Foo) + assert.Equal(t, "10", obj.Bar) + assert.Equal(t, 10, obj.BarAsInt) + assert.Equal(t, []string{"first", "second"}, obj.Array) + assert.Empty(t, obj.ID) + assert.Equal(t, "31/12/2016 14:55", obj.TimeLocal.Format("02/01/2006 15:04")) + assert.Equal(t, time.Local, obj.TimeLocal.Location()) + assert.Equal(t, "31/12/2016 14:55", obj.TimeUTC.Format("02/01/2006 15:04")) + assert.Equal(t, time.UTC, obj.TimeUTC.Location()) + loc, _ := time.LoadLocation("Asia/Tokyo") + assert.Equal(t, "31/12/2016 14:55", obj.TimeLocation.Format("02/01/2006 15:04")) + assert.Equal(t, loc, obj.TimeLocation.Location()) assert.True(t, obj.BlankTime.IsZero()) value, ok := c.GetQuery("foo") assert.False(t, ok) assert.Empty(t, value) assert.Empty(t, c.Query("bar")) - assert.Equal(t, c.DefaultQuery("id", "nothing"), "nothing") + assert.Equal(t, "nothing", c.DefaultQuery("id", "nothing")) value, ok = c.GetPostForm("foo") assert.True(t, ok) - assert.Equal(t, value, "bar") - assert.Equal(t, c.PostForm("foo"), "bar") + assert.Equal(t, "bar", value) + assert.Equal(t, "bar", c.PostForm("foo")) value, ok = c.GetPostForm("array") assert.True(t, ok) - assert.Equal(t, value, "first") - assert.Equal(t, c.PostForm("array"), "first") + assert.Equal(t, "first", value) + assert.Equal(t, "first", c.PostForm("array")) - assert.Equal(t, c.DefaultPostForm("bar", "nothing"), "10") + assert.Equal(t, "10", c.DefaultPostForm("bar", "nothing")) value, ok = c.GetPostForm("id") assert.True(t, ok) @@ -450,7 +497,7 @@ func TestContextPostFormMultipart(t *testing.T) { value, ok = c.GetPostForm("nokey") assert.False(t, ok) assert.Empty(t, value) - assert.Equal(t, c.DefaultPostForm("nokey", "nothing"), "nothing") + assert.Equal(t, "nothing", c.DefaultPostForm("nokey", "nothing")) values, ok := c.GetPostFormArray("array") assert.True(t, ok) @@ -472,13 +519,13 @@ func TestContextPostFormMultipart(t *testing.T) { func TestContextSetCookie(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) c.SetCookie("user", "gin", 1, "/", "localhost", true, true) - assert.Equal(t, c.Writer.Header().Get("Set-Cookie"), "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure") + assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure", c.Writer.Header().Get("Set-Cookie")) } func TestContextSetCookiePathEmpty(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) c.SetCookie("user", "gin", 1, "", "localhost", true, true) - assert.Equal(t, c.Writer.Header().Get("Set-Cookie"), "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure") + assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure", c.Writer.Header().Get("Set-Cookie")) } func TestContextGetCookie(t *testing.T) { @@ -486,17 +533,17 @@ func TestContextGetCookie(t *testing.T) { c.Request, _ = http.NewRequest("GET", "/get", nil) c.Request.Header.Set("Cookie", "user=gin") cookie, _ := c.Cookie("user") - assert.Equal(t, cookie, "gin") + assert.Equal(t, "gin", cookie) _, err := c.Cookie("nokey") assert.Error(t, err) } func TestContextBodyAllowedForStatus(t *testing.T) { - assert.Equal(t, false, bodyAllowedForStatus(102)) - assert.Equal(t, false, bodyAllowedForStatus(204)) - assert.Equal(t, false, bodyAllowedForStatus(304)) - assert.Equal(t, true, bodyAllowedForStatus(500)) + assert.False(t, false, bodyAllowedForStatus(102)) + assert.False(t, false, bodyAllowedForStatus(204)) + assert.False(t, false, bodyAllowedForStatus(304)) + assert.True(t, true, bodyAllowedForStatus(500)) } type TestPanicRender struct { @@ -542,7 +589,7 @@ func TestContextRenderNoContentJSON(t *testing.T) { c.JSON(204, H{"foo": "bar"}) assert.Equal(t, 204, w.Code) - assert.Equal(t, "", w.Body.String()) + assert.Empty(t, w.Body.String()) assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) } @@ -569,7 +616,7 @@ func TestContextRenderNoContentAPIJSON(t *testing.T) { c.JSON(204, H{"foo": "bar"}) assert.Equal(t, 204, w.Code) - assert.Equal(t, "", w.Body.String()) + assert.Empty(t, w.Body.String()) assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/vnd.api+json") } @@ -581,7 +628,7 @@ func TestContextRenderIndentedJSON(t *testing.T) { c.IndentedJSON(201, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}}) - assert.Equal(t, w.Code, 201) + assert.Equal(t, 201, w.Code) assert.Equal(t, "{\n \"bar\": \"foo\",\n \"foo\": \"bar\",\n \"nested\": {\n \"foo\": \"bar\"\n }\n}", w.Body.String()) assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) } @@ -594,7 +641,7 @@ func TestContextRenderNoContentIndentedJSON(t *testing.T) { c.IndentedJSON(204, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}}) assert.Equal(t, 204, w.Code) - assert.Equal(t, "", w.Body.String()) + assert.Empty(t, w.Body.String()) assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) } @@ -607,9 +654,9 @@ func TestContextRenderSecureJSON(t *testing.T) { router.SecureJsonPrefix("&&&START&&&") c.SecureJSON(201, []string{"foo", "bar"}) - assert.Equal(t, w.Code, 201) - assert.Equal(t, w.Body.String(), "&&&START&&&[\"foo\",\"bar\"]") - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8") + assert.Equal(t, 201, w.Code) + assert.Equal(t, "&&&START&&&[\"foo\",\"bar\"]", w.Body.String()) + assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) } // Tests that no Custom JSON is rendered if code is 204 @@ -620,8 +667,8 @@ func TestContextRenderNoContentSecureJSON(t *testing.T) { c.SecureJSON(204, []string{"foo", "bar"}) assert.Equal(t, 204, w.Code) - assert.Equal(t, "", w.Body.String()) - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8") + assert.Empty(t, w.Body.String()) + assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) } // Tests that the response executes the templates @@ -629,14 +676,39 @@ func TestContextRenderNoContentSecureJSON(t *testing.T) { func TestContextRenderHTML(t *testing.T) { w := httptest.NewRecorder() c, router := CreateTestContext(w) + templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) router.SetHTMLTemplate(templ) c.HTML(201, "t", H{"name": "alexandernyquist"}) - assert.Equal(t, w.Code, 201) - assert.Equal(t, w.Body.String(), "Hello alexandernyquist") - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") + assert.Equal(t, 201, w.Code) + assert.Equal(t, "Hello alexandernyquist", w.Body.String()) + assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type")) +} + +func TestContextRenderHTML2(t *testing.T) { + w := httptest.NewRecorder() + c, router := CreateTestContext(w) + + // print debug warning log when Engine.trees > 0 + router.addRoute("GET", "/", HandlersChain{func(_ *Context) {}}) + assert.Len(t, router.trees, 1) + + var b bytes.Buffer + setup(&b) + defer teardown() + + templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) + router.SetHTMLTemplate(templ) + + assert.Equal(t, "[GIN-debug] [WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called\nat initialization. ie. before any route is registered or the router is listening in a socket:\n\n\trouter := gin.Default()\n\trouter.SetHTMLTemplate(template) // << good place\n\n", b.String()) + + c.HTML(201, "t", H{"name": "alexandernyquist"}) + + assert.Equal(t, 201, w.Code) + assert.Equal(t, "Hello alexandernyquist", w.Body.String()) + assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type")) } // Tests that no HTML is rendered if code is 204 @@ -649,8 +721,8 @@ func TestContextRenderNoContentHTML(t *testing.T) { c.HTML(204, "t", H{"name": "alexandernyquist"}) assert.Equal(t, 204, w.Code) - assert.Equal(t, "", w.Body.String()) - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") + assert.Empty(t, w.Body.String()) + assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type")) } // TestContextXML tests that the response is serialized as XML @@ -661,9 +733,9 @@ func TestContextRenderXML(t *testing.T) { c.XML(201, H{"foo": "bar"}) - assert.Equal(t, w.Code, 201) - assert.Equal(t, w.Body.String(), "") - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/xml; charset=utf-8") + assert.Equal(t, 201, w.Code) + assert.Equal(t, "", w.Body.String()) + assert.Equal(t, "application/xml; charset=utf-8", w.HeaderMap.Get("Content-Type")) } // Tests that no XML is rendered if code is 204 @@ -674,8 +746,8 @@ func TestContextRenderNoContentXML(t *testing.T) { c.XML(204, H{"foo": "bar"}) assert.Equal(t, 204, w.Code) - assert.Equal(t, "", w.Body.String()) - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/xml; charset=utf-8") + assert.Empty(t, w.Body.String()) + assert.Equal(t, "application/xml; charset=utf-8", w.HeaderMap.Get("Content-Type")) } // TestContextString tests that the response is returned @@ -686,9 +758,9 @@ func TestContextRenderString(t *testing.T) { c.String(201, "test %s %d", "string", 2) - assert.Equal(t, w.Code, 201) - assert.Equal(t, w.Body.String(), "test string 2") - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") + assert.Equal(t, 201, w.Code) + assert.Equal(t, "test string 2", w.Body.String()) + assert.Equal(t, "text/plain; charset=utf-8", w.HeaderMap.Get("Content-Type")) } // Tests that no String is rendered if code is 204 @@ -699,8 +771,8 @@ func TestContextRenderNoContentString(t *testing.T) { c.String(204, "test %s %d", "string", 2) assert.Equal(t, 204, w.Code) - assert.Equal(t, "", w.Body.String()) - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") + assert.Empty(t, w.Body.String()) + assert.Equal(t, "text/plain; charset=utf-8", w.HeaderMap.Get("Content-Type")) } // TestContextString tests that the response is returned @@ -712,9 +784,9 @@ func TestContextRenderHTMLString(t *testing.T) { c.Header("Content-Type", "text/html; charset=utf-8") c.String(201, "%s %d", "string", 3) - assert.Equal(t, w.Code, 201) - assert.Equal(t, w.Body.String(), "string 3") - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") + assert.Equal(t, 201, w.Code) + assert.Equal(t, "string 3", w.Body.String()) + assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type")) } // Tests that no HTML String is rendered if code is 204 @@ -726,8 +798,8 @@ func TestContextRenderNoContentHTMLString(t *testing.T) { c.String(204, "%s %d", "string", 3) assert.Equal(t, 204, w.Code) - assert.Equal(t, "", w.Body.String()) - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") + assert.Empty(t, w.Body.String()) + assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type")) } // TestContextData tests that the response can be written from `bytesting` @@ -738,9 +810,9 @@ func TestContextRenderData(t *testing.T) { c.Data(201, "text/csv", []byte(`foo,bar`)) - assert.Equal(t, w.Code, 201) - assert.Equal(t, w.Body.String(), "foo,bar") - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/csv") + assert.Equal(t, 201, w.Code) + assert.Equal(t, "foo,bar", w.Body.String()) + assert.Equal(t, "text/csv", w.HeaderMap.Get("Content-Type")) } // Tests that no Custom Data is rendered if code is 204 @@ -751,8 +823,8 @@ func TestContextRenderNoContentData(t *testing.T) { c.Data(204, "text/csv", []byte(`foo,bar`)) assert.Equal(t, 204, w.Code) - assert.Equal(t, "", w.Body.String()) - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/csv") + assert.Empty(t, w.Body.String()) + assert.Equal(t, "text/csv", w.HeaderMap.Get("Content-Type")) } func TestContextRenderSSE(t *testing.T) { @@ -779,9 +851,9 @@ func TestContextRenderFile(t *testing.T) { c.Request, _ = http.NewRequest("GET", "/", nil) c.File("./gin.go") - assert.Equal(t, w.Code, 200) + assert.Equal(t, 200, w.Code) assert.Contains(t, w.Body.String(), "func New() *Engine {") - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") + assert.Equal(t, "text/plain; charset=utf-8", w.HeaderMap.Get("Content-Type")) } // TestContextRenderYAML tests that the response is serialized as YAML @@ -792,9 +864,9 @@ func TestContextRenderYAML(t *testing.T) { c.YAML(201, H{"foo": "bar"}) - assert.Equal(t, w.Code, 201) - assert.Equal(t, w.Body.String(), "foo: bar\n") - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/x-yaml; charset=utf-8") + assert.Equal(t, 201, w.Code) + assert.Equal(t, "foo: bar\n", w.Body.String()) + assert.Equal(t, "application/x-yaml; charset=utf-8", w.HeaderMap.Get("Content-Type")) } func TestContextHeaders(t *testing.T) { @@ -802,13 +874,13 @@ func TestContextHeaders(t *testing.T) { c.Header("Content-Type", "text/plain") c.Header("X-Custom", "value") - assert.Equal(t, c.Writer.Header().Get("Content-Type"), "text/plain") - assert.Equal(t, c.Writer.Header().Get("X-Custom"), "value") + assert.Equal(t, "text/plain", c.Writer.Header().Get("Content-Type")) + assert.Equal(t, "value", c.Writer.Header().Get("X-Custom")) c.Header("Content-Type", "text/html") c.Header("X-Custom", "") - assert.Equal(t, c.Writer.Header().Get("Content-Type"), "text/html") + assert.Equal(t, "text/html", c.Writer.Header().Get("Content-Type")) _, exist := c.Writer.Header()["X-Custom"] assert.False(t, exist) } @@ -824,8 +896,8 @@ func TestContextRenderRedirectWithRelativePath(t *testing.T) { c.Redirect(301, "/path") c.Writer.WriteHeaderNow() - assert.Equal(t, w.Code, 301) - assert.Equal(t, w.Header().Get("Location"), "/path") + assert.Equal(t, 301, w.Code) + assert.Equal(t, "/path", w.Header().Get("Location")) } func TestContextRenderRedirectWithAbsolutePath(t *testing.T) { @@ -836,8 +908,8 @@ func TestContextRenderRedirectWithAbsolutePath(t *testing.T) { c.Redirect(302, "http://google.com") c.Writer.WriteHeaderNow() - assert.Equal(t, w.Code, 302) - assert.Equal(t, w.Header().Get("Location"), "http://google.com") + assert.Equal(t, 302, w.Code) + assert.Equal(t, "http://google.com", w.Header().Get("Location")) } func TestContextRenderRedirectWith201(t *testing.T) { @@ -848,8 +920,8 @@ func TestContextRenderRedirectWith201(t *testing.T) { c.Redirect(201, "/resource") c.Writer.WriteHeaderNow() - assert.Equal(t, w.Code, 201) - assert.Equal(t, w.Header().Get("Location"), "/resource") + assert.Equal(t, 201, w.Code) + assert.Equal(t, "/resource", w.Header().Get("Location")) } func TestContextRenderRedirectAll(t *testing.T) { @@ -930,8 +1002,8 @@ func TestContextNegotiationFormat(t *testing.T) { c.Request, _ = http.NewRequest("POST", "", nil) assert.Panics(t, func() { c.NegotiateFormat() }) - assert.Equal(t, c.NegotiateFormat(MIMEJSON, MIMEXML), MIMEJSON) - assert.Equal(t, c.NegotiateFormat(MIMEHTML, MIMEJSON), MIMEHTML) + assert.Equal(t, MIMEJSON, c.NegotiateFormat(MIMEJSON, MIMEXML)) + assert.Equal(t, MIMEHTML, c.NegotiateFormat(MIMEHTML, MIMEJSON)) } func TestContextNegotiationFormatWithAccept(t *testing.T) { @@ -939,9 +1011,9 @@ func TestContextNegotiationFormatWithAccept(t *testing.T) { c.Request, _ = http.NewRequest("POST", "/", nil) c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") - assert.Equal(t, c.NegotiateFormat(MIMEJSON, MIMEXML), MIMEXML) - assert.Equal(t, c.NegotiateFormat(MIMEXML, MIMEHTML), MIMEHTML) - assert.Equal(t, c.NegotiateFormat(MIMEJSON), "") + assert.Equal(t, MIMEXML, c.NegotiateFormat(MIMEJSON, MIMEXML)) + assert.Equal(t, MIMEHTML, c.NegotiateFormat(MIMEXML, MIMEHTML)) + assert.Empty(t, c.NegotiateFormat(MIMEJSON)) } func TestContextNegotiationFormatCustum(t *testing.T) { @@ -952,9 +1024,9 @@ func TestContextNegotiationFormatCustum(t *testing.T) { c.Accepted = nil c.SetAccepted(MIMEJSON, MIMEXML) - assert.Equal(t, c.NegotiateFormat(MIMEJSON, MIMEXML), MIMEJSON) - assert.Equal(t, c.NegotiateFormat(MIMEXML, MIMEHTML), MIMEXML) - assert.Equal(t, c.NegotiateFormat(MIMEJSON), MIMEJSON) + assert.Equal(t, MIMEJSON, c.NegotiateFormat(MIMEJSON, MIMEXML)) + assert.Equal(t, MIMEXML, c.NegotiateFormat(MIMEXML, MIMEHTML)) + assert.Equal(t, MIMEJSON, c.NegotiateFormat(MIMEJSON)) } func TestContextIsAborted(t *testing.T) { @@ -980,9 +1052,9 @@ func TestContextAbortWithStatus(t *testing.T) { c.index = 4 c.AbortWithStatus(401) - assert.Equal(t, c.index, abortIndex) - assert.Equal(t, c.Writer.Status(), 401) - assert.Equal(t, w.Code, 401) + assert.Equal(t, abortIndex, c.index) + assert.Equal(t, 401, c.Writer.Status()) + assert.Equal(t, 401, w.Code) assert.True(t, c.IsAborted()) } @@ -1002,13 +1074,13 @@ func TestContextAbortWithStatusJSON(t *testing.T) { c.AbortWithStatusJSON(415, in) - assert.Equal(t, c.index, abortIndex) - assert.Equal(t, c.Writer.Status(), 415) - assert.Equal(t, w.Code, 415) + assert.Equal(t, abortIndex, c.index) + assert.Equal(t, 415, c.Writer.Status()) + assert.Equal(t, 415, w.Code) assert.True(t, c.IsAborted()) contentType := w.Header().Get("Content-Type") - assert.Equal(t, contentType, "application/json; charset=utf-8") + assert.Equal(t, "application/json; charset=utf-8", contentType) buf := new(bytes.Buffer) buf.ReadFrom(w.Body) @@ -1022,7 +1094,7 @@ func TestContextError(t *testing.T) { c.Error(errors.New("first error")) assert.Len(t, c.Errors, 1) - assert.Equal(t, c.Errors.String(), "Error #01: first error\n") + assert.Equal(t, "Error #01: first error\n", c.Errors.String()) c.Error(&Error{ Err: errors.New("second error"), @@ -1031,13 +1103,13 @@ func TestContextError(t *testing.T) { }) assert.Len(t, c.Errors, 2) - assert.Equal(t, c.Errors[0].Err, errors.New("first error")) + assert.Equal(t, errors.New("first error"), c.Errors[0].Err) assert.Nil(t, c.Errors[0].Meta) - assert.Equal(t, c.Errors[0].Type, ErrorTypePrivate) + assert.Equal(t, ErrorTypePrivate, c.Errors[0].Type) - assert.Equal(t, c.Errors[1].Err, errors.New("second error")) - assert.Equal(t, c.Errors[1].Meta, "some data 2") - assert.Equal(t, c.Errors[1].Type, ErrorTypePublic) + assert.Equal(t, errors.New("second error"), c.Errors[1].Err) + assert.Equal(t, "some data 2", c.Errors[1].Meta) + assert.Equal(t, ErrorTypePublic, c.Errors[1].Type) assert.Equal(t, c.Errors.Last(), c.Errors[1]) @@ -1055,12 +1127,12 @@ func TestContextTypedError(t *testing.T) { c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate) for _, err := range c.Errors.ByType(ErrorTypePublic) { - assert.Equal(t, err.Type, ErrorTypePublic) + assert.Equal(t, ErrorTypePublic, err.Type) } for _, err := range c.Errors.ByType(ErrorTypePrivate) { - assert.Equal(t, err.Type, ErrorTypePrivate) + assert.Equal(t, ErrorTypePrivate, err.Type) } - assert.Equal(t, c.Errors.Errors(), []string{"externo 0", "interno 0"}) + assert.Equal(t, []string{"externo 0", "interno 0"}, c.Errors.Errors()) } func TestContextAbortWithError(t *testing.T) { @@ -1069,8 +1141,8 @@ func TestContextAbortWithError(t *testing.T) { c.AbortWithError(401, errors.New("bad input")).SetMeta("some input") - assert.Equal(t, w.Code, 401) - assert.Equal(t, c.index, abortIndex) + assert.Equal(t, 401, w.Code) + assert.Equal(t, abortIndex, c.index) assert.True(t, c.IsAborted()) } @@ -1101,7 +1173,7 @@ func TestContextClientIP(t *testing.T) { // no port c.Request.RemoteAddr = "50.50.50.50" - assert.Equal(t, "", c.ClientIP()) + assert.Empty(t, c.ClientIP()) } func TestContextContentType(t *testing.T) { @@ -1109,7 +1181,7 @@ func TestContextContentType(t *testing.T) { c.Request, _ = http.NewRequest("POST", "/", nil) c.Request.Header.Set("Content-Type", "application/json; charset=utf-8") - assert.Equal(t, c.ContentType(), "application/json") + assert.Equal(t, "application/json", c.ContentType()) } func TestContextAutoBindJSON(t *testing.T) { @@ -1122,8 +1194,8 @@ func TestContextAutoBindJSON(t *testing.T) { Bar string `json:"bar"` } assert.NoError(t, c.Bind(&obj)) - assert.Equal(t, obj.Bar, "foo") - assert.Equal(t, obj.Foo, "bar") + assert.Equal(t, "foo", obj.Bar) + assert.Equal(t, "bar", obj.Foo) assert.Empty(t, c.Errors) } @@ -1139,9 +1211,25 @@ func TestContextBindWithJSON(t *testing.T) { Bar string `json:"bar"` } assert.NoError(t, c.BindJSON(&obj)) - assert.Equal(t, obj.Bar, "foo") - assert.Equal(t, obj.Foo, "bar") - assert.Equal(t, w.Body.Len(), 0) + assert.Equal(t, "foo", obj.Bar) + assert.Equal(t, "bar", obj.Foo) + assert.Equal(t, 0, w.Body.Len()) +} + +func TestContextBindWithQuery(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.Request, _ = http.NewRequest("POST", "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused")) + + var obj struct { + Foo string `form:"foo"` + Bar string `form:"bar"` + } + assert.NoError(t, c.BindQuery(&obj)) + assert.Equal(t, "foo", obj.Bar) + assert.Equal(t, "bar", obj.Foo) + assert.Equal(t, 0, w.Body.Len()) } func TestContextBadAutoBind(t *testing.T) { @@ -1161,10 +1249,77 @@ func TestContextBadAutoBind(t *testing.T) { assert.Empty(t, obj.Bar) assert.Empty(t, obj.Foo) - assert.Equal(t, w.Code, 400) + assert.Equal(t, 400, w.Code) assert.True(t, c.IsAborted()) } +func TestContextAutoShouldBindJSON(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) + c.Request.Header.Add("Content-Type", MIMEJSON) + + var obj struct { + Foo string `json:"foo"` + Bar string `json:"bar"` + } + assert.NoError(t, c.ShouldBind(&obj)) + assert.Equal(t, "foo", obj.Bar) + assert.Equal(t, "bar", obj.Foo) + assert.Empty(t, c.Errors) +} + +func TestContextShouldBindWithJSON(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) + c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type + + var obj struct { + Foo string `json:"foo"` + Bar string `json:"bar"` + } + assert.NoError(t, c.ShouldBindJSON(&obj)) + assert.Equal(t, "foo", obj.Bar) + assert.Equal(t, "bar", obj.Foo) + assert.Equal(t, 0, w.Body.Len()) +} + +func TestContextShouldBindWithQuery(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.Request, _ = http.NewRequest("POST", "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused")) + + var obj struct { + Foo string `form:"foo"` + Bar string `form:"bar"` + } + assert.NoError(t, c.ShouldBindQuery(&obj)) + assert.Equal(t, "foo", obj.Bar) + assert.Equal(t, "bar", obj.Foo) + assert.Equal(t, 0, w.Body.Len()) +} + +func TestContextBadAutoShouldBind(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.Request, _ = http.NewRequest("POST", "http://example.com", bytes.NewBufferString("\"foo\":\"bar\", \"bar\":\"foo\"}")) + c.Request.Header.Add("Content-Type", MIMEJSON) + var obj struct { + Foo string `json:"foo"` + Bar string `json:"bar"` + } + + assert.False(t, c.IsAborted()) + assert.Error(t, c.ShouldBind(&obj)) + + assert.Empty(t, obj.Bar) + assert.Empty(t, obj.Foo) + assert.False(t, c.IsAborted()) +} + func TestContextGolangContext(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) @@ -1177,7 +1332,7 @@ func TestContextGolangContext(t *testing.T) { assert.Nil(t, c.Value("foo")) c.Set("foo", "bar") - assert.Equal(t, c.Value("foo"), "bar") + assert.Equal(t, "bar", c.Value("foo")) assert.Nil(t, c.Value(1)) } @@ -1209,7 +1364,7 @@ func TestGetRequestHeaderValue(t *testing.T) { c.Request.Header.Set("Gin-Version", "1.0.0") assert.Equal(t, "1.0.0", c.GetHeader("Gin-Version")) - assert.Equal(t, "", c.GetHeader("Connection")) + assert.Empty(t, c.GetHeader("Connection")) } func TestContextGetRawData(t *testing.T) { diff --git a/coverage.sh b/coverage.sh new file mode 100644 index 00000000..81437f91 --- /dev/null +++ b/coverage.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -e + +echo "mode: count" > coverage.out + +for d in $(go list ./... | grep -E 'gin$|binding$|render$'); do + go test -v -covermode=count -coverprofile=profile.out $d + if [ -f profile.out ]; then + cat profile.out | grep -v "mode:" >> coverage.out + rm profile.out + fi +done diff --git a/debug.go b/debug.go index b31ca685..897c4943 100644 --- a/debug.go +++ b/debug.go @@ -15,7 +15,7 @@ func init() { } // IsDebugging returns true if the framework is running in debug mode. -// Use SetMode(gin.Release) to disable debug mode. +// Use SetMode(gin.ReleaseMode) to disable debug mode. func IsDebugging() bool { return ginMode == debugCode } @@ -46,6 +46,12 @@ func debugPrint(format string, values ...interface{}) { } } +func debugPrintWARNINGDefault() { + debugPrint(`[WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. + +`) +} + func debugPrintWARNINGNew() { debugPrint(`[WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release diff --git a/debug_test.go b/debug_test.go index 2b010858..dfd54c82 100644 --- a/debug_test.go +++ b/debug_test.go @@ -42,7 +42,7 @@ func TestDebugPrint(t *testing.T) { SetMode(DebugMode) debugPrint("these are %d %s\n", 2, "error messages") - assert.Equal(t, w.String(), "[GIN-debug] these are 2 error messages\n") + assert.Equal(t, "[GIN-debug] these are 2 error messages\n", w.String()) } func TestDebugPrintError(t *testing.T) { @@ -55,7 +55,7 @@ func TestDebugPrintError(t *testing.T) { assert.Empty(t, w.String()) debugPrintError(errors.New("this is an error")) - assert.Equal(t, w.String(), "[GIN-debug] [ERROR] this is an error\n") + assert.Equal(t, "[GIN-debug] [ERROR] this is an error\n", w.String()) } func TestDebugPrintRoutes(t *testing.T) { @@ -83,7 +83,25 @@ func TestDebugPrintWARNINGSetHTMLTemplate(t *testing.T) { defer teardown() debugPrintWARNINGSetHTMLTemplate() - assert.Equal(t, w.String(), "[GIN-debug] [WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called\nat initialization. ie. before any route is registered or the router is listening in a socket:\n\n\trouter := gin.Default()\n\trouter.SetHTMLTemplate(template) // << good place\n\n") + assert.Equal(t, "[GIN-debug] [WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called\nat initialization. ie. before any route is registered or the router is listening in a socket:\n\n\trouter := gin.Default()\n\trouter.SetHTMLTemplate(template) // << good place\n\n", w.String()) +} + +func TestDebugPrintWARNINGDefault(t *testing.T) { + var w bytes.Buffer + setup(&w) + defer teardown() + + debugPrintWARNINGDefault() + assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", w.String()) +} + +func TestDebugPrintWARNINGNew(t *testing.T) { + var w bytes.Buffer + setup(&w) + defer teardown() + + debugPrintWARNINGNew() + assert.Equal(t, "[GIN-debug] [WARNING] Running in \"debug\" mode. Switch to \"release\" mode in production.\n - using env:\texport GIN_MODE=release\n - using code:\tgin.SetMode(gin.ReleaseMode)\n\n", w.String()) } func setup(w io.Writer) { diff --git a/deprecated.go b/deprecated.go index 7b50dc70..ab447429 100644 --- a/deprecated.go +++ b/deprecated.go @@ -15,7 +15,7 @@ import ( func (c *Context) BindWith(obj interface{}, b binding.Binding) error { log.Println(`BindWith(\"interface{}, binding.Binding\") error is going to be deprecated, please check issue #662 and either use MustBindWith() if you - want HTTP 400 to be automatically returned if any error occur, of use + want HTTP 400 to be automatically returned if any error occur, or use ShouldBindWith() if you need to manage the error.`) return c.MustBindWith(obj, b) } diff --git a/deprecated_test.go b/deprecated_test.go new file mode 100644 index 00000000..7a875fe4 --- /dev/null +++ b/deprecated_test.go @@ -0,0 +1,31 @@ +// 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. + +package gin + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin/binding" + "github.com/stretchr/testify/assert" +) + +func TestBindWith(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.Request, _ = http.NewRequest("POST", "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused")) + + var obj struct { + Foo string `form:"foo"` + Bar string `form:"bar"` + } + assert.NoError(t, c.BindWith(&obj, binding.Form)) + assert.Equal(t, "foo", obj.Bar) + assert.Equal(t, "bar", obj.Foo) + assert.Equal(t, 0, w.Body.Len()) +} diff --git a/errors.go b/errors.go index 26d4e474..dbfccd85 100644 --- a/errors.go +++ b/errors.go @@ -9,11 +9,9 @@ import ( "fmt" "reflect" - "github.com/json-iterator/go" + "github.com/gin-gonic/gin/json" ) -var json = jsoniter.ConfigCompatibleWithStandardLibrary - type ErrorType uint64 const ( @@ -67,7 +65,7 @@ func (msg *Error) JSON() interface{} { return json } -// MarshalJSON implements the json.Marshaller interface +// MarshalJSON implements the json.Marshaller interface. func (msg *Error) MarshalJSON() ([]byte, error) { return json.Marshal(msg.JSON()) } @@ -82,7 +80,7 @@ func (msg *Error) IsType(flags ErrorType) bool { } // ByType returns a readonly copy filtered the byte. -// ie ByType(gin.ErrorTypePublic) returns a slice of errors with type=ErrorTypePublic +// ie ByType(gin.ErrorTypePublic) returns a slice of errors with type=ErrorTypePublic. func (a errorMsgs) ByType(typ ErrorType) errorMsgs { if len(a) == 0 { return nil @@ -100,7 +98,7 @@ func (a errorMsgs) ByType(typ ErrorType) errorMsgs { } // Last returns the last error in the slice. It returns nil if the array is empty. -// Shortcut for errors[len(errors)-1] +// Shortcut for errors[len(errors)-1]. func (a errorMsgs) Last() *Error { if length := len(a); length > 0 { return a[length-1] @@ -150,7 +148,7 @@ func (a errorMsgs) String() string { } var buffer bytes.Buffer for i, msg := range a { - fmt.Fprintf(&buffer, "Error #%02d: %s\n", (i + 1), msg.Err) + fmt.Fprintf(&buffer, "Error #%02d: %s\n", i+1, msg.Err) if msg.Meta != nil { fmt.Fprintf(&buffer, " Meta: %v\n", msg.Meta) } diff --git a/errors_test.go b/errors_test.go index a0d95504..a666d7c1 100644 --- a/errors_test.go +++ b/errors_test.go @@ -8,6 +8,7 @@ import ( "errors" "testing" + "github.com/gin-gonic/gin/json" "github.com/stretchr/testify/assert" ) @@ -59,7 +60,7 @@ func TestError(t *testing.T) { data string } err.SetMeta(customError{status: "200", data: "other data"}) - assert.Equal(t, err.JSON(), customError{status: "200", data: "other data"}) + assert.Equal(t, customError{status: "200", data: "other data"}, err.JSON()) } func TestErrorSlice(t *testing.T) { @@ -70,33 +71,33 @@ func TestErrorSlice(t *testing.T) { } assert.Equal(t, errs, errs.ByType(ErrorTypeAny)) - assert.Equal(t, errs.Last().Error(), "third") - assert.Equal(t, errs.Errors(), []string{"first", "second", "third"}) - assert.Equal(t, errs.ByType(ErrorTypePublic).Errors(), []string{"third"}) - assert.Equal(t, errs.ByType(ErrorTypePrivate).Errors(), []string{"first", "second"}) - assert.Equal(t, errs.ByType(ErrorTypePublic|ErrorTypePrivate).Errors(), []string{"first", "second", "third"}) + assert.Equal(t, "third", errs.Last().Error()) + assert.Equal(t, []string{"first", "second", "third"}, errs.Errors()) + assert.Equal(t, []string{"third"}, errs.ByType(ErrorTypePublic).Errors()) + assert.Equal(t, []string{"first", "second"}, errs.ByType(ErrorTypePrivate).Errors()) + assert.Equal(t, []string{"first", "second", "third"}, errs.ByType(ErrorTypePublic|ErrorTypePrivate).Errors()) assert.Empty(t, errs.ByType(ErrorTypeBind)) assert.Empty(t, errs.ByType(ErrorTypeBind).String()) - assert.Equal(t, errs.String(), `Error #01: first + assert.Equal(t, `Error #01: first Error #02: second Meta: some data Error #03: third Meta: map[status:400] -`) - assert.Equal(t, errs.JSON(), []interface{}{ +`, errs.String()) + assert.Equal(t, []interface{}{ H{"error": "first"}, H{"error": "second", "meta": "some data"}, H{"error": "third", "status": "400"}, - }) + }, errs.JSON()) jsonBytes, _ := json.Marshal(errs) assert.Equal(t, "[{\"error\":\"first\"},{\"error\":\"second\",\"meta\":\"some data\"},{\"error\":\"third\",\"status\":\"400\"}]", string(jsonBytes)) errs = errorMsgs{ {Err: errors.New("first"), Type: ErrorTypePrivate}, } - assert.Equal(t, errs.JSON(), H{"error": "first"}) + assert.Equal(t, H{"error": "first"}, errs.JSON()) jsonBytes, _ = json.Marshal(errs) - assert.Equal(t, string(jsonBytes), "{\"error\":\"first\"}") + assert.Equal(t, "{\"error\":\"first\"}", string(jsonBytes)) errs = errorMsgs{} assert.Nil(t, errs.Last()) diff --git a/examples/app-engine/README.md b/examples/app-engine/README.md index 48505de8..b3dd7c78 100644 --- a/examples/app-engine/README.md +++ b/examples/app-engine/README.md @@ -1,7 +1,8 @@ # Guide to run Gin under App Engine LOCAL Development Server 1. Download, install and setup Go in your computer. (That includes setting your `$GOPATH`.) -2. Download SDK for your platform from here: `https://developers.google.com/appengine/downloads?hl=es#Google_App_Engine_SDK_for_Go` +2. Download SDK for your platform from [here](https://cloud.google.com/appengine/docs/standard/go/download): `https://cloud.google.com/appengine/docs/standard/go/download` 3. Download Gin source code using: `$ go get github.com/gin-gonic/gin` -4. Navigate to examples folder: `$ cd $GOPATH/src/github.com/gin-gonic/gin/examples/` -5. Run it: `$ goapp serve app-engine/` \ No newline at end of file +4. Navigate to examples folder: `$ cd $GOPATH/src/github.com/gin-gonic/gin/examples/app-engine/` +5. Run it: `$ dev_appserver.py .` (notice that you have to run this script by Python2) + diff --git a/examples/app-engine/hello.go b/examples/app-engine/hello.go index da7e4ae4..f569dadb 100644 --- a/examples/app-engine/hello.go +++ b/examples/app-engine/hello.go @@ -13,10 +13,10 @@ func init() { // Define your handlers r.GET("/", func(c *gin.Context) { - c.String(200, "Hello World!") + c.String(http.StatusOK, "Hello World!") }) r.GET("/ping", func(c *gin.Context) { - c.String(200, "pong") + c.String(http.StatusOK, "pong") }) // Handle all requests using net/http diff --git a/examples/auto-tls/example1.go b/examples/auto-tls/example1/main.go similarity index 100% rename from examples/auto-tls/example1.go rename to examples/auto-tls/example1/main.go diff --git a/examples/auto-tls/example2.go b/examples/auto-tls/example2/main.go similarity index 100% rename from examples/auto-tls/example2.go rename to examples/auto-tls/example2/main.go diff --git a/examples/basic/main.go b/examples/basic/main.go index 984c06ab..473c6a09 100644 --- a/examples/basic/main.go +++ b/examples/basic/main.go @@ -6,7 +6,7 @@ import ( var DB = make(map[string]string) -func main() { +func setupRouter() *gin.Engine { // Disable Console Color // gin.DisableConsoleColor() r := gin.Default() @@ -53,6 +53,11 @@ func main() { } }) + return r +} + +func main() { + r := setupRouter() // Listen and Server in 0.0.0.0:8080 r.Run(":8080") } diff --git a/examples/basic/main_test.go b/examples/basic/main_test.go new file mode 100644 index 00000000..61203d66 --- /dev/null +++ b/examples/basic/main_test.go @@ -0,0 +1,20 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPingRoute(t *testing.T) { + router := setupRouter() + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/ping", nil) + router.ServeHTTP(w, req) + + assert.Equal(t, 200, w.Code) + assert.Equal(t, "pong", w.Body.String()) +} diff --git a/examples/custom-validation/server.go b/examples/custom-validation/server.go new file mode 100644 index 00000000..dea0c302 --- /dev/null +++ b/examples/custom-validation/server.go @@ -0,0 +1,49 @@ +package main + +import ( + "net/http" + "reflect" + "time" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "gopkg.in/go-playground/validator.v8" +) + +type Booking struct { + CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"` + CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"` +} + +func bookableDate( + v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value, + field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string, +) bool { + if date, ok := field.Interface().(time.Time); ok { + today := time.Now() + if today.Year() > date.Year() || today.YearDay() > date.YearDay() { + return false + } + } + return true +} + +func main() { + route := gin.Default() + + if v, ok := binding.Validator.Engine().(*validator.Validate); ok { + v.RegisterValidation("bookabledate", bookableDate) + } + + route.GET("/bookable", getBookable) + route.Run(":8085") +} + +func getBookable(c *gin.Context) { + var b Booking + if err := c.ShouldBindWith(&b, binding.Query); err == nil { + c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"}) + } else { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } +} diff --git a/examples/graceful-shutdown/close/server.go b/examples/graceful-shutdown/close/server.go index 54778393..9c4e90fa 100644 --- a/examples/graceful-shutdown/close/server.go +++ b/examples/graceful-shutdown/close/server.go @@ -41,5 +41,5 @@ func main() { } } - log.Println("Server exist") + log.Println("Server exiting") } diff --git a/examples/graceful-shutdown/graceful-shutdown/server.go b/examples/graceful-shutdown/graceful-shutdown/server.go index 060de081..af4f2146 100644 --- a/examples/graceful-shutdown/graceful-shutdown/server.go +++ b/examples/graceful-shutdown/graceful-shutdown/server.go @@ -27,8 +27,8 @@ func main() { go func() { // service connections - if err := srv.ListenAndServe(); err != nil { - log.Printf("listen: %s\n", err) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("listen: %s\n", err) } }() @@ -44,5 +44,5 @@ func main() { if err := srv.Shutdown(ctx); err != nil { log.Fatal("Server Shutdown:", err) } - log.Println("Server exist") + log.Println("Server exiting") } diff --git a/examples/multiple-service/main.go b/examples/multiple-service/main.go new file mode 100644 index 00000000..ceddaa2e --- /dev/null +++ b/examples/multiple-service/main.go @@ -0,0 +1,74 @@ +package main + +import ( + "log" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "golang.org/x/sync/errgroup" +) + +var ( + g errgroup.Group +) + +func router01() http.Handler { + e := gin.New() + e.Use(gin.Recovery()) + e.GET("/", func(c *gin.Context) { + c.JSON( + http.StatusOK, + gin.H{ + "code": http.StatusOK, + "error": "Welcome server 01", + }, + ) + }) + + return e +} + +func router02() http.Handler { + e := gin.New() + e.Use(gin.Recovery()) + e.GET("/", func(c *gin.Context) { + c.JSON( + http.StatusOK, + gin.H{ + "code": http.StatusOK, + "error": "Welcome server 02", + }, + ) + }) + + return e +} + +func main() { + server01 := &http.Server{ + Addr: ":8080", + Handler: router01(), + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + } + + server02 := &http.Server{ + Addr: ":8081", + Handler: router02(), + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + } + + g.Go(func() error { + return server01.ListenAndServe() + }) + + g.Go(func() error { + return server02.ListenAndServe() + }) + + if err := g.Wait(); err != nil { + log.Fatal(err) + } +} diff --git a/examples/realtime-advanced/stats.go b/examples/realtime-advanced/stats.go index 4bca3ae4..4afedcb5 100644 --- a/examples/realtime-advanced/stats.go +++ b/examples/realtime-advanced/stats.go @@ -29,8 +29,8 @@ func statsWorker() { "timestamp": uint64(time.Now().Unix()), "HeapInuse": stats.HeapInuse, "StackInuse": stats.StackInuse, - "Mallocs": (stats.Mallocs - lastMallocs), - "Frees": (stats.Frees - lastFrees), + "Mallocs": stats.Mallocs - lastMallocs, + "Frees": stats.Frees - lastFrees, "Inbound": uint64(messages.Get("inbound")), "Outbound": uint64(messages.Get("outbound")), "Connected": connectedUsers(), diff --git a/examples/struct-lvl-validations/README.md b/examples/struct-lvl-validations/README.md new file mode 100644 index 00000000..1bd57f03 --- /dev/null +++ b/examples/struct-lvl-validations/README.md @@ -0,0 +1,50 @@ +## Struct level validations + +Validations can also be registered at the `struct` level when field level validations +don't make much sense. This can also be used to solve cross-field validation elegantly. +Additionally, it can be combined with tag validations. Struct Level validations run after +the structs tag validations. + +### Example requests + +```shell +# Validation errors are generated for struct tags as well as at the struct level +$ curl -s -X POST http://localhost:8085/user \ + -H 'content-type: application/json' \ + -d '{}' | jq +{ + "error": "Key: 'User.Email' Error:Field validation for 'Email' failed on the 'required' tag\nKey: 'User.FirstName' Error:Field validation for 'FirstName' failed on the 'fnameorlname' tag\nKey: 'User.LastName' Error:Field validation for 'LastName' failed on the 'fnameorlname' tag", + "message": "User validation failed!" +} + +# Validation fails at the struct level because neither first name nor last name are present +$ curl -s -X POST http://localhost:8085/user \ + -H 'content-type: application/json' \ + -d '{"email": "george@vandaley.com"}' | jq +{ + "error": "Key: 'User.FirstName' Error:Field validation for 'FirstName' failed on the 'fnameorlname' tag\nKey: 'User.LastName' Error:Field validation for 'LastName' failed on the 'fnameorlname' tag", + "message": "User validation failed!" +} + +# No validation errors when either first name or last name is present +$ curl -X POST http://localhost:8085/user \ + -H 'content-type: application/json' \ + -d '{"fname": "George", "email": "george@vandaley.com"}' +{"message":"User validation successful."} + +$ curl -X POST http://localhost:8085/user \ + -H 'content-type: application/json' \ + -d '{"lname": "Contanza", "email": "george@vandaley.com"}' +{"message":"User validation successful."} + +$ curl -X POST http://localhost:8085/user \ + -H 'content-type: application/json' \ + -d '{"fname": "George", "lname": "Costanza", "email": "george@vandaley.com"}' +{"message":"User validation successful."} +``` + +### Useful links + +- Validator docs - https://godoc.org/gopkg.in/go-playground/validator.v8#Validate.RegisterStructValidation +- Struct level example - https://github.com/go-playground/validator/blob/v8.18.2/examples/struct-level/struct_level.go +- Validator release notes - https://github.com/go-playground/validator/releases/tag/v8.7 diff --git a/examples/struct-lvl-validations/server.go b/examples/struct-lvl-validations/server.go new file mode 100644 index 00000000..be807b78 --- /dev/null +++ b/examples/struct-lvl-validations/server.go @@ -0,0 +1,64 @@ +package main + +import ( + "net/http" + "reflect" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + validator "gopkg.in/go-playground/validator.v8" +) + +// User contains user information. +type User struct { + FirstName string `json:"fname"` + LastName string `json:"lname"` + Email string `binding:"required,email"` +} + +// UserStructLevelValidation contains custom struct level validations that don't always +// make sense at the field validation level. For example, this function validates that either +// FirstName or LastName exist; could have done that with a custom field validation but then +// would have had to add it to both fields duplicating the logic + overhead, this way it's +// only validated once. +// +// NOTE: you may ask why wouldn't not just do this outside of validator. Doing this way +// hooks right into validator and you can combine with validation tags and still have a +// common error output format. +func UserStructLevelValidation(v *validator.Validate, structLevel *validator.StructLevel) { + user := structLevel.CurrentStruct.Interface().(User) + + if len(user.FirstName) == 0 && len(user.LastName) == 0 { + structLevel.ReportError( + reflect.ValueOf(user.FirstName), "FirstName", "fname", "fnameorlname", + ) + structLevel.ReportError( + reflect.ValueOf(user.LastName), "LastName", "lname", "fnameorlname", + ) + } + + // plus can to more, even with different tag than "fnameorlname" +} + +func main() { + route := gin.Default() + + if v, ok := binding.Validator.Engine().(*validator.Validate); ok { + v.RegisterStructValidation(UserStructLevelValidation, User{}) + } + + route.POST("/user", validateUser) + route.Run(":8085") +} + +func validateUser(c *gin.Context) { + var u User + if err := c.ShouldBindJSON(&u); err == nil { + c.JSON(http.StatusOK, gin.H{"message": "User validation successful."}) + } else { + c.JSON(http.StatusBadRequest, gin.H{ + "message": "User validation failed!", + "error": err.Error(), + }) + } +} diff --git a/examples/template/main.go b/examples/template/main.go new file mode 100644 index 00000000..f9e611df --- /dev/null +++ b/examples/template/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "fmt" + "html/template" + "net/http" + "time" + + "github.com/gin-gonic/gin" +) + +func formatAsDate(t time.Time) string { + year, month, day := t.Date() + return fmt.Sprintf("%d%02d/%02d", year, month, day) +} + +func main() { + router := gin.Default() + router.Delims("{[{", "}]}") + router.SetFuncMap(template.FuncMap{ + "formatAsDate": formatAsDate, + }) + router.LoadHTMLFiles("../../fixtures/basic/raw.tmpl") + + router.GET("/raw", func(c *gin.Context) { + c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{ + "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), + }) + }) + + router.Run(":8080") +} diff --git a/examples/upload-file/multiple/main.go b/examples/upload-file/multiple/main.go index 22588348..a55325ed 100644 --- a/examples/upload-file/multiple/main.go +++ b/examples/upload-file/multiple/main.go @@ -2,15 +2,15 @@ package main import ( "fmt" - "io" "net/http" - "os" "github.com/gin-gonic/gin" ) func main() { router := gin.Default() + // Set a lower memory limit for multipart forms (default is 32 MiB) + router.MaxMultipartMemory = 8 << 20 // 8 MiB router.Static("/", "./public") router.POST("/upload", func(c *gin.Context) { name := c.PostForm("name") @@ -25,24 +25,10 @@ func main() { files := form.File["files"] for _, file := range files { - // Source - src, err := file.Open() - if err != nil { - c.String(http.StatusBadRequest, fmt.Sprintf("file open err: %s", err.Error())) + if err := c.SaveUploadedFile(file, file.Filename); err != nil { + c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error())) return } - defer src.Close() - - // Destination - dst, err := os.Create(file.Filename) - if err != nil { - c.String(http.StatusBadRequest, fmt.Sprintf("Create file err: %s", err.Error())) - return - } - defer dst.Close() - - // Copy - io.Copy(dst, src) } c.String(http.StatusOK, fmt.Sprintf("Uploaded successfully %d files with fields name=%s and email=%s.", len(files), name, email)) diff --git a/examples/upload-file/single/main.go b/examples/upload-file/single/main.go index 1e9596cb..5d438651 100644 --- a/examples/upload-file/single/main.go +++ b/examples/upload-file/single/main.go @@ -2,15 +2,15 @@ package main import ( "fmt" - "io" "net/http" - "os" "github.com/gin-gonic/gin" ) func main() { router := gin.Default() + // Set a lower memory limit for multipart forms (default is 32 MiB) + router.MaxMultipartMemory = 8 << 20 // 8 MiB router.Static("/", "./public") router.POST("/upload", func(c *gin.Context) { name := c.PostForm("name") @@ -22,23 +22,11 @@ func main() { c.String(http.StatusBadRequest, fmt.Sprintf("get form err: %s", err.Error())) return } - src, err := file.Open() - if err != nil { - c.String(http.StatusBadRequest, fmt.Sprintf("file open err: %s", err.Error())) + + if err := c.SaveUploadedFile(file, file.Filename); err != nil { + c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error())) return } - defer src.Close() - - // Destination - dst, err := os.Create(file.Filename) - if err != nil { - c.String(http.StatusBadRequest, fmt.Sprintf("Create file err: %s", err.Error())) - return - } - defer dst.Close() - - // Copy - io.Copy(dst, src) c.String(http.StatusOK, fmt.Sprintf("File %s uploaded successfully with fields name=%s and email=%s.", file.Filename, name, email)) }) diff --git a/fixtures/testdata/cert.pem b/fixtures/testdata/cert.pem new file mode 100644 index 00000000..c1d3d632 --- /dev/null +++ b/fixtures/testdata/cert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC9DCCAdygAwIBAgIQUNSK+OxWHYYFxHVJV0IlpDANBgkqhkiG9w0BAQsFADAS +MRAwDgYDVQQKEwdBY21lIENvMB4XDTE3MTExNjEyMDA0N1oXDTE4MTExNjEyMDA0 +N1owEjEQMA4GA1UEChMHQWNtZSBDbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAKmyj/YZpD59Bpy4w3qf6VzMw9uUBsWp+IP4kl7z5cmGHYUHn/YopTLH +vR23GAB12p6Km5QWzCBuJF4j61PJXHfg3/rjShZ77JcQ3kzxuy1iKDI+DNKN7Klz +rdjJ49QD0lczZHeBvvCL7JsJFKFjGy62rnStuW8LaIEdtjXT+GUZTxJh6G7yPYfD +MS1IsdMQGOdbGwNa+qogMuQPh0TzHw+C73myKrjY6pREijknMC/rnIMz9dLPt6Kl +xXy4br443dpY6dYGIhDuKhROT+vZ05HKasuuQUFhY7v/KoUpEZMB9rfUSzjQ5fak +eDUAMniXRcd+DmwvboG2TI6ixmuPK+ECAwEAAaNGMEQwDgYDVR0PAQH/BAQDAgWg +MBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwDwYDVR0RBAgwBocE +fwAAATANBgkqhkiG9w0BAQsFAAOCAQEAMXOLvj7BFsxdbcfRPBd0OFrH/8lI7vPV +LRcJ6r5iv0cnNvZXXbIOQLbg4clJAWjoE08nRm1KvNXhKdns0ELEV86YN2S6jThq +rIGrBqKtaJLB3M9BtDSiQ6SGPLYrWvmhj3Avi8PbSGy51bpGbqopd16j6LYU7Cp2 +TefMRlOAFtHojpCVon1CMpqcNxS0WNlQ3lUBSrw3HB0o12x++roja2ibF54tSHXB +KUuadoEzN+mMBwenEBychmAGzdiG4GQHRmhigh85+mtW6UMGiqyCZHs0EgE9FCLL +sRrsTI/VOzLz6lluygXkOsXrP+PP0SvmE3eylWjj9e2nj/u/Cy2YKg== +-----END CERTIFICATE----- diff --git a/fixtures/testdata/key.pem b/fixtures/testdata/key.pem new file mode 100644 index 00000000..c2a0181f --- /dev/null +++ b/fixtures/testdata/key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAqbKP9hmkPn0GnLjDep/pXMzD25QGxan4g/iSXvPlyYYdhQef +9iilMse9HbcYAHXanoqblBbMIG4kXiPrU8lcd+Df+uNKFnvslxDeTPG7LWIoMj4M +0o3sqXOt2Mnj1APSVzNkd4G+8IvsmwkUoWMbLraudK25bwtogR22NdP4ZRlPEmHo +bvI9h8MxLUix0xAY51sbA1r6qiAy5A+HRPMfD4LvebIquNjqlESKOScwL+ucgzP1 +0s+3oqXFfLhuvjjd2ljp1gYiEO4qFE5P69nTkcpqy65BQWFju/8qhSkRkwH2t9RL +ONDl9qR4NQAyeJdFx34ObC9ugbZMjqLGa48r4QIDAQABAoIBAD5mhd+GMEo2KU9J +9b/Ku8I/HapJtW/L/7Fvn0tBPncrVQGM+zpGWfDhV95sbGwG6lwwNeNvuqIWPlNL +vAY0XkdKrrIQEDdSXH50WnpKzXxzwrou7QIj5Cmvevbjzl4xBZDBOilj0XWczmV4 +IljyG5XC4UXQeAaoWEZaSZ1jk8yAt2Zq1Hgg7HqhHsK/arWXBgax+4K5nV/s9gZx +yjKU9mXTIs7k/aNnZqwQKqcZF+l3mvbZttOaFwsP14H0I8OFWhnM9hie54Dejqxi +f4/llNxDqUs6lqJfP3qNxtORLcFe75M+Yl8v7g2hkjtLdZBakPzSTEx3TAK/UHgi +aM8DdxECgYEA3fmg/PI4EgUEj0C3SCmQXR/CnQLMUQgb54s0asp4akvp+M7YCcr1 +pQd3HFUpBwhBcJg5LeSe87vLupY7pHCKk56cl9WY6hse0b9sP/7DWJuGiO62m0E0 +vNjQ2jpG99oR2ROIHHeWsGCpGLmrRT/kY+vR3M+AOLZniXlOCw8k0aUCgYEAw7WL +XFWLxgZYQYilywqrQmfv1MBfaUCvykO6oWB+f6mmnihSFjecI+nDw/b3yXVYGEgy +0ebkuw0jP8suC8wBqX9WuXj+9nZNomJRssJyOMiEhDEqUiTztFPSp9pdruoakLTh +Wk1p9NralOqGPUmxpXlFKVmYRTUbluikVxDypI0CgYBn6sqEQH0hann0+o4TWWn9 +PrYkPUAbm1k8771tVTZERR/W3Dbldr/DL5iCihe39BR2urziEEqdvkglJNntJMar +TzDuIBADYQjvltb9qq4XGFBGYMLaMg+XbUVxNKEuvUdnwa4R7aZ9EfN34MwekkfA +w5Cu9/GGG1ajVEfGA6PwBQKBgA3o71jGs8KFXOx7e90sivOTU5Z5fc6LTHNB0Rf7 +NcJ5GmCPWRY/KZfb25AoE4B8GKDRMNt+X69zxZeZJ1KrU0rqxA02rlhyHB54gnoE +G/4xMkn6/JkOC0w70PMhMBtohC7YzFOQwQEoNPT0nkno3Pl33xSLS6lPlwBo1JVj +nPtZAoGACXNLXYkR5vexE+w6FGl59r4RQhu1XU8Mr5DIHeB7kXPN3RKbS201M+Tb +SB5jbu0iDV477XkzSNmhaksFf2wM9MT6CaE+8n3UU5tMa+MmBGgwYTp/i9HkqVh5 +jjpJifn1VWBINd4cpNzwCg9LXoo0tbtUPWwGzqVeyo/YE5GIHGo= +-----END RSA PRIVATE KEY----- diff --git a/fs.go b/fs.go index 8570a9a9..7a6738a6 100644 --- a/fs.go +++ b/fs.go @@ -29,7 +29,7 @@ func Dir(root string, listDirectory bool) http.FileSystem { return &onlyfilesFS{fs} } -// Open conforms to http.Filesystem +// Open conforms to http.Filesystem. func (fs onlyfilesFS) Open(name string) (http.File, error) { f, err := fs.fs.Open(name) if err != nil { @@ -38,7 +38,7 @@ func (fs onlyfilesFS) Open(name string) (http.File, error) { return neuteredReaddirFile{f}, nil } -// Readdir overrides the http.File default implementation +// Readdir overrides the http.File default implementation. func (f neuteredReaddirFile) Readdir(count int) ([]os.FileInfo, error) { // this disables directory listing return nil, nil diff --git a/gin.go b/gin.go index 8388ac7b..4205eff0 100644 --- a/gin.go +++ b/gin.go @@ -14,12 +14,17 @@ import ( "github.com/gin-gonic/gin/render" ) -// Version is Framework's version -const Version = "v1.2" +const ( + // Version is Framework's version. + Version = "v1.2" + defaultMultipartMemory = 32 << 20 // 32 MB +) -var default404Body = []byte("404 page not found") -var default405Body = []byte("405 method not allowed") -var defaultAppEngine bool +var ( + default404Body = []byte("404 page not found") + default405Body = []byte("405 method not allowed") + defaultAppEngine bool +) type HandlerFunc func(*Context) type HandlersChain []HandlerFunc @@ -44,16 +49,6 @@ type RoutesInfo []RouteInfo // Create an instance of Engine, by using New() or Default() type Engine struct { RouterGroup - delims render.Delims - secureJsonPrefix string - HTMLRender render.HTMLRender - FuncMap template.FuncMap - allNoRoute HandlersChain - allNoMethod HandlersChain - noRoute HandlersChain - noMethod HandlersChain - pool sync.Pool - trees methodTrees // Enables automatic redirection if the current route can't be matched but a // handler for the path with (without) the trailing slash exists. @@ -88,10 +83,26 @@ type Engine struct { // If enabled, the url.RawPath will be used to find parameters. UseRawPath bool + // If true, the path value will be unescaped. // If UseRawPath is false (by default), the UnescapePathValues effectively is true, // as url.Path gonna be used, which is already unescaped. UnescapePathValues bool + + // Value of 'maxMemory' param that is given to http.Request's ParseMultipartForm + // method call. + MaxMultipartMemory int64 + + delims render.Delims + secureJsonPrefix string + HTMLRender render.HTMLRender + FuncMap template.FuncMap + allNoRoute HandlersChain + allNoMethod HandlersChain + noRoute HandlersChain + noMethod HandlersChain + pool sync.Pool + trees methodTrees } var _ IRouter = &Engine{} @@ -120,8 +131,9 @@ func New() *Engine { AppEngine: defaultAppEngine, UseRawPath: false, UnescapePathValues: true, + MaxMultipartMemory: defaultMultipartMemory, trees: make(methodTrees, 0, 9), - delims: render.Delims{"{{", "}}"}, + delims: render.Delims{Left: "{{", Right: "}}"}, secureJsonPrefix: "while(1);", } engine.RouterGroup.engine = engine @@ -133,6 +145,7 @@ func New() *Engine { // Default returns an Engine instance with the Logger and Recovery middleware already attached. func Default() *Engine { + debugPrintWARNINGDefault() engine := New() engine.Use(Logger(), Recovery()) return engine @@ -143,34 +156,45 @@ func (engine *Engine) allocateContext() *Context { } func (engine *Engine) Delims(left, right string) *Engine { - engine.delims = render.Delims{left, right} + engine.delims = render.Delims{Left: left, Right: right} return engine } +// SecureJsonPrefix sets the secureJsonPrefix used in Context.SecureJSON. func (engine *Engine) SecureJsonPrefix(prefix string) *Engine { engine.secureJsonPrefix = prefix return engine } +// LoadHTMLGlob loads HTML files identified by glob pattern +// and associates the result with HTML renderer. func (engine *Engine) LoadHTMLGlob(pattern string) { + left := engine.delims.Left + right := engine.delims.Right + if IsDebugging() { - debugPrintLoadTemplate(template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseGlob(pattern))) + debugPrintLoadTemplate(template.Must(template.New("").Delims(left, right).Funcs(engine.FuncMap).ParseGlob(pattern))) engine.HTMLRender = render.HTMLDebug{Glob: pattern, FuncMap: engine.FuncMap, Delims: engine.delims} - } else { - templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseGlob(pattern)) - engine.SetHTMLTemplate(templ) + return } + + templ := template.Must(template.New("").Delims(left, right).Funcs(engine.FuncMap).ParseGlob(pattern)) + engine.SetHTMLTemplate(templ) } +// LoadHTMLFiles loads a slice of HTML files +// and associates the result with HTML renderer. func (engine *Engine) LoadHTMLFiles(files ...string) { if IsDebugging() { engine.HTMLRender = render.HTMLDebug{Files: files, FuncMap: engine.FuncMap, Delims: engine.delims} - } else { - templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseFiles(files...)) - engine.SetHTMLTemplate(templ) + return } + + templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseFiles(files...)) + engine.SetHTMLTemplate(templ) } +// SetHTMLTemplate associate a template with HTML renderer. func (engine *Engine) SetHTMLTemplate(templ *template.Template) { if len(engine.trees) > 0 { debugPrintWARNINGSetHTMLTemplate() @@ -179,6 +203,7 @@ func (engine *Engine) SetHTMLTemplate(templ *template.Template) { engine.HTMLRender = render.HTMLProduction{Template: templ.Funcs(engine.FuncMap)} } +// SetFuncMap sets the FuncMap used for template.FuncMap. func (engine *Engine) SetFuncMap(funcMap template.FuncMap) { engine.FuncMap = funcMap } @@ -189,7 +214,7 @@ func (engine *Engine) NoRoute(handlers ...HandlerFunc) { engine.rebuild404Handlers() } -// NoMethod sets the handlers called when... TODO +// NoMethod sets the handlers called when... TODO. func (engine *Engine) NoMethod(handlers ...HandlerFunc) { engine.noMethod = handlers engine.rebuild405Handlers() @@ -215,7 +240,7 @@ func (engine *Engine) rebuild405Handlers() { func (engine *Engine) addRoute(method, path string, handlers HandlersChain) { assert1(path[0] == '/', "path must begin with '/'") - assert1(len(method) > 0, "HTTP method can not be empty") + assert1(method != "", "HTTP method can not be empty") assert1(len(handlers) > 0, "there must be at least one handler") debugPrintRoute(method, path, handlers) @@ -266,7 +291,7 @@ func (engine *Engine) Run(addr ...string) (err error) { // 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. -func (engine *Engine) RunTLS(addr string, certFile string, keyFile string) (err error) { +func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) { debugPrint("Listening and serving HTTPS on %s\n", addr) defer func() { debugPrintError(err) }() @@ -312,16 +337,13 @@ func (engine *Engine) HandleContext(c *Context) { engine.pool.Put(c) } -func (engine *Engine) handleHTTPRequest(context *Context) { - httpMethod := context.Request.Method - var path string - var unescape bool - if engine.UseRawPath && len(context.Request.URL.RawPath) > 0 { - path = context.Request.URL.RawPath +func (engine *Engine) handleHTTPRequest(c *Context) { + httpMethod := c.Request.Method + path := c.Request.URL.Path + unescape := false + if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 { + path = c.Request.URL.RawPath unescape = engine.UnescapePathValues - } else { - path = context.Request.URL.Path - unescape = false } // Find root of the tree for the given HTTP method @@ -330,20 +352,20 @@ func (engine *Engine) handleHTTPRequest(context *Context) { if t[i].method == httpMethod { root := t[i].root // Find route in tree - handlers, params, tsr := root.getValue(path, context.Params, unescape) + handlers, params, tsr := root.getValue(path, c.Params, unescape) if handlers != nil { - context.handlers = handlers - context.Params = params - context.Next() - context.writermem.WriteHeaderNow() + c.handlers = handlers + c.Params = params + c.Next() + c.writermem.WriteHeaderNow() return } if httpMethod != "CONNECT" && path != "/" { if tsr && engine.RedirectTrailingSlash { - redirectTrailingSlash(context) + redirectTrailingSlash(c) return } - if engine.RedirectFixedPath && redirectFixedPath(context, root, engine.RedirectFixedPath) { + if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) { return } } @@ -355,15 +377,15 @@ func (engine *Engine) handleHTTPRequest(context *Context) { for _, tree := range engine.trees { if tree.method != httpMethod { if handlers, _, _ := tree.root.getValue(path, nil, unescape); handlers != nil { - context.handlers = engine.allNoMethod - serveError(context, 405, default405Body) + c.handlers = engine.allNoMethod + serveError(c, 405, default405Body) return } } } } - context.handlers = engine.allNoRoute - serveError(context, 404, default404Body) + c.handlers = engine.allNoRoute + serveError(c, 404, default404Body) } var mimePlain = []string{MIMEPlain} @@ -389,8 +411,8 @@ func redirectTrailingSlash(c *Context) { code = 307 } - if len(path) > 1 && path[len(path)-1] == '/' { - req.URL.Path = path[:len(path)-1] + if length := len(path); length > 1 && path[length-1] == '/' { + req.URL.Path = path[:length-1] } else { req.URL.Path = path + "/" } diff --git a/ginS/gins.go b/ginS/gins.go index d40d1c3a..ee00b381 100644 --- a/ginS/gins.go +++ b/ginS/gins.go @@ -9,15 +9,15 @@ import ( "net/http" "sync" - . "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin" ) var once sync.Once -var internalEngine *Engine +var internalEngine *gin.Engine -func engine() *Engine { +func engine() *gin.Engine { once.Do(func() { - internalEngine = Default() + internalEngine = gin.Default() }) return internalEngine } @@ -35,65 +35,65 @@ func SetHTMLTemplate(templ *template.Template) { } // NoRoute adds handlers for NoRoute. It return a 404 code by default. -func NoRoute(handlers ...HandlerFunc) { +func NoRoute(handlers ...gin.HandlerFunc) { engine().NoRoute(handlers...) } // NoMethod sets the handlers called when... TODO -func NoMethod(handlers ...HandlerFunc) { +func NoMethod(handlers ...gin.HandlerFunc) { engine().NoMethod(handlers...) } // Group creates a new router group. You should add all the routes that have common middlwares or the same path prefix. // For example, all the routes that use a common middlware for authorization could be grouped. -func Group(relativePath string, handlers ...HandlerFunc) *RouterGroup { +func Group(relativePath string, handlers ...gin.HandlerFunc) *gin.RouterGroup { return engine().Group(relativePath, handlers...) } -func Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes { +func Handle(httpMethod, relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes { return engine().Handle(httpMethod, relativePath, handlers...) } // POST is a shortcut for router.Handle("POST", path, handle) -func POST(relativePath string, handlers ...HandlerFunc) IRoutes { +func POST(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes { return engine().POST(relativePath, handlers...) } // GET is a shortcut for router.Handle("GET", path, handle) -func GET(relativePath string, handlers ...HandlerFunc) IRoutes { +func GET(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes { return engine().GET(relativePath, handlers...) } // DELETE is a shortcut for router.Handle("DELETE", path, handle) -func DELETE(relativePath string, handlers ...HandlerFunc) IRoutes { +func DELETE(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes { return engine().DELETE(relativePath, handlers...) } // PATCH is a shortcut for router.Handle("PATCH", path, handle) -func PATCH(relativePath string, handlers ...HandlerFunc) IRoutes { +func PATCH(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes { return engine().PATCH(relativePath, handlers...) } // PUT is a shortcut for router.Handle("PUT", path, handle) -func PUT(relativePath string, handlers ...HandlerFunc) IRoutes { +func PUT(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes { return engine().PUT(relativePath, handlers...) } // OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle) -func OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes { +func OPTIONS(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes { return engine().OPTIONS(relativePath, handlers...) } // HEAD is a shortcut for router.Handle("HEAD", path, handle) -func HEAD(relativePath string, handlers ...HandlerFunc) IRoutes { +func HEAD(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes { return engine().HEAD(relativePath, handlers...) } -func Any(relativePath string, handlers ...HandlerFunc) IRoutes { +func Any(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes { return engine().Any(relativePath, handlers...) } -func StaticFile(relativePath, filepath string) IRoutes { +func StaticFile(relativePath, filepath string) gin.IRoutes { return engine().StaticFile(relativePath, filepath) } @@ -103,18 +103,18 @@ func StaticFile(relativePath, filepath string) IRoutes { // To use the operating system's file system implementation, // use : // router.Static("/static", "/var/www") -func Static(relativePath, root string) IRoutes { +func Static(relativePath, root string) gin.IRoutes { return engine().Static(relativePath, root) } -func StaticFS(relativePath string, fs http.FileSystem) IRoutes { +func StaticFS(relativePath string, fs http.FileSystem) gin.IRoutes { return engine().StaticFS(relativePath, fs) } // Use attachs a global middleware to the router. ie. the middlewares attached though Use() will be // included in the handlers chain for every single request. Even 404, 405, static files... // For example, this is the right place for a logger or error management middleware. -func Use(middlewares ...HandlerFunc) IRoutes { +func Use(middlewares ...gin.HandlerFunc) gin.IRoutes { return engine().Use(middlewares...) } diff --git a/gin_integration_test.go b/gin_integration_test.go index f45dd6c1..52f78842 100644 --- a/gin_integration_test.go +++ b/gin_integration_test.go @@ -94,7 +94,7 @@ func TestUnixSocket(t *testing.T) { c, err := net.Dial("unix", "/tmp/unix_unit_test") assert.NoError(t, err) - fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n") + fmt.Fprint(c, "GET /example HTTP/1.0\r\n\r\n") scanner := bufio.NewScanner(c) var response string for scanner.Scan() { diff --git a/gin_test.go b/gin_test.go index bdf5a9a9..3ac60577 100644 --- a/gin_test.go +++ b/gin_test.go @@ -5,6 +5,7 @@ package gin import ( + "crypto/tls" "fmt" "html/template" "io/ioutil" @@ -21,9 +22,9 @@ func formatAsDate(t time.Time) string { return fmt.Sprintf("%d/%02d/%02d", year, month, day) } -func setupHTMLFiles(t *testing.T) func() { +func setupHTMLFiles(t *testing.T, mode string, tls bool) func() { go func() { - SetMode(TestMode) + SetMode(mode) router := New() router.Delims("{[{", "}]}") router.SetFuncMap(template.FuncMap{ @@ -38,16 +39,21 @@ func setupHTMLFiles(t *testing.T) func() { "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), }) }) - router.Run(":8888") + if tls { + // these files generated by `go run $GOROOT/src/crypto/tls/generate_cert.go --host 127.0.0.1` + router.RunTLS(":9999", "./fixtures/testdata/cert.pem", "./fixtures/testdata/key.pem") + } else { + router.Run(":8888") + } }() t.Log("waiting 1 second for server startup") time.Sleep(1 * time.Second) return func() {} } -func setupHTMLGlob(t *testing.T) func() { +func setupHTMLGlob(t *testing.T, mode string, tls bool) func() { go func() { - SetMode(DebugMode) + SetMode(mode) router := New() router.Delims("{[{", "}]}") router.SetFuncMap(template.FuncMap{ @@ -62,16 +68,20 @@ func setupHTMLGlob(t *testing.T) func() { "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), }) }) - router.Run(":8888") + if tls { + // these files generated by `go run $GOROOT/src/crypto/tls/generate_cert.go --host 127.0.0.1` + router.RunTLS(":9999", "./fixtures/testdata/cert.pem", "./fixtures/testdata/key.pem") + } else { + router.Run(":8888") + } }() t.Log("waiting 1 second for server startup") time.Sleep(1 * time.Second) return func() {} } -//TODO func TestLoadHTMLGlob(t *testing.T) { - td := setupHTMLGlob(t) + td := setupHTMLGlob(t, DebugMode, false) res, err := http.Get("http://127.0.0.1:8888/test") if err != nil { fmt.Println(err) @@ -83,9 +93,55 @@ func TestLoadHTMLGlob(t *testing.T) { td() } +func TestLoadHTMLGlob2(t *testing.T) { + td := setupHTMLGlob(t, TestMode, false) + res, err := http.Get("http://127.0.0.1:8888/test") + if err != nil { + fmt.Println(err) + } + + resp, _ := ioutil.ReadAll(res.Body) + assert.Equal(t, "