/ 5 min read
How to create a go server using the standard library
Introduction
A go server has two components:
- a
server
that sits and listens to a port and routes requests to ahandler
. - a
handler
aka a router that handles the request passed to it.
Server
A Server
in the net/http
package is nothing but a struct with some fields to describe it’s functionality.
server := http.Server{ Addr: ":8080", Handler: handler, ReadTimeout: 10 * time.Second, // optional WriteTimeout: 10 * time.Second, // optional MaxHeaderBytes: 1 << 20, // optional}}
There’s a whole bunch of other fields as well that we’re not going to go into. But you can read more about them here.
Handler
The Handler
in net/http
is an interface that implements the ServeHTTP
function. Let’s look at the source code to see what we’re talking about.
// ...type Handler interface { ServeHTTP(ResponseWriter, *Request)}// ...
This means that any struct that implements the function ServeHTTP
can satisfy the handler
and be passed to the server.
Thankfully, the net/http
package already gives us such an implementation for it in the form of a multiplexer called ServeMux
. This not only implements the Handler
interface, but also allows us to describe complex routes that can be handled by us using the Handle
and HandleFunc
methods.
Building a Basic Server
Let’s use what we’ve learnt to build a basic server.
package main
import ( "net/http" "log")
func main() { router := http.NewServeMux() // creates an empty `ServeMux` struct // that we can configure and attach routes to
server := http.Server{ Addr: ":8080", Handler: router, } // creates a server and configures // it with the bare essentials, i.e. // The address to listen to, and a handler
server.ListenAndServe() // starts the server on the specified port}
Great! Our server is up and running!
Let’s try getting requesting something from http://localhost:8080/ping
and see how it does.
HTTP/1.1 404 Not FoundContent-Type: text/plain; charset=utf-8X-Content-Type-Options: nosniffDate: Sat, 11 Jan 2025 07:36:54 GMTContent-Length: 19
404 page not found
It throws a 404! That’s because while the server is up and running, it doesn’t have routes registered that it can handle. Let’s fix that.
Handling Requests
Let’s create a request handler function that will handle the /ping
route and return a pong
.
We’ll import the fmt
package to help us write to our ResponseWriter
and use the HandleFunc
method on our ServeMux
router to help us define a route.
The HandleFunc
method on the ServeMux
takes two arguments:
pattern
a string that takes in the pattern that the route needs to match. For now, it’ll be a simple/pong
. But we can also define complex routes that take in parameters, something we’ll get to in later blog posts.handler
, a function with the following signature
func (writer http.ResponseWriter, request *http.Request)// `http.ResponseWriter`: This is where you'll write your responses to// `*http.Request`: This is pointer to the actual request object// that contains information about what is being requested.
Great! Let’s write our ping-pong
handler.
package main
import ( "net/http" "log" "fmt")
func pingPongHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "pong") // This writes `pong` to the response writer}
func main() { router := http.NewServeMux() server := http.Server{ Addr: ":8080", Handler: router, }
router.HandleFunc("/ping", pingPongHandler)
server.ListenAndServe()}
And now if you go and make a request to http://localhost:8080/ping
, you get:
HTTP/1.1 200 OKDate: Sat, 11 Jan 2025 08:00:49 GMTContent-Length: 4Content-Type: text/plain; charset=utf-8
pong
Yay! We’ve got our basic server working!
Cleaning Up
But that took some setting up to do.
Thankfully, the net/http
packages comes with a lot of quality of life features. One of which is the DefaultServeMux
, and the other is the ListenAndServe
function.
Which means we don’t have to create our own router handler just to spin up a server. And we don’t have to explicitly define a Server
struct either! ListenAndServe
can create one for us.
Let’s clean up our server code using these built in defaults:
package main
import ( "net/http" "log" "fmt")
func pingPongHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "pong") // This writes `pong` to the response writer}
func main() { http.HandleFunc("/ping", pingPongHandler) // This creates the route on the `DefaultServeMux`
http.ListenAndServe(":8080", nil) // This creates a basic server on the address and spins it up // You can also optinally pass in a handler // if nil, it defaults to using the `DefaultServeMux`}
The way this works is that if the server being used returns a nil
handler, it defaults to using the DefaultServerMux
. We can see this behaviour in the way http.ListenAndServe
is written as well.
func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe()}
All this function does is create a server and calls the ListenAndServe
on it.
And if we were to do the same thing in our application like so:
// ...func main() { http.HandleFunc("/ping", pingPongHandler)
server := http.Server{Addr: ":8080"} // `Handler` is `nil` server.ListenAndServe()}// ...
We haven’t actually created a Handler
for the server here, which means it defaults to using DefaultServerMux
. And we registered a route with it using the http.HandleFunc
function.
Running this we do see that the /ping
route works as expected.
So there’s plenty of ways to handle servers and routing but underlying structure and logic around it doesn’t change.
Even with the built in defaults and quality of life, there’s still the Server
and the Handler
. And these two things are all you need to get a server going!
All the best! And happy builing!
PS: If you found some inaccuracies in the post, please let me know, and I will correct them! I’m still learning Go and this is my way of sharing what I’m learning with everyone!
Thank you for reading!