Skip to content

Instantly share code, notes, and snippets.

@weatherglass
Last active July 28, 2017 15:36
Show Gist options
  • Save weatherglass/62bd8a704d4dfdc608fe5c5cb5a6980c to your computer and use it in GitHub Desktop.
Save weatherglass/62bd8a704d4dfdc608fe5c5cb5a6980c to your computer and use it in GitHub Desktop.
Comparing a mux to handwritten handlers
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)
}
}
@Merovius
Copy link

Merovius commented Jul 28, 2017

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.

@weatherglass
Copy link
Author

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