Finally the [Context](https://pkg.go.dev/context#Context) interface clicked for me. I was reading the [Network Programming with Go](https://nostarch.com/networkprogrammingwithgo) book and the example of the UDP server uses the context package in the following way.
func echoServer(addr string) (net.Addr, error) {
    s, err := net.ListenPacket("udp", addr)
    if err != nil {
        return nil, fmt.Errorf("binding to udp: %s: %w", addr, err)
    }
    go func() {
        ...// do some work here
    }
    return s.LocalAddr(), nil
}
The author does not intend to keep the server `s` around but he still has to manage its life-cycle, e.g. eventually calling the `Close()` method. The function returns the local address of the server because that is all the author needed but there is no way to close the server later on without returning the variable `s` to the caller. This problem warrant the use of messaging through channels. Channels are a data type that is used to sync two goroutines. We can pass a channel `ch` we control to the `echoServer` function and have a goroutine wait for a message on it to close the underlying server `s`.
func echoServer(ch <-struct{}, addr string) (net.Addr, error) {
    s, err := net.ListenPacket("udp", addr)
    if err != nil {
        return nil, fmt.Errorf("binding to udp: %s: %w", addr, err)
    }
    go func() {
        go func() {
            // this read from channel ch will block until we write to it
            <-ch
            s.Close()
        }
        ...
    }
    return s.LocalAddr(), nil
}
Now the caller is in charge of closing the underlying server and the resource will not leak. The context package provides a way to reuse this pattern using an interface instead of a channel. A context interface can be used to propagate a cancellation message to the server because it wraps a channel. To access the channel of the context we user the `Done()` method.
func echoServer(ctx context.Context, addr string) (net.Addr, error) {
    s, err := net.ListenPacket("udp", addr)
    if err != nil {
        return nil, fmt.Errorf("binding to udp: %s: %w", addr, err)
    }
    go func() {
        go func() {
            <-ctx.Done()
            s.Close()
        }
        ...
    }
    return s.LocalAddr(), nil
}
The selling point of the `Context` type is that it is an interface. Many types can be used in place of a `Context` argument. This means that we can have different behavior as long as we satisfy the interface. The most common context types are the one provided by the [context package](https://pkg.go.dev/context). 1. `Background()` 2. `TODO()` And the rest are derived using the `WithCancel`, `WithDeadline` and `WithTimeout` functions. Finally we can see a full example using the `Context` interface to propagate cancellation from a goroutine to another.
package main

import "context"

type S struct {}

func (*S) Close() {
    fmt.Println("Closing")
}

func spawn(ctx context.Context) {
    s := new(S)
    go func () {
        <-ctx
        s.Close()
        return
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    spawn(ctx)
    time.Sleep(1 * time.Second)
}