Back

/ 5 min read

How to create a go server using the standard library

Introduction

A go server has two components:

  1. a server that sits and listens to a port and routes requests to a handler.
  2. 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
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.

net/http/server.go
// ...
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.

main.go
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 Found
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Sat, 11 Jan 2025 07:36:54 GMT
Content-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:

  1. 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.
  2. 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.

main.go
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 OK
Date: Sat, 11 Jan 2025 08:00:49 GMT
Content-Length: 4
Content-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:

main.go
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.

net/http/server.go
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!