Stopping HTTP Server Gracefully: Context vs Channels vs SyncGroup
Text version of the code is available here
I am developing a CLI application that requires it to authenticate and obtain a token from an API. I had a problem of gracefully shutting down the HTTP server from another function (handle), in this case, after a token is received. Go 1.8 introduced
Shutdown that gracefully shuts down the server without interrupting any active connections.
In this post, we will look at using three ways to tell the server to shut down gracefully. Also, I am using Gorilla’s mux router.
Before we go into the details, there are few common functions between these three implementations:
- There are two handles (routes);
HomeHandler— that routes to
http://127.0.0.1:8000/, which is our index page and
ExitHandler— that routes to
http://127.0.0.1:8000/exit, which is used to shut down the server.
- The server always starts in a goroutine.
- The program doesn’t exit until some kind of wait request is completed.
Using with Channels
Channels are like pathways; it joins goroutines to send and receive messages. For example:
In the above example, we created a channel called
message, then an anonymous goroutine that prints a text to console runs, and finally, a string is sent to the
message channel. The channel doesn’t let the program end unless a message is received to
receivedMessage. Once a message is received, the text is assigned to
receivedMessage, then prints it out and eventually exits the program.
Using the channels, let’s see how we can shut down the server from a different handle:
In the above example, we have a global channel —
stopHTTPServerChan (line 13) of type
bool. In the
main() function, let’s a make a channel that has a data type of `bool` and assign it to our global variable
stopHTTPServerChan (line 34). When the server starts it won’t end abruptly, because of
<-stopHTTPServerChan (line 58), the program waits here till a boolean signal is received. Under
ExitHandler() send a message, a boolean signal as
stopHTTPServerChan <- true (line 30, this could also be
false), so whenever you go to
http://127.0.0.1:8000/exit, a signal is sent to
stopHTTPServerChan, once the boolean value is received, the wait is over then it proceeds to the next line.
Using with Context
Note: According to the documentation, contexts should never be stored in
structtype, but rather it should be passed through as an argument to a function. There are a few exceptions;
context.CancelFuncis one such exception that can be put in a structure.
Contexts in the backend use channels to send and receive messages, but in a server scenario, every request received runs on a goroutine. Some HTTP requests might take more time than required, for fewer request the server should usually be able to handle them without using too many resources. Still, when there are 1000’s of request per-second the system might crash or take more resources. For this reason, the context library comes with few helpful functions, such as — WithCancel, WithDeadline, WithTimeout, and WithValue — that helps in destroying a request if it takes more time that is allocated to it.
Above example works exactly like channels example, only that you don’t have to make a channel and looks pretty.
From the above example, let’s create a
httpServerHelper structure with
cancelFunc of type
context.CancelFunc (line 13–15). The
context.WithCancel(), returns a context and a cancel function, let's assign it to
cancel (line 36), assign the
cancel() function to
cancelFunc and call it
serverHelper (line 37). When you go to
cancel() function is invoked which sends a
Done() signal at line
61. When there is no error and all the channels are executed,
context.Canceled returns a string else an Error.
Using with WaitGroup
Unlike channels and context, SyncGroup doesn’t use channels but uses a low level “mutual exclusion locks”.
sync.WaitGroup structure has three functions
Add() takes in an integer value greater than
Done() decrements the integer value by
Wait() stops the program going further till the integer becomes
In the above example, assign
wg, because there is only one goroutine so adding
1 should be enough, if there are say four goroutines then you should have
wg.Done() should be called four times. Once, goroutine reaches
1 is decremented to
eg.Wait() check for it and continues the program.
In the above program, let’s create a global variable
wg of type
sync.WaitGroup (line 14). In the
main() function add
wg.Add(1) (line 37). In the
defer wg.Done() (line 26), the `defer` keyword executes at the end of the function.
wg.Wait() checks the counter has decremented to zero or not, in this case, it would when you visit
http://127.0.0.1:8000/exit, this then continues to the next line.
Channels are the star of Go language’s coroutines, but they make it easier for users to use different ways to control the execution of coroutines in a program. It is up to you which one to use, and it mostly depends on the problem too, for a simple coroutines communication, channels should be fine. Context, on the other hand, was made to use with servers but can also be used in simple programs. The sync library was developed to have low-level APIs for Go language to use internally. Still, SyncGroup can be used as a high-level API to control coroutines execution too.
I hope this post helps you in choosing the right one.