├── .gitignore ├── LICENSE.txt ├── README.md ├── chi └── route.go ├── go.mod ├── go.sum ├── gorilla └── route.go ├── main.go ├── main_test.go ├── match └── route.go ├── pat └── route.go ├── reswitch └── route.go ├── retable └── route.go ├── shiftpath └── route.go ├── split └── route.go └── stdlib ├── route.go └── route_pre1.22.go /.gitignore: -------------------------------------------------------------------------------- 1 | go-routing 2 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ben Hoyt 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-routing 2 | 3 | This repository contains the code for my article, [Different approaches to HTTP routing in Go](https://benhoyt.com/writings/go-routing/). 4 | -------------------------------------------------------------------------------- /chi/route.go: -------------------------------------------------------------------------------- 1 | // Routing based on the go-chi/chi router 2 | 3 | package chi 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | "strconv" 9 | 10 | "github.com/go-chi/chi" 11 | ) 12 | 13 | var Serve http.Handler 14 | 15 | func init() { 16 | r := chi.NewRouter() 17 | 18 | r.Get("/", home) 19 | r.Get("/contact", contact) 20 | r.Get("/api/widgets", apiGetWidgets) 21 | r.Post("/api/widgets", apiCreateWidget) 22 | r.Post("/api/widgets/{slug}", apiUpdateWidget) 23 | r.Post("/api/widgets/{slug}/parts", apiCreateWidgetPart) 24 | r.Post("/api/widgets/{slug}/parts/{id:[0-9]+}/update", apiUpdateWidgetPart) 25 | r.Post("/api/widgets/{slug}/parts/{id:[0-9]+}/delete", apiDeleteWidgetPart) 26 | r.Get("/{slug}", widgetGet) 27 | r.Get("/{slug}/admin", widgetAdmin) 28 | r.Post("/{slug}/image", widgetImage) 29 | 30 | Serve = r 31 | } 32 | 33 | func home(w http.ResponseWriter, r *http.Request) { 34 | fmt.Fprint(w, "home\n") 35 | } 36 | 37 | func contact(w http.ResponseWriter, r *http.Request) { 38 | fmt.Fprint(w, "contact\n") 39 | } 40 | 41 | func apiGetWidgets(w http.ResponseWriter, r *http.Request) { 42 | fmt.Fprint(w, "apiGetWidgets\n") 43 | } 44 | 45 | func apiCreateWidget(w http.ResponseWriter, r *http.Request) { 46 | fmt.Fprint(w, "apiCreateWidget\n") 47 | } 48 | 49 | func apiUpdateWidget(w http.ResponseWriter, r *http.Request) { 50 | slug := chi.URLParam(r, "slug") 51 | fmt.Fprintf(w, "apiUpdateWidget %s\n", slug) 52 | } 53 | 54 | func apiCreateWidgetPart(w http.ResponseWriter, r *http.Request) { 55 | slug := chi.URLParam(r, "slug") 56 | fmt.Fprintf(w, "apiCreateWidgetPart %s\n", slug) 57 | } 58 | 59 | func apiUpdateWidgetPart(w http.ResponseWriter, r *http.Request) { 60 | slug := chi.URLParam(r, "slug") 61 | id, _ := strconv.Atoi(chi.URLParam(r, "id")) 62 | fmt.Fprintf(w, "apiUpdateWidgetPart %s %d\n", slug, id) 63 | } 64 | 65 | func apiDeleteWidgetPart(w http.ResponseWriter, r *http.Request) { 66 | slug := chi.URLParam(r, "slug") 67 | id, _ := strconv.Atoi(chi.URLParam(r, "id")) 68 | fmt.Fprintf(w, "apiDeleteWidgetPart %s %d\n", slug, id) 69 | } 70 | 71 | func widgetGet(w http.ResponseWriter, r *http.Request) { 72 | slug := chi.URLParam(r, "slug") 73 | fmt.Fprintf(w, "widget %s\n", slug) 74 | } 75 | 76 | func widgetAdmin(w http.ResponseWriter, r *http.Request) { 77 | slug := chi.URLParam(r, "slug") 78 | fmt.Fprintf(w, "widgetAdmin %s\n", slug) 79 | } 80 | 81 | func widgetImage(w http.ResponseWriter, r *http.Request) { 82 | slug := chi.URLParam(r, "slug") 83 | fmt.Fprintf(w, "widgetImage %s\n", slug) 84 | } 85 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/benhoyt/go-routing 2 | 3 | go 1.22.0 4 | 5 | require ( 6 | github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40 7 | github.com/go-chi/chi v4.1.2+incompatible 8 | github.com/gorilla/mux v1.7.4 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40 h1:y4B3+GPxKlrigF1ha5FFErxK+sr6sWxQovRMzwMhejo= 2 | github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40/go.mod h1:8rLXio+WjiTceGBHIoTvn60HIbs7Hm7bcHjyrSqYB9c= 3 | github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= 4 | github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= 5 | github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= 6 | github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 7 | -------------------------------------------------------------------------------- /gorilla/route.go: -------------------------------------------------------------------------------- 1 | // Routing based on the gorilla/mux router 2 | 3 | package gorilla 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | "strconv" 9 | 10 | "github.com/gorilla/mux" 11 | ) 12 | 13 | var Serve http.Handler 14 | 15 | func init() { 16 | r := mux.NewRouter() 17 | 18 | r.HandleFunc("/", home).Methods("GET") 19 | r.HandleFunc("/contact", contact).Methods("GET") 20 | r.HandleFunc("/api/widgets", apiGetWidgets).Methods("GET") 21 | r.HandleFunc("/api/widgets", apiCreateWidget).Methods("POST") 22 | r.HandleFunc("/api/widgets/{slug}", apiUpdateWidget).Methods("POST") 23 | r.HandleFunc("/api/widgets/{slug}/parts", apiCreateWidgetPart).Methods("POST") 24 | r.HandleFunc("/api/widgets/{slug}/parts/{id:[0-9]+}/update", apiUpdateWidgetPart).Methods("POST") 25 | r.HandleFunc("/api/widgets/{slug}/parts/{id:[0-9]+}/delete", apiDeleteWidgetPart).Methods("POST") 26 | r.HandleFunc("/{slug}", widgetGet).Methods("GET") 27 | r.HandleFunc("/{slug}/admin", widgetAdmin).Methods("GET") 28 | r.HandleFunc("/{slug}/image", widgetImage).Methods("POST") 29 | 30 | Serve = r 31 | } 32 | 33 | func home(w http.ResponseWriter, r *http.Request) { 34 | fmt.Fprint(w, "home\n") 35 | } 36 | 37 | func contact(w http.ResponseWriter, r *http.Request) { 38 | fmt.Fprint(w, "contact\n") 39 | } 40 | 41 | func apiGetWidgets(w http.ResponseWriter, r *http.Request) { 42 | fmt.Fprint(w, "apiGetWidgets\n") 43 | } 44 | 45 | func apiCreateWidget(w http.ResponseWriter, r *http.Request) { 46 | fmt.Fprint(w, "apiCreateWidget\n") 47 | } 48 | 49 | func apiUpdateWidget(w http.ResponseWriter, r *http.Request) { 50 | slug := mux.Vars(r)["slug"] 51 | fmt.Fprintf(w, "apiUpdateWidget %s\n", slug) 52 | } 53 | 54 | func apiCreateWidgetPart(w http.ResponseWriter, r *http.Request) { 55 | slug := mux.Vars(r)["slug"] 56 | fmt.Fprintf(w, "apiCreateWidgetPart %s\n", slug) 57 | } 58 | 59 | func apiUpdateWidgetPart(w http.ResponseWriter, r *http.Request) { 60 | vars := mux.Vars(r) 61 | slug := vars["slug"] 62 | id, _ := strconv.Atoi(vars["id"]) 63 | fmt.Fprintf(w, "apiUpdateWidgetPart %s %d\n", slug, id) 64 | } 65 | 66 | func apiDeleteWidgetPart(w http.ResponseWriter, r *http.Request) { 67 | vars := mux.Vars(r) 68 | slug := vars["slug"] 69 | id, _ := strconv.Atoi(vars["id"]) 70 | fmt.Fprintf(w, "apiDeleteWidgetPart %s %d\n", slug, id) 71 | } 72 | 73 | func widgetGet(w http.ResponseWriter, r *http.Request) { 74 | slug := mux.Vars(r)["slug"] 75 | fmt.Fprintf(w, "widget %s\n", slug) 76 | } 77 | 78 | func widgetAdmin(w http.ResponseWriter, r *http.Request) { 79 | slug := mux.Vars(r)["slug"] 80 | fmt.Fprintf(w, "widgetAdmin %s\n", slug) 81 | } 82 | 83 | func widgetImage(w http.ResponseWriter, r *http.Request) { 84 | slug := mux.Vars(r)["slug"] 85 | fmt.Fprintf(w, "widgetImage %s\n", slug) 86 | } 87 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Test various ways to do HTTP method+path routing in Go 2 | 3 | // Each router handles the 11 URLs below: 4 | // 5 | // GET / # home 6 | // GET /contact # contact 7 | // GET /api/widgets # apiGetWidgets 8 | // POST /api/widgets # apiCreateWidget 9 | // POST /api/widgets/:slug # apiUpdateWidget 10 | // POST /api/widgets/:slug/parts # apiCreateWidgetPart 11 | // POST /api/widgets/:slug/parts/:id/update # apiUpdateWidgetPart 12 | // POST /api/widgets/:slug/parts/:id/delete # apiDeleteWidgetPart 13 | // GET /:slug # widget 14 | // GET /:slug/admin # widgetAdmin 15 | // POST /:slug/image # widgetImage 16 | 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | "log" 22 | "net/http" 23 | "os" 24 | "sort" 25 | "strings" 26 | 27 | "github.com/benhoyt/go-routing/chi" 28 | "github.com/benhoyt/go-routing/gorilla" 29 | "github.com/benhoyt/go-routing/match" 30 | "github.com/benhoyt/go-routing/pat" 31 | "github.com/benhoyt/go-routing/reswitch" 32 | "github.com/benhoyt/go-routing/retable" 33 | "github.com/benhoyt/go-routing/shiftpath" 34 | "github.com/benhoyt/go-routing/split" 35 | "github.com/benhoyt/go-routing/stdlib" 36 | ) 37 | 38 | const port = 8080 39 | 40 | func main() { 41 | if len(os.Args) < 2 || routers[os.Args[1]] == nil { 42 | fmt.Fprintf(os.Stderr, "usage: go-routing router\n\n") 43 | fmt.Fprintf(os.Stderr, "router is one of: %s\n", strings.Join(routerNames, ", ")) 44 | os.Exit(1) 45 | } 46 | routerName := os.Args[1] 47 | router := routers[routerName] 48 | 49 | fmt.Printf("listening on port %d using %s router\n", port, routerName) 50 | log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), router)) 51 | } 52 | 53 | var routers = map[string]http.Handler{ 54 | "chi": chi.Serve, 55 | "gorilla": gorilla.Serve, 56 | "match": http.HandlerFunc(match.Serve), 57 | "pat": pat.Serve, 58 | "reswitch": http.HandlerFunc(reswitch.Serve), 59 | "retable": http.HandlerFunc(retable.Serve), 60 | "shiftpath": http.HandlerFunc(shiftpath.Serve), 61 | "split": http.HandlerFunc(split.Serve), 62 | "stdlib": stdlib.Serve, // nil if Go version <1.22 63 | } 64 | 65 | var routerNames = func() []string { 66 | routerNames := []string{} 67 | for k, v := range routers { 68 | if v == nil { 69 | continue 70 | } 71 | routerNames = append(routerNames, k) 72 | } 73 | sort.Strings(routerNames) 74 | return routerNames 75 | }() 76 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | // Test the routers 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "net/http" 8 | "net/http/httptest" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func TestRouters(t *testing.T) { 14 | tests := []struct { 15 | method string 16 | path string 17 | status int 18 | body string 19 | }{ 20 | {"GET", "/", 200, "home\n"}, 21 | {"POST", "/", 405, ""}, 22 | 23 | {"GET", "/contact", 200, "contact\n"}, 24 | {"POST", "/contact", 405, ""}, 25 | {"GET", "/contact/", 404, ""}, 26 | {"GET", "/contact/no", 404, ""}, 27 | 28 | {"GET", "/api/widgets", 200, "apiGetWidgets\n"}, 29 | {"GET", "/api/widgets/", 404, ""}, 30 | 31 | {"POST", "/api/widgets", 200, "apiCreateWidget\n"}, 32 | {"POST", "/api/widgets/", 404, ""}, 33 | 34 | {"POST", "/api/widgets/foo", 200, "apiUpdateWidget foo\n"}, 35 | {"POST", "/api/widgets/bar-baz", 200, "apiUpdateWidget bar-baz\n"}, 36 | {"POST", "/api/widgets/foo/", 404, ""}, 37 | {"GET", "/api/widgets/foo", 405, ""}, 38 | 39 | {"POST", "/api/widgets/foo/parts", 200, "apiCreateWidgetPart foo\n"}, 40 | {"POST", "/api/widgets/bar-baz/parts", 200, "apiCreateWidgetPart bar-baz\n"}, 41 | {"POST", "/api/widgets/foo/parts/", 404, ""}, 42 | {"GET", "/api/widgets/foo/parts", 405, ""}, 43 | {"POST", "/api/widgets/foo/zarts", 404, ""}, 44 | 45 | {"POST", "/api/widgets/foo/parts/1/update", 200, "apiUpdateWidgetPart foo 1\n"}, 46 | {"POST", "/api/widgets/foo/parts/1/update/no", 404, ""}, 47 | {"POST", "/api/widgets/foo/parts/42/update", 200, "apiUpdateWidgetPart foo 42\n"}, 48 | {"POST", "/api/widgets/foo/parts/bar/update", 404, ""}, 49 | {"POST", "/api/widgets/bar-baz/parts/99/update", 200, "apiUpdateWidgetPart bar-baz 99\n"}, 50 | {"GET", "/api/widgets/foo/parts/1/update", 405, ""}, 51 | 52 | {"POST", "/api/widgets/foo/parts/1/delete", 200, "apiDeleteWidgetPart foo 1\n"}, 53 | {"POST", "/api/widgets/foo/parts/1/delete/no", 404, ""}, 54 | {"POST", "/api/widgets/foo/parts/42/delete", 200, "apiDeleteWidgetPart foo 42\n"}, 55 | {"POST", "/api/widgets/foo/parts/bar/delete", 404, ""}, 56 | {"POST", "/api/widgets/bar-baz/parts/99/delete", 200, "apiDeleteWidgetPart bar-baz 99\n"}, 57 | {"GET", "/api/widgets/foo/parts/1/delete", 405, ""}, 58 | {"POST", "/api/widgets/foo/parts/1/no", 404, ""}, 59 | 60 | {"GET", "/foo", 200, "widget foo\n"}, 61 | {"GET", "/bar-baz", 200, "widget bar-baz\n"}, 62 | {"GET", "/foo/", 404, ""}, 63 | {"POST", "/foo", 405, ""}, 64 | 65 | {"GET", "/foo/admin", 200, "widgetAdmin foo\n"}, 66 | {"GET", "/bar-baz/admin", 200, "widgetAdmin bar-baz\n"}, 67 | {"GET", "/foo/admin/", 404, ""}, 68 | {"GET", "/foo/admin/no", 404, ""}, 69 | {"POST", "/foo/admin", 405, ""}, 70 | 71 | {"POST", "/foo/image", 200, "widgetImage foo\n"}, 72 | {"POST", "/foo/image/no", 404, ""}, 73 | {"GET", "/foo/image", 405, ""}, 74 | {"POST", "/bar-baz/image", 200, "widgetImage bar-baz\n"}, 75 | {"POST", "/foo/image/", 404, ""}, 76 | {"GET", "/foo/image", 405, ""}, 77 | {"GET", "/foo/no", 404, ""}, 78 | } 79 | for _, name := range routerNames { 80 | router := routers[name] 81 | t.Run(name, func(t *testing.T) { 82 | for _, test := range tests { 83 | path := strings.ReplaceAll(test.path, "/", "_") 84 | t.Run(test.method+path, func(t *testing.T) { 85 | recorder := httptest.NewRecorder() 86 | request, err := http.NewRequest(test.method, test.path, &bytes.Buffer{}) 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | router.ServeHTTP(recorder, request) 91 | if recorder.Code != test.status { 92 | t.Fatalf("expected status %d, got %d", test.status, recorder.Code) 93 | } 94 | if test.status == 200 { 95 | body := recorder.Body.String() 96 | if body != test.body { 97 | t.Fatalf("expected body %q, got %q", test.body, body) 98 | } 99 | } 100 | }) 101 | } 102 | }) 103 | } 104 | } 105 | 106 | func BenchmarkRouters(b *testing.B) { 107 | method := "POST" 108 | path := "/api/widgets/foo/parts/1/update" 109 | 110 | // Could use httptest.ResponseRecorder, but that's slow-ish 111 | responseWriter := &noopResponseWriter{} 112 | 113 | names := make([]string, len(routerNames)) 114 | copy(names, routerNames) 115 | names = append(names, "noop") 116 | 117 | for _, name := range names { 118 | router := routers[name] 119 | if router == nil { 120 | router = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) 121 | } 122 | b.Run(name, func(b *testing.B) { 123 | for i := 0; i < b.N; i++ { 124 | request, err := http.NewRequest(method, path, &bytes.Buffer{}) 125 | if err != nil { 126 | b.Fatal(err) 127 | } 128 | router.ServeHTTP(responseWriter, request) 129 | } 130 | }) 131 | } 132 | } 133 | 134 | type noopResponseWriter struct{} 135 | 136 | func (r *noopResponseWriter) Header() http.Header { 137 | return nil 138 | } 139 | 140 | func (r *noopResponseWriter) Write(b []byte) (int, error) { 141 | return len(b), nil 142 | } 143 | 144 | func (r *noopResponseWriter) WriteHeader(statusCode int) { 145 | } 146 | -------------------------------------------------------------------------------- /match/route.go: -------------------------------------------------------------------------------- 1 | // Go HTTP router based on a simple custom match() function 2 | 3 | package match 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | func Serve(w http.ResponseWriter, r *http.Request) { 13 | var h http.Handler 14 | var slug string 15 | var id int 16 | 17 | p := r.URL.Path 18 | switch { 19 | case match(p, "/"): 20 | h = get(home) 21 | case match(p, "/contact"): 22 | h = get(contact) 23 | case match(p, "/api/widgets") && r.Method == "GET": 24 | h = get(apiGetWidgets) 25 | case match(p, "/api/widgets"): 26 | h = post(apiCreateWidget) 27 | case match(p, "/api/widgets/+", &slug): 28 | h = post(apiWidget{slug}.update) 29 | case match(p, "/api/widgets/+/parts", &slug): 30 | h = post(apiWidget{slug}.createPart) 31 | case match(p, "/api/widgets/+/parts/+/update", &slug, &id): 32 | h = post(apiWidgetPart{slug, id}.update) 33 | case match(p, "/api/widgets/+/parts/+/delete", &slug, &id): 34 | h = post(apiWidgetPart{slug, id}.delete) 35 | case match(p, "/+", &slug): 36 | h = get(widget{slug}.widget) 37 | case match(p, "/+/admin", &slug): 38 | h = get(widget{slug}.admin) 39 | case match(p, "/+/image", &slug): 40 | h = post(widget{slug}.image) 41 | default: 42 | http.NotFound(w, r) 43 | return 44 | } 45 | h.ServeHTTP(w, r) 46 | } 47 | 48 | // match reports whether path matches the given pattern, which is a 49 | // path with '+' wildcards wherever you want to use a parameter. Path 50 | // parameters are assigned to the pointers in vars (len(vars) must be 51 | // the number of wildcards), which must be of type *string or *int. 52 | func match(path, pattern string, vars ...interface{}) bool { 53 | for ; pattern != "" && path != ""; pattern = pattern[1:] { 54 | switch pattern[0] { 55 | case '+': 56 | // '+' matches till next slash in path 57 | slash := strings.IndexByte(path, '/') 58 | if slash < 0 { 59 | slash = len(path) 60 | } 61 | segment := path[:slash] 62 | path = path[slash:] 63 | switch p := vars[0].(type) { 64 | case *string: 65 | *p = segment 66 | case *int: 67 | n, err := strconv.Atoi(segment) 68 | if err != nil || n < 0 { 69 | return false 70 | } 71 | *p = n 72 | default: 73 | panic("vars must be *string or *int") 74 | } 75 | vars = vars[1:] 76 | case path[0]: 77 | // non-'+' pattern byte must match path byte 78 | path = path[1:] 79 | default: 80 | return false 81 | } 82 | } 83 | return path == "" && pattern == "" 84 | } 85 | 86 | func allowMethod(h http.HandlerFunc, method string) http.HandlerFunc { 87 | return func(w http.ResponseWriter, r *http.Request) { 88 | if method != r.Method { 89 | w.Header().Set("Allow", method) 90 | http.Error(w, "405 method not allowed", http.StatusMethodNotAllowed) 91 | return 92 | } 93 | h(w, r) 94 | } 95 | } 96 | 97 | func get(h http.HandlerFunc) http.HandlerFunc { 98 | return allowMethod(h, "GET") 99 | } 100 | 101 | func post(h http.HandlerFunc) http.HandlerFunc { 102 | return allowMethod(h, "POST") 103 | } 104 | 105 | func home(w http.ResponseWriter, r *http.Request) { 106 | fmt.Fprint(w, "home\n") 107 | } 108 | 109 | func contact(w http.ResponseWriter, r *http.Request) { 110 | fmt.Fprint(w, "contact\n") 111 | } 112 | 113 | func apiGetWidgets(w http.ResponseWriter, r *http.Request) { 114 | fmt.Fprint(w, "apiGetWidgets\n") 115 | } 116 | 117 | func apiCreateWidget(w http.ResponseWriter, r *http.Request) { 118 | fmt.Fprint(w, "apiCreateWidget\n") 119 | } 120 | 121 | type apiWidget struct { 122 | slug string 123 | } 124 | 125 | func (h apiWidget) update(w http.ResponseWriter, r *http.Request) { 126 | fmt.Fprintf(w, "apiUpdateWidget %s\n", h.slug) 127 | } 128 | 129 | func (h apiWidget) createPart(w http.ResponseWriter, r *http.Request) { 130 | fmt.Fprintf(w, "apiCreateWidgetPart %s\n", h.slug) 131 | } 132 | 133 | type apiWidgetPart struct { 134 | slug string 135 | id int 136 | } 137 | 138 | func (h apiWidgetPart) update(w http.ResponseWriter, r *http.Request) { 139 | fmt.Fprintf(w, "apiUpdateWidgetPart %s %d\n", h.slug, h.id) 140 | } 141 | 142 | func (h apiWidgetPart) delete(w http.ResponseWriter, r *http.Request) { 143 | fmt.Fprintf(w, "apiDeleteWidgetPart %s %d\n", h.slug, h.id) 144 | } 145 | 146 | type widget struct { 147 | slug string 148 | } 149 | 150 | func (h widget) widget(w http.ResponseWriter, r *http.Request) { 151 | fmt.Fprintf(w, "widget %s\n", h.slug) 152 | } 153 | 154 | func (h widget) admin(w http.ResponseWriter, r *http.Request) { 155 | fmt.Fprintf(w, "widgetAdmin %s\n", h.slug) 156 | } 157 | 158 | func (h widget) image(w http.ResponseWriter, r *http.Request) { 159 | fmt.Fprintf(w, "widgetImage %s\n", h.slug) 160 | } 161 | -------------------------------------------------------------------------------- /pat/route.go: -------------------------------------------------------------------------------- 1 | // Routing based on the bmizerany/pat router 2 | 3 | package pat 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | "strconv" 9 | 10 | "github.com/bmizerany/pat" 11 | ) 12 | 13 | var Serve http.Handler 14 | 15 | func init() { 16 | r := pat.New() 17 | 18 | r.Get("/", http.HandlerFunc(home)) 19 | r.Get("/contact", http.HandlerFunc(contact)) 20 | r.Get("/api/widgets", http.HandlerFunc(apiGetWidgets)) 21 | r.Post("/api/widgets", http.HandlerFunc(apiCreateWidget)) 22 | r.Post("/api/widgets/:slug", http.HandlerFunc(apiUpdateWidget)) 23 | r.Post("/api/widgets/:slug/parts", http.HandlerFunc(apiCreateWidgetPart)) 24 | r.Post("/api/widgets/:slug/parts/:id/update", http.HandlerFunc(apiUpdateWidgetPart)) 25 | r.Post("/api/widgets/:slug/parts/:id/delete", http.HandlerFunc(apiDeleteWidgetPart)) 26 | r.Get("/:slug", http.HandlerFunc(widgetGet)) 27 | r.Get("/:slug/admin", http.HandlerFunc(widgetAdmin)) 28 | r.Post("/:slug/image", http.HandlerFunc(widgetImage)) 29 | 30 | Serve = r 31 | } 32 | 33 | func home(w http.ResponseWriter, r *http.Request) { 34 | fmt.Fprint(w, "home\n") 35 | } 36 | 37 | func contact(w http.ResponseWriter, r *http.Request) { 38 | fmt.Fprint(w, "contact\n") 39 | } 40 | 41 | func apiGetWidgets(w http.ResponseWriter, r *http.Request) { 42 | fmt.Fprint(w, "apiGetWidgets\n") 43 | } 44 | 45 | func apiCreateWidget(w http.ResponseWriter, r *http.Request) { 46 | fmt.Fprint(w, "apiCreateWidget\n") 47 | } 48 | 49 | func apiUpdateWidget(w http.ResponseWriter, r *http.Request) { 50 | slug := r.URL.Query().Get(":slug") 51 | fmt.Fprintf(w, "apiUpdateWidget %s\n", slug) 52 | } 53 | 54 | func apiCreateWidgetPart(w http.ResponseWriter, r *http.Request) { 55 | slug := r.URL.Query().Get(":slug") 56 | fmt.Fprintf(w, "apiCreateWidgetPart %s\n", slug) 57 | } 58 | 59 | func apiUpdateWidgetPart(w http.ResponseWriter, r *http.Request) { 60 | slug := r.URL.Query().Get(":slug") 61 | id, err := strconv.Atoi(r.URL.Query().Get(":id")) 62 | if err != nil { 63 | http.NotFound(w, r) 64 | return 65 | } 66 | fmt.Fprintf(w, "apiUpdateWidgetPart %s %d\n", slug, id) 67 | } 68 | 69 | func apiDeleteWidgetPart(w http.ResponseWriter, r *http.Request) { 70 | slug := r.URL.Query().Get(":slug") 71 | id, err := strconv.Atoi(r.URL.Query().Get(":id")) 72 | if err != nil { 73 | http.NotFound(w, r) 74 | return 75 | } 76 | fmt.Fprintf(w, "apiDeleteWidgetPart %s %d\n", slug, id) 77 | } 78 | 79 | func widgetGet(w http.ResponseWriter, r *http.Request) { 80 | slug := r.URL.Query().Get(":slug") 81 | fmt.Fprintf(w, "widget %s\n", slug) 82 | } 83 | 84 | func widgetAdmin(w http.ResponseWriter, r *http.Request) { 85 | slug := r.URL.Query().Get(":slug") 86 | fmt.Fprintf(w, "widgetAdmin %s\n", slug) 87 | } 88 | 89 | func widgetImage(w http.ResponseWriter, r *http.Request) { 90 | slug := r.URL.Query().Get(":slug") 91 | fmt.Fprintf(w, "widgetImage %s\n", slug) 92 | } 93 | -------------------------------------------------------------------------------- /reswitch/route.go: -------------------------------------------------------------------------------- 1 | // Go HTTP router based on a regexp matching function and "switch" 2 | 3 | package reswitch 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | "regexp" 9 | "strconv" 10 | "sync" 11 | ) 12 | 13 | func Serve(w http.ResponseWriter, r *http.Request) { 14 | var h http.Handler 15 | var slug string 16 | var id int 17 | 18 | p := r.URL.Path 19 | switch { 20 | case match(p, "/"): 21 | h = get(home) 22 | case match(p, "/contact"): 23 | h = get(contact) 24 | case match(p, "/api/widgets") && r.Method == "GET": 25 | h = get(apiGetWidgets) 26 | case match(p, "/api/widgets"): 27 | h = post(apiCreateWidget) 28 | case match(p, "/api/widgets/([^/]+)", &slug): 29 | h = post(apiWidget{slug}.update) 30 | case match(p, "/api/widgets/([^/]+)/parts", &slug): 31 | h = post(apiWidget{slug}.createPart) 32 | case match(p, "/api/widgets/([^/]+)/parts/([0-9]+)/update", &slug, &id): 33 | h = post(apiWidgetPart{slug, id}.update) 34 | case match(p, "/api/widgets/([^/]+)/parts/([0-9]+)/delete", &slug, &id): 35 | h = post(apiWidgetPart{slug, id}.delete) 36 | case match(p, "/([^/]+)", &slug): 37 | h = get(widget{slug}.widget) 38 | case match(p, "/([^/]+)/admin", &slug): 39 | h = get(widget{slug}.admin) 40 | case match(p, "/([^/]+)/image", &slug): 41 | h = post(widget{slug}.image) 42 | default: 43 | http.NotFound(w, r) 44 | return 45 | } 46 | h.ServeHTTP(w, r) 47 | } 48 | 49 | // match reports whether path matches ^regex$, and if it matches, 50 | // assigns any capture groups to the *string or *int vars. 51 | func match(path, pattern string, vars ...interface{}) bool { 52 | regex := mustCompileCached(pattern) 53 | matches := regex.FindStringSubmatch(path) 54 | if len(matches) <= 0 { 55 | return false 56 | } 57 | for i, match := range matches[1:] { 58 | switch p := vars[i].(type) { 59 | case *string: 60 | *p = match 61 | case *int: 62 | n, err := strconv.Atoi(match) 63 | if err != nil { 64 | return false 65 | } 66 | *p = n 67 | default: 68 | panic("vars must be *string or *int") 69 | } 70 | } 71 | return true 72 | } 73 | 74 | var ( 75 | regexen = make(map[string]*regexp.Regexp) 76 | relock sync.Mutex 77 | ) 78 | 79 | func mustCompileCached(pattern string) *regexp.Regexp { 80 | relock.Lock() 81 | defer relock.Unlock() 82 | 83 | regex := regexen[pattern] 84 | if regex == nil { 85 | regex = regexp.MustCompile("^" + pattern + "$") 86 | regexen[pattern] = regex 87 | } 88 | return regex 89 | } 90 | 91 | // allowMethod takes a HandlerFunc and wraps it in a handler that only 92 | // responds if the request method is the given method, otherwise it 93 | // responds with HTTP 405 Method Not Allowed. 94 | func allowMethod(h http.HandlerFunc, method string) http.HandlerFunc { 95 | return func(w http.ResponseWriter, r *http.Request) { 96 | if method != r.Method { 97 | w.Header().Set("Allow", method) 98 | http.Error(w, "405 method not allowed", http.StatusMethodNotAllowed) 99 | return 100 | } 101 | h(w, r) 102 | } 103 | } 104 | 105 | // get takes a HandlerFunc and wraps it to only allow the GET method 106 | func get(h http.HandlerFunc) http.HandlerFunc { 107 | return allowMethod(h, "GET") 108 | } 109 | 110 | // post takes a HandlerFunc and wraps it to only allow the POST method 111 | func post(h http.HandlerFunc) http.HandlerFunc { 112 | return allowMethod(h, "POST") 113 | } 114 | 115 | func home(w http.ResponseWriter, r *http.Request) { 116 | fmt.Fprint(w, "home\n") 117 | } 118 | 119 | func contact(w http.ResponseWriter, r *http.Request) { 120 | fmt.Fprint(w, "contact\n") 121 | } 122 | 123 | func apiGetWidgets(w http.ResponseWriter, r *http.Request) { 124 | fmt.Fprint(w, "apiGetWidgets\n") 125 | } 126 | 127 | func apiCreateWidget(w http.ResponseWriter, r *http.Request) { 128 | fmt.Fprint(w, "apiCreateWidget\n") 129 | } 130 | 131 | type apiWidget struct { 132 | slug string 133 | } 134 | 135 | func (h apiWidget) update(w http.ResponseWriter, r *http.Request) { 136 | fmt.Fprintf(w, "apiUpdateWidget %s\n", h.slug) 137 | } 138 | 139 | func (h apiWidget) createPart(w http.ResponseWriter, r *http.Request) { 140 | fmt.Fprintf(w, "apiCreateWidgetPart %s\n", h.slug) 141 | } 142 | 143 | type apiWidgetPart struct { 144 | slug string 145 | id int 146 | } 147 | 148 | func (h apiWidgetPart) update(w http.ResponseWriter, r *http.Request) { 149 | fmt.Fprintf(w, "apiUpdateWidgetPart %s %d\n", h.slug, h.id) 150 | } 151 | 152 | func (h apiWidgetPart) delete(w http.ResponseWriter, r *http.Request) { 153 | fmt.Fprintf(w, "apiDeleteWidgetPart %s %d\n", h.slug, h.id) 154 | } 155 | 156 | type widget struct { 157 | slug string 158 | } 159 | 160 | func (h widget) widget(w http.ResponseWriter, r *http.Request) { 161 | fmt.Fprintf(w, "widget %s\n", h.slug) 162 | } 163 | 164 | func (h widget) admin(w http.ResponseWriter, r *http.Request) { 165 | fmt.Fprintf(w, "widgetAdmin %s\n", h.slug) 166 | } 167 | 168 | func (h widget) image(w http.ResponseWriter, r *http.Request) { 169 | fmt.Fprintf(w, "widgetImage %s\n", h.slug) 170 | } 171 | -------------------------------------------------------------------------------- /retable/route.go: -------------------------------------------------------------------------------- 1 | // Go HTTP router based on a table of regexes 2 | 3 | package retable 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "net/http" 9 | "regexp" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | var routes = []route{ 15 | newRoute("GET", "/", home), 16 | newRoute("GET", "/contact", contact), 17 | newRoute("GET", "/api/widgets", apiGetWidgets), 18 | newRoute("POST", "/api/widgets", apiCreateWidget), 19 | newRoute("POST", "/api/widgets/([^/]+)", apiUpdateWidget), 20 | newRoute("POST", "/api/widgets/([^/]+)/parts", apiCreateWidgetPart), 21 | newRoute("POST", "/api/widgets/([^/]+)/parts/([0-9]+)/update", apiUpdateWidgetPart), 22 | newRoute("POST", "/api/widgets/([^/]+)/parts/([0-9]+)/delete", apiDeleteWidgetPart), 23 | newRoute("GET", "/([^/]+)", widget), 24 | newRoute("GET", "/([^/]+)/admin", widgetAdmin), 25 | newRoute("POST", "/([^/]+)/image", widgetImage), 26 | } 27 | 28 | func newRoute(method, pattern string, handler http.HandlerFunc) route { 29 | return route{method, regexp.MustCompile("^" + pattern + "$"), handler} 30 | } 31 | 32 | type route struct { 33 | method string 34 | regex *regexp.Regexp 35 | handler http.HandlerFunc 36 | } 37 | 38 | func Serve(w http.ResponseWriter, r *http.Request) { 39 | var allow []string 40 | for _, route := range routes { 41 | matches := route.regex.FindStringSubmatch(r.URL.Path) 42 | if len(matches) > 0 { 43 | if r.Method != route.method { 44 | allow = append(allow, route.method) 45 | continue 46 | } 47 | ctx := context.WithValue(r.Context(), ctxKey{}, matches[1:]) 48 | route.handler(w, r.WithContext(ctx)) 49 | return 50 | } 51 | } 52 | if len(allow) > 0 { 53 | w.Header().Set("Allow", strings.Join(allow, ", ")) 54 | http.Error(w, "405 method not allowed", http.StatusMethodNotAllowed) 55 | return 56 | } 57 | http.NotFound(w, r) 58 | } 59 | 60 | type ctxKey struct{} 61 | 62 | func getField(r *http.Request, index int) string { 63 | fields := r.Context().Value(ctxKey{}).([]string) 64 | return fields[index] 65 | } 66 | 67 | func home(w http.ResponseWriter, r *http.Request) { 68 | fmt.Fprint(w, "home\n") 69 | } 70 | 71 | func contact(w http.ResponseWriter, r *http.Request) { 72 | fmt.Fprint(w, "contact\n") 73 | } 74 | 75 | func apiGetWidgets(w http.ResponseWriter, r *http.Request) { 76 | fmt.Fprint(w, "apiGetWidgets\n") 77 | } 78 | 79 | func apiCreateWidget(w http.ResponseWriter, r *http.Request) { 80 | fmt.Fprint(w, "apiCreateWidget\n") 81 | } 82 | 83 | func apiUpdateWidget(w http.ResponseWriter, r *http.Request) { 84 | slug := getField(r, 0) 85 | fmt.Fprintf(w, "apiUpdateWidget %s\n", slug) 86 | } 87 | 88 | func apiCreateWidgetPart(w http.ResponseWriter, r *http.Request) { 89 | slug := getField(r, 0) 90 | fmt.Fprintf(w, "apiCreateWidgetPart %s\n", slug) 91 | } 92 | 93 | func apiUpdateWidgetPart(w http.ResponseWriter, r *http.Request) { 94 | slug := getField(r, 0) 95 | id, _ := strconv.Atoi(getField(r, 1)) 96 | fmt.Fprintf(w, "apiUpdateWidgetPart %s %d\n", slug, id) 97 | } 98 | 99 | func apiDeleteWidgetPart(w http.ResponseWriter, r *http.Request) { 100 | slug := getField(r, 0) 101 | id, _ := strconv.Atoi(getField(r, 1)) 102 | fmt.Fprintf(w, "apiDeleteWidgetPart %s %d\n", slug, id) 103 | } 104 | 105 | func widget(w http.ResponseWriter, r *http.Request) { 106 | slug := getField(r, 0) 107 | fmt.Fprintf(w, "widget %s\n", slug) 108 | } 109 | 110 | func widgetAdmin(w http.ResponseWriter, r *http.Request) { 111 | slug := getField(r, 0) 112 | fmt.Fprintf(w, "widgetAdmin %s\n", slug) 113 | } 114 | 115 | func widgetImage(w http.ResponseWriter, r *http.Request) { 116 | slug := getField(r, 0) 117 | fmt.Fprintf(w, "widgetImage %s\n", slug) 118 | } 119 | -------------------------------------------------------------------------------- /shiftpath/route.go: -------------------------------------------------------------------------------- 1 | // Routing based on Axel Wagner's ShiftPath approach: 2 | // https://blog.merovius.de/2017/06/18/how-not-to-use-an-http-router.html 3 | 4 | package shiftpath 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | "path" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | var Serve = noTrailingSlash(serve) 15 | 16 | func serve(w http.ResponseWriter, r *http.Request) { 17 | var head string 18 | head, r.URL.Path = shiftPath(r.URL.Path) 19 | switch head { 20 | case "": 21 | serveHome(w, r) 22 | case "api": 23 | serveApi(w, r) 24 | case "contact": 25 | serveContact(w, r) 26 | default: 27 | widget{head}.ServeHTTP(w, r) 28 | } 29 | } 30 | 31 | // shiftPath splits the given path into the first segment (head) and 32 | // the rest (tail). For example, "/foo/bar/baz" gives "foo", "/bar/baz". 33 | func shiftPath(p string) (head, tail string) { 34 | p = path.Clean("/" + p) 35 | i := strings.Index(p[1:], "/") + 1 36 | if i <= 0 { 37 | return p[1:], "/" 38 | } 39 | return p[1:i], p[i:] 40 | } 41 | 42 | // ensureMethod is a helper that reports whether the request's method is 43 | // the given method, writing an Allow header and a 405 Method Not Allowed 44 | // if not. The caller should return from the handler if this returns false. 45 | func ensureMethod(w http.ResponseWriter, r *http.Request, method string) bool { 46 | if method != r.Method { 47 | w.Header().Set("Allow", method) 48 | http.Error(w, "405 method not allowed", http.StatusMethodNotAllowed) 49 | return false 50 | } 51 | return true 52 | } 53 | 54 | // noTrailingSlash is a HandlerFunc wrapper (decorator) that return 55 | // 404 Not Found for any URL with a trailing slash (except "/" itself). 56 | // This is needed for our URLs, as the ShiftPath approach doesn't 57 | // distinguish between no trailing slash and trailing slash, and I 58 | // can't find a simple way to make it do that. 59 | func noTrailingSlash(h http.HandlerFunc) http.HandlerFunc { 60 | return func(w http.ResponseWriter, r *http.Request) { 61 | if r.URL.Path != "/" && strings.HasSuffix(r.URL.Path, "/") { 62 | http.NotFound(w, r) 63 | return 64 | } 65 | h(w, r) 66 | } 67 | } 68 | 69 | func serveHome(w http.ResponseWriter, r *http.Request) { 70 | if !ensureMethod(w, r, "GET") { 71 | return 72 | } 73 | fmt.Fprint(w, "home\n") 74 | } 75 | 76 | func serveContact(w http.ResponseWriter, r *http.Request) { 77 | var head string 78 | head, r.URL.Path = shiftPath(r.URL.Path) 79 | if head != "" { 80 | http.NotFound(w, r) 81 | return 82 | } 83 | if !ensureMethod(w, r, "GET") { 84 | return 85 | } 86 | fmt.Fprint(w, "contact\n") 87 | } 88 | 89 | func serveApi(w http.ResponseWriter, r *http.Request) { 90 | var head string 91 | head, r.URL.Path = shiftPath(r.URL.Path) 92 | switch head { 93 | case "widgets": 94 | serveApiWidgets(w, r) 95 | default: 96 | http.NotFound(w, r) 97 | } 98 | } 99 | 100 | func serveApiWidgets(w http.ResponseWriter, r *http.Request) { 101 | var head string 102 | head, r.URL.Path = shiftPath(r.URL.Path) 103 | switch head { 104 | case "": 105 | if r.Method == "GET" { 106 | serveApiGetWidgets(w, r) 107 | } else { 108 | serveApiCreateWidget(w, r) 109 | } 110 | default: 111 | apiWidget{head}.ServeHTTP(w, r) 112 | } 113 | } 114 | 115 | func serveApiGetWidgets(w http.ResponseWriter, r *http.Request) { 116 | if !ensureMethod(w, r, "GET") { 117 | return 118 | } 119 | fmt.Fprint(w, "apiGetWidgets\n") 120 | } 121 | 122 | func serveApiCreateWidget(w http.ResponseWriter, r *http.Request) { 123 | if !ensureMethod(w, r, "POST") { 124 | return 125 | } 126 | fmt.Fprint(w, "apiCreateWidget\n") 127 | } 128 | 129 | type apiWidget struct { 130 | slug string 131 | } 132 | 133 | func (h apiWidget) ServeHTTP(w http.ResponseWriter, r *http.Request) { 134 | var head string 135 | head, r.URL.Path = shiftPath(r.URL.Path) 136 | switch head { 137 | case "": 138 | h.serveUpdate(w, r) 139 | case "parts": 140 | h.serveParts(w, r) 141 | default: 142 | http.NotFound(w, r) 143 | } 144 | } 145 | 146 | func (h apiWidget) serveUpdate(w http.ResponseWriter, r *http.Request) { 147 | if !ensureMethod(w, r, "POST") { 148 | return 149 | } 150 | fmt.Fprintf(w, "apiUpdateWidget %s\n", h.slug) 151 | } 152 | 153 | func (h apiWidget) serveParts(w http.ResponseWriter, r *http.Request) { 154 | var head string 155 | head, r.URL.Path = shiftPath(r.URL.Path) 156 | switch head { 157 | case "": 158 | h.serveCreatePart(w, r) 159 | default: 160 | id, err := strconv.Atoi(head) 161 | if err != nil || id <= 0 { 162 | http.NotFound(w, r) 163 | return 164 | } 165 | apiWidgetPart{h.slug, id}.ServeHTTP(w, r) 166 | } 167 | } 168 | 169 | func (h apiWidget) serveCreatePart(w http.ResponseWriter, r *http.Request) { 170 | if !ensureMethod(w, r, "POST") { 171 | return 172 | } 173 | fmt.Fprintf(w, "apiCreateWidgetPart %s\n", h.slug) 174 | } 175 | 176 | type apiWidgetPart struct { 177 | slug string 178 | id int 179 | } 180 | 181 | func (h apiWidgetPart) ServeHTTP(w http.ResponseWriter, r *http.Request) { 182 | var head string 183 | head, r.URL.Path = shiftPath(r.URL.Path) 184 | switch head { 185 | case "update": 186 | h.serveUpdate(w, r) 187 | case "delete": 188 | h.serveDelete(w, r) 189 | default: 190 | http.NotFound(w, r) 191 | } 192 | } 193 | 194 | func (h apiWidgetPart) serveUpdate(w http.ResponseWriter, r *http.Request) { 195 | var head string 196 | head, r.URL.Path = shiftPath(r.URL.Path) 197 | if head != "" { 198 | http.NotFound(w, r) 199 | return 200 | } 201 | if !ensureMethod(w, r, "POST") { 202 | return 203 | } 204 | fmt.Fprintf(w, "apiUpdateWidgetPart %s %d\n", h.slug, h.id) 205 | } 206 | 207 | func (h apiWidgetPart) serveDelete(w http.ResponseWriter, r *http.Request) { 208 | var head string 209 | head, r.URL.Path = shiftPath(r.URL.Path) 210 | if head != "" { 211 | http.NotFound(w, r) 212 | return 213 | } 214 | if !ensureMethod(w, r, "POST") { 215 | return 216 | } 217 | fmt.Fprintf(w, "apiDeleteWidgetPart %s %d\n", h.slug, h.id) 218 | } 219 | 220 | type widget struct { 221 | slug string 222 | } 223 | 224 | func (h widget) ServeHTTP(w http.ResponseWriter, r *http.Request) { 225 | var head string 226 | head, r.URL.Path = shiftPath(r.URL.Path) 227 | switch head { 228 | case "": 229 | h.serveGet(w, r) 230 | case "admin": 231 | h.serveAdmin(w, r) 232 | case "image": 233 | h.serveUpdateImage(w, r) 234 | default: 235 | http.NotFound(w, r) 236 | } 237 | } 238 | 239 | func (h widget) serveGet(w http.ResponseWriter, r *http.Request) { 240 | if !ensureMethod(w, r, "GET") { 241 | return 242 | } 243 | fmt.Fprintf(w, "widget %s\n", h.slug) 244 | } 245 | 246 | func (h widget) serveAdmin(w http.ResponseWriter, r *http.Request) { 247 | var head string 248 | head, r.URL.Path = shiftPath(r.URL.Path) 249 | if head != "" { 250 | http.NotFound(w, r) 251 | return 252 | } 253 | if !ensureMethod(w, r, "GET") { 254 | return 255 | } 256 | fmt.Fprintf(w, "widgetAdmin %s\n", h.slug) 257 | } 258 | 259 | func (h widget) serveUpdateImage(w http.ResponseWriter, r *http.Request) { 260 | var head string 261 | head, r.URL.Path = shiftPath(r.URL.Path) 262 | if head != "" { 263 | http.NotFound(w, r) 264 | return 265 | } 266 | if !ensureMethod(w, r, "POST") { 267 | return 268 | } 269 | fmt.Fprintf(w, "widgetImage %s\n", h.slug) 270 | } 271 | -------------------------------------------------------------------------------- /split/route.go: -------------------------------------------------------------------------------- 1 | // Go HTTP router based on strings.Split() with a switch statement 2 | 3 | package split 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | func Serve(w http.ResponseWriter, r *http.Request) { 13 | // Split path into slash-separated parts, for example, path "/foo/bar" 14 | // gives p==["foo", "bar"] and path "/" gives p==[""]. 15 | p := strings.Split(r.URL.Path, "/")[1:] 16 | n := len(p) 17 | 18 | var h http.Handler 19 | var id int 20 | switch { 21 | case n == 1 && p[0] == "": 22 | h = get(home) 23 | case n == 1 && p[0] == "contact": 24 | h = get(contact) 25 | case n == 2 && p[0] == "api" && p[1] == "widgets" && r.Method == "GET": 26 | h = get(apiGetWidgets) 27 | case n == 2 && p[0] == "api" && p[1] == "widgets": 28 | h = post(apiCreateWidget) 29 | case n == 3 && p[0] == "api" && p[1] == "widgets" && p[2] != "": 30 | h = post(apiWidget{p[2]}.update) 31 | case n == 4 && p[0] == "api" && p[1] == "widgets" && p[2] != "" && p[3] == "parts": 32 | h = post(apiWidget{p[2]}.createPart) 33 | case n == 6 && p[0] == "api" && p[1] == "widgets" && p[2] != "" && p[3] == "parts" && isId(p[4], &id) && p[5] == "update": 34 | h = post(apiWidgetPart{p[2], id}.update) 35 | case n == 6 && p[0] == "api" && p[1] == "widgets" && p[2] != "" && p[3] == "parts" && isId(p[4], &id) && p[5] == "delete": 36 | h = post(apiWidgetPart{p[2], id}.delete) 37 | case n == 1: 38 | h = get(widget{p[0]}.widget) 39 | case n == 2 && p[1] == "admin": 40 | h = get(widget{p[0]}.admin) 41 | case n == 2 && p[1] == "image": 42 | h = post(widget{p[0]}.image) 43 | default: 44 | http.NotFound(w, r) 45 | return 46 | } 47 | h.ServeHTTP(w, r) 48 | } 49 | 50 | func allowMethod(h http.HandlerFunc, method string) http.HandlerFunc { 51 | return func(w http.ResponseWriter, r *http.Request) { 52 | if method != r.Method { 53 | w.Header().Set("Allow", method) 54 | http.Error(w, "405 method not allowed", http.StatusMethodNotAllowed) 55 | return 56 | } 57 | h(w, r) 58 | } 59 | } 60 | 61 | func get(h http.HandlerFunc) http.HandlerFunc { 62 | return allowMethod(h, "GET") 63 | } 64 | 65 | func post(h http.HandlerFunc) http.HandlerFunc { 66 | return allowMethod(h, "POST") 67 | } 68 | 69 | func isId(s string, p *int) bool { 70 | id, err := strconv.Atoi(s) 71 | if err != nil || id <= 0 { 72 | return false 73 | } 74 | *p = id 75 | return true 76 | } 77 | 78 | func home(w http.ResponseWriter, r *http.Request) { 79 | fmt.Fprint(w, "home\n") 80 | } 81 | 82 | func contact(w http.ResponseWriter, r *http.Request) { 83 | fmt.Fprint(w, "contact\n") 84 | } 85 | 86 | func apiGetWidgets(w http.ResponseWriter, r *http.Request) { 87 | fmt.Fprint(w, "apiGetWidgets\n") 88 | } 89 | 90 | func apiCreateWidget(w http.ResponseWriter, r *http.Request) { 91 | fmt.Fprint(w, "apiCreateWidget\n") 92 | } 93 | 94 | type apiWidget struct { 95 | slug string 96 | } 97 | 98 | func (h apiWidget) update(w http.ResponseWriter, r *http.Request) { 99 | fmt.Fprintf(w, "apiUpdateWidget %s\n", h.slug) 100 | } 101 | 102 | func (h apiWidget) createPart(w http.ResponseWriter, r *http.Request) { 103 | fmt.Fprintf(w, "apiCreateWidgetPart %s\n", h.slug) 104 | } 105 | 106 | type apiWidgetPart struct { 107 | slug string 108 | id int 109 | } 110 | 111 | func (h apiWidgetPart) update(w http.ResponseWriter, r *http.Request) { 112 | fmt.Fprintf(w, "apiUpdateWidgetPart %s %d\n", h.slug, h.id) 113 | } 114 | 115 | func (h apiWidgetPart) delete(w http.ResponseWriter, r *http.Request) { 116 | fmt.Fprintf(w, "apiDeleteWidgetPart %s %d\n", h.slug, h.id) 117 | } 118 | 119 | type widget struct { 120 | slug string 121 | } 122 | 123 | func (h widget) widget(w http.ResponseWriter, r *http.Request) { 124 | fmt.Fprintf(w, "widget %s\n", h.slug) 125 | } 126 | 127 | func (h widget) admin(w http.ResponseWriter, r *http.Request) { 128 | fmt.Fprintf(w, "widgetAdmin %s\n", h.slug) 129 | } 130 | 131 | func (h widget) image(w http.ResponseWriter, r *http.Request) { 132 | fmt.Fprintf(w, "widgetImage %s\n", h.slug) 133 | } 134 | -------------------------------------------------------------------------------- /stdlib/route.go: -------------------------------------------------------------------------------- 1 | // Routing based on Go 1.22's http.ServeMux enhancements 2 | 3 | //go:build go1.22 4 | 5 | package stdlib 6 | 7 | import ( 8 | "fmt" 9 | "net/http" 10 | "strconv" 11 | ) 12 | 13 | var Serve http.Handler 14 | 15 | func init() { 16 | r := http.NewServeMux() 17 | 18 | r.HandleFunc("GET /{$}", home) 19 | r.HandleFunc("GET /contact", contact) 20 | r.HandleFunc("GET /api/widgets", apiGetWidgets) 21 | r.HandleFunc("POST /api/widgets", apiCreateWidget) 22 | r.HandleFunc("POST /api/widgets/{slug}", apiUpdateWidget) 23 | r.HandleFunc("POST /api/widgets/{slug}/parts", apiCreateWidgetPart) 24 | r.HandleFunc("POST /api/widgets/{slug}/parts/{id}/update", apiUpdateWidgetPart) 25 | r.HandleFunc("POST /api/widgets/{slug}/parts/{id}/delete", apiDeleteWidgetPart) 26 | r.HandleFunc("GET /{slug}", widgetGet) 27 | r.HandleFunc("GET /{slug}/admin", widgetAdmin) 28 | r.HandleFunc("POST /{slug}/image", widgetImage) 29 | 30 | Serve = r 31 | } 32 | 33 | func home(w http.ResponseWriter, r *http.Request) { 34 | fmt.Fprint(w, "home\n") 35 | } 36 | 37 | func contact(w http.ResponseWriter, r *http.Request) { 38 | fmt.Fprint(w, "contact\n") 39 | } 40 | 41 | func apiGetWidgets(w http.ResponseWriter, r *http.Request) { 42 | fmt.Fprint(w, "apiGetWidgets\n") 43 | } 44 | 45 | func apiCreateWidget(w http.ResponseWriter, r *http.Request) { 46 | fmt.Fprint(w, "apiCreateWidget\n") 47 | } 48 | 49 | func apiUpdateWidget(w http.ResponseWriter, r *http.Request) { 50 | slug := r.PathValue("slug") 51 | fmt.Fprintf(w, "apiUpdateWidget %s\n", slug) 52 | } 53 | 54 | func apiCreateWidgetPart(w http.ResponseWriter, r *http.Request) { 55 | slug := r.PathValue("slug") 56 | fmt.Fprintf(w, "apiCreateWidgetPart %s\n", slug) 57 | } 58 | 59 | func apiUpdateWidgetPart(w http.ResponseWriter, r *http.Request) { 60 | slug := r.PathValue("slug") 61 | id, err := strconv.Atoi(r.PathValue("id")) 62 | if err != nil { 63 | http.NotFound(w, r) 64 | return 65 | } 66 | fmt.Fprintf(w, "apiUpdateWidgetPart %s %d\n", slug, id) 67 | } 68 | 69 | func apiDeleteWidgetPart(w http.ResponseWriter, r *http.Request) { 70 | slug := r.PathValue("slug") 71 | id, err := strconv.Atoi(r.PathValue("id")) 72 | if err != nil { 73 | http.NotFound(w, r) 74 | return 75 | } 76 | fmt.Fprintf(w, "apiDeleteWidgetPart %s %d\n", slug, id) 77 | } 78 | 79 | func widgetGet(w http.ResponseWriter, r *http.Request) { 80 | slug := r.PathValue("slug") 81 | fmt.Fprintf(w, "widget %s\n", slug) 82 | } 83 | 84 | func widgetAdmin(w http.ResponseWriter, r *http.Request) { 85 | slug := r.PathValue("slug") 86 | fmt.Fprintf(w, "widgetAdmin %s\n", slug) 87 | } 88 | 89 | func widgetImage(w http.ResponseWriter, r *http.Request) { 90 | slug := r.PathValue("slug") 91 | fmt.Fprintf(w, "widgetImage %s\n", slug) 92 | } 93 | -------------------------------------------------------------------------------- /stdlib/route_pre1.22.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.22 2 | 3 | package stdlib 4 | 5 | import ( 6 | "net/http" 7 | ) 8 | 9 | var Serve http.Handler 10 | --------------------------------------------------------------------------------