Extend ServeMux to register method handler
By Audren Bouëssel du Bourg
When building a web api with the stdlib, a typical handler may look like this:
mux := http.NewServeMux()
mux.HandleFunc("/ping", handlePing)
func handlePing(w http.ResponseWriter, r *http.Method) {
    switch r.Method {
        case http.MethodGet: 
            fmt.Fprintf(w, "pong")
        default:
            http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
            return
    }
}
It is quite powerful as is and you might not need anything else. It is very clear how methods are handled.
One thing I really appreciate when building a rest api is the ability to register routes based on method provided, such as:
r := newRouter() // a router not implemented yet
r.GET("/ping", handlePing)
func handlePing(w http.ResponseWriter, r *http.Method) {
-    switch r.Method {
-        case http.MethodGet: 
            fmt.Fprintf(w, "pong")
-        default:
-            http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
-            return
-    }
}
Let’s try to reproduce that behavior with http.ServeMux to extend it a little bit!
You can find the github for this article here: https://github.com/audrenbdb/router
First of all we want a router struct, keeping track of our endpoints and their handlers for each methods:
type router struct {
	endpoints []endpoint
}
type endpoint struct {
	pattern string
	handler methodHandler
}
// methodHandler is a map of request method : http.Handler
type methodHandler map[string]http.Handler
The router shall have the ability to register a an endpoint method and its handler.
func (r *router) POST(pattern string, handlerFunc http.HandlerFunc) {
	r.registerEndpoint(http.MethodPost, pattern, handlerFunc)
}
func (r *router) PUT(pattern string, handlerFunc http.HandlerFunc) {
	r.registerEndpoint(http.MethodPut, pattern, handlerFunc)
}
func (r *router) PATCH(pattern string, handlerFunc http.HandlerFunc) {
	r.registerEndpoint(http.MethodPatch, pattern, handlerFunc)
}
func (r *router) DELETE(pattern string, handlerFunc http.HandlerFunc) {
	r.registerEndpoint(http.MethodDelete, pattern, handlerFunc)
}
// registerEndpoint adds an endpoint handler 
// from given method to router.
// 
// an endpoint with same method cannot be registered twice
func (r *router) registerEndpoint(method, pattern string, handler http.Handler) {
	ep := r.findEndpoint(pattern)
	if ep == nil {
		newEndpoint := endpoint{
			pattern: pattern,
			handler: map[string]http.Handler{},
		}
		r.endpoints = append(r.endpoints, newEndpoint)
		ep = &newEndpoint
	}
	_, methodFound := ep.handler[method]
	if methodFound {
		msg := fmt.Sprintf("method %s already registered for pattern %s", method, pattern)
		log.Fatal(msg)
	}
	ep.handler[method] = handler
}
func (r *router) findEndpoint(pattern string) *endpoint {
	for _, ep := range r.endpoints {
		if ep.pattern == pattern {
			return &ep
		}
	}
	return nil
}
Our method handler will dispatch request based on its method, or error with http.StatusMethodNotAllowed if request method is not registered on given endpoint. See below:
func (m methodHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	h, ok := m[r.Method]
	if !ok {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}
	h.ServeHTTP(w, r)
}
Finally, we need to ensure that are router implements aswell the http.Handler interface, so that we can easily use it with http.ListenAndServe(":8080", router).
Remember, in the end, a regular http.ServeMux will be used.
For that we need a little adjustment on our router structure:
type router struct {
+   muxMutex  sync.Mutex
+	mux       *http.ServeMux    
	endpoints []*endpoint
}
Then, we can implement http.Handler. The router will set up mux on the first request.
To prevent race condition, we use a mutex so that mux is only going to be initialized once.
Once initialized, router will use ServeMux of our router to handle request.
func (r *router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	if r.mux == nil {
		r.initializeMux()
	}
	r.mux.ServeHTTP(w, req)
}
func (r *router) initializeMux() {
	r.muxMutex.Lock()
	defer r.muxMutex.Unlock()
	if r.mux != nil {
		return
	}
	r.mux = http.NewServeMux()
	for _, ep := range r.endpoints {
		r.mux.Handle(ep.pattern, ep.handler)
	}
}
Usage
func main() {
    r := router.New()
    r.GET("/foo", handleFoo)
    
    http.ListenAndServe(":8080", r)
}
func handleFoo(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "bar")
}
Code: https://github.com/audrenbdb/router
Closing thoughts
Other feature I like from popular routers is the named parameters such as /posts/:id/authors.
When not a requirement, stdlib is perfect.