Responding to requests via simple route matching is built in to Go's net/http
standard library package. Just register the path prefixes and callbacks you want invoked and then call the ListenAndServe
to have the default request handler invoked on each request. For example:
package main
import (
"io"
"log"
"net/http"
)
func main() {
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
io.WriteString(w, "Hello world\n")
})
err := http.ListenAndServe(":9000", nil)
if err != nil {
log.Fatalf("Could not start server: %s\n", err.Error())
}
}
While it may look strange to pass nil
as the second parameter to ListenAndServe
, this causes Go to use the DefaultServeMux
request multiplexer. Requests to the configured endpoint look like this:
$ curl -i http://localhost:9000/hello
HTTP/1.1 200 OK
Content-Type: text/plain
Date: Mon, 26 Jun 2017 23:58:40 GMT
Content-Length: 12
Hello world
By default, this handler will respond with a 404 when it can't find a match:
$ curl -i http://localhost:9000/
HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Mon, 26 Jun 2017 23:59:11 GMT
Content-Length: 19
404 page not found
As you can see above, we're not controlling the content of the response, so the Content-Type
header reverts to Go's default. If you would prefer not to rely on the "magic" of the default multiplexer, you can configure your own by creating a ServeMux
instance.
The process of registering callbacks with this method is similar to the previous example, but in this case we call HandleFunc
on the multiplexer that we create:
package main
import (
"fmt"
"io"
"log"
"net/http"
"strings"
)
func main() {
handler := http.NewServeMux()
handler.HandleFunc("/hello/", func(w http.ResponseWriter, r *http.Request) {
name := strings.Replace(r.URL.Path, "/hello/", "", 1)
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
io.WriteString(w, fmt.Sprintf("Hello %s\n", name))
})
handler.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
io.WriteString(w, "Hello world\n")
})
handler.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusNotFound)
io.WriteString(w, "Not found\n")
})
err := http.ListenAndServe(":9000", handler)
if err != nil {
log.Fatalf("Could not start server: %s\n", err.Error())
}
}
You'll notice two changes here:
- The route prefix for
/hello/
that will match against any subtree that matches this prefix - The custom handler for
/
that responds with a 404 status when the request was not matched by any previous pattern
Here are the responses for each:
$ curl -i http://localhost:9000/hello/Patrick
HTTP/1.1 200 OK
Content-Type: text/plain
Date: Tue, 27 Jun 2017 04:44:30 GMT
Content-Length: 14
Hello Patrick
$ curl -i http://localhost:9000/asdf
HTTP/1.1 404 Not Found
Content-Type: text/plain
Date: Tue, 27 Jun 2017 04:44:36 GMT
Content-Length: 10
Not found
The duplication present in each handler is something that can be easily refactored by moving away from interacting with http.ResponseWriter
directly, so we'll do that next.
The common tasks of writing the Content-Type
header, setting the HTTP status, and returning the body can be moved to a single function by embedding http.ResponseWriter
in a new struct and moving the duplicate code to our new struct. For example:
package main
import (
"fmt"
"io"
"log"
"net/http"
"strings"
)
type Response struct {
http.ResponseWriter
}
func (r *Response) Text(code int, body string) {
r.Header().Set("Content-Type", "text/plain")
r.WriteHeader(code)
io.WriteString(r, fmt.Sprintf("%s\n", body))
}
func main() {
handler := http.NewServeMux()
handler.HandleFunc("/hello/", func(w http.ResponseWriter, r *http.Request) {
name := strings.Replace(r.URL.Path, "/hello/", "", 1)
resp := Response{w}
resp.Text(http.StatusOK, fmt.Sprintf("Hello %s", name))
})
handler.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
resp := Response{w}
resp.Text(http.StatusOK, "Hello world")
})
handler.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
resp := Response{w}
resp.Text(http.StatusNotFound, "Not found")
})
err := http.ListenAndServe(":9000", handler)
if err != nil {
log.Fatalf("Could not start server: %s\n", err.Error())
}
}
The only difference between this and the previous example is that we've condensed the 3 lines needed to write a response down to a single call to Response.Text()
to set the status and send the body to the client. While assigning a local Response
instance is tedious, it's necessary in this case due to the specific function signature required by HandleFunc
. To see how we might simplify this, we'll have to write our own HTTP handler.
Per the documentation, a struct can be treated as a handler if it implements the ServeHTTP()
method. So, if we wanted to ditch the request multiplexer altogether, we could respond to requests directly from a custom handler defined in a new struct:
package main
import (
"io"
"log"
"net/http"
)
type App struct{}
func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
io.WriteString(w, "Hello world\n")
}
func main() {
err := http.ListenAndServe(":9000", &App{})
if err != nil {
log.Fatalf("Could not start server: %s\n", err.Error())
}
}
Since our App
struct responds to ServeHTTP
, we can pass it directly to ListenAndServe
and it will receive all HTTP requests. In this simple example, all requests receive the same response regardless of the request path:
$ curl -i http://localhost:9000/ok/pal
HTTP/1.1 200 OK
Content-Type: text/plain
Date: Tue, 27 Jun 2017 04:58:39 GMT
Content-Length: 12
Hello world
This isn't very interesting in itself, but it does now give us the ability to wrap each incoming request before passing it off to a custom handler function.
By using a custom handler, we now have control over when the requests are intercepted, we can pass our own request and response structs to the matched route. Since we are no longer using a http.ServeMux
instance, I've introduced a custom regular expression-based router:
package main
import (
"fmt"
"io"
"log"
"net/http"
"regexp"
)
type Handler func(*Response, *Request)
type Route struct {
Pattern *regexp.Regexp
Handler Handler
}
type App struct {
Routes []Route
DefaultRoute Handler
}
func NewApp() *App {
app := &App{
DefaultRoute: func(resp *Response, req *Request) {
resp.Text(http.StatusNotFound, "Not found")
},
}
return app
}
func (a *App) Handle(pattern string, handler Handler) {
re := regexp.MustCompile(pattern)
route := Route{Pattern: re, Handler: handler}
a.Routes = append(a.Routes, route)
}
func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
req := &Request{Request: r}
resp := &Response{w}
for _, rt := range a.Routes {
if matches := rt.Pattern.FindStringSubmatch(r.URL.Path); len(matches) > 0 {
if len(matches) > 1 {
req.Params = matches[1:]
}
rt.Handler(resp, req)
return
}
}
a.DefaultRoute(resp, req)
}
type Request struct {
*http.Request
Params []string
}
type Response struct {
http.ResponseWriter
}
func (r *Response) Text(code int, body string) {
r.Header().Set("Content-Type", "text/plain")
r.WriteHeader(code)
io.WriteString(r, fmt.Sprintf("%s\n", body))
}
func main() {
app := NewApp()
app.Handle(`^/hello$`, func(resp *Response, req *Request) {
resp.Text(http.StatusOK, "Hello world")
})
app.Handle(`/hello/([\w\._-]+)$`, func(resp *Response, req *Request) {
resp.Text(http.StatusOK, fmt.Sprintf("Hello %s", req.Params[0]))
})
err := http.ListenAndServe(":9000", app)
if err != nil {
log.Fatalf("Could not start server: %s\n", err.Error())
}
}
The App
struct now has a collection of routes, each with a corresponding callback. If the request path matches the configured pattern, that callback will be triggered. Otherwise, the default route will be invoked and the server will respond with a 404 status:
$ curl -i http://localhost:9000/hello
HTTP/1.1 200 OK
Content-Type: text/plain
Date: Tue, 27 Jun 2017 14:39:24 GMT
Content-Length: 12
Hello world
$ curl -i http://localhost:9000/hello/Patrick
HTTP/1.1 200 OK
Content-Type: text/plain
Date: Tue, 27 Jun 2017 14:39:28 GMT
Content-Length: 14
Hello Patrick
$ curl -i http://localhost:9000/missing
HTTP/1.1 404 Not Found
Content-Type: text/plain
Date: Tue, 27 Jun 2017 14:39:32 GMT
Content-Length: 10
Not found
The custom Request
struct is worth examining -- rather than performing substring replacement to determine the dynamic message, it keeps track of any capture groups in the route pattern and exposes them through the Params
field. While this gets the job done, it's not ideal from a design perspective.
Rather than storing Params
on the Request
struct, we can instead introduce another struct to capture these values and use type embedding to have http.Request
andhttp.ResponseWriter
handle the traditional HTTP interactions. This will also simplify the Handler
signature as it now only needs to take a single Context
struct and can perform all the response handling needed:
package main
import (
"fmt"
"io"
"log"
"net/http"
"regexp"
)
type Handler func(*Context)
type Route struct {
Pattern *regexp.Regexp
Handler Handler
}
type App struct {
Routes []Route
DefaultRoute Handler
}
func NewApp() *App {
app := &App{
DefaultRoute: func(ctx *Context) {
ctx.Text(http.StatusNotFound, "Not found")
},
}
return app
}
func (a *App) Handle(pattern string, handler Handler) {
re := regexp.MustCompile(pattern)
route := Route{Pattern: re, Handler: handler}
a.Routes = append(a.Routes, route)
}
func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := &Context{Request: r, ResponseWriter: w}
for _, rt := range a.Routes {
if matches := rt.Pattern.FindStringSubmatch(ctx.URL.Path); len(matches) > 0 {
if len(matches) > 1 {
ctx.Params = matches[1:]
}
rt.Handler(ctx)
return
}
}
a.DefaultRoute(ctx)
}
type Context struct {
http.ResponseWriter
*http.Request
Params []string
}
func (c *Context) Text(code int, body string) {
c.ResponseWriter.Header().Set("Content-Type", "text/plain")
c.WriteHeader(code)
io.WriteString(c.ResponseWriter, fmt.Sprintf("%s\n", body))
}
func main() {
app := NewApp()
app.Handle(`^/hello$`, func(ctx *Context) {
ctx.Text(http.StatusOK, "Hello world")
})
app.Handle(`/hello/([\w\._-]+)$`, func(ctx *Context) {
ctx.Text(http.StatusOK, fmt.Sprintf("Hello %s", ctx.Params[0]))
})
err := http.ListenAndServe(":9000", app)
if err != nil {
log.Fatalf("Could not start server: %s\n", err.Error())
}
}
This is just the start of what's possible when customizing an HTTP response handler. You can take this further by:
- Matching requests against a specific HTTP method (e.g. GET / POST) and having different handler for the different request types
- Matching against
Content-Type
to invoke a separate handler for different content requests (e.g.text/html
,application/json
, etc...) - Adding additional response methods (e.g.
ctx.JSON
) to send a more appropriate response for the requestedContent-Type