-
-
Save weatherglass/62bd8a704d4dfdc608fe5c5cb5a6980c to your computer and use it in GitHub Desktop.
package main | |
import ( | |
"log" | |
"net/http" | |
) | |
func main() { | |
go func() { | |
log.Println(http.ListenAndServe("127.0.0.1:8111", mux)) | |
}() | |
log.Println(http.ListenAndServe("127.0.0.1:8222", app)) | |
} |
package main | |
import ( | |
"fmt" | |
"net/http" | |
"path" | |
"strconv" | |
"strings" | |
) | |
func ShiftPath(p string) (head, tail string) { | |
if p == "" { | |
return "", "/" | |
} | |
p = strings.TrimPrefix(path.Clean(p), "/") | |
i := strings.Index(p, "/") | |
if i < 0 { | |
return p, "/" | |
} | |
return p[:i], p[i:] | |
} | |
type App struct { | |
UserHandler *UserHandler | |
} | |
func (h *App) ServeHTTP(res http.ResponseWriter, req *http.Request) { | |
var head string | |
head, req.URL.Path = ShiftPath(req.URL.Path) | |
if head == "user" { | |
h.UserHandler.ServeHTTP(res, req) | |
return | |
} | |
http.Error(res, "Not Found", http.StatusNotFound) | |
} | |
type UserHandler struct { | |
ProfileHandler *ProfileHandler | |
AccountHandler *AccountHandler | |
} | |
func (h *UserHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) { | |
var head string | |
head, req.URL.Path = ShiftPath(req.URL.Path) | |
switch req.Method { | |
case "GET": | |
if req.URL.Path != "/" { | |
id, err := strconv.Atoi(head) | |
if err != nil { | |
http.Error(res, fmt.Sprintf("Invalid user id %q", head), http.StatusBadRequest) | |
return | |
} | |
head, tail := ShiftPath(req.URL.Path) | |
if tail != "/" { | |
http.Error(res, "Not Found", http.StatusNotFound) | |
return | |
} | |
switch head { | |
case "profile": | |
h.ProfileHandler.Handler(id).ServeHTTP(res, req) | |
case "account": | |
h.AccountHandler.Handler(id).ServeHTTP(res, req) | |
default: | |
http.Error(res, "Not Found", http.StatusNotFound) | |
} | |
return | |
} | |
http.Error(res, "Not Found", http.StatusNotFound) | |
case "POST": | |
if head == "" && req.URL.Path == "/" { | |
h.NewUser(res, req) | |
} else { | |
http.Error(res, "Not Found", http.StatusNotFound) | |
} | |
default: | |
http.Error(res, "Only GET and POST are allowed", http.StatusMethodNotAllowed) | |
} | |
} | |
func (h *UserHandler) NewUser(res http.ResponseWriter, req *http.Request) { | |
fmt.Fprintf(res, "Creating new user...") | |
} | |
type ProfileHandler struct{} | |
func (h *ProfileHandler) Handler(id int) http.Handler { | |
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { | |
fmt.Fprintf(res, "Profile for user id: %d", id) | |
}) | |
} | |
type AccountHandler struct{} | |
func (h *AccountHandler) Handler(id int) http.Handler { | |
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { | |
fmt.Fprintf(res, "Account for user id: %d", id) | |
}) | |
} | |
var app = &App{UserHandler: new(UserHandler)} |
package main | |
import ( | |
"net/http/httptest" | |
"testing" | |
) | |
func BenchmarkApp(b *testing.B) { | |
for i := 0; i < b.N; i++ { | |
b.StopTimer() | |
req1 := httptest.NewRequest("GET", "/user/1234/profile", nil) | |
res1 := httptest.NewRecorder() | |
req2 := httptest.NewRequest("GET", "/user/1234/account", nil) | |
res2 := httptest.NewRecorder() | |
req3 := httptest.NewRequest("POST", "/user", nil) | |
res3 := httptest.NewRecorder() | |
b.StartTimer() | |
app.ServeHTTP(res1, req1) | |
app.ServeHTTP(res2, req2) | |
app.ServeHTTP(res3, req3) | |
} | |
} |
package main | |
import ( | |
"fmt" | |
"log" | |
"net/http" | |
"strconv" | |
"github.com/go-util/router" | |
) | |
func GetUserProfile(res http.ResponseWriter, req *http.Request) { | |
id, err := strconv.Atoi(req.URL.Query().Get("id")) | |
if err != nil { | |
http.Error(res, "400 Invalid User ID", 400) | |
} | |
fmt.Fprintf(res, "Profile for user id: %d", id) | |
} | |
func GetUserAccount(res http.ResponseWriter, req *http.Request) { | |
id, err := strconv.Atoi(req.URL.Query().Get("id")) | |
if err != nil { | |
http.Error(res, "400 Invalid User ID", 400) | |
} | |
fmt.Fprintf(res, "Account for user id: %d", id) | |
} | |
func NewUser(res http.ResponseWriter, req *http.Request) { | |
fmt.Fprintf(res, "Creating new user...") | |
} | |
var mux = router.NewRouter() | |
func init() { | |
if err := mux.Get("/user/:id:[0-9]+/profile", GetUserProfile); err != nil { | |
log.Fatal(err) | |
} | |
if err := mux.Get("/user/:id:[0-9]+/account", GetUserAccount); err != nil { | |
log.Fatal(err) | |
} | |
if err := mux.Get("/user/:invalid", mux.Error(400)); err != nil { | |
log.Fatal(err) | |
} | |
if err := mux.Post("/user", NewUser); err != nil { | |
log.Fatal(err) | |
} | |
} |
package main | |
import ( | |
"net/http/httptest" | |
"testing" | |
) | |
func BenchmarkMux(b *testing.B) { | |
for i := 0; i < b.N; i++ { | |
b.StopTimer() | |
req1 := httptest.NewRequest("GET", "/user/1234/profile", nil) | |
res1 := httptest.NewRecorder() | |
req2 := httptest.NewRequest("GET", "/user/1234/account", nil) | |
res2 := httptest.NewRecorder() | |
req3 := httptest.NewRequest("POST", "/user", nil) | |
res3 := httptest.NewRecorder() | |
b.StartTimer() | |
mux.ServeHTTP(res1, req1) | |
mux.ServeHTTP(res2, req2) | |
mux.ServeHTTP(res3, req3) | |
} | |
} |
The App approach is from here:
https://blog.merovius.de/2017/06/18/how-not-to-use-an-http-router.html
When I wrote ShiftPath, I made the deliberate choice to concatenate in a "/" to make more readable code (as should be common practice). As you've demonstrated that it's posing a problem in this micro-benchmark, here is a slightly less clear, but zero-alloc version:
func ShiftPath(p string) (head, tail string) {
if p == "" {
return "", "/"
}
p = strings.TrimPrefix(path.Clean(p), "/")
i := strings.Index(p, "/")
if i < 0 {
return p, "/"
}
return p[:i], p[i:]
}
with it, the App-benchmark is both faster and allocates less.
Also note, that you are doing different things in the handlers. In the App-Handler, you are using Sprintf, whereas in the Mux handler, you are using concatenation (as your router only extracts the ID as a string, not as an int).
Lastly, you are doing res.Write([]byte(fmt.Sprintf("…", …)))
, which should be replaced by fmt.Fprintf(res, "…", …)"
; basically, you are doing a string->[]byte->string->[]byte conversion in the App-Handlers, but a string->[]byte conversion in the Mux-Handlers.
I updated the code so the work is more equivalent, thanks for pointing that out. The new benchmarks show improvement in the "app" approach, and degradation in the "mux" approach.
BenchmarkApp-8 300000 5645 ns/op 2407 B/op 19 allocs/op
BenchmarkMux-8 200000 7507 ns/op 3288 B/op 27 allocs/op
For other who find this, here's a link to our disussion on reddit:
https://www.reddit.com/r/golang/comments/6pw8od/go_http_router_i_decided_to_try_my_hand_at/
The two approaches are very similar in performance: