A mini post on GoLang context


In a GoLang web server, every request coming in will be handled by a goroutine. In the request handler, the logic may also need to create new goroutine to handle other tasks like RPC call. When the request is processed and response is returned, these goroutines created need to be exited so that no goroutine leak should happen.

package main import ( "fmt" "log" "net/http" ) func main() { http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) { fmt.Println(&r) w.Write([]byte("hello")) }) log.Fatal(http.ListenAndServe(":8080", nil)) }
go run main.go 0xc000006010 0xc000076048 0xc000076050

In some cases, if not handled carefully, the end result is that the goroutine created will not be exited and it stays there but does nothing. For example, below is an example snippet to have a monitor goroutine which prints "req is processing" every 1 second.

package main import ( "fmt" "log" "net/http" "time" ) func main() { http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) { // monitor go func() { for range time.Tick(time.Second) { fmt.Println("req is processing") } }() // assume req processing takes 3s time.Sleep(3 * time.Second) w.Write([]byte("hello")) }) log.Fatal(http.ListenAndServe(":8080", nil)) }

Assume the request takes 3 seconds to process, the expected number of monitor log printed should be 3 times. However, the output of above program will keep printing the log even if the request is processed. The reason is because the lifecycle of the goroutine is not handled properly, there is no message/signal to terminate the goroutine.

In GoLang, Context can be used to handle this kind of situation. In the giroutine, it can have some logic to check whether the request context is Done and then determine whether should exit the goroutine. 

package main import ( "fmt" "log" "net/http" "time" ) func main() { http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) { // monitor go func() { for range time.Tick(time.Second) { select { case <-r.Context().Done(): fmt.Println("req is outgoing") return default: fmt.Println("req is processing") } } }() // assume req processing takes 3s time.Sleep(3 * time.Second) w.Write([]byte("hello")) }) log.Fatal(http.ListenAndServe(":8080", nil)) }

Context can be used to pass values among different domains as well and can also be used to handle request cancellation and timeout so that all related parties are aware of the current situation and handle them properly. 

Below is the definition of Context interface in GoLang.

// A Context carries a deadline, cancelation signal, and request-scoped values // across API boundaries. Its methods are safe for simultaneous use by multiple // goroutines. type Context interface { // Done returns a channel that is closed when this Context is canceled // or times out. Done() <-chan struct{} // Err indicates why this context was canceled, after the Done channel // is closed. Err() error // Deadline returns the time when this Context will be canceled, if any. Deadline() (deadline time.Time, ok bool) // Value returns the value associated with key or nil if none. Value(key interface{}) interface{} }

Done() function returns a channel which will be closed when the Context is cancelled or times out. Err() function will return the reason of cancellation.

Context doesn't implement cancel() function by itself, its channel in Done() function is also used for receiving signals only. Parent goroutine will create child goroutines and pass the context, and child goroutines should not instruct parent goroutine to cancel.

There are some built-in Context defined in GoLang which are for different purposes. For example, context.Backgroun().

// Background returns an empty Context. It is never canceled, has no deadline, // and has no values. Background is typically used in main, init, and tests, // and as the top-level Context for incoming requests. func Background() Context // WithCancel returns a copy of parent whose Done channel is closed as soon as // parent.Done is closed or cancel is called. func WithCancel(parent Context) (ctx Context, cancel CancelFunc) // A CancelFunc cancels a Context. type CancelFunc func() // A CancelFunc cancels a Context. type CancelFunc func() // WithTimeout returns a copy of parent whose Done channel is closed as soon as // parent.Done is closed, cancel is called, or timeout elapses. The new // Context's Deadline is the sooner of now+timeout and the parent's deadline, if // any. If the timer is still running, the cancel function releases its // resources. func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) // WithValue returns a copy of parent whose Value method returns val for key. func WithValue(parent Context, key interface{}, val interface{}) Context

Background context is the root of all other Context, it will never be cancelled. There are some functions which can be used to create other types of Context. For example, WithCancel(), WithTimeout() and WithDeadline().

WithCancel() can be used to create chained Context which will be cancelled when the parent Context passed in gets cancelled.

package main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() // monitor go func() { for range time.Tick(time.Second) { select { case <-ctx.Done(): return default: fmt.Println("monitor woring") } } }() time.Sleep(3 * time.Second) }

Above a Context whose parent Context is Background is created. The monitor goroutine will print the log every second. After 3 seconds, the main goroutine cancels and the signal is passed to ctx.Done() and hence the monitor goroutine would exit.

WithTimeout() can be used to create Context which will times out in specified amount of time. 

package main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() select { case <-time.After(4 * time.Second): fmt.Println("overslept") case <-ctx.Done(): fmt.Println(ctx.Err()) } }

After 3 seconds, the Context will time out and the cancellation will be triggered and the program will exit.

Finally, there is another important function WithValue() which can be used to set values in the Context so that the values can be shared among different goroutines when it's passed around. This is very important when need to share some common values along the journey of the request like requestID.

package main import ( "context" "fmt" ) type ctxKey string func main() { ctx := context.WithValue(context.Background(), ctxKey("a"), "a") get := func(ctx context.Context, k ctxKey) { if v, ok := ctx.Value(k).(string); ok { fmt.Println(v) } } get(ctx, ctxKey("a")) get(ctx, ctxKey("b")) }

Reference: