Gopher says
Context is how Go passes cancellation signals, deadlines, and request-scoped data through a chain of function calls. It’s the invisible thread connecting every layer of your application.
In baton-junction, every function takes ctx context.Context as its first
parameter. If the SDK cancels a sync, that signal propagates through the
connector, the client, all the way to the HTTP request.
Why Context Exists
Go supports concurrent operations via goroutines. When multiple operations run at once, you need a way to tell them to stop. If a user disconnects mid-request, the remaining API calls are pointless.
context.Context carries three things through a call chain:
- Cancellation signals - “stop, nobody needs the result”
- Deadlines - “give up after 30 seconds”
- Request-scoped data - loggers, trace IDs, auth info
Context as the First Parameter
When every function takes context in the same place, you never have to guess where to pass it. The convention exists so tooling, linters, and other developers can rely on consistent behavior across the entire ecosystem.
Go convention: context is always first, always named ctx:
func (c *Client) GetUsers(ctx context.Context, cursor string) ([]User, string, error) This convention is universal across the Go ecosystem - every library, every framework.
Context Flowing Through the Baton App
Why does this flow matter? If the SDK cancels a sync, that signal must reach the HTTP request so it stops immediately. No wasted API calls, no orphaned goroutines. The same chain also carries request-scoped data like loggers for tracing.
Context starts at main() and flows unbroken through every layer:
If the SDK cancels the context, the HTTP request is aborted immediately. No wasted API calls.
Creating a Root Context
Every context chain has to start somewhere. You need a root context when your program begins (in main()) or when writing tests that don’t have a real caller. Everywhere else, you receive context from above.
Two functions create top-level contexts:
ctx := context.Background() // the root - used in main() and tests
ctx := context.TODO() // same thing, but signals "I need a real context later"
Every other function receives context from its caller. Only main() and tests create new ones.
The Receive-Pass Pattern
Here’s the good news: working with context is mostly plumbing. You rarely create or cancel it yourself. Your job as a connector author is simple:
func (o *userBuilder) List(ctx context.Context, ...) (...) {
l := ctxzap.Extract(ctx) // 1. optionally extract logger
users, _, err := o.client.GetUsers(ctx, cursor) // 2. pass ctx along
// ...
}
Receive context. Optionally extract data from it (like a logger). Pass it to every function you call. Never ignore it, never replace it.
The Underscore Pattern
Go’s compiler requires you to use every declared variable. The blank identifier _ is how you tell it “I’m intentionally ignoring this.” It shows up in three places:
func (o *userBuilder) ResourceType(_ context.Context) *v2.ResourceType { ... }
for _, u := range users { ... }
_, err := client.Delete(ctx, id)
The first discards a parameter the interface requires but you don’t need. The second discards the index in a range loop. The third discards the result of a function when you only care about the error. You’ll see all three forms throughout baton-junction.
The SDK stores a configured logger in context. Extract it with ctxzap:
l := ctxzap.Extract(ctx)
l.Debug("listed users", zap.Int("count", len(users)))
l.Info("granting membership", zap.String("group_id", groupID))The logger comes pre-configured with request metadata - you just use it.
Quick Check
What function creates the root context in main()?
Exercise: Thread Context Through
Write three functions that pass context through a chain. FetchUser should call GetFromAPI with the user path. GetFromAPI should call DoRequest. DoRequest should format and return the method and path. Every function receives context as its first parameter and passes it to the next. All three return a string and an error.
Gopher says
Context seems like ceremony at first, but it’s the thread that connects your entire application. Receive it, pass it along, and the framework handles cancellation and timeouts.
In baton-junction, context flows from main.go through the connector, into
each builder, down to the HTTP client - if the SDK cancels, every in-flight
API request aborts immediately.
Next: the standard library!
Next Lesson
Standard Library

