The Secret Life of Go: Hidden Dependencies in Context
The Secret Life of Go: Hidden Dependencies in Context
Context values, dependency injection, and the testing nightmare
#Go #Coding #SoftwareArchitecture #BackendDev
Eleanor is a senior software engineer. Ethan is her junior colleague. They work in a beautiful beaux arts library in Lower Manhattan — the kind of place where coding languages are discussed like poetry.
Episode 36
Ethan was cleaning up the function signatures in the billing service. He had just discovered a feature in Go's standard library that he felt was going to change his life.
"I solved the dependency problem," Ethan announced as Eleanor passed by his desk. "I had all these functions that needed the database connection, the logger, and the current user ID. The function signatures were getting ridiculously long. But then I found context.WithValue."
He showed her his updated code:
// Ethan's refactored handler
func HandlePayment(ctx context.Context, amount float64) error {
// Extracting dependencies magically from the context!
db := ctx.Value("database").(*sql.DB)
logger := ctx.Value("logger").(*log.Logger)
userID := ctx.Value("userID").(int)
logger.Printf("processing payment for %d", userID)
return db.ExecContext(ctx, "INSERT INTO payments...")
}
Eleanor stopped and stared at the screen. She pulled up a chair. "You have turned the Context into a magic bag. This is one of the most dangerous anti-patterns in Go."
Ethan looked confused. "But it makes the function signature so clean. It just takes a context and the amount."
"A function signature is a contract," Eleanor explained. "When I look at your function, the signature promises me that it only needs a generic Context and a float. It is not telling me the truth. If I call your function from a test and pass context.Background(), what happens?"
Ethan traced the logic. "The ctx.Value returns nil. The type assertion panics, and the program crashes."
"Exactly," Eleanor said. "By hiding your dependencies inside the context, you have bypassed the compiler entirely. You dropped compile-time type safety for runtime panics. You made the code challenging to discover, document, and test. Let's revise it now."
Eleanor deleted his type assertions. She created a struct to hold the dependencies instead.
type PaymentHandler struct {
db *sql.DB
logger *log.Logger
}
func (h *PaymentHandler) HandlePayment(ctx context.Context, userID int, amount float64) error {
h.logger.Printf("processing payment for %d", userID)
return h.db.ExecContext(ctx, "INSERT INTO payments...")
}
"Core dependencies go into the struct," Eleanor said. "Required data, like the user ID, goes into the explicit function arguments. Now the compiler will enforce our rules, and any developer reading this knows exactly what the handler needs to survive."
Ethan looked at the empty context parameter. "Then what is context.WithValue actually used for? Why does it exist?"
"Context values are for request-scoped metadata," Eleanor replied. "The rule of thumb is this: if your function cannot successfully execute its core business logic without the value, the value does not belong in the context."
Ethan thought about it. "So a database connection is core logic. But what is metadata?"
"A Trace ID for distributed tracing," Eleanor offered. "Your payment logic doesn't care what the Trace ID is. It doesn't need it to charge the credit card. But your logger might extract it from the context down the line to append it to log lines. Or perhaps an authentication token that middleware intercepts. Things the system needs to observe or route the request, but not things the function needs to process the request."
Ethan nodded, understanding the boundary. "Dependencies are explicit. Metadata is implicit."
"Precisely," Eleanor smiled. "Keep the magic bag closed. Let your function signatures tell the truth."
Key Concepts Introduced
Context as a Magic Bag
Developers often misuse context.WithValue to pass database connections, loggers, or required business data down the call stack to avoid typing long function signatures. This is a severe anti-pattern.
The Cost of Hidden Dependencies
When you hide dependencies in a context, the function signature lies to the caller. The compiler cannot warn you if a dependency is missing, resulting in runtime panics from failed type assertions. It also obscures what a function actually needs to run, making unit testing incredibly difficult.
Dependency Injection
Instead of placing core dependencies in the context, pass them explicitly. The standard pattern is to define a struct that holds pointers to your database, logger, or external clients, and then attach your methods to that struct.
The Architect's Rule for Context Values
Only use context.WithValue for request-scoped metadata. If a function requires a piece of data to execute its core business logic correctly, that data must be an explicit parameter or a struct field. Acceptable uses for context values include Trace IDs, span data, or incoming IP addresses for telemetry.
Aaron Rose is a software engineer and technology writer at tech-reader.blog.
Catch up on the latest explainer videos, podcasts, and industry discussions below.


Comments
Post a Comment