Last active
July 28, 2017 15:36
-
-
Save weatherglass/62bd8a704d4dfdc608fe5c5cb5a6980c to your computer and use it in GitHub Desktop.
Comparing a mux to handwritten handlers
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
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/
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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:
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 byfmt.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.