One way to think of context in go is that it allows you to pass a “context” to your program. A context that signals a time limit, deadline or channel to indicate when to stop working and come back. For example, if you are requesting a web server or running a system command, it is usually a good idea to have a timeout for production systems. This is because if an API you depend on is running slowly, you wouldn’t want to log too many requests on your application, as this could end up increasing the load and degrading the performance of all the requests you are serving. Like a waterfall effect. This is where a delay or delay context can come in handy.
package main
import (
"context"
"fmt"
)
func doSomething(ctx context.Context) {
fmt.Println("Doing something!")
}
func main() {
ctx := context.TODO()
doSomething(ctx)
}
Go playground: https://go.dev/play/p/Kq7YyLxqgB3.
Another example, by passing this context.Context value into a function that then makes a call to your DB, the database query will also be terminated if it is still running when the client disconnects.
package main
import (
"fmt"
"net/http"
"time"
)
func example(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
fmt.Println("start")
defer fmt.Println("end")
select {
case <-time.After(10 * time.Second):
fmt.Fprintf(w, "example\n")
case <-ctx.Done():
err := ctx.Err()
fmt.Println("error:", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func main() {
http.HandleFunc("/", example)
http.ListenAndServe(":8080", nil)
}
Go playground: https://go.dev/play/p/3vneiEUIMfr.
Output:
$ go run .
start
error: context canceled
end
This way some of the libraries you use or your own can stop the actions and avoid overloading your service.
You can also use the context to pass variables:
package main
import (
"context"
"fmt"
)
func doSomething(ctx context.Context) {
fmt.Printf("doSomething: myKey's value is %s\n", ctx.Value("myKey"))
}
func main() {
ctx := context.Background()
ctx = context.WithValue(ctx, "myKey", "myValue")
doSomething(ctx)
}
Go playground: https://go.dev/play/p/R1MhUD2v96Q.
This is what is used a lot to propagate the logger. And many logging libraries will offer you a handler to fill the logger with information on each request, e.g. path, request ID etc. And a function to retrieve this logger from the context.
package main
import (
"context"
"net/http"
"os"
"time"
"github.com/justinas/alice"
"github.com/rs/zerolog"
"github.com/rs/zerolog/hlog"
"github.com/rs/zerolog/log"
)
func main() {
log := zerolog.New(os.Stdout).With().
Timestamp().
Str("role", "my-example").
Logger()
c := alice.New()
// Install the logger handler with default output on the console
c = c.Append(hlog.NewHandler(log))
// Install some provided extra handler to set some request's context fields.
// Thanks to that handler, all our logs will come with some prepopulated fields.
c = c.Append(hlog.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) {
hlog.FromRequest(r).Info().
Str("method", r.Method).
Stringer("url", r.URL).
Int("status", status).
Int("size", size).
Dur("duration", duration).
Msg("")
getDB(r.Context())
}))
c = c.Append(hlog.RemoteAddrHandler("ip"))
c = c.Append(hlog.UserAgentHandler("user_agent"))
c = c.Append(hlog.RefererHandler("referer"))
c = c.Append(hlog.RequestIDHandler("req_id", "Request-Id"))
// Here is your final handler
h := c.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get the logger from the request's context. You can safely assume it
// will be always there: if the handler is removed, hlog.FromRequest
// will return a no-op logger.
hlog.FromRequest(r).Info().Msg("Hello this is my example")
}))
http.Handle("/", h)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal().Err(err).Msg("Startup failed")
}
}
func getDB(ctx context.Context) {
log.Ctx(ctx).Info().Msg("get from db")
}
Go playground: https://go.dev/play/p/hUrDLZVGxd2.
Ouput:
{"level":"info","role":"my-example","ip":"127.0.0.1:54224","user_agent":"curl/7.68.0","req_id":"cbuk241ludm8ge0akv80","time":"2022-08-17T21:27:12+02:00","message":"Hello this is my example"}
{"level":"info","role":"my-example","ip":"127.0.0.1:54224","user_agent":"curl/7.68.0","req_id":"cbuk241ludm8ge0akv80","method":"HEAD","url":"/","status":0,"size":0,"duration":0.2451,"time":"2022-08-17T21:27:12+02:00"}
{"level":"info","role":"my-example","ip":"127.0.0.1:54224","user_agent":"curl/7.68.0","req_id":"cbuk241ludm8ge0akv80","time":"2022-08-17T21:27:12+02:00","message":"get from db"}
As you can see even up to getDB you get the logger and its information.
https://www.digitalocean.com/community/tutorials/how-to-use-contexts-in-go