Go
Goroutines and the scheduler, channels, implicit interfaces, errors as values, and context.
Goroutines and the scheduler
beginnerA goroutine is a function running concurrently, started by putting go in front of a call. It begins with a stack of about two kilobytes that grows and shrinks as needed, which is why a program can hold hundreds of thousands of them where the same number of operating system threads would exhaust the machine.
The runtime multiplexes them onto a small number of operating system threads, one per available core by default. When a goroutine blocks on a channel or on IO the scheduler runs something else on that thread, and when it blocks in a system call the runtime can hand the thread's queue of work to another thread entirely. All of it is invisible from the code, which is the point: you write blocking code and get non-blocking behaviour.
What is not invisible is that goroutines are not free of responsibility. Starting one without knowing how it ends is the standard Go bug: a goroutine blocked forever on a channel nobody will send to is a leak, holding its stack and whatever it captured, and nothing will report it. Every goroutine wants an answer to how it stops, usually a context or a closed channel.
There is also no supervision. An unrecovered panic in any goroutine takes the whole process down, not just that goroutine, so a background worker doing risky work needs its own recover at its top level. And a goroutine's result has to be delivered deliberately, through a channel or a WaitGroup, because there is nothing to return to.
The mental model that keeps this straight is that go is cheap and coordination is not. Starting work concurrently costs almost nothing; knowing when it finished, what it produced and how it stops is the actual design, and it is the part the language deliberately leaves to you.
Cheap concurrency changes what is worth doing concurrently, which is the language's central bet. The cost is that lifecycle is now your problem in every case: a goroutine with no defined ending is a leak that nothing detects and nothing reports.
Go's own documentation states the rule plainly, that a goroutine's lifetime should be clear before it is started. The runtime detects the total deadlock where every goroutine is asleep and exits, and it cannot detect the far more common case of one goroutine blocked forever while the rest carry on.
What is the standard goroutine leak?
Channels, select and the patterns that work
intermediateA channel is a typed conduit with a rule attached: send and receive block until the other side is ready. An unbuffered channel is a rendezvous, so the send completes at the moment a receive happens, which makes it a synchronisation primitive as much as a data one. A buffered channel decouples them up to its capacity and then behaves like the unbuffered one, which is bounded backpressure by construction.
select waits on several channel operations and proceeds with whichever is ready, choosing at random between several that are. Combined with a case on a context's done channel, it is how nearly every cancellable Go loop is written, and combined with a default case it is how a non-blocking attempt is expressed.
The conventions around closing are worth learning exactly, because breaking them panics. The sender closes, never the receiver, since only the sender knows there is nothing more coming. Closing twice panics. Sending on a closed channel panics. Receiving from a closed channel returns the zero value immediately and forever, which is why the two-value receive exists to distinguish a real value from a closed channel.
Three patterns cover most real use. A worker pool: several goroutines receiving from one channel of jobs and sending to one of results. Fan-in: several producers writing to a channel a single consumer drains. And done-channel cancellation: a channel closed to broadcast to every listener at once, which works because a closed channel is permanently readable and is the reason cancellation is a close rather than a send.
The advice the community repeats, share memory by communicating, is not a prohibition on mutexes. A mutex around a struct is the right answer for protecting shared state, and a channel is the right answer for handing ownership of a value from one goroutine to another. Reaching for a channel where a mutex would do produces the most convoluted Go code there is.
Channels are a synchronisation mechanism that happens to carry data, which is why an unbuffered one is a rendezvous rather than a queue. Buffered channels are the language's bounded queue, and that bound is what turns overload into blocking rather than into unbounded memory growth.
The done-channel pattern works because a closed channel is readable by every receiver at once, which makes close the natural broadcast. That is also why context cancellation is implemented as a channel that gets closed rather than as a value that gets sent.
What does an unbuffered channel provide beyond data transfer?
Interfaces, satisfied implicitly
intermediateA Go type satisfies an interface by having the methods, with no declaration that it intends to. That means an interface can be defined after the types that satisfy it, and by the package that consumes them rather than the one that provides them, which inverts the usual dependency direction and is the single most distinctive thing about designing in Go.
The convention that follows is to keep interfaces small and to define them where they are used. A consumer that needs one method declares a one-method interface and accepts anything with it, so the provider package does not have to know the consumer exists. The standard library sets the tone: io.Reader and io.Writer are one method each and compose into most of the ecosystem.
The trap is the typed nil. An interface value holds a type and a value, so an interface holding a nil pointer of a concrete type is itself not nil, and the classic version is a function returning a concrete error type as an error interface: the pointer is nil, the interface is not, and the caller's check for nil is false. Return the interface type, and return a literal nil for the no-error case.
Type assertions and type switches recover the concrete type when it is genuinely needed, and the two-value form is the safe one, because the single-value assertion panics on a mismatch. Reaching for them constantly is usually a sign that the interface is the wrong shape rather than that the language is in the way.
Generics, added in Go 1.18, cover the cases interfaces never could: a function that works over any ordered type without boxing, a container parameterised by element type. They did not replace interfaces, which remain the mechanism for behaviour, and the useful split is that generics abstract over types while interfaces abstract over behaviour.
Implicit satisfaction lets the consumer define the contract, which is a genuinely different way to arrange a dependency: the package that needs the behaviour owns the interface, and the package that provides it never imports the one that uses it.
io.Reader is one method, and it is the reason a file, a network connection, a buffer and a decompressor are interchangeable throughout the standard library. Nothing was declared to be a Reader; each simply has the method.
What is the safe form of a type assertion?
Errors as values
beginnerGo has no exceptions for ordinary failure. A function that can fail returns an error alongside its result, and the caller deals with both. The consequence is visible immediately in the code: the failure path is written out rather than implied, which is verbose and is the point, because the alternative is a failure path nobody looked at.
Errors are values satisfying a one-method interface, so they can be compared, wrapped, inspected and defined by any package. Wrapping with the percent-w verb records the cause, and errors.Is walks that chain to answer whether a particular sentinel is anywhere inside it, while errors.As finds a specific error type and gives you access to its fields. Those three together replaced a decade of string matching on error messages.
The habit that makes the verbosity pay is adding context at each layer. Returning an error unchanged from five levels of call stack produces a message with no idea where it came from; wrapping it with what this layer was attempting produces a sentence that reads like a trace, which is what somebody wants at three in the morning.
panic is for programmer error and for situations where continuing is meaningless, not for a file that is missing or a request that failed. recover exists mainly to stop a panic in one request taking down a server, which is what an HTTP framework does at the top of each handler, and it should not be used as a general error mechanism because it hides the failure path the language deliberately exposes.
The thing to watch for is the ignored error, written as an underscore or simply omitted. It is the same swallowing that an empty catch block does in another language, with the difference that Go's linters can see it, which is why vetting for unchecked errors is standard in any serious pipeline.
Failure is in the signature, so a caller cannot be unaware of it and a reader can see every path. The cost is verbosity, and the payment for it is context: wrapping at each layer is what turns a bare message into something that identifies where and why.
errors.Is and errors.As, with the percent-w wrapping verb, arrived in Go 1.13 and ended the practice of matching on error strings. Any codebase still comparing message text is carrying a habit the standard library removed the need for.
What does wrapping an error with the %w verb preserve?
Context, cancellation and deadlines
advancedA context carries a deadline, a cancellation signal and request-scoped values across API boundaries. By convention it is the first parameter of any function that does IO or can block, which is why almost every signature in a Go service starts with it, and the convention exists so that cancellation can reach anywhere without every layer inventing its own mechanism.
Cancellation propagates down a tree. Deriving a context gives a child that is cancelled when its parent is, so cancelling at the top reaches every operation started beneath it, and a deadline set at the edge becomes the budget for the whole call rather than a fresh timeout at each hop. That is the propagating deadline that other ecosystems build by hand, available as a language convention.
Using it correctly is a short list. Select on ctx.Done in any loop that might run long. Pass the context to every call that takes one, including the database driver and the HTTP client, since a cancellation that stops at your code and not at the query has cancelled nothing expensive. Call the cancel function returned when deriving a context, always with defer, because failing to do so leaks the context and its timer.
Values in a context are the part most often misused. They are for request-scoped data that genuinely crosses layers, a trace id, a request id, an authenticated principal, not for passing dependencies that a function could have been given as parameters. Because the map is untyped, anything hidden in it is invisible to the compiler and to the next reader, which is why the practice is to keep it to a handful of well-known keys.
The most common bug is a cancelled context reaching only part of the work. A handler returns, the context is cancelled, and a goroutine started with the request context finds its context already dead, or worse, a background task was given the request context and dies when the request completes. Background work needs its own context with its own lifetime, and saying so explicitly is cheaper than diagnosing it.
Context is how a deadline set at the edge becomes a budget the whole call tree respects, which is the property that stops a service working on results nobody is waiting for. It only holds if every layer passes it on: one call that ignores it becomes the place where cancellation stops.
The standard library takes a context in the database and HTTP interfaces precisely so a cancelled request stops the query it started. A codebase that passes context.Background into those calls has kept the shape of cancellation without any of the effect.
What belongs in a context value?