Skip to content

Instantly share code, notes, and snippets.

@mtilson
Last active June 5, 2023 18:31
Show Gist options
  • Save mtilson/00f72d7cbd98e3d1b9cf2c8bb9ec39b7 to your computer and use it in GitHub Desktop.
Save mtilson/00f72d7cbd98e3d1b9cf2c8bb9ec39b7 to your computer and use it in GitHub Desktop.
how to use context for graceful shutdown [golang]
package main
import (
"context"
"log"
"net"
"os"
"os/signal"
"time"
)
const (
// PORT to listen to
PORT = ":8080"
)
func main() {
// get new empty context with cancel func
ctx, cancelFunc := context.WithCancel(context.Background())
// spawn func which will listen for `os.Interrupt` signal (`Ctrl-C`) from OS
go goroutineProcessSignals(cancelFunc)
// start our server - this is blocking func
if err := listenSocket(ctx); err != nil {
log.Fatal(err)
}
}
func goroutineProcessSignals(cancelFunc context.CancelFunc) {
// channel of type `os.Signal`
signalChan := make(chan os.Signal)
// notify OS that we would like to receive (register) signal `os.Interrupt` on the `signalChan` channel
signal.Notify(signalChan, os.Interrupt)
// receive all registered signals
// it is not necessary to have loop here, cause we registered only for one signal, but it is useful
// example in case we will sign up for other ones, like `SIGHUP`
for {
// read from `signalChan` channel as we ordered with help of `signal.Notify()` above
// here the only `os.Interrupt` signal can be received, as it is the only one we registered
// in out `signal.Notify()` call, and because of this it is not necessary to use `for {}` loop
// in this case, but if we would like to process more than 1 signal (e.g. `syscall.SIGTERM`)
// then we do need this `for {}` loop
sig := <-signalChan
switch sig {
// check if the received signal type is `os.Interrupt`
case os.Interrupt:
// call our cancel func received as an argument and return
log.Println("Signal SIGINT is received, probably due to `Ctrl-C`, exiting ...")
cancelFunc()
return
}
}
}
func listenSocket(ctx context.Context) error {
// it is necessary to be able to use in net.ListenTCP()
localAddr, err := net.ResolveTCPAddr("tcp", PORT)
if err != nil {
return err
}
// l, err := net.Listen("tcp", PORT) // return (Listener, error)
// is not possible to use `net.Listen()` as it returns `Listener` interface which has no method to setup timeout
// we need to use net.ListenTCP() as it returns `TCPListener` which has method `SetDeadline()` to setup
// timeout, which we need to be able to periodically check if channel, returned by `ctx.Done()`, is not
// closed by processing `os.Interrupt` case in our goroutineProcessSignals() handler
l, err := net.ListenTCP("tcp", localAddr) // return (*TCPListener, error)
if err != nil {
return err
}
defer l.Close()
log.Println("Start listening on the TCP socket", PORT, ".")
// accept all connnections received to the created TCP-socket and also monitor for receiving
// `os.Interrupt` signal (by processing `Ctrl-C`)
for {
select {
// `ctx.Done()` return RO channel which will be closed if `cancelFunc()` is called;
// we will be able to read from this channel and determine that the channel is closed
case <-ctx.Done():
log.Println("Stop listening on the TCP socket", PORT, ".")
// `l.Close()` will be called on return here (and on all other return operations) as we
// registered it with `defer l.Close()`
return nil
default:
// without this timeout we will wait here (in this default selection) till somebody
// connected to the listening socket; this means that we can miss `os.Interrupt` signal,
// i.e. it will be processed long after the moment it was generated
// timeout can be applied only to TCP listner, it means we cannot use `net.Listen()`
// but should use `net.ListenTCP()`
// timeout will be applied to the "blocking" operation `l.Accept()` and every time
// timeout is expired an error of type `os.IsTimeout` will be returned by `l.Accept()`
if err := l.SetDeadline(time.Now().Add(time.Second)); err != nil {
// this is an error for registering timeout with SetDeadline()
return err
}
// this is "blocking" operation of our main.listenSocket() function
_, err := l.Accept()
if err != nil {
// if it is due to out timeout expiration we will continue
if os.IsTimeout(err) {
continue
}
// exit on other errors; another option is just Log(err) and continue this loop
return err
}
// if `l.Accept()` returned error is `nil`, then we have new connection on the listening socket
log.Println("New connection to the listening TCP socket", PORT, ".")
}
}
}
2019/12/21 15:54:32 Start listening on the TCP socket ':8080' .
2019/12/21 15:54:42 New connection to the listening TCP socket ':8080' .
2019/12/21 15:54:56 New connection to the listening TCP socket ':8080' .
^C2019/12/21 15:55:00 Signal SIGINT is received, probably due to `Ctrl-C`, exiting ...
2019/12/21 15:55:00 Stop listening on the TCP socket ':8080' .
@PeterlitsZo
Copy link

I think moving the select <-ctx.Done() statement to a goroutine is a better idea.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment