Building a Robust Web Server in Go: A Step-by-Step Guide

Building a Robust Web Server in Go: A Step-by-Step Guide

We continue our journey implementing a real world web application in Golang in this blog post.

In the previous post we setup our docker development environment that just prints hello world.

In this post we are going to setup a real webserver that it's going to serve our web application.

We are going to learn how to:

  • setup the golang http.Server

  • automatically obtain a TLS certificate using let's encrypt

  • create and HTTP handler using Echo Framework

Webserver

Golang offers a very capable HTTP server in the package http . We are going to utilize that.

Le't first create a new git branch:

git checkout -b basic-webserver

Let's see our current project layout:

We will put the code of our webserver in the http package. Let's not complicate things and create extra packages or abstractions for now.

In the http/http.go

package http

import (
    "context"
    "crypto/tls"
    "net/http"
    "time"
)

type ServerParams struct {
    Handler http.Handler
}

type Server struct {
    websrv *http.Server
}

func New(params *ServerParams) *Server {
    ans := Server{
        websrv: &http.Server{
            Addr:              ":443",
            Handler:           params.Handler,
            ReadTimeout:       5 * time.Second,
            WriteTimeout:      10 * time.Second,
            IdleTimeout:       5 * time.Second,
            ReadHeaderTimeout: 5 * time.Second,
            MaxHeaderBytes:    1 << 20,
        },
    }

    return &ans
}

func (s *Server) Start(ctx context.Context) error {
    return s.websrv.ListenAndServeTLS("", "")
}

As you noticed all the settings are hardcoded for the moment. However, we have a params struct passed as an argument to the function so we can customize.

An interesting part is that our server listens to port 443 (TLS) but we haven't configured any SSL certificate.

Let's see the behavior

in cmd/main.go :

package main

import (
    "context"

    "invoicehub/http"
)

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    params := http.ServerParams{}

    srv := http.New(&params)

    err := srv.Start(ctx)
    if err != nil {
        panic(err)
    }
}

and then:

we need a valid SSL certificate.

We have actually two things to do:

  1. Make our server to automatically obtain the SSL certificate when we deploy to a server

  2. Provide a valid certificate when we work locally

Valid local certificate

Create a folder certs and add to gitignore

mkdir certs
echo "certs/*" > .gitignore

Install the awesome mkcert tool for your OS .

then add in your Makefile

create-certs: ## generate self-signed certificates
    @mkcert -install
    @mkcert -cert-file ./certs/local-cert.crt \
       -key-file ./certs/local-cert.key \
       local.freelance-invoice-hub.com localhost 127.0.0.1 ::1

And run make certs

the above created 2 files:
./certs/local-cert.crt

./certs/local-cert.key

We need to modify our code to use these certs when we work locally.

We read an environment variable called FIH_DOMAIN , if this is empty then use the certificates we just created.

(also import the os package in your imports)

Let's try that:

So for localhost it works, let's also configure the domain
local.freelance-invoice-hub.com.

For this to work we need to add an entry on

On linux add an entry on /etc/hosts like:

for other operating systems do the equivalent as described here

and try again :

Of course the HTTP status code we get is perfectly valid since we haven't registered anything on our server yet.

Obtain a TLS certificate automatically using Auto TLS

To obtain automatically a valid TLS certificate using Golang we are going to install two packages:

When in your root directory do :

go get "golang.org/x/crypto/acme" "golang.org/x/crypto/acme/autocert"

This will install the 2 packages . Actually these two package are part of the Go project but they are in different repos. That's why they are prefixed with an /x/ .

Now add a method in the Server struct:

func (s *Server) setupAutoTLS(domains []string) {
    const defaultCertCache = "/.cache/.certs"
    autoTLSManager := autocert.Manager{
        Prompt:     autocert.AcceptTOS,
        Cache:      autocert.DirCache(defaultCertCache),
        HostPolicy: autocert.HostWhitelist(domains...),
    }
    // https://ssl-config.mozilla.org/#server=go&version=1.22.0&config=intermediate&guideline=5.7
    s.websrv.TLSConfig = &tls.Config{
        MinVersion:               tls.VersionTLS12,
        PreferServerCipherSuites: true,
        CipherSuites: []uint16{
            tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
            tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
            tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
            tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
            tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
            tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
        },
        CurvePreferences: []tls.CurveID{
            tls.CurveP256,
            tls.X25519,
        },
        GetCertificate: autoTLSManager.GetCertificate,
        NextProtos: []string{
            "h2", "http/1.1", // enable HTTP/2
            acme.ALPNProto, // enable tls-alpn ACME challenges
        },
    }
}

and don't forget to import on top

    "golang.org/x/crypto/acme"
    "golang.org/x/crypto/acme/autocert"

Important note:

When I created the TLS configuration I selected some CipherSuites and Elliptic Curves. I used the ones in the code since they are the recommended ones by

๐Ÿ‘‰ mozilla.

In order to test that we need a domain and setup the Nameservers to point to our hosting server. In the next blog post we are going to deploy what we have so far in a real webserver. For now let's continue.

What is missing

Our server now does not gracefully exits when a SIGTERM is received.
We will leave it for now. It's good enough but we should revisit .

Let's finally make our server do something useful

We will finally add an HTTP handler that returns something super useful .

What's better from a HELLO WORLD that we can see in our browser ๐Ÿš€

First install the echo framework.

Wait a second why Echo? Echo makes our life a little bit easier without being huge and does not go into our way a lot. Another very good choice is chi . Basically I don't know, there are so many frameworks and benchmarks out there just pick Echo.

In any case, the way we are going to code our application we should be able to switch frameworks without having to rewrite everything (but let's avoid that).

So, install echo:

go get github.com/labstack/echo/v4

and then create a file http/router.go

package http

import (
    "net/http"

    "github.com/labstack/echo/v4"
)

type Router struct {
    e *echo.Echo
}

func NewRouter() *Router {
    ans := Router{
        e: echo.New(),
    }

    ans.e.Debug = true

    baseHandler := baseHandler{
        e: ans.e,
    }

    handlers := []handler{
        &baseHandler,
    }

    for _, h := range handlers {
        h.RegisterRoutes()
    }

    return &ans
}

func (r *Router) Handler() http.Handler {
    return r.e
}

type handler interface {
    RegisterRoutes()
}

type baseHandler struct {
    e *echo.Echo
}

func (b *baseHandler) RegisterRoutes() {
    b.e.GET("/", func(c echo.Context) error {
        return c.String(http.StatusOK, "HELLO WORLD")
    })
}

Here we just created an echo.Echo instance and attached a function to run
when we visit the path / on the server .

If you follow the tutorial so far then visit:

make dev

and visit:

https://local.freelance-invoice-hub.com

or

https://localhost

๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ

Commit

As usual we should commit our branch and you can find it in :
the github branch

Summary and what's next

In today's blog we learned how to create a webserver in Go, how to attach a router (echo in our case). Additionally we learned how to utilize golang's features to obtain automatically TLS certificates using Let's Encrypt (I forgot to mention that Go takes care of certificate renewal automatically). We also created valid TSL certificates for local development.

So far in the series we are just building our foundation, kind of our own framework to ease and speedup our development procedure. Most of the code we wrote is the same for many web apps and we could extract some libraries out of it or a template that we use in other projects.

In the next blog post we are going to add Basic authentication and show you how you can deploy the application in a cheap VPS. We wil also configure github CI/CD when we merge to main branch.

That's all for today.

โ“ If you have any questions or something is not running on your machine I am happy to help. Reach out via a comment and I will try to help

โค๏ธ Please subscribe to my newsletter and follow me on X or LinkedIn .

ย