├── go.mod ├── benchmarks ├── README.md ├── go.mod ├── go.sum ├── benchmark.md └── benchmark_test.go ├── LICENSE ├── example_test.go ├── README.md ├── tinyrouter_test.go └── tinyrouter.go /go.mod: -------------------------------------------------------------------------------- 1 | module go101.org/tinyrouter 2 | -------------------------------------------------------------------------------- /benchmarks/README.md: -------------------------------------------------------------------------------- 1 | The benchmarks are placed in this subfolder to 2 | avoid Go programs depending on TinyRouter 3 | downloading the third-party packages these benchmarks depend on. -------------------------------------------------------------------------------- /benchmarks/go.mod: -------------------------------------------------------------------------------- 1 | module example.com/myapp 2 | 3 | require ( 4 | github.com/davecgh/go-spew v1.1.1 // indirect 5 | github.com/dimfeld/httptreemux v5.0.1+incompatible // indirect 6 | github.com/go-chi/chi v3.3.3+incompatible 7 | github.com/gorilla/context v1.1.1 // indirect 8 | github.com/gorilla/mux v1.6.2 9 | github.com/julienschmidt/httprouter v0.0.0-20180715161854-348b672cd90d 10 | github.com/pmezard/go-difflib v1.0.0 // indirect 11 | github.com/stretchr/testify v1.2.2 // indirect 12 | github.com/teambition/trie-mux v1.4.2 13 | go101.org/tinyrouter v1.0.1 14 | ) 15 | 16 | replace go101.org/tinyrouter => github.com/go101/tinyrouter v1.0.1 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Go101.org 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /benchmarks/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/dimfeld/httptreemux v5.0.1+incompatible/go.mod h1:rbUlSV+CCpv/SuqUTP/8Bk2O3LyUV436/yaRGkhP6Z0= 3 | github.com/go-chi/chi v3.3.3+incompatible h1:KHkmBEMNkwKuK4FdQL7N2wOeB9jnIx7jR5wsuSBEFI8= 4 | github.com/go-chi/chi v3.3.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= 5 | github.com/go101/tinyrouter v1.0.0 h1:9uLiwOGJOUYknOwge3dg0VvktqY6ryzsDzocOS6RnOE= 6 | github.com/go101/tinyrouter v1.0.0/go.mod h1:i+KM4NnfEXEzQwVw0hpHY+WSIEurGRqKE80/fsg4cqQ= 7 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 8 | github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk= 9 | github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 10 | github.com/julienschmidt/httprouter v0.0.0-20180715161854-348b672cd90d h1:of6+TpypLAaiv4JxgH5aplBZnt0b65B4v4c8q5oy+Sk= 11 | github.com/julienschmidt/httprouter v0.0.0-20180715161854-348b672cd90d/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 14 | github.com/teambition/trie-mux v1.4.2 h1:HgbwXfQDsingRLzyYdxEyut3i2Z9To/GOlVZD2gKRiM= 15 | github.com/teambition/trie-mux v1.4.2/go.mod h1:ZWBopELDBGsgw9l8lFD4WCkpZTmmEKhu/8w3FbsxBgo= 16 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package tinyrouter_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | tiny "go101.org/tinyrouter" 9 | ) 10 | 11 | func Example() { 12 | routes := []tiny.Route{ 13 | { 14 | Method: "GET", 15 | Pattern: "/a/b/:c", 16 | HandleFunc: func(w http.ResponseWriter, req *http.Request) { 17 | params := tiny.PathParams(req) 18 | fmt.Fprintln(w, "/a/b/:c", "c =", params.Value("c")) 19 | }, 20 | }, 21 | { 22 | Method: "GET", 23 | Pattern: "/a/:b/c", 24 | HandleFunc: func(w http.ResponseWriter, req *http.Request) { 25 | params := tiny.PathParams(req) 26 | fmt.Fprintln(w, "/a/:b/c", "b =", params.Value("b")) 27 | }, 28 | }, 29 | { 30 | Method: "GET", 31 | Pattern: "/a/:b/:c", 32 | HandleFunc: func(w http.ResponseWriter, req *http.Request) { 33 | params := tiny.PathParams(req) 34 | fmt.Fprintln(w, "/a/:b/:c", "b =", params.Value("b"), "c =", params.Value("c")) 35 | }, 36 | }, 37 | { 38 | Method: "GET", 39 | Pattern: "/:a/b/c", 40 | HandleFunc: func(w http.ResponseWriter, req *http.Request) { 41 | params := tiny.PathParams(req) 42 | fmt.Fprintln(w, "/:a/b/c", "a =", params.Value("a")) 43 | }, 44 | }, 45 | { 46 | Method: "GET", 47 | Pattern: "/:a/:b/:c", 48 | HandleFunc: func(w http.ResponseWriter, req *http.Request) { 49 | params := tiny.PathParams(req) 50 | fmt.Fprintln(w, "/:a/:b/:c", "a =", params.Value("a"), "b =", params.Value("b"), "c =", params.Value("c")) 51 | }, 52 | }, 53 | } 54 | 55 | router := tiny.New(tiny.Config{Routes: routes}) 56 | 57 | log.Println("Starting service ...") 58 | log.Fatal(http.ListenAndServe(":8080", router)) 59 | 60 | /* 61 | $ curl localhost:8080/a/b/c 62 | /a/b/:c c = c 63 | $ curl localhost:8080/a/x/c 64 | /a/:b/c b = x 65 | $ curl localhost:8080/a/x/y 66 | /a/:b/:c b = x c = y 67 | $ curl localhost:8080/x/b/c 68 | /:a/b/c a = x 69 | $ curl localhost:8080/x/y/z 70 | /:a/:b/:c a = x b = y c = z 71 | */ 72 | } 73 | 74 | -------------------------------------------------------------------------------- /benchmarks/benchmark.md: -------------------------------------------------------------------------------- 1 | 2 | ### Code 3 | 4 | [benchmark_test.go](benchmark_test.go) 5 | 6 | ### Results 7 | 8 | ``` 9 | $ go test -bench=. -benchmem -benchtime=3s 10 | goos: linux 11 | goarch: amd64 12 | pkg: github.com/go101/tinyrouter 13 | Benchmark_TinyRouter_Void-4 200000 24136 ns/op 7800 B/op 78 allocs/op 14 | Benchmark_HttpRouter_Void-4 500000 7285 ns/op 1160 B/op 26 allocs/op 15 | Benchmark_GorillaMux_Void-4 30000 124647 ns/op 16936 B/op 143 allocs/op 16 | Benchmark_TrieMux_Void-4 200000 19228 ns/op 5096 B/op 52 allocs/op 17 | Benchmark_ChiRouter_Void-4 200000 24352 ns/op 5721 B/op 52 allocs/op 18 | Benchmark_TinyRouter_0bytes-4 200000 31769 ns/op 11232 B/op 117 allocs/op 19 | Benchmark_HttpRouter_0bytes-4 300000 16479 ns/op 4592 B/op 65 allocs/op 20 | Benchmark_GorillaMux_0bytes-4 30000 138028 ns/op 20368 B/op 182 allocs/op 21 | Benchmark_TrieMux_0bytes-4 200000 27597 ns/op 8528 B/op 91 allocs/op 22 | Benchmark_ChiRouter_0bytes-4 200000 34292 ns/op 9154 B/op 91 allocs/op 23 | Benchmark_TinyRouter_16bytes-4 200000 32740 ns/op 11232 B/op 117 allocs/op 24 | Benchmark_HttpRouter_16bytes-4 300000 17331 ns/op 4592 B/op 65 allocs/op 25 | Benchmark_GorillaMux_16bytes-4 30000 139416 ns/op 20368 B/op 182 allocs/op 26 | Benchmark_TrieMux_16bytes-4 200000 28259 ns/op 8528 B/op 91 allocs/op 27 | Benchmark_ChiRouter_16bytes-4 200000 34339 ns/op 9153 B/op 91 allocs/op 28 | Benchmark_TinyRouter_256bytes-4 100000 55050 ns/op 17264 B/op 143 allocs/op 29 | Benchmark_HttpRouter_256bytes-4 100000 39026 ns/op 10624 B/op 91 allocs/op 30 | Benchmark_GorillaMux_256bytes-4 30000 139608 ns/op 20368 B/op 182 allocs/op 31 | Benchmark_TrieMux_256bytes-4 100000 52348 ns/op 14560 B/op 117 allocs/op 32 | Benchmark_ChiRouter_256bytes-4 100000 60024 ns/op 15187 B/op 117 allocs/op 33 | Benchmark_TinyRouter_1024bytes-4 50000 112967 ns/op 42224 B/op 169 allocs/op 34 | Benchmark_HttpRouter_1024bytes-4 50000 97225 ns/op 35584 B/op 117 allocs/op 35 | Benchmark_GorillaMux_1024bytes-4 20000 239795 ns/op 51360 B/op 234 allocs/op 36 | Benchmark_TrieMux_1024bytes-4 50000 111293 ns/op 39520 B/op 143 allocs/op 37 | Benchmark_ChiRouter_1024bytes-4 50000 120545 ns/op 40153 B/op 143 allocs/op 38 | Benchmark_TinyRouter_8192bytes-4 10000 532320 ns/op 280176 B/op 208 allocs/op 39 | Benchmark_HttpRouter_8192bytes-4 10000 502834 ns/op 273536 B/op 156 allocs/op 40 | Benchmark_GorillaMux_8192bytes-4 10000 681497 ns/op 289312 B/op 273 allocs/op 41 | Benchmark_TrieMux_8192bytes-4 10000 524041 ns/op 277472 B/op 182 allocs/op 42 | Benchmark_ChiRouter_8192bytes-4 10000 547148 ns/op 278153 B/op 182 allocs/op 43 | Benchmark_TinyRouter_65536bytes-4 2000 3526160 ns/op 2143857 B/op 247 allocs/op 44 | Benchmark_HttpRouter_65536bytes-4 2000 3481502 ns/op 2137217 B/op 195 allocs/op 45 | Benchmark_GorillaMux_65536bytes-4 1000 3760136 ns/op 2152994 B/op 312 allocs/op 46 | Benchmark_TrieMux_65536bytes-4 2000 3558243 ns/op 2141153 B/op 221 allocs/op 47 | Benchmark_ChiRouter_65536bytes-4 2000 3572608 ns/op 2142220 B/op 223 allocs/op 48 | Benchmark_TinyRouter_FlexiblePatterns_0bytes-4 100000 37425 ns/op 12336 B/op 140 allocs/op 49 | Benchmark_GorillaMux_FlexiblePatterns_0bytes-4 20000 193210 ns/op 24800 B/op 223 allocs/op 50 | Benchmark_TrieMux_FlexiblePatterns_0bytes-4 200000 35188 ns/op 10160 B/op 110 allocs/op 51 | Benchmark_ChiRouter_FlexiblePatterns_0bytes-4 100000 42719 ns/op 11268 B/op 112 allocs/op 52 | ``` 53 | 54 | Note: the last group of benchmarks doesn't include HttpRouter, 55 | for HttpRouter will panic on those flexible URL patterns. 56 | 57 | From the results, we can find that 58 | * (from the first group of benchmarks), HttpRouter performs best for pure router function. 59 | (It is proved that the main reason TinyRouter and ChiRouter look slower than HttpRouter is they both support get path parameters from 60 | request context but HttpRouter doesn't defaultly. When all the three packages use request context to return path parameters, 61 | their performance differences are smaller than 20%.) 62 | * (from the middle groups of benchmarks), with more and more workload for a single request, the advantage of HttpRouter becomes smaller and smaller. 63 | 64 | ### Conclusions 65 | 66 | * If your server project needs a high URL pattern flexibility, select any route libraries used in the above benchmarks except HttpRouter. 67 | * If the average response size is large than 8k bytes, any router libraries used in the above benchmarks are capable. 68 | * If the average response size is very small and you don't care about the URL pattern flexibility, HttpRouter may be the best choice.] 69 | 70 | ### Other Comparisons 71 | 72 | Use standard `http.HandlerFunc` as handler functions defaultly? 73 | * TinyRouter: Yes 74 | * HttpRouter: No 75 | * GorillaMux: Yes 76 | * TrieMux: No 77 | * ChiRouter: Yes 78 | 79 | Use standard `http.Request.WithContext` to store parameters defaultly? 80 | * TinyRouter: Yes 81 | * HttpRouter: No 82 | * GorillaMux: No 83 | * TrieMux: No 84 | * ChiRouter: Yes 85 | 86 | Lines of code: 87 | * TinyRouter: 500 88 | * HttpRouter: 1000 89 | * GorillaMux: 1500 90 | * TrieMux: 500 91 | * ChiRouter: 1500 92 | 93 | Features: 94 | * TinyRouter: limited 95 | * HttpRouter: limited 96 | * GorillaMux: rich 97 | * TrieMux: limited 98 | * ChiRouter: rich 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | **NOTE**: if your project supports Go modules, then the import path of this package is `go101.org/tinyrouter`, 3 | 4 | ### What? 5 | 6 | TineyRouter is a tiny Go http router supporting custom parameters in paths 7 | (500 lines of code). 8 | 9 | The Go package implements an **_O(2k)_** complexity algorithm (usual case) to route HTTP requests. 10 | where **_k_** is the length of a HTTP request path. 11 | 12 | ### Why? 13 | 14 | For a long time, Julien Schmidt's [HttpRouter](https://github.com/julienschmidt/HttpRouter) 15 | is my favorite http router and is used in my many Go projects. 16 | For most cases, HttpRouter works very well. 17 | However, sometimes HttpRouter is some frustrating for [lacking of flexibity](https://github.com/julienschmidt/HttpRouter/search?q=conflicts&type=Issues). 18 | For example, the path patterns in each of the following groups are conflicted with each other in HttpRouter. 19 | 20 | ``` 21 | // 1 22 | /organizations/:param1/members/:param2 23 | /organizations/:abc/projects/:param2 24 | 25 | // 2 26 | /v1/user/selection 27 | /v1/:name/selection 28 | 29 | // 3 30 | /v2/:user/info 31 | /v2/:user/:group 32 | 33 | // 4 34 | /v3/user/selection 35 | /v3/:name 36 | 37 | // 5 38 | /sub/:group/:item 39 | /sub/:id 40 | 41 | // 6 42 | /a/b/:c 43 | /a/:b/c 44 | /a/:b/:c 45 | /:a/b/c 46 | /:a/:b/:c 47 | ``` 48 | 49 | TinyRouter is router implementation between HttpRouter and [gorilla/mux](https://github.com/gorilla/mux), 50 | from both performance and flexibility views. 51 | In practice, for most general cases, TinyRouter is pretty fast. 52 | And, the above routes which don't work in HttpRouter all work fine in TinyRouter. 53 | 54 | Like many other router packages, a token in path patterns starting a `:` 55 | is viewed as a parameter. Regexp patterns are not supported by TinyRouter. 56 | 57 | An example by using TinyRouter: 58 | 59 | ```golang 60 | package main 61 | 62 | import ( 63 | "fmt" 64 | "log" 65 | "net/http" 66 | 67 | tiny "github.com/go101/tinyrouter" 68 | // tiny "go101.org/tinyrouter" // If your project supports Go modules, please use this line instead. 69 | ) 70 | 71 | func main() { 72 | routes := []tiny.Route{ 73 | { 74 | Method: "GET", 75 | Pattern: "/design/:user/:slot", 76 | HandleFunc: func(w http.ResponseWriter, req *http.Request) { 77 | params := tiny.PathParams(req) 78 | fmt.Fprintln(w, "/design/:user/:slot", "user:", params.Value("user"), "slot:", params.Value("slot")) 79 | }, 80 | }, 81 | { 82 | Method: "GET", 83 | Pattern: "/design/:user/:slot/settings/show", 84 | HandleFunc: func(w http.ResponseWriter, req *http.Request) { 85 | params := tiny.PathParams(req) 86 | fmt.Fprintln(w, "/design/:user/:slot/settings/show", "user:", params.Value("user"), "slot:", params.Value("slot")) 87 | }, 88 | }, 89 | { 90 | Method: "POST", 91 | Pattern: "/design/:user/:slot/settings/update", 92 | HandleFunc: func(w http.ResponseWriter, req *http.Request) { 93 | params := tiny.PathParams(req) 94 | fmt.Fprintln(w, "/design/:user/:slot/settings/update", "user:", params.Value("user"), "slot:", params.Value("slot")) 95 | }, 96 | }, 97 | { 98 | Method: "POST", 99 | Pattern: "/design/:user/:slot/delete", 100 | HandleFunc: func(w http.ResponseWriter, req *http.Request) { 101 | params := tiny.PathParams(req) 102 | fmt.Fprintln(w, "/design/:user/:slot/delete", "user:", params.Value("user"), "slot:", params.Value("slot")) 103 | }, 104 | }, 105 | { 106 | Method: "GET", 107 | Pattern: "/design/:uuid", 108 | HandleFunc: func(w http.ResponseWriter, req *http.Request) { 109 | params := tiny.PathParams(req) 110 | fmt.Fprintln(w, "/design/:uuid", "uuid =", params.Value("uuid")) 111 | }, 112 | }, 113 | { 114 | Method: "GET", 115 | Pattern: "/design/:uuid/stat", 116 | HandleFunc: func(w http.ResponseWriter, req *http.Request) { 117 | params := tiny.PathParams(req) 118 | fmt.Fprintln(w, "/design/:uuid/stat", "uuid =", params.Value("uuid")) 119 | }, 120 | }, 121 | { 122 | Method: "GET", 123 | Pattern: "/", 124 | HandleFunc: func(w http.ResponseWriter, req *http.Request) { 125 | fmt.Fprintln(w, "/") 126 | }, 127 | }, 128 | { 129 | Method: "GET", 130 | Pattern: "/:sitepage", 131 | HandleFunc: func(w http.ResponseWriter, req *http.Request) { 132 | params := tiny.PathParams(req) 133 | fmt.Fprintln(w, "/", "sitepage =", params.Value("sitepage")) 134 | }, 135 | }, 136 | } 137 | 138 | config := tiny.Config{ 139 | Routes: routes, 140 | OthersHandleFunc: func(w http.ResponseWriter, req *http.Request) { 141 | fmt.Fprintln(w, "other pages") 142 | }, 143 | } 144 | 145 | router := tiny.New(config) 146 | 147 | log.Println("Starting service ...") 148 | log.Fatal(http.ListenAndServe(":8080", router)) 149 | } 150 | ``` 151 | 152 | Fixed tokens in patterns have higher precedences than parameterized ones. 153 | Left tokens have higher precedences than right ones. 154 | The following patterns are shown by their precedence: 155 | ``` 156 | 1: /a/b/:c 157 | 2: /a/:b/c 158 | 4: /a/:b/:c 159 | 3: /:a/b/c 160 | 5: /:a/:b/:c 161 | ``` 162 | So, 163 | * `/a/b/c` will match the 1st one 164 | * `a/x/c` will match the 2nd one, 165 | * `a/x/y` will match the 2nd one, 166 | * `/x/b/c` will match the 4th one. 167 | * `/y/x/z` will match the 5th one. 168 | 169 | The match rules and results are the same as Gorilla/Mux. 170 | 171 | ### How? 172 | 173 | The TinyRouter implementation groups routes: 174 | 1. first by request methods. 175 | 1. then by number of tokens (or called segments) in path patterns. 176 | 1. then (for the 1st segment in patterns), by wildcard (parameterized) or not. Non-wildcard segments are called fixed segments. 177 | 1. then for the segments in the fixed group in the last step, group them by their length. 178 | 1. for each group with the same token length, sort the segments in it. 179 | 180 | (Repeat the last two steps for 2nd, 3rd, ..., segments.) 181 | 182 | When a request comes, its URL path will be parsed into tokens (one **k** in **_O(2k + N)_**). 183 | 1. The route group with the exact reqest method will be selected. 184 | 1. Then the route sub-group (by number of tokens) with the exact number of tokens will be selected. 185 | 1. Then, for the 1st token, find the start segment with the same length in the fixed groups 186 | and start comparing the token with the same-length segments. 187 | Most `len(token)` bytes will be compared in this comparision. 188 | If a fixed match is found, then try to find the match for the next token. 189 | If no matches are found, then try to find the match for next token in the wildcard group. 190 | 191 | (Repeat the last step, until a match is found or return without any matches. 192 | Another **k** and the **N** happen in the process. 193 | Some micro-optimizations, by using some one-time built information, 194 | in the process make the usual time complexity become to **_O(2k + N/m)_**.) 195 | 196 | For a project with 20 routes per method with a certain number of segments in path, 197 | **_N/m_** would be about 5, whcih is much smaller than **k**, which is about 16-64. 198 | So the usual time complexity of this algorithm is about two times of a radix implementation 199 | (see [the benchmarks](benchmarks/benchmark.md) for details). 200 | The benefit is there are less limits for the route patterns. 201 | 202 | -------------------------------------------------------------------------------- /tinyrouter_test.go: -------------------------------------------------------------------------------- 1 | package tinyrouter 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | ) 10 | 11 | func TestTinyRouter(t *testing.T) { 12 | 13 | type requestCase struct { 14 | urlPath string 15 | expectedParams map[string]string 16 | expectedValues []string 17 | expectedPattern string // for invert testing. If this is set, route.Pattern must be unset. 18 | } 19 | 20 | type routeCase struct { 21 | route Route 22 | requests []requestCase 23 | } 24 | 25 | type responseCase struct { 26 | Method string 27 | Pattern string 28 | Params map[string]string 29 | Values []string 30 | } 31 | 32 | buildHandler := func(rc routeCase) func(http.ResponseWriter, *http.Request) { 33 | return func(w http.ResponseWriter, r *http.Request) { 34 | params, values := PathParams(r).ToMapAndSlice() 35 | res := responseCase{ 36 | Method: r.Method, 37 | Pattern: rc.route.Pattern, 38 | Params: params, 39 | Values: values, 40 | } 41 | data, _ := json.Marshal(res) 42 | w.Write(data) 43 | } 44 | } 45 | 46 | // ... 47 | var routeCases = []routeCase{ 48 | { 49 | route: Route{ 50 | Method: "GET", 51 | Pattern: "/v1/projects/:project/apps/:app", 52 | }, 53 | requests: []requestCase{ 54 | { 55 | urlPath: "/v1/projects/k8s/apps/controller", 56 | expectedParams: map[string]string{"project": "k8s", "app": "controller"}, 57 | expectedValues: []string{"k8s", "controller"}, 58 | }, 59 | }, 60 | }, 61 | { 62 | route: Route{ 63 | Method: "GET", 64 | Pattern: "/:item", 65 | }, 66 | requests: []requestCase{ 67 | { 68 | urlPath: "/about", 69 | expectedParams: map[string]string{"item": "about"}, 70 | expectedValues: []string{"about"}, 71 | }, 72 | { 73 | urlPath: "/sitemap", 74 | expectedParams: map[string]string{"item": "sitemap"}, 75 | expectedValues: []string{"sitemap"}, 76 | }, 77 | }, 78 | }, 79 | { 80 | route: Route{ 81 | Method: "GET", 82 | Pattern: "/aaabbb", 83 | }, 84 | requests: []requestCase{ 85 | { 86 | urlPath: "/aaabbb", 87 | expectedParams: map[string]string{}, 88 | expectedValues: []string{}, 89 | }, 90 | }, 91 | }, 92 | { 93 | route: Route{ 94 | Method: "GET", 95 | Pattern: "/cccddd", 96 | }, 97 | requests: []requestCase{ 98 | { 99 | urlPath: "/cccddd", 100 | expectedParams: map[string]string{}, 101 | expectedValues: []string{}, 102 | }, 103 | }, 104 | }, 105 | { 106 | route: Route{ 107 | Method: "GET", 108 | }, 109 | requests: []requestCase{ 110 | { 111 | urlPath: "/aaaddd", 112 | expectedParams: map[string]string{"item": "aaaddd"}, 113 | expectedValues: []string{"aaaddd"}, 114 | expectedPattern: "/:item", 115 | }, 116 | }, 117 | }, 118 | { 119 | route: Route{ 120 | Method: "GET", 121 | Pattern: "/accounts/admin/info", 122 | }, 123 | requests: []requestCase{ 124 | { 125 | urlPath: "/accounts/admin/info", 126 | expectedParams: map[string]string{}, 127 | expectedValues: []string{}, 128 | }, 129 | }, 130 | }, 131 | { 132 | route: Route{ 133 | Method: "GET", 134 | Pattern: "/accounts/:name/info", 135 | }, 136 | requests: []requestCase{ 137 | { 138 | urlPath: "/accounts/Alice/info", 139 | expectedParams: map[string]string{"name": "Alice"}, 140 | expectedValues: []string{"Alice"}, 141 | }, 142 | { 143 | urlPath: "/accounts/zhang/info", 144 | expectedParams: map[string]string{"name": "zhang"}, 145 | expectedValues: []string{"zhang"}, 146 | }, 147 | }, 148 | }, 149 | { 150 | route: Route{ 151 | Method: "POST", 152 | Pattern: "/organizations/:param1/members/:param2", 153 | }, 154 | requests: []requestCase{ 155 | { 156 | urlPath: "/organizations/Google/members/Wang", 157 | expectedParams: map[string]string{"param1": "Google", "param2": "Wang"}, 158 | expectedValues: []string{"Google", "Wang"}, 159 | }, 160 | { 161 | urlPath: "/organizations/Apple/members/Yang", 162 | expectedParams: map[string]string{"param1": "Apple", "param2": "Yang"}, 163 | expectedValues: []string{"Apple", "Yang"}, 164 | }, 165 | }, 166 | }, 167 | { 168 | route: Route{ 169 | Method: "POST", 170 | Pattern: "/organizations/:/projects/:param2", 171 | }, 172 | requests: []requestCase{ 173 | { 174 | urlPath: "/organizations/Google/projects/Android", 175 | expectedParams: map[string]string{"": "Google", "param2": "Android"}, 176 | expectedValues: []string{"Google", "Android"}, 177 | }, 178 | { 179 | urlPath: "/organizations/Apple/projects/iPhone", 180 | expectedParams: map[string]string{"": "Apple", "param2": "iPhone"}, 181 | expectedValues: []string{"Apple", "iPhone"}, 182 | }, 183 | }, 184 | }, 185 | { 186 | route: Route{ 187 | Method: "PUT", 188 | Pattern: "/v2/foo/bar", 189 | }, 190 | requests: []requestCase{ 191 | { 192 | urlPath: "/v2/foo/bar", 193 | expectedParams: map[string]string{}, 194 | expectedValues: []string{}, 195 | }, 196 | }, 197 | }, 198 | { 199 | route: Route{ 200 | Method: "PUT", 201 | Pattern: "/v2/:foo/bar", 202 | }, 203 | requests: []requestCase{ 204 | { 205 | urlPath: "/v2/abc/bar", 206 | expectedParams: map[string]string{"foo": "abc"}, 207 | expectedValues: []string{"abc"}, 208 | }, 209 | }, 210 | }, 211 | { 212 | route: Route{ 213 | Method: "PUT", 214 | Pattern: "/v2/foo/:bar", 215 | }, 216 | requests: []requestCase{ 217 | { 218 | urlPath: "/v2/foo/xyz", 219 | expectedParams: map[string]string{"bar": "xyz"}, 220 | expectedValues: []string{"xyz"}, 221 | }, 222 | }, 223 | }, 224 | { 225 | route: Route{ 226 | Method: "PUT", 227 | Pattern: "/v2/fo/:bar", 228 | }, 229 | requests: []requestCase{ 230 | { 231 | urlPath: "/v2/fo/xyz", 232 | expectedParams: map[string]string{"bar": "xyz"}, 233 | expectedValues: []string{"xyz"}, 234 | }, 235 | }, 236 | }, 237 | { 238 | route: Route{ 239 | Method: "PUT", 240 | Pattern: "/v2/:foo/:bar", 241 | }, 242 | requests: []requestCase{ 243 | { 244 | urlPath: "/v2/zhang/san", 245 | expectedParams: map[string]string{"foo": "zhang", "bar": "san"}, 246 | expectedValues: []string{"zhang", "san"}, 247 | }, 248 | { 249 | urlPath: "/v2/John/Smith", 250 | expectedParams: map[string]string{"foo": "John", "bar": "Smith"}, 251 | expectedValues: []string{"John", "Smith"}, 252 | }, 253 | }, 254 | }, 255 | { 256 | route: Route{ 257 | Method: "PUT", 258 | Pattern: "/v2/:who", 259 | }, 260 | requests: []requestCase{ 261 | { 262 | urlPath: "/v2/zhang", 263 | expectedParams: map[string]string{"who": "zhang"}, 264 | expectedValues: []string{"zhang"}, 265 | }, 266 | { 267 | urlPath: "/v2/John", 268 | expectedParams: map[string]string{"who": "John"}, 269 | expectedValues: []string{"John"}, 270 | }, 271 | }, 272 | }, 273 | 274 | { 275 | route: Route{ 276 | Method: "GET", 277 | Pattern: "/a/b/c/d/:x", 278 | }, 279 | requests: []requestCase{ 280 | { 281 | urlPath: "/a/b/c/d/123", 282 | expectedParams: map[string]string{"x": "123"}, 283 | expectedValues: []string{"123"}, 284 | }, 285 | }, 286 | }, 287 | { 288 | route: Route{ 289 | Method: "GET", 290 | Pattern: "/a/b/:y/d/e", 291 | }, 292 | requests: []requestCase{ 293 | { 294 | urlPath: "/a/b/123/d/e", 295 | expectedParams: map[string]string{"y": "123"}, 296 | expectedValues: []string{"123"}, 297 | }, 298 | }, 299 | }, 300 | { 301 | route: Route{ 302 | Method: "GET", 303 | Pattern: "/:z/b/c/d/e", 304 | }, 305 | requests: []requestCase{ 306 | { 307 | urlPath: "/123/b/c/d/e", 308 | expectedParams: map[string]string{"z": "123"}, 309 | expectedValues: []string{"123"}, 310 | }, 311 | }, 312 | }, 313 | 314 | { 315 | route: Route{ 316 | Method: "GET", 317 | Pattern: "/a123456789/b123456789/c123456789/d123000000", 318 | }, 319 | requests: []requestCase{ 320 | { 321 | urlPath: "/a123456789/b123456789/c123456789/d123000000", 322 | expectedParams: map[string]string{}, 323 | expectedValues: []string{}, 324 | }, 325 | }, 326 | }, 327 | { 328 | route: Route{ 329 | Method: "GET", 330 | Pattern: "/a123456789/b123456789/:parameter/d123000000", 331 | }, 332 | requests: []requestCase{ 333 | { 334 | urlPath: "/a123456789/b123456789/x/d123000000", 335 | expectedParams: map[string]string{"parameter": "x"}, 336 | expectedValues: []string{"x"}, 337 | }, 338 | }, 339 | }, 340 | { 341 | route: Route{ 342 | Method: "GET", 343 | Pattern: "/a123456789/:parameter/c123456789/d123456789", 344 | }, 345 | requests: []requestCase{ 346 | { 347 | urlPath: "/a123456789/y/c123456789/d123456789", 348 | expectedParams: map[string]string{"parameter": "y"}, 349 | expectedValues: []string{"y"}, 350 | }, 351 | }, 352 | }, 353 | { 354 | route: Route{ 355 | Method: "GET", 356 | Pattern: "/:parameter/b123456789/c123456789/d123456789", 357 | }, 358 | requests: []requestCase{ 359 | { 360 | urlPath: "/z/b123456789/c123456789/d123456789", 361 | expectedParams: map[string]string{"parameter": "z"}, 362 | expectedValues: []string{"z"}, 363 | }, 364 | }, 365 | }, 366 | { 367 | route: Route{ 368 | Method: "GET", 369 | Pattern: "/:parameter/b123456789/:an_option/d123456789", 370 | }, 371 | requests: []requestCase{ 372 | { 373 | urlPath: "/m/b123456789/n/d123456789", 374 | expectedParams: map[string]string{"parameter": "m", "an_option": "n"}, 375 | expectedValues: []string{"m", "n"}, 376 | }, 377 | }, 378 | }, 379 | { 380 | route: Route{ 381 | Method: "GET", 382 | Pattern: "/:parameter/:an_option/c123456789/d123456789", 383 | }, 384 | requests: []requestCase{ 385 | { 386 | urlPath: "/p/q/c123456789/d123456789", 387 | expectedParams: map[string]string{"parameter": "p", "an_option": "q"}, 388 | expectedValues: []string{"p", "q"}, 389 | }, 390 | }, 391 | }, 392 | { 393 | route: Route{ 394 | Method: "GET", 395 | Pattern: "/:parameter/:an_option/:_whatever/d123456789", 396 | }, 397 | requests: []requestCase{ 398 | { 399 | urlPath: "/h/i/j/d123456789", 400 | expectedParams: map[string]string{"parameter": "h", "an_option": "i", "_whatever": "j"}, 401 | expectedValues: []string{"h", "i", "j"}, 402 | }, 403 | }, 404 | }, 405 | } 406 | 407 | // ... 408 | routes := []Route{} 409 | for _, rc := range routeCases { 410 | route := rc.route 411 | if route.Pattern != "" { 412 | route.HandleFunc = buildHandler(rc) 413 | routes = append(routes, route) 414 | } 415 | } 416 | router := New(Config{Routes: routes}) 417 | 418 | // t.Log(router.DumpInfo()) 419 | 420 | // ... 421 | OuterMost: 422 | for _, rc := range routeCases { 423 | for _, reqc := range rc.requests { 424 | req := httptest.NewRequest(rc.route.Method, "http://example.com"+reqc.urlPath, nil) 425 | w := httptest.NewRecorder() 426 | router.ServeHTTP(w, req) 427 | resp := w.Result() 428 | defer resp.Body.Close() 429 | body, _ := ioutil.ReadAll(resp.Body) 430 | var resc responseCase 431 | _ = json.Unmarshal(body, &resc) 432 | if resc.Method != rc.route.Method { 433 | t.Errorf("method not match: %s : %s %s", resc.Method, rc.route.Method, rc.route.Pattern) 434 | break OuterMost 435 | } 436 | if reqc.expectedPattern != "" && resc.Pattern != reqc.expectedPattern { 437 | t.Errorf("pattern not match (1): %s : %s %s", resc.Pattern, rc.route.Method, reqc.expectedPattern) 438 | break OuterMost 439 | } 440 | if rc.route.Pattern != "" && resc.Pattern != rc.route.Pattern { 441 | t.Errorf("pattern not match (2): %s : %s %s", resc.Pattern, rc.route.Method, rc.route.Pattern) 442 | break OuterMost 443 | } 444 | for k, v := range resc.Params { 445 | if v != reqc.expectedParams[k] { 446 | t.Errorf("param value not match: [%s] / %s / %s : %s %s", k, v, reqc.expectedParams[k], rc.route.Method, rc.route.Pattern) 447 | break OuterMost 448 | } 449 | } 450 | for k, v := range reqc.expectedParams { 451 | if v != resc.Params[k] { 452 | t.Errorf("param value not match: [%s] / %s / %s : %s %s", k, resc.Params[k], v, rc.route.Method, rc.route.Pattern) 453 | break OuterMost 454 | } 455 | } 456 | if len(resc.Values) != len(reqc.expectedValues) { 457 | t.Errorf("number of params not match: %d / %d : %s %s", len(resc.Values), len(reqc.expectedValues), rc.route.Method, rc.route.Pattern) 458 | break OuterMost 459 | } 460 | for i, v := range resc.Values { 461 | if reqc.expectedValues[i] != v { 462 | t.Errorf("param value not match: [%d] / %s / %s : %s %s", i, reqc.expectedValues[i], v, rc.route.Method, rc.route.Pattern) 463 | break OuterMost 464 | } 465 | } 466 | } 467 | } 468 | } 469 | -------------------------------------------------------------------------------- /tinyrouter.go: -------------------------------------------------------------------------------- 1 | // Tinyrouter is Go http router supporting custom parameters in paths. 2 | // The implementation contains only 500 lines of code. 3 | package tinyrouter 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "net/http" 9 | "sort" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | // A Params encapsulates the parameters in request URL path. 15 | type Params struct { 16 | path *path 17 | tokens []string 18 | } 19 | 20 | // Value returns the parameter value corresponds to key. 21 | // This method will never panic. 22 | func (p Params) Value(key string) string { 23 | if p.path != nil { 24 | for _, seg := range p.path.wildcards { 25 | if seg.token == key { 26 | return p.tokens[seg.colIndex] 27 | } 28 | } 29 | } 30 | return "" 31 | } 32 | 33 | // ValueByIndex returns the parameter value corresponds to index i. 34 | // This method will never panic. 35 | func (p Params) ValueByIndex(i int) string { 36 | if p.path != nil && i >= 0 && i < len(p.path.wildcards) { 37 | return p.tokens[p.path.wildcards[i].colIndex] 38 | } 39 | return "" 40 | } 41 | 42 | // Convert a Params to a map[string]string and a []string. 43 | // Mainly for debug purpose. 44 | func (p Params) ToMapAndSlice() (kvs map[string]string, vs []string) { 45 | if p.path != nil { 46 | kvs = make(map[string]string, p.path.numParams) 47 | vs = make([]string, 0, p.path.numParams) 48 | for _, seg := range p.path.segments { 49 | if seg.wildcard() { 50 | vs = append(vs, p.tokens[seg.colIndex]) 51 | kvs[seg.token] = p.tokens[seg.colIndex] 52 | } 53 | } 54 | } 55 | return 56 | } 57 | 58 | // To avoid being overwritten by outer code. 59 | type paramsKeyType struct{} 60 | 61 | // PathParams returns parameters passed from URL path. 62 | func PathParams(req *http.Request) Params { 63 | p, _ := req.Context().Value(paramsKeyType{}).(Params) 64 | return p 65 | } 66 | 67 | type segment struct { 68 | // Which path this segment belongs to. 69 | path *path 70 | 71 | // For fixed segment, this is the text needed to be matched exactly. 72 | // For wildcard segment, this is the parameter name. 73 | token string 74 | 75 | // The next segment in path and the same0column segment in the next paths. 76 | nextInRow, nextInCol *segment 77 | 78 | // The first segment (at the same column) with a larger token, but 79 | // with the same length. A non-nil startLarger can't be wildcard. 80 | startLarger *segment 81 | 82 | // The first segment (at the same column) with a longer token. 83 | // A startLonger may be equal to startWildcard. 84 | startLonger *segment 85 | 86 | // The first wildcard segment (at the same column). 87 | // If seg.startWildcard == seg, then segment seg is wildcard. 88 | startWildcard *segment 89 | 90 | // rowIndex is for debug only. 91 | rowIndex, colIndex int32 92 | 93 | // How many equal prefix bytes with startLarger. 94 | numSameBytes int32 95 | } 96 | 97 | func (seg *segment) wildcard() bool { 98 | return seg.startWildcard == seg 99 | } 100 | 101 | func (seg *segment) next() *segment { 102 | if seg == nil { 103 | return nil 104 | } 105 | return seg.nextInRow 106 | } 107 | 108 | func (seg *segment) row() int { 109 | if seg == nil { 110 | return -1 111 | } 112 | return int(seg.rowIndex) 113 | } 114 | 115 | type path struct { 116 | raw string // unparsed pattern 117 | segments []*segment // []segment is better? Need benchmark. (or [][]segments for a path group?) 118 | wildcards []*segment // for fast parameter value look-up 119 | handle func(http.ResponseWriter, *http.Request) 120 | numParams int32 // how many wildcard segments in this path 121 | row int32 // row index in a path group 122 | } 123 | 124 | func compareSegments(sa, sb *segment) int { 125 | if sa.wildcard() && sb.wildcard() { 126 | return 0 127 | } 128 | 129 | if sa.wildcard() { 130 | return 1 131 | } else if sb.wildcard() { 132 | return -1 133 | } else if len(sa.token) > len(sb.token) { 134 | return 1 135 | } else if len(sa.token) < len(sb.token) { 136 | return -1 137 | } 138 | 139 | return strings.Compare(sa.token, sb.token) 140 | } 141 | 142 | func comparePaths(x, y *path) int { 143 | if len(x.segments) != len(y.segments) { 144 | panic("only paths with the same number of segments can be compared") 145 | } 146 | 147 | a := x.segments 148 | b := y.segments[:len(a)] 149 | for col := 0; col < len(a); col++ { 150 | if r := compareSegments(a[col], b[col]); r != 0 { 151 | return r 152 | } 153 | } 154 | 155 | return 0 156 | } 157 | 158 | func parsePath(r Route) *path { 159 | if len(r.Pattern) == 0 || r.Pattern[0] != '/' { 160 | panic("a pattern shell start with a slash: " + r.Pattern) 161 | } 162 | 163 | path := &path{raw: r.Pattern, handle: r.HandleFunc} 164 | 165 | buildSegment := func(pattern string, segs []*segment) (seg *segment) { 166 | if strings.HasPrefix(pattern, ":") { 167 | for _, seg := range segs { 168 | if seg.wildcard() && seg.token == pattern[1:] { 169 | panic("duplicated parameter name [" + pattern[1:] + "] in " + r.Pattern) 170 | } 171 | } 172 | seg = &segment{path: path, token: pattern[1:]} 173 | seg.startWildcard = seg 174 | path.numParams++ 175 | path.wildcards = append(path.wildcards, seg) 176 | } else { 177 | seg = &segment{path: path, token: pattern} 178 | } 179 | 180 | if seg.colIndex = int32(len(segs)); seg.colIndex > 0 { 181 | segs[seg.colIndex-1].nextInRow = seg 182 | } 183 | return 184 | } 185 | 186 | var segs []*segment 187 | for pattern := r.Pattern[1:]; ; { 188 | i := strings.IndexRune(pattern, '/') 189 | if i >= 0 { 190 | segs = append(segs, buildSegment(pattern[:i], segs)) 191 | pattern = pattern[i+1:] 192 | } else { 193 | segs = append(segs, buildSegment(pattern, segs)) 194 | break 195 | } 196 | } 197 | if len(segs) > maxSegmentsInPath { 198 | panic("too many segments in path: " + r.Pattern) 199 | } 200 | path.segments = segs 201 | 202 | return path 203 | } 204 | 205 | func buildSegmentRelations(startSeg, endSeg *segment) { 206 | if startSeg == nil { 207 | return 208 | } 209 | 210 | seg, lastSeg, shortStart, smallerStart := startSeg, startSeg, startSeg, startSeg 211 | 212 | updateStartLargers := func() { 213 | if seg == nil || seg.wildcard() || len(lastSeg.token) != len(seg.token) { 214 | return 215 | } 216 | for smaller := smallerStart; smaller != seg; smaller = smaller.nextInCol { 217 | if smaller.wildcard() { 218 | panic("smaller is wildcard") 219 | } 220 | if lastSeg.wildcard() { 221 | panic("lastSeg is wildcard") 222 | } 223 | smaller.startLarger = seg 224 | } 225 | } 226 | 227 | updateStartLongers := func() { 228 | for short := shortStart; short != seg; short = short.nextInCol { 229 | //if short.wildcard() {panic("short is wildcard")} 230 | short.startLonger = seg 231 | } 232 | } 233 | 234 | updateStartWildcards := func() { 235 | for fixed := startSeg; fixed != seg; fixed = fixed.nextInCol { 236 | if fixed.wildcard() { 237 | panic("fixed is wildcard") 238 | } 239 | fixed.startWildcard = seg 240 | } 241 | } 242 | 243 | for ; seg != endSeg; lastSeg, seg = seg, seg.nextInCol { 244 | if seg.wildcard() { 245 | updateStartLongers() 246 | updateStartWildcards() 247 | buildSegmentRelations(smallerStart.next(), seg.next()) 248 | break 249 | } 250 | 251 | if len(seg.token) > len(shortStart.token) { 252 | updateStartLargers() 253 | updateStartLongers() 254 | buildSegmentRelations(smallerStart.next(), seg.next()) 255 | shortStart, smallerStart = seg, seg 256 | continue 257 | } 258 | 259 | if compareSegments(seg, smallerStart) > 0 { 260 | updateStartLargers() 261 | buildSegmentRelations(smallerStart.next(), seg.next()) 262 | smallerStart = seg 263 | } 264 | } 265 | 266 | // Come here for two reasons: wildcard or endSeg encountered. 267 | if seg == endSeg { 268 | buildSegmentRelations(smallerStart.next(), endSeg.next()) 269 | return 270 | } 271 | 272 | if seg == nil { 273 | panic("seg is nil") 274 | } 275 | if !seg.wildcard() { 276 | panic("seg is not wildcard") 277 | } 278 | buildSegmentRelations(seg.next(), endSeg.next()) 279 | } 280 | 281 | func findHandlePath(tokens []string, entrySeg *segment) *path { 282 | for token, seg := tokens[0], entrySeg; seg != entrySeg.startWildcard; { 283 | if len(seg.token) > len(token) { 284 | break 285 | } 286 | if len(seg.token) < len(token) { 287 | seg = seg.startLonger 288 | continue 289 | } 290 | 291 | var k, n = uint(0), uint(len(token)) 292 | Next: 293 | if len(seg.token) == len(token) { // BCE 294 | for k < n { 295 | if seg.token[k] > token[k] { // BCEed 296 | goto Wildcard 297 | } 298 | if seg.token[k] < token[k] { 299 | if seg.startLarger == nil || seg.numSameBytes < int32(k) { 300 | goto Wildcard 301 | } 302 | seg = seg.startLarger 303 | goto Next 304 | } 305 | k++ 306 | } 307 | } 308 | 309 | if seg.nextInRow == nil { 310 | return seg.path 311 | } 312 | 313 | path := findHandlePath(tokens[1:], seg.nextInRow) 314 | if path != nil { 315 | return path 316 | } 317 | 318 | goto Wildcard 319 | } 320 | 321 | Wildcard: 322 | entrySeg = entrySeg.startWildcard 323 | if entrySeg == nil { 324 | return nil 325 | } 326 | 327 | if entrySeg.nextInRow == nil { 328 | return entrySeg.path 329 | } 330 | 331 | return findHandlePath(tokens[1:], entrySeg.nextInRow) 332 | } 333 | 334 | // Hard limit for maximum number of segments in path. 335 | const maxSegmentsInPath = 32 336 | 337 | type TinyRouter struct { 338 | // First by the number of tokens, then by method. 339 | // Used in initialization phase and in dumping. 340 | pathsByMethod map[string]*[maxSegmentsInPath][]*path 341 | 342 | // Used in serving phase. The entry segment is paths[0].segments[0]. 343 | entryByMethod map[string]*[maxSegmentsInPath]*segment 344 | 345 | // To avoid power exhausting attacks in request path parsing. 346 | maxNumTokens int 347 | 348 | // The default one in the standard http package is used on nil. 349 | othersHandleFunc http.HandlerFunc 350 | } 351 | 352 | // A Config value specifies the properties of a TinyRouter. 353 | type Config struct { 354 | // This routing table 355 | Routes []Route 356 | 357 | // Handler function for unmatched paths. 358 | // Nil means http.NotFound. 359 | OthersHandleFunc http.HandlerFunc 360 | 361 | // todo: 362 | // Ignore tailing slash or not. 363 | // Explicit routes have higher priorities. 364 | //IgnoreTailingSlash bool 365 | } 366 | 367 | // A Route value specifies a request method, path pattern and 368 | // the corresponding http handler function. 369 | type Route struct { 370 | Method, Pattern string 371 | HandleFunc http.HandlerFunc 372 | } 373 | 374 | // New returns a *TinyRouter value, which is also a http.Handler value. 375 | func New(c Config) *TinyRouter { 376 | tr := &TinyRouter{othersHandleFunc: c.OthersHandleFunc} 377 | if tr.othersHandleFunc == nil { 378 | tr.othersHandleFunc = http.NotFound 379 | } 380 | tr.pathsByMethod = make(map[string]*[maxSegmentsInPath][]*path, 8) 381 | tr.entryByMethod = make(map[string]*[maxSegmentsInPath]*segment, 8) 382 | 383 | for _, r := range c.Routes { 384 | if r.HandleFunc == nil { 385 | panic("HandleFunc of a Route can't be nil") 386 | } 387 | rpath := parsePath(r) 388 | if len(rpath.segments) > tr.maxNumTokens { 389 | tr.maxNumTokens = len(rpath.segments) 390 | } 391 | if tr.entryByMethod[r.Method] == nil { 392 | tr.pathsByMethod[r.Method] = &[maxSegmentsInPath][]*path{} 393 | tr.entryByMethod[r.Method] = &[maxSegmentsInPath]*segment{} 394 | } 395 | paths := tr.pathsByMethod[r.Method][len(rpath.segments)-1] 396 | if paths == nil { 397 | paths = make([]*path, 0, 4) 398 | } 399 | tr.pathsByMethod[r.Method][len(rpath.segments)-1] = append(paths, rpath) 400 | } 401 | 402 | for method, pathsByNumTokens := range tr.pathsByMethod { 403 | for numTokens, paths := range pathsByNumTokens { 404 | if paths == nil { 405 | continue 406 | } 407 | 408 | sort.Slice(paths, func(i, j int) bool { 409 | return comparePaths(paths[i], paths[j]) < 0 410 | }) 411 | 412 | for prevPath, i, row := paths[0], 1, int32(0); i < len(paths); i++ { 413 | path := paths[i] 414 | if comparePaths(prevPath, path) == 0 { 415 | panic(fmt.Sprintf("Equal paths are not allowed:\n %s\n %s", prevPath.raw, path.raw)) 416 | } 417 | 418 | prevSeg, seg := prevPath.segments[0], path.segments[0] 419 | for seg != nil { 420 | prevSeg.rowIndex, seg.rowIndex = row, row+1 // for debug 421 | prevSeg.nextInCol = seg 422 | prevSeg, seg = prevSeg.nextInRow, seg.nextInRow 423 | } 424 | 425 | prevPath, row = path, row+1 426 | } 427 | tr.entryByMethod[method][numTokens] = paths[0].segments[0] 428 | 429 | buildSegmentRelations(paths[0].segments[0], nil) 430 | 431 | statSamePrefixBytes := func(a, b string, num *int32) { 432 | for ; *num < int32(len(a)) && a[*num] == b[*num]; *num++ { 433 | } 434 | } 435 | for col := 0; col < len(paths[0].segments); col++ { 436 | for row := 0; row < len(paths); row++ { 437 | seg := paths[row].segments[col] 438 | if seg.startLarger != nil { 439 | statSamePrefixBytes(seg.token, seg.startLarger.token, &seg.numSameBytes) 440 | } 441 | } 442 | } 443 | } 444 | } 445 | return tr 446 | } 447 | 448 | // DumpInfo is for debug purpose. 449 | func (tr *TinyRouter) DumpInfo() string { 450 | var b strings.Builder 451 | for method, pathsByNumTokens := range tr.pathsByMethod { 452 | for numTokens, paths := range pathsByNumTokens { 453 | if len(paths) == 0 { 454 | continue 455 | } 456 | 457 | b.WriteString(fmt.Sprintf("\nmethod %s with %d tokens:", method, numTokens+1)) 458 | for i, path := range paths { 459 | b.WriteString(fmt.Sprint("\n ", i, "> ")) 460 | for _, seg := range path.segments { 461 | b.WriteString("[") 462 | if seg.wildcard() { 463 | b.WriteString(":") 464 | } 465 | b.WriteString(seg.token) 466 | b.WriteString(" ") 467 | b.WriteString(strconv.Itoa(seg.startLarger.row())) 468 | b.WriteString(" ") 469 | b.WriteString(strconv.Itoa(seg.startLonger.row())) 470 | b.WriteString(" ") 471 | b.WriteString(strconv.Itoa(seg.startWildcard.row())) 472 | b.WriteString(" ") 473 | b.WriteString(strconv.Itoa(int(seg.numSameBytes))) 474 | b.WriteString("]") 475 | } 476 | } 477 | } 478 | } 479 | return b.String() 480 | } 481 | 482 | // ServeHTTP lets *TinyRouter implement http.Handler interface. 483 | func (tr *TinyRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { 484 | urlPath := req.URL.Path[1:] 485 | if len(urlPath) > 1024 { 486 | urlPath = urlPath[:1024] 487 | } 488 | 489 | entryByNumTokens := tr.entryByMethod[req.Method] 490 | if entryByNumTokens == nil { 491 | tr.othersHandleFunc(w, req) 492 | return 493 | } 494 | 495 | tokens := strings.SplitN(urlPath, "/", tr.maxNumTokens) 496 | entrySegment := entryByNumTokens[len(tokens)-1] 497 | if entrySegment == nil { 498 | tr.othersHandleFunc(w, req) 499 | return 500 | } 501 | 502 | path := findHandlePath(tokens, entrySegment) 503 | if path == nil { 504 | tr.othersHandleFunc(w, req) 505 | return 506 | } 507 | 508 | if path.numParams > 0 { 509 | req = req.WithContext(context.WithValue(req.Context(), paramsKeyType{}, Params{path, tokens})) 510 | } 511 | path.handle(w, req) 512 | } 513 | -------------------------------------------------------------------------------- /benchmarks/benchmark_test.go: -------------------------------------------------------------------------------- 1 | package tinyrouter 2 | 3 | import "io" 4 | import "io/ioutil" 5 | import "net/http" 6 | import "net/http/httptest" 7 | import "strings" 8 | import "testing" 9 | 10 | import TinyRouter "go101.org/tinyrouter" 11 | import HttpRouter "github.com/julienschmidt/httprouter" 12 | import GorillaMux "github.com/gorilla/mux" 13 | import TrieMux "github.com/teambition/trie-mux/mux" 14 | import ChiRouter "github.com/go-chi/chi" 15 | 16 | var _ = ioutil.ReadAll 17 | 18 | var requestPatterns = []string{ 19 | "/v1/namespaces/:param0/apps/:param1", 20 | "/v1/namespaces/:param0/apps/:param1/settings", 21 | "/v1/namespaces/:param0/apps/:param1/stars", 22 | "/v1/namespaces/:param0/apps/:param1/stars/by/:param2", 23 | "/v1/namespaces/:param0/pods/:param1", 24 | "/v1/namespaces/:param0/pods/:param1/info", 25 | "/v1/namespaces/:param0/services/:param1", 26 | "/v1/namespaces/:param0/services/:param1/status", 27 | "/v1/namespaces/:param0/services/:param1/logs", 28 | "/v1/namespaces/:param0/services/:param1/logs/:param2", 29 | "/v1/accounts/:param0", 30 | "/v1/accounts/:param0/about", 31 | "/v1/accounts/:param0/settings", 32 | //"/v1/:uuid", // HttpRouter will panic for this pattern. 33 | } 34 | var requestURLs = []string{ 35 | "/v1/namespaces/google/apps/android", 36 | "/v1/namespaces/google/apps/android/settings", 37 | "/v1/namespaces/google/apps/android/stars", 38 | "/v1/namespaces/google/apps/android/stars/by/trump", 39 | "/v1/namespaces/google/pods/android", 40 | "/v1/namespaces/google/pods/:pod/info", 41 | "/v1/namespaces/google/services/play", 42 | "/v1/namespaces/google/services/play/status", 43 | "/v1/namespaces/google/services/play/logs", 44 | "/v1/namespaces/google/services/play/logs/457a8a5e-89c0-11e8-8893-cf8b2f0abf07", 45 | "/v1/accounts/trump", 46 | "/v1/accounts/trump/about", 47 | "/v1/accounts/trump/settings", 48 | } 49 | var requests []*http.Request 50 | 51 | var requestPatterns_2 = []string{ 52 | "/aaaaaaaaaa/bbbbbbbbbb/ccccccccccc/ddddddddddd", 53 | "/aaaaaaaaaa/bbbbbbbbbb/ccccccccccc/:param0", 54 | "/aaaaaaaaaa/bbbbbbbbbb/:param0/ddddddddddd", 55 | "/aaaaaaaaaa/bbbbbbbbbb/:param0/:param1", 56 | "/aaaaaaaaaa/:param0/ccccccccccc/ddddddddddd", 57 | "/aaaaaaaaaa/:param0/ccccccccccc/:param1", 58 | "/aaaaaaaaaa/:param0/:param1/ddddddddddd", 59 | "/aaaaaaaaaa/:param0/:param1/:param2", 60 | "/:param0/bbbbbbbbbb/ccccccccccc/ddddddddddd", 61 | "/:param0/bbbbbbbbbb/ccccccccccc/:param1", 62 | "/:param0/bbbbbbbbbb/:param1/ddddddddddd", 63 | "/:param0/bbbbbbbbbb/:param1/:param2", 64 | "/:param0/:param1/ccccccccccc/ddddddddddd", 65 | "/:param0/:param1/ccccccccccc/:param2", 66 | "/:param0/:param1/:param2/ddddddddddd", 67 | "/:param0/:param1/:param2/:param3", 68 | } 69 | var requestURLs_2 = []string{ 70 | "/aaaaaaaaaa/bbbbbbbbbb/ccccccccccc/ddddddddddd", 71 | "/aaaaaaaaaa/bbbbbbbbbb/ccccccccccc/xxxxxxxxxxx", 72 | "/aaaaaaaaaa/bbbbbbbbbb/xxxxxxxxxxx/ddddddddddd", 73 | "/aaaaaaaaaa/bbbbbbbbbb/xxxxxxxxxxx/yyyyyyyyyyy", 74 | "/aaaaaaaaaa/xxxxxxxxxx/ccccccccccc/ddddddddddd", 75 | "/aaaaaaaaaa/xxxxxxxxxx/ccccccccccc/yyyyyyyyyyy", 76 | "/aaaaaaaaaa/xxxxxxxxxx/yyyyyyyyyyy/ddddddddddd", 77 | "/aaaaaaaaaa/xxxxxxxxxx/yyyyyyyyyyy/zzzzzzzzzzz", 78 | "/xxxxxxxxxx/bbbbbbbbbb/ccccccccccc/ddddddddddd", 79 | "/xxxxxxxxxx/bbbbbbbbbb/ccccccccccc/yyyyyyyyyyy", 80 | "/xxxxxxxxxx/bbbbbbbbbb/yyyyyyyyyyy/ddddddddddd", 81 | "/xxxxxxxxxx/bbbbbbbbbb/yyyyyyyyyyy/zzzzzzzzzzz", 82 | "/xxxxxxxxxx/yyyyyyyyyy/ccccccccccc/ddddddddddd", 83 | "/xxxxxxxxxx/yyyyyyyyyy/ccccccccccc/zzzzzzzzzzz", 84 | "/xxxxxxxxxx/yyyyyyyyyy/zzzzzzzzzzz/ddddddddddd", 85 | "/xxxxxxxxxx/yyyyyyyyyy/zzzzzzzzzzz/wwwwwwwwwww", 86 | } 87 | var requests_2 []*http.Request 88 | 89 | func semicolonParam2BraceParam(pattern string) string { 90 | tokens := strings.Split(pattern, "/") 91 | pattern = "" 92 | for i, t := range tokens { 93 | if i := strings.IndexByte(t, ':'); i >= 0 { 94 | pattern += "{" + t[1:] + "}" 95 | } else { 96 | pattern += t 97 | } 98 | if i+1 < len(tokens) { 99 | pattern += "/" 100 | } 101 | } 102 | return pattern 103 | } 104 | 105 | type VoidResponseWriter struct { 106 | h http.Header 107 | } 108 | func (w *VoidResponseWriter) Header() http.Header { 109 | if w.h == nil { 110 | w.h = http.Header{} 111 | } 112 | return w.h 113 | } 114 | func (*VoidResponseWriter) WriteHeader(statusCode int) { 115 | } 116 | func (*VoidResponseWriter) Write(data []byte) (int, error) { 117 | return len(data), nil 118 | } 119 | 120 | func handle(w http.ResponseWriter, req *http.Request, handler http.Handler) { 121 | handler.ServeHTTP(w, req) 122 | //resp := w.Result() 123 | //defer resp.Body.Close() 124 | //_, _ = ioutil.ReadAll(resp.Body) 125 | } 126 | 127 | var helloworld = []byte{'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', ' ', ' ', ' '} 128 | 129 | func write0bytes(w io.Writer) { 130 | } 131 | 132 | func write16bytes(w io.Writer) { 133 | w.Write(helloworld) 134 | } 135 | 136 | func write256bytes(w io.Writer) { 137 | for i := 0; i < 16; i++ { 138 | write16bytes(w) 139 | } 140 | } 141 | 142 | func write1024bytes(w io.Writer) { 143 | for i := 0; i < 64; i++ { 144 | write16bytes(w) 145 | } 146 | } 147 | 148 | func write8192bytes(w io.Writer) { 149 | for i := 0; i < 512; i++ { 150 | write16bytes(w) 151 | } 152 | } 153 | 154 | func write65536bytes(w io.Writer) { 155 | for i := 0; i < 4096; i++ { 156 | write16bytes(w) 157 | } 158 | } 159 | 160 | func handlerTinyRouter(f func(io.Writer)) func(http.ResponseWriter, *http.Request) { 161 | return func(w http.ResponseWriter, req *http.Request) { 162 | params := TinyRouter.PathParams(req) 163 | _, _, _ = params.Value("param0"), params.Value("param1"), params.Value("param2") 164 | w.WriteHeader(http.StatusOK) 165 | f(w) 166 | } 167 | } 168 | func handlerHttpRouter(f func(io.Writer)) func(http.ResponseWriter, *http.Request, HttpRouter.Params) { 169 | return func(w http.ResponseWriter, req *http.Request, params HttpRouter.Params) { 170 | _, _, _ = params.ByName("param0"), params.ByName("param1"), params.ByName("param2") 171 | w.WriteHeader(http.StatusOK) 172 | f(w) 173 | } 174 | } 175 | func handlerGorillaMux(f func(io.Writer)) func(http.ResponseWriter, *http.Request) { 176 | return func(w http.ResponseWriter, req *http.Request) { 177 | params := GorillaMux.Vars(req) 178 | _, _, _ = params["param0"], params["param1"], params["param2"] 179 | w.WriteHeader(http.StatusOK) 180 | f(w) 181 | } 182 | } 183 | 184 | func handlerTriemuxRouter(f func(io.Writer)) func(http.ResponseWriter, *http.Request, TrieMux.Params) { 185 | return func(w http.ResponseWriter, req *http.Request, params TrieMux.Params) { 186 | _, _, _ = params["param0"], params["param1"], params["param2"] 187 | w.WriteHeader(http.StatusOK) 188 | f(w) 189 | } 190 | } 191 | 192 | func handlerChiRouter(f func(io.Writer)) func(http.ResponseWriter, *http.Request) { 193 | return func(w http.ResponseWriter, req *http.Request) { 194 | _, _, _ = ChiRouter.URLParam(req, "param0"), ChiRouter.URLParam(req, "param1"), ChiRouter.URLParam(req, "param2") 195 | w.WriteHeader(http.StatusOK) 196 | f(w) 197 | } 198 | } 199 | 200 | 201 | 202 | var tinyRouter0, tinyRouter16, tinyRouter256, tinyRouter1024, tinyRouter8192, tinyRouter65536, tinyRouter0_b * TinyRouter.TinyRouter 203 | var httpRouter0, httpRouter16, httpRouter256, httpRouter1024, httpRouter8192, httpRouter65536 *HttpRouter.Router 204 | var gorillaRouter0, gorillaRouter16, gorillaRouter256, gorillaRouter1024, gorillaRouter8192, gorillaRouter65536, gorillaRouter0_b *GorillaMux.Router 205 | var trieRouter0, trieRouter16, trieRouter256, trieRouter1024, trieRouter8192, trieRouter65536, trieRouter0_b *TrieMux.Mux 206 | var chiRouter0, chiRouter16, chiRouter256, chiRouter1024, chiRouter8192, chiRouter65536, chiRouter0_b *ChiRouter.Mux 207 | 208 | 209 | 210 | func init() { 211 | // ... 212 | requests = make([]*http.Request, 0, len(requestURLs)) 213 | for _, path := range requestURLs { 214 | requests = append(requests, httptest.NewRequest("GET", "http://example.com"+path, nil)) 215 | } 216 | requests_2 = make([]*http.Request, 0, len(requestURLs)) 217 | for _, path := range requestURLs_2 { 218 | requests_2 = append(requests_2, httptest.NewRequest("GET", "http://example.com"+path, nil)) 219 | } 220 | 221 | // TinyRouter 222 | tinyroutes0 := make([] TinyRouter.Route, 0, len(requestPatterns)) 223 | tinyroutes16 := make([] TinyRouter.Route, 0, len(requestPatterns)) 224 | tinyroutes256 := make([] TinyRouter.Route, 0, len(requestPatterns)) 225 | tinyroutes1024 := make([] TinyRouter.Route, 0, len(requestPatterns)) 226 | tinyroutes8192 := make([] TinyRouter.Route, 0, len(requestPatterns)) 227 | tinyroutes65536 := make([] TinyRouter.Route, 0, len(requestPatterns)) 228 | for _, pattern := range requestPatterns { 229 | tinyroutes0 = append(tinyroutes0, TinyRouter.Route{ 230 | Method: "GET", 231 | Pattern: pattern, 232 | HandleFunc: handlerTinyRouter(write0bytes), 233 | }) 234 | tinyroutes16 = append(tinyroutes16, TinyRouter.Route{ 235 | Method: "GET", 236 | Pattern: pattern, 237 | HandleFunc: handlerTinyRouter(write16bytes), 238 | }) 239 | tinyroutes256 = append(tinyroutes256, TinyRouter.Route{ 240 | Method: "GET", 241 | Pattern: pattern, 242 | HandleFunc: handlerTinyRouter(write256bytes), 243 | }) 244 | tinyroutes1024 = append(tinyroutes1024, TinyRouter.Route{ 245 | Method: "GET", 246 | Pattern: pattern, 247 | HandleFunc: handlerTinyRouter(write1024bytes), 248 | }) 249 | tinyroutes8192 = append(tinyroutes8192, TinyRouter.Route{ 250 | Method: "GET", 251 | Pattern: pattern, 252 | HandleFunc: handlerTinyRouter(write8192bytes), 253 | }) 254 | tinyroutes65536 = append(tinyroutes65536, TinyRouter.Route{ 255 | Method: "GET", 256 | Pattern: pattern, 257 | HandleFunc: handlerTinyRouter(write65536bytes), 258 | }) 259 | } 260 | tinyRouter0 = TinyRouter.New( TinyRouter.Config{Routes: tinyroutes0}) 261 | tinyRouter16 = TinyRouter.New( TinyRouter.Config{Routes: tinyroutes16}) 262 | tinyRouter256 = TinyRouter.New( TinyRouter.Config{Routes: tinyroutes256}) 263 | tinyRouter1024 = TinyRouter.New( TinyRouter.Config{Routes: tinyroutes1024}) 264 | tinyRouter8192 = TinyRouter.New( TinyRouter.Config{Routes: tinyroutes8192}) 265 | tinyRouter65536 = TinyRouter.New( TinyRouter.Config{Routes: tinyroutes65536}) 266 | 267 | tinyroutes0_b := make([] TinyRouter.Route, 0, len(requestPatterns)) 268 | for _, pattern := range requestPatterns_2 { 269 | tinyroutes0_b = append(tinyroutes0_b, TinyRouter.Route{ 270 | Method: "GET", 271 | Pattern: pattern, 272 | HandleFunc: handlerTinyRouter(write0bytes), 273 | }) 274 | } 275 | tinyRouter0_b = TinyRouter.New( TinyRouter.Config{Routes: tinyroutes0_b}) 276 | 277 | // HttpRouter 278 | httpRouter0 = HttpRouter.New() 279 | httpRouter16 = HttpRouter.New() 280 | httpRouter256 = HttpRouter.New() 281 | httpRouter1024 = HttpRouter.New() 282 | httpRouter8192 = HttpRouter.New() 283 | httpRouter65536 = HttpRouter.New() 284 | for _, pattern := range requestPatterns { 285 | httpRouter0.GET(pattern, handlerHttpRouter(write0bytes)) 286 | httpRouter16.GET(pattern, handlerHttpRouter(write16bytes)) 287 | httpRouter256.GET(pattern, handlerHttpRouter(write256bytes)) 288 | httpRouter1024.GET(pattern, handlerHttpRouter(write1024bytes)) 289 | httpRouter8192.GET(pattern, handlerHttpRouter(write8192bytes)) 290 | httpRouter65536.GET(pattern, handlerHttpRouter(write65536bytes)) 291 | } 292 | 293 | // GorillaMux 294 | gorillaRouter0 = GorillaMux.NewRouter() 295 | gorillaRouter16 = GorillaMux.NewRouter() 296 | gorillaRouter256 = GorillaMux.NewRouter() 297 | gorillaRouter1024 = GorillaMux.NewRouter() 298 | gorillaRouter8192 = GorillaMux.NewRouter() 299 | gorillaRouter65536 = GorillaMux.NewRouter() 300 | for _, pattern := range requestPatterns { 301 | pattern = semicolonParam2BraceParam(pattern) 302 | gorillaRouter0.HandleFunc(pattern, handlerGorillaMux(write0bytes)).Methods("GET") 303 | gorillaRouter16.HandleFunc(pattern, handlerGorillaMux(write16bytes)).Methods("GET") 304 | gorillaRouter256.HandleFunc(pattern, handlerGorillaMux(write16bytes)).Methods("GET") 305 | gorillaRouter1024.HandleFunc(pattern, handlerGorillaMux(write1024bytes)).Methods("GET") 306 | gorillaRouter8192.HandleFunc(pattern, handlerGorillaMux(write8192bytes)).Methods("GET") 307 | gorillaRouter65536.HandleFunc(pattern, handlerGorillaMux(write65536bytes)).Methods("GET") 308 | } 309 | 310 | gorillaRouter0_b = GorillaMux.NewRouter() 311 | for _, pattern := range requestPatterns_2 { 312 | pattern = semicolonParam2BraceParam(pattern) 313 | gorillaRouter0_b.HandleFunc(pattern, handlerGorillaMux(write0bytes)).Methods("GET") 314 | } 315 | 316 | // Trie Mux 317 | trieRouter0 = TrieMux.New() 318 | trieRouter16 = TrieMux.New() 319 | trieRouter256 = TrieMux.New() 320 | trieRouter1024 = TrieMux.New() 321 | trieRouter8192 = TrieMux.New() 322 | trieRouter65536 = TrieMux.New() 323 | for _, pattern := range requestPatterns { 324 | trieRouter0.Get(pattern, handlerTriemuxRouter(write0bytes)) 325 | trieRouter16.Get(pattern, handlerTriemuxRouter(write16bytes)) 326 | trieRouter256.Get(pattern, handlerTriemuxRouter(write256bytes)) 327 | trieRouter1024.Get(pattern, handlerTriemuxRouter(write1024bytes)) 328 | trieRouter8192.Get(pattern, handlerTriemuxRouter(write8192bytes)) 329 | trieRouter65536.Get(pattern, handlerTriemuxRouter(write65536bytes)) 330 | } 331 | 332 | trieRouter0_b = TrieMux.New() 333 | for _, pattern := range requestPatterns_2 { 334 | trieRouter0_b.Get(pattern, handlerTriemuxRouter(write0bytes)) 335 | } 336 | 337 | // Chi Router 338 | chiRouter0 = ChiRouter.NewRouter() 339 | chiRouter16 = ChiRouter.NewRouter() 340 | chiRouter256 = ChiRouter.NewRouter() 341 | chiRouter1024 = ChiRouter.NewRouter() 342 | chiRouter8192 = ChiRouter.NewRouter() 343 | chiRouter65536 = ChiRouter.NewRouter() 344 | for _, pattern := range requestPatterns { 345 | pattern = semicolonParam2BraceParam(pattern) 346 | chiRouter0.Get(pattern, handlerChiRouter(write0bytes)) 347 | chiRouter16.Get(pattern, handlerChiRouter(write16bytes)) 348 | chiRouter256.Get(pattern, handlerChiRouter(write256bytes)) 349 | chiRouter1024.Get(pattern, handlerChiRouter(write1024bytes)) 350 | chiRouter8192.Get(pattern, handlerChiRouter(write8192bytes)) 351 | chiRouter65536.Get(pattern, handlerChiRouter(write65536bytes)) 352 | } 353 | 354 | chiRouter0_b = ChiRouter.NewRouter() 355 | for _, pattern := range requestPatterns_2 { 356 | pattern = semicolonParam2BraceParam(pattern) 357 | chiRouter0_b.Get(pattern, handlerChiRouter(write0bytes)) 358 | } 359 | } 360 | 361 | 362 | 363 | // void 364 | 365 | func Benchmark_TinyRouter_Void(b *testing.B) { 366 | for i := 0; i < b.N; i++ { 367 | for _, req := range requests { 368 | handle(&VoidResponseWriter{}, req, tinyRouter0) 369 | } 370 | } 371 | } 372 | 373 | func Benchmark_HttpRouter_Void(b *testing.B) { 374 | for i := 0; i < b.N; i++ { 375 | for _, req := range requests { 376 | handle(&VoidResponseWriter{}, req, httpRouter0) 377 | } 378 | } 379 | } 380 | 381 | func Benchmark_GorillaMux_Void(b *testing.B) { 382 | for i := 0; i < b.N; i++ { 383 | for _, req := range requests { 384 | handle(&VoidResponseWriter{}, req, gorillaRouter0) 385 | } 386 | } 387 | } 388 | 389 | func Benchmark_TrieMux_Void(b *testing.B) { 390 | for i := 0; i < b.N; i++ { 391 | for _, req := range requests { 392 | handle(&VoidResponseWriter{}, req, trieRouter0) 393 | } 394 | } 395 | } 396 | 397 | func Benchmark_ChiRouter_Void(b *testing.B) { 398 | for i := 0; i < b.N; i++ { 399 | for _, req := range requests { 400 | handle(&VoidResponseWriter{}, req, chiRouter0) 401 | } 402 | } 403 | } 404 | 405 | // 0 bytes 406 | 407 | func Benchmark_TinyRouter_0bytes(b *testing.B) { 408 | for i := 0; i < b.N; i++ { 409 | for _, req := range requests { 410 | handle(httptest.NewRecorder(), req, tinyRouter0) 411 | } 412 | } 413 | } 414 | 415 | func Benchmark_HttpRouter_0bytes(b *testing.B) { 416 | for i := 0; i < b.N; i++ { 417 | for _, req := range requests { 418 | handle(httptest.NewRecorder(), req, httpRouter0) 419 | } 420 | } 421 | } 422 | 423 | func Benchmark_GorillaMux_0bytes(b *testing.B) { 424 | for i := 0; i < b.N; i++ { 425 | for _, req := range requests { 426 | handle(httptest.NewRecorder(), req, gorillaRouter0) 427 | } 428 | } 429 | } 430 | 431 | func Benchmark_TrieMux_0bytes(b *testing.B) { 432 | for i := 0; i < b.N; i++ { 433 | for _, req := range requests { 434 | handle(httptest.NewRecorder(), req, trieRouter0) 435 | } 436 | } 437 | } 438 | 439 | func Benchmark_ChiRouter_0bytes(b *testing.B) { 440 | for i := 0; i < b.N; i++ { 441 | for _, req := range requests { 442 | handle(httptest.NewRecorder(), req, chiRouter0) 443 | } 444 | } 445 | } 446 | 447 | // 16 bytes 448 | 449 | func Benchmark_TinyRouter_16bytes(b *testing.B) { 450 | for i := 0; i < b.N; i++ { 451 | for _, req := range requests { 452 | handle(httptest.NewRecorder(), req, tinyRouter16) 453 | } 454 | } 455 | } 456 | 457 | func Benchmark_HttpRouter_16bytes(b *testing.B) { 458 | for i := 0; i < b.N; i++ { 459 | for _, req := range requests { 460 | handle(httptest.NewRecorder(), req, httpRouter16) 461 | } 462 | } 463 | } 464 | 465 | func Benchmark_GorillaMux_16bytes(b *testing.B) { 466 | for i := 0; i < b.N; i++ { 467 | for _, req := range requests { 468 | handle(httptest.NewRecorder(), req, gorillaRouter16) 469 | } 470 | } 471 | } 472 | 473 | func Benchmark_TrieMux_16bytes(b *testing.B) { 474 | for i := 0; i < b.N; i++ { 475 | for _, req := range requests { 476 | handle(httptest.NewRecorder(), req, trieRouter16) 477 | } 478 | } 479 | } 480 | 481 | func Benchmark_ChiRouter_16bytes(b *testing.B) { 482 | for i := 0; i < b.N; i++ { 483 | for _, req := range requests { 484 | handle(httptest.NewRecorder(), req, chiRouter16) 485 | } 486 | } 487 | } 488 | 489 | // 256 bytes 490 | 491 | func Benchmark_TinyRouter_256bytes(b *testing.B) { 492 | for i := 0; i < b.N; i++ { 493 | for _, req := range requests { 494 | handle(httptest.NewRecorder(), req, tinyRouter256) 495 | } 496 | } 497 | } 498 | 499 | func Benchmark_HttpRouter_256bytes(b *testing.B) { 500 | for i := 0; i < b.N; i++ { 501 | for _, req := range requests { 502 | handle(httptest.NewRecorder(), req, httpRouter256) 503 | } 504 | } 505 | } 506 | 507 | func Benchmark_GorillaMux_256bytes(b *testing.B) { 508 | for i := 0; i < b.N; i++ { 509 | for _, req := range requests { 510 | handle(httptest.NewRecorder(), req, gorillaRouter256) 511 | } 512 | } 513 | } 514 | 515 | func Benchmark_TrieMux_256bytes(b *testing.B) { 516 | for i := 0; i < b.N; i++ { 517 | for _, req := range requests { 518 | handle(httptest.NewRecorder(), req, trieRouter256) 519 | } 520 | } 521 | } 522 | 523 | func Benchmark_ChiRouter_256bytes(b *testing.B) { 524 | for i := 0; i < b.N; i++ { 525 | for _, req := range requests { 526 | handle(httptest.NewRecorder(), req, chiRouter256) 527 | } 528 | } 529 | } 530 | 531 | // 1024 bytes 532 | 533 | func Benchmark_TinyRouter_1024bytes(b *testing.B) { 534 | for i := 0; i < b.N; i++ { 535 | for _, req := range requests { 536 | handle(httptest.NewRecorder(), req, tinyRouter1024) 537 | } 538 | } 539 | } 540 | 541 | func Benchmark_HttpRouter_1024bytes(b *testing.B) { 542 | for i := 0; i < b.N; i++ { 543 | for _, req := range requests { 544 | handle(httptest.NewRecorder(), req, httpRouter1024) 545 | } 546 | } 547 | } 548 | 549 | func Benchmark_GorillaMux_1024bytes(b *testing.B) { 550 | for i := 0; i < b.N; i++ { 551 | for _, req := range requests { 552 | handle(httptest.NewRecorder(), req, gorillaRouter1024) 553 | } 554 | } 555 | } 556 | 557 | func Benchmark_TrieMux_1024bytes(b *testing.B) { 558 | for i := 0; i < b.N; i++ { 559 | for _, req := range requests { 560 | handle(httptest.NewRecorder(), req, trieRouter1024) 561 | } 562 | } 563 | } 564 | 565 | func Benchmark_ChiRouter_1024bytes(b *testing.B) { 566 | for i := 0; i < b.N; i++ { 567 | for _, req := range requests { 568 | handle(httptest.NewRecorder(), req, chiRouter1024) 569 | } 570 | } 571 | } 572 | 573 | // 8192 bytes 574 | 575 | func Benchmark_TinyRouter_8192bytes(b *testing.B) { 576 | for i := 0; i < b.N; i++ { 577 | for _, req := range requests { 578 | handle(httptest.NewRecorder(), req, tinyRouter8192) 579 | } 580 | } 581 | } 582 | 583 | func Benchmark_HttpRouter_8192bytes(b *testing.B) { 584 | for i := 0; i < b.N; i++ { 585 | for _, req := range requests { 586 | handle(httptest.NewRecorder(), req, httpRouter8192) 587 | } 588 | } 589 | } 590 | 591 | func Benchmark_GorillaMux_8192bytes(b *testing.B) { 592 | for i := 0; i < b.N; i++ { 593 | for _, req := range requests { 594 | handle(httptest.NewRecorder(), req, gorillaRouter8192) 595 | } 596 | } 597 | } 598 | 599 | func Benchmark_TrieMux_8192bytes(b *testing.B) { 600 | for i := 0; i < b.N; i++ { 601 | for _, req := range requests { 602 | handle(httptest.NewRecorder(), req, trieRouter8192) 603 | } 604 | } 605 | } 606 | 607 | func Benchmark_ChiRouter_8192bytes(b *testing.B) { 608 | for i := 0; i < b.N; i++ { 609 | for _, req := range requests { 610 | handle(httptest.NewRecorder(), req, chiRouter8192) 611 | } 612 | } 613 | } 614 | 615 | // 65536 bytes 616 | 617 | func Benchmark_TinyRouter_65536bytes(b *testing.B) { 618 | for i := 0; i < b.N; i++ { 619 | for _, req := range requests { 620 | handle(httptest.NewRecorder(), req, tinyRouter65536) 621 | } 622 | } 623 | } 624 | 625 | func Benchmark_HttpRouter_65536bytes(b *testing.B) { 626 | for i := 0; i < b.N; i++ { 627 | for _, req := range requests { 628 | handle(httptest.NewRecorder(), req, httpRouter65536) 629 | } 630 | } 631 | } 632 | 633 | func Benchmark_GorillaMux_65536bytes(b *testing.B) { 634 | for i := 0; i < b.N; i++ { 635 | for _, req := range requests { 636 | handle(httptest.NewRecorder(), req, gorillaRouter65536) 637 | } 638 | } 639 | } 640 | 641 | func Benchmark_TrieMux_65536bytes(b *testing.B) { 642 | for i := 0; i < b.N; i++ { 643 | for _, req := range requests { 644 | handle(httptest.NewRecorder(), req, trieRouter65536) 645 | } 646 | } 647 | } 648 | 649 | func Benchmark_ChiRouter_65536bytes(b *testing.B) { 650 | for i := 0; i < b.N; i++ { 651 | for _, req := range requests { 652 | handle(httptest.NewRecorder(), req, chiRouter65536) 653 | } 654 | } 655 | } 656 | 657 | // flexible patterns 658 | 659 | func Benchmark_TinyRouter_FlexiblePatterns_0bytes(b *testing.B) { 660 | for i := 0; i < b.N; i++ { 661 | for _, req := range requests_2 { 662 | handle(httptest.NewRecorder(), req, tinyRouter0_b) 663 | } 664 | } 665 | } 666 | 667 | func Benchmark_GorillaMux_FlexiblePatterns_0bytes(b *testing.B) { 668 | for i := 0; i < b.N; i++ { 669 | for _, req := range requests_2 { 670 | handle(httptest.NewRecorder(), req, gorillaRouter0_b) 671 | } 672 | } 673 | } 674 | 675 | func Benchmark_TrieMux_FlexiblePatterns_0bytes(b *testing.B) { 676 | for i := 0; i < b.N; i++ { 677 | for _, req := range requests_2 { 678 | handle(httptest.NewRecorder(), req, trieRouter0_b) 679 | } 680 | } 681 | } 682 | 683 | func Benchmark_ChiRouter_FlexiblePatterns_0bytes(b *testing.B) { 684 | for i := 0; i < b.N; i++ { 685 | for _, req := range requests_2 { 686 | handle(httptest.NewRecorder(), req, chiRouter0_b) 687 | } 688 | } 689 | } 690 | --------------------------------------------------------------------------------