📜 ⬆️ ⬇️

Web server development on Golang - from easy to hard



Five years ago, I began to develop Gophish , this gave the opportunity to explore Golang. I realized that Go is a powerful language, the capabilities of which are complemented by a multitude of libraries. Go is universal: in particular, it can be used to develop server applications without any problems.

This article is dedicated to writing a server on Go. Let's start with simple things, like “Hello world!”, And end with an application with these features:
')
- Use Let's Encrypt for HTTPS.
- Work as an API router.
- Work with middleware.
- Processing static files.
- Correct completion of work.

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

We remind: for all readers of "Habr" - a discount of 10,000 rubles when writing to any Skillbox course on the promotional code "Habr".

Hello, world!


You can create a web server on Go very quickly. Here is an example of using a handler that returns the promised “Hello, world!” 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 start the application and open the localhost page, 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


The example used the net/http package; this is the main tool in Go 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 analyzes it and generates a response. Handlers in Go are implemented as follows:

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

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

In other words, the handlers in Golang are represented by a single interface, which provides many 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 specific point in time?

Request routing


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

If support of difficult routing is necessary, then it is better to use third-party libraries. Some of the most advanced are gorilla / mux and go-chi / chi , these libraries make it possible to implement intermediate processing without any special problems. With their help, you can configure wildcard-routing and perform a number of other tasks. Their plus is compatibility with standard HTTP handlers. As a result, you can write simple code with the possibility of its modification in the future.

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

Query Processing


In addition, we need a component that will “listen” to 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, for example, work on the TLS protocol. To implement the http.ListenAndServer call, a standard HTTP server is used.

Now let's look at more complex examples.

Adding Let's Encrypt


By default, our application works using the HTTP protocol, however, it is recommended to use the HTTPS protocol. Go can do this without problems. If you received a certificate and private key, then it is enough to register ListenAndServeTLS with the indication of 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 ability to automatically upgrade. In order to use the service, you need the autocert package.

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

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

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

If you need a more thorough setup, you should use the autocert.Manager 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 non-standard routes


The default router included in the standard library is good, but it is very simple. Most applications require more complex routing, including nested and wildcard routes or the procedure for installing patterns and path parameters.

In this case, it is worth using the gorilla / mux and go-chi / chi packages. 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 } 

Set the prefix api / vq for the routes in the main file.

We can then install our router under it / 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 working with complex routes in Go makes it possible to simplify structuring with the maintenance of large complex applications.

Work 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, consider the http.Handler interface, with its help we will write a handler with the 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, for example, chi, which allow you to extend the functionality of intermediate processing.

Work with static files


The standard Go library includes features for working with static content, including images, as well as JavaScript and CSS files. Access to them can be obtained through the function http.FileServer. 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 in the event that it does not contain the main index.html file. In this case, in order to avoid directory compromise, you should use the unindexed package.

Correct shutdown


In Go there is such a function as the correct shutdown of the HTTP server. This can be done using the Shutdown () method. The server starts in the gorutin, and then the channel is listened to receive the 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 a practically universal standard library. Its default capabilities are very wide, and you can enhance them with the help of interfaces - this allows you to develop truly reliable HTTP servers.
Skillbox recommends:

Source: https://habr.com/ru/post/446454/


All Articles