« Back to Index

Go: Why choose tailscale.com/util/ctxkey over Go standard context package

View original Gist on GitHub

Tags: #go #ctx

ctxkey.md

https://pkg.go.dev/tailscale.com/util/ctxkey

Example Playground: https://play.golang.com/p/aZ0joNec3Xl

package main

import (
	"context"
	"fmt"
	"time"

	"tailscale.com/util/ctxkey"
)

var TimeoutKey = ctxkey.New("mapreduce.Timeout", 5*time.Second)

func main() {
	ctx := context.Background()
	fmt.Println(TimeoutKey.Value(ctx))

	// Have to overwrite the ctx with the returned value.
	// Otherwise the default value will still be associated with ctx.
	ctx = TimeoutKey.WithValue(ctx, 10*time.Second)
	fmt.Println(TimeoutKey.Value(ctx))
}

Why choose ctxkey over standard Go context?

The core difference lies in type safety.

  1. Standard context Package (context.WithValue, ctx.Value)

    • How it works: You associate a value with a key using context.WithValue(parentCtx, key, value). The key is typically an unexported custom type (like type myKey struct{}) to prevent collisions. You retrieve the value using val := ctx.Value(key).
    • The Drawback: ctx.Value(key) returns a value of type interface{}. This means you must perform a type assertion to get the value back in its original type: realVal, ok := val.(ExpectedType).
    • The Problem: This check happens at runtime. ^1^ If you make a mistake (e.g., assert the wrong type, forget to check the ok boolean), your program might panic or behave unexpectedly only when that specific code path is executed. There’s no compile-time guarantee that the value associated with a key is of the type you expect. This can lead to subtle bugs that are harder to catch during development.
  2. tailscale.com/util/ctxkey

    • How it works: This package leverages Go generics (introduced in Go 1.18). You define a key specifically for a certain type of value, e.g. uniqueCtxKey = ctxkey.New(""unique-key-name"", uint32(1)) (and you can assign a DEFAULT value, 1 in this case).
    • Setting Values: You use uniqueCtxKey.WithValue(ctx, 2).
    • Getting Values: You use uniqueCtxKey.Value(ctx).
    • The Advantage: Notice there’s no type assertion needed when retrieving the value. The Value function returns the specific type associated with the key (uint32 in the example above). The Go compiler checks this at compile time.
    • The Benefit: If you try to retrieve a value using a key that expects a different type, or if you try to use the retrieved value as the wrong type, the compiler will flag it as an error before you even run the program. This significantly reduces the risk of runtime type errors related to context values. It makes your code safer and easier to refactor.

In Summary: Why Choose tailscale.com/util/ctxkey?

Why Stick with Standard context?

Conclusion

You would want to use tailscale.com/util/ctxkey primarily when you want stronger, compile-time guarantees about the types of values stored in your context. This is particularly beneficial in larger projects or teams where maintaining type consistency across different parts of the codebase is crucial for preventing runtime errors and improving maintainability. The trade-off is adding an external dependency.