An Overview of Server-Side HTTP APIs in Go
Exploring the net/http package

Welcome to another edition of Just Enough Go — a series on the Go programming language which covers some of the most commonly used Go standard library packages: encoding/json
, io
, net/http
, sync
, etc.
In this piece, we’ll explore the net/http
package which provides the server and client-side APIs for HTTP services. This part will give provide an overview of the important server components (client APIs will be covered in another post)
All of the code examples are available on GitHub.
Let’s start off with the fundamental building block: ServeMux
and Server
ServeMux (Multiplexer)
Simply put, ServeMux
is an HTTP request multiplexer. It’s responsible for matching the URL in the request to an appropriate handler and executing it. You can create one by calling http.NewServeMux
. The next thing you do is attach URLs and their respective handler implementations to a ServeMux
instance using theHandle
and HandleFunc
methods.
Let’s look at how to use the Handle
method. It accepts a String
and an http.Handler
:
func (mux *ServeMux) Handle(pattern string, handler Handler)
http.Handler
is an interface (second parameter in the Handle
method) with the ServeHTTP
method:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
We can simply use a struct
to provide the implementation. For example:
func (h home) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
rw.Write([]byte("Welcome to the Just Enough Go! blog series!"))
}
Then attach this to the multiplexer as follows:
mux := http.NewServeMux()
mux.Handle("/", home{})
Let’s add another handler to our ServeMux
(mux
). This time, we'll use the HandleFunc
variant whose signature is the following:
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request))
Unlike the Handle
method, HandleFunc
accepts the handler implementation in the form of a function (along with the path for which it is to be invoked). You can use it like this:
mux.HandleFunc("/posts", func(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte("Visit http://bit.ly/just-enough-go to get started"))
})
Server (HTTP server)
Now we have a multiplexer which can respond if a user navigates to the root of our service; i.e. /
as well as /posts
. Let's tie it all together with a Server
. It's easy to create a new instance of a Server
:
server := http.Server{Addr: ":8080", Handler: mux}
There are a bunch of parameters which we can define for our HTTP server, but let’s look at a couple of important ones i.e. Addr
and Handler
(highlighted above) - Addr
is the address on which the server listens e.g. http://localhost:8080
and Handler
is actually an http.Handler
instance.
The Handler
bit is interesting because we just saw how the Handle
method in ServeMux
also accepts an http.Handler
. So, do we pass the same instance here as we did for the Handle
method in ServeMux
? And, if so, what's the point of doing it again?
If you just had route or path which you wanted to handle, you can pass an instance of an http.Handler
(e.g. home{}
in this case) and skip the ServeMux
altogether. Otherwise, for most cases, you can/should pass an instance of a ServeMux
so that you can handle multiple routes/paths (e.g. /home
, /items
etc.) This is possible because it implements http.Handler
. Internally, it works by dispatching or routing to the appropriate handler based on the path (URL) in http.Request
.
It defines a ServeHTTP
method as required by the http.Handler
interface:
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)
Note: The Handler
can be nil
- this scenario is discussed later in this post.
Great! So far, we have a ServeMux
with two handlers and we have associated the Server
with the multiplexer and defined where it will listen. Finally, you just need to start
it using the ListenAndServe
method:
server.ListenAndServe()
That’s it. Here is the consolidated code (it’s pretty small!):
package mainimport (
"log"
"net/http"
)func main() {
mux := http.NewServeMux() mux.Handle("/", home{}) mux.HandleFunc("/posts", func(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte("Visit http://bit.ly/just-enough-go to get started")) }) server := http.Server{Addr: ":8080", Handler: mux}
log.Fatal(server.ListenAndServe())
}type home struct{}func (h home) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
rw.Write([]byte("Welcome to the \"Just Enough Go\" blog series!!"))
}
To try this:
- simply save the code in a file (e.g.
go-http-1.go
) - run it —
go run go-http-1.go
- access the endpoints —
curl http://localhost:8080/
andcurl http://localhost:8080/posts
Default multiplexer
To make things simpler, there’s a ready-to-use multiplexer DefaultServeMux
. You don't need to use an explicit ServeMux
. The Handle
and HandleFunc
methods available in a ServeMux
are also exposed as global functions in the net/http
package for this purpose — you can use them the same way!
http.Handle("/users",myHandler{})http.HandleFunc("/items",func(rw http.ResponseWriter, req *http.Request){
//handler logic
})
To start the HTTP server, you can use http.ListenAndServe
function, just as you would with a Server
instance.
func ListenAndServe(addr string, handler Handler) error
The handler
parameter can be nil
if you have used http.Handle
and/or http.HandleFunc
to specify the handler implementations for the respective routes.
Functions as handlers
So far, we’ve seen how to use a struct in order to implement http.Handler
interface and use it in HandleFunc
. You might also want to use a standalone function without declaring a struct. The net/http
package defines a function type for this: http.HandlerFunc
type HandlerFunc func(ResponseWriter, *Request)
HandlerFunc
allows you to use ordinary functions as HTTP handlers. For example:
func welcome(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte("Welcome to Just Enough Go"))
}
welcome
is a standalone function with the required signature. You can use this in the Handle
method which accepts a http.Handler
as follows:
http.ListenAndServe(":8080", http.HandlerFunc(welcome))
Note: HandlerFunc(f)
is a Handler that calls the function f
The code looks like this:
package mainimport "net/http"func main() {
http.Handle("/welcome", http.HandlerFunc(welcome))
http.ListenAndServe(":8080", nil)
}func welcome(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte("Welcome to Just Enough Go"))
}
To try this:
- simply save the code in a file (e.g.
go-http-2.go
) and - run it —
go run go-http-2.go
- access the endpoint —
curl http://localhost:8080/welcome
That’s all for this blog where we covered the basic constructs of the server-side HTTP APIs offered by the net/http
package.
I really hope you enjoyed and learned something from this. Please like and follow if you did!