Developing web servers in Golang - from simple to complex

Developing web servers in Golang - from simple to complex

Five years ago I started develop Gophish, it made it possible to learn Golang. I realized that Go is a powerful language, which is complemented by many libraries. Go is versatile: in particular, you can easily develop server-side applications with it.

This article is about writing a server in Go. Let's start with simple things like "Hello world!" and end up with an application with these features:

- Using Let's Encrypt for HTTPS.
- Work as an API router.
- Working with middleware.
- Handling static files.
- Correct shutdown.

Skillbox recommends: Practical course "Python developer from scratch".

We remind you: for all readers of "Habr" - a discount of 10 rubles when enrolling in any Skillbox course using the "Habr" promotional code.

Hello, world!

Creating a web server in Go can be very fast. Here is an example of using a handler that returns the "Hello, world!" promised above.

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 that, if you run the application and open the page localhost, then you will immediately see the text "Hello, world!" (of course, if everything works correctly).

We'll use the handler repeatedly in the following, but first let's understand how it all works.

net/http

The example used the package net/http, is Go's primary tool for developing both servers and HTTP clients. In order to understand the code, let's understand the meaning of three important elements: http.Handler, http.ServeMux and http.Server.

HTTP Handlers

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

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

The first example uses the http.HandleFunc helper function. It wraps another function which in turn accepts http.ResponseWriter and http.Request in ServeHTTP.

In other words, handlers in Golang are represented by a single interface, which gives a lot of opportunities for the programmer. So, for example, middleware is implemented using a handler, where ServeHTTP first does something and then calls the ServeHTTP method of another handler.

As mentioned above, handlers simply form responses to requests. But which handler should be used at a particular time?

Request Routing

In order to make the right choice, use the HTTP multiplexer. In a number of libraries it is called muxer or router, but they are all the same. The function of the multiplexer is to analyze the request path and select the appropriate handler.

If you need support for complex routing, then it is better to use third-party libraries. One of the most advanced gorilla/mux и go-chi/chi, these libraries make it possible to implement intermediate processing without any problems. 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 that can be modified in the future.

Working with complex frameworks in a normal situation will require not quite standard solutions, and this greatly complicates the use of default handlers. The combination of the default library and a simple router will suffice to create the vast majority of applications.

Query Processing

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

The following shows that the server is responsible for all tasks that are related to handling connections. This is, for example, work on the TLS protocol. A standard HTTP server is used to implement the http.ListenAndServer call.

Now let's look at more complex examples.

Adding Let's Encrypt

By default, our application runs over the HTTP protocol, but it is recommended to use the HTTPS protocol. In Go, this can be done without problems. If you received a certificate and a private key, then it is enough to write ListenAndServeTLS with the correct certificate and key files.

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

You can always do better.

Let's Encrypt gives free certificates with the possibility of automatic renewal. In order to use the service, you need a package autocert.

The easiest way to set it up is to use the autocert.NewListener method in combination with http.Serve. The method allows you to receive and renew TLS certificates while the HTTP server processes requests:

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

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

If you need more detailed configuration, then you should use the autocert.Manager. Then we create our own http.Server instance (so far we have used it by default) and add the manager to the TLSConfig server:

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 an easy way to implement full HTTPS support with automatic certificate renewal.

Adding Custom Routes

The default router included in the standard library is nice, but very basic. Most applications need more complex routing, including nested and wildcard routes, or setting patterns and path parameters.

In this case, you should use packages gorilla/mux и go-chi/chi. We will learn how to work with the latter - an example is shown below.

Given is the api/v1/api.go file containing the 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 the api/vq prefix for the 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 ease of working with complex routes in Go makes it possible to simplify the structuring and maintenance of large complex applications.

Working with middleware

In the case of intermediate processing, wrapping one HTTP handler with another is used, which makes it possible to quickly perform authentication, compression, logging, and some other functions.

As an example, let's consider the http.Handler interface, with its help we will write a handler with authentication of 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 third-party routers, such as chi, that allow you to extend the functionality of intermediate processing.

Working with static files

Go's standard library includes facilities 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 distributes files from a specific directory.

func NewRouter() http.Handler {
    router := chi.NewRouter()
    r.Get("/{name}", HelloName)
 
// Настройка раздачи статических файлов
staticPath, _ := filepath.Abs("../../static/")
fs := http.FileServer(http.Dir(staticPath))
    router.Handle("/*", fs)
    
    return r

Be sure to remember that http.Dir displays the contents of the directory if it does not contain the main index.html file. In this case, in order to prevent the directory from being compromised, you should use the package unindexed.

Correct shutdown

Go also has such a feature as a graceful shutdown of the HTTP server. This can be done using the Shutdown() method. The server is started in a goroutine, and then the channel is listened for to receive an interrupt signal. As soon as the signal is received, the server turns off, but 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)

As a conclusion

Go is a powerful language with an almost universal standard library. Its default capabilities are very wide, and you can strengthen them with the help of interfaces - this allows you to develop truly reliable HTTP servers.

Skillbox recommends:

Source: habr.com

Add a comment