Always pass the context.Context as the first argument to every function in your call chain, from the entry point down to the database or external service calls. This ensures that cancellation signals, deadlines, and request-scoped values propagate correctly through all layers of your application.
The most common mistake is creating a new context inside a lower layer or failing to pass it through an intermediate service. If you lose the context, you lose the ability to cancel long-running operations or retrieve request-specific data like trace IDs.
Here is a practical pattern for passing context through a typical handler, service, and repository layer:
// Handler layer: Receives the context from the HTTP request
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
// r.Context() is the context tied to the incoming request
user, err := h.service.GetUser(r.Context(), r.URL.Query().Get("id"))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(user)
}
// Service layer: Passes context to the repository
func (s *Service) GetUser(ctx context.Context, id string) (*User, error) {
// You can derive a new context here if needed (e.g., adding a deadline)
// but you must pass the original ctx down if you want to respect the caller's cancellation.
return s.repo.FindByID(ctx, id)
}
// Repository layer: Uses context for DB operations
func (r *Repository) FindByID(ctx context.Context, id string) (*User, error) {
var user User
// The DB driver checks ctx for cancellation or timeout automatically
err := r.db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = $1", id).Scan(&user)
if err != nil {
return nil, err
}
return &user, nil
}
If you need to attach request-specific metadata (like a correlation ID) to the context, do it at the entry point and retrieve it in lower layers using context.Value. Avoid passing the context through struct fields; always pass it explicitly as an argument.
// Entry point: Attach metadata
func (h *Handler) Handle(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Create a new context with a value, but keep the original cancellation behavior
ctx = context.WithValue(ctx, "traceID", r.Header.Get("X-Trace-ID"))
// Pass the enriched context down
h.service.Process(ctx, r)
}
// Lower layer: Retrieve the value
func (s *Service) Process(ctx context.Context, r *http.Request) {
traceID, ok := ctx.Value("traceID").(string)
if !ok {
traceID = "unknown"
}
// Use traceID for logging or tracing
log.Printf("Processing request with trace: %s", traceID)
}
Remember that context.WithValue should only be used for request-scoped data, not for passing configuration or state that could be passed as regular arguments. If a function doesn't need the context (e.g., a pure calculation), don't force it into the signature.