Developing Web Servers with Golang — From Simple to Complex

Developing Web Servers with Golang — From Simple to Complex

Five years ago, I started developing Gophish, which allowed me to learn Golang. I realized that Go is a powerful language, and its capabilities are enhanced by numerous libraries. Go is versatile: in particular, it allows for seamless server application development.

This article is dedicated to writing a server in Go. We'll start with simple things like "Hello, world!" and finish with an application featuring the following capabilities:

— Using Let’s Encrypt for HTTPS.
— Functioning as an API router.
— Working with middleware.
— Handling static files.
— Graceful shutdown.

Skillbox recommends: Practical Course "Python Developer from Scratch".

Reminder: for all readers of 'Habr' - a discount of 10,000 rubles when enrolling in any Skillbox course with the promo code 'Habr'.

Hello, world!

Creating a web server in Go can be done very quickly. Here’s an example of a handler that returns the previously mentioned "Hello, world!".

package main
 
import (
"fmt"
"net/http"
)
 
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World!")
})
http.ListenAndServe(":80", nil)
}

After this, if you run the application and open the page localhost, you will immediately see the text "Hello, world!" (of course, if everything works correctly).

Next, we will repeatedly use the handler, but first, let’s understand how everything works.

net/http

In the example, the package was used net/http, which is the primary tool in Go for developing both servers and HTTP clients. To understand the code, let’s clarify the meaning of three essential elements: http.Handler, http.ServeMux, and http.Server.

HTTP Handlers

When we receive a request, the handler analyzes it and generates a response. Handlers in Go are implemented as follows:

type Handler interface {
        ServeHTTP(ResponseWriter, *Request)
}

In the first example, the helper function http.HandleFunc is used. It wraps another function that, in turn, accepts http.ResponseWriter and http.Request in ServeHTTP.

In other words, handlers in Golang are represented by a unified interface, which provides many opportunities for the programmer. For instance, middleware is implemented using a handler, where ServeHTTP first does something, and then calls the ServeHTTP method of another handler.

As mentioned earlier, handlers simply create responses to requests. But which specific handler should be used at a given moment?

Routing Requests

To make the right choice, use an HTTP multiplexer. In some libraries, it's called a muxer or router, but they all refer to the same thing. The function of the multiplexer is to analyze the request path and select the appropriate handler.

If you need support for complex routing, it's better to use third-party libraries. Some of the most advanced ones are gorilla/mux and go-chi/chi, these libraries allow for intermediate processing without much hassle. With their help, you can set up wildcard routing and perform a number of other tasks. Their advantage is compatibility with standard HTTP handlers. As a result, you can write simple code with the possibility of future modifications.

Working with complex frameworks in a normal situation will require somewhat non-standard solutions, which significantly complicates the use of default handlers. For the overwhelming majority of applications, a combination of the default library and a simple router will suffice.

Request handling

In addition, we need a component that will "listen" for incoming connections and redirect all requests to the correct handler. This task can be easily handled by http.Server.

Below it is shown that the server is responsible for all tasks related to connection handling. This includes, for example, working with the TLS protocol. The standard HTTP server is used for the implementation of http.ListenAndServe.

Now let’s look at more complex examples.

Adding Let’s Encrypt

By default, our application works over the HTTP protocol, but it is recommended to use the HTTPS protocol. In Go, this can be done effortlessly. If you have obtained a certificate and a private key, then it is sufficient to specify ListenAndServeTLS with the correct certificate and key file paths.

http.ListenAndServeTLS(":443", "cert.pem", "key.pem", nil)

There is always room for improvement.

Let’s Encrypt provides free certificates with the ability to automatically renew them. To use the service, you need the package autocert.

The simplest way to set it up is to use the autocert.NewListener method in combination with http.Serve. This method allows obtaining and renewing TLS certificates while the HTTP server handles requests:

http.Serve(autocert.NewListener("example.com"), nil)

If we open in the browser example.com, we get the HTTPS response "Hello, world!".

If more detailed configuration is needed, it's worth using the autocert.Manager. Then we create our own instance of http.Server (up to this point we have used the default one) and add the manager to the server's TLSConfig:

m := &autocert.Manager{
Cache:      autocert.DirCache("golang-autocert"),
Prompt:     autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist("example.org", "www.example.org"),
}
server := &http.Server{
    Addr:      ":443",
    TLSConfig: m.TLSConfig(),
}
server.ListenAndServeTLS("", "")

This is a simple way to implement full HTTPS support with automatic certificate renewal.

Adding custom routes

The default router included in the standard library is good, but it is very basic. Most applications require more complex routing, including nested and wildcard routes or a procedure for setting up templates and path parameters.

In this case, it's worth using packages gorilla/mux and go-chi/chi. We will learn to work with the latter — an example is shown below.

Given — the file api/v1/api.go, containing routes for our API:

/ HelloResponse is the JSON representation for a customized message
type HelloResponse struct {
Message string `json:"message"`
}
 
// HelloName returns a personalized JSON message
func HelloName(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
response := HelloResponse{
Message: fmt.Sprintf("Hello %s!", name),
}
jsonResponse(w, response, http.StatusOK)
}
 
// NewRouter returns an HTTP handler that implements the routes for the API
func NewRouter() http.Handler {
r := chi.NewRouter()
r.Get("/{name}", HelloName)
return r
}

We set a prefix api/vq for routes in the main file.

We can then mount this to our main router under the api/v1/ prefix back in our main application:

// NewRouter returns a new HTTP handler that implements the main server routes
func NewRouter() http.Handler {
router := chi.NewRouter()
    router.Mount("/api/v1/", v1.NewRouter())
    return router
}
http.Serve(autocert.NewListener("example.com"), NewRouter())

The simplicity of handling complex routes in Go makes it possible to simplify the structure of maintaining large complex applications.

Working with middleware

In the case of middleware, it involves wrapping one HTTP handler with another, allowing for quick authentication, compression, logging, and some other functions.

For example, let's consider the http.Handler interface, and using it, we'll write a handler for authenticating service users.

func RequireAuthentication(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if !isAuthenticated(r) {
            http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
            return
        }
        // Assuming authentication passed, run the original handler
        next.ServeHTTP(w, r)
    })
}

There are external routers, for example, chi, that allow extending the functionality of middleware.

Working with static files

The standard library of Go includes capabilities for working with static content, including images, as well as JavaScript and CSS files. They can be accessed through the http.FileServer function. It returns a handler that serves files from a specified directory.

func NewRouter() http.Handler {
    router := chi.NewRouter()
    r.Get("/{name}", HelloName)
 
// Setting up static file distribution
staticPath, _ := filepath.Abs("../..//static/")
fs := http.FileServer(http.Dir(staticPath))
    router.Handle("/*", fs)
    
    return r

It is important to remember that http.Dir outputs the contents of the directory if there is no main index.html file in it. In this case, to prevent directory compromise, it is advisable to use the package. unindexed.

Graceful Shutdown

In Go, there is also a function for the graceful shutdown of an HTTP server. This can be done using the Shutdown() method. The server is started in a goroutine, and then a channel is listened to for an interrupt signal. Once the signal is received, the server shuts down, not immediately, but after a few seconds.

handler := server.NewRouter()
srv := &http.Server{
    Handler: handler,
}
 
go func() {
srv.Serve(autocert.NewListener(domains...))
}()
 
// Wait for an interrupt
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c
 
// Attempt a graceful shutdown
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
srv.Shutdown(ctx)

In conclusion

Go is a powerful language with an almost universal standard library. Its default capabilities are quite broad, and they can be enhanced through interfaces — this allows for the development of truly reliable HTTP servers.

Skillbox recommends:

Source: habr.com

Buy reliable website hosting with DDoS protection, VPS VDS servers 🔥 Buy reliable website hosting with DDoS protection, VPS VDS servers | ProHoster