Golang Web Server Development - From Easy to Complex
- Transfer
Five years ago, I started developing Gophish , which made it possible to learn Golang. I realized that Go is a powerful language, the capabilities of which are complemented by many libraries. Go is universal: in particular, with its help it is possible to develop server applications without problems.
This article is about writing a server in Go. Let's start with simple things, like “Hello world!”, And end with an application with the following features:
- Using Let's Encrypt for HTTPS.
- Work as an API router.
- Work with middleware.
- Processing of static files.
- Correct shutdown.
Skillbox recommends: The Python Developer from scratch hands-on course .
We remind you: for all readers of “Habr” - a discount of 10,000 rubles when registering for any Skillbox course using the “Habr” promo code.
Hello world!
Creating a web server on Go is 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 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 a package
net/http
, this is the main tool in Go for developing both servers and HTTP clients. In order to understand the code, let's look at 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 forms a response. Go handlers 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, Golang handlers 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 generate responses to requests. But which particular handler should be used at a particular point in time?
Request Routing
To make the right choice, use the HTTP multiplexer. It is called muxer or router in a number of libraries, but it's all the same. The multiplexer function 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 is gorilla / mux and go-chi / chi , these libraries make it possible to implement intermediate processing without any 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 a normal situation will require not quite standard solutions, and this greatly complicates the use of default handlers. To create the vast majority of applications, a combination of the default library and a simple router is enough.
Query Processing
In addition, we need a component that will “listen” to incoming connections and redirect all requests to the correct handler. This task can easily be done by http.Server.
The following shows that the server is responsible for all tasks that are related to connection processing. 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 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 just register 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 ability to automatically renew. In order to use the service, you need a package
autocert
. 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 renew TLS certificates, while the HTTP server processes requests:
http.Serve(autocert.NewListener("example.com"), nil)
If we open example.com in the browser , we get the HTTPS response “Hello, world!”.
If you need a more thorough configuration, then you should use the autocert.Manager manager. Then we create our own http.Server instance (until now we 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 good, but it is very simple. Most applications require more complex routing, including nested and wildcard routes, or the procedure for setting patterns and path parameters.
In this case, you should use 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
}
We set api / vq prefix 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 working with complex routes in Go makes it possible to simplify structuring with serving 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 authenticate, compress, journal and some other functions.
As an example, consider the interface http.Handler, with its help we write a handler with authentication of users of the service.
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 Go standard library includes features 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
It is worth remembering that http.Dir displays the contents of the directory if it does not have the main index.html file. In this case, in order to prevent the catalog from being compromised, you should use the package
unindexed
.Correct shutdown
Go also has a feature such as shutting down the HTTP server correctly. This can be done using the Shutdown () method. The server starts in goroutine, and then the channel is listened to receive an interrupt signal. As soon as the signal is received, the server shuts down, 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)
In 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:
- Two-year practical course "I am a PRO web developer . "
- Educational online course "Profession Java-developer" .
- Practical annual course "PHP-developer from 0 to PRO" .