What Go's defer Actually Taught Me About Functions
Defer's evaluation timing and named return values that get mutated after the return statement: two Go function features with no real TypeScript equivalent.
TypeScript has finally for cleanup and lets you return whatever shape of object you want for multiple values. Go has defer and named returns instead, and both work in ways I didn’t expect from reading the syntax once.
defer Captures Arguments Immediately, Not When It Runs
The part that got me: a deferred call’s arguments are evaluated the moment you write defer, not when the deferred function actually executes.
func process() { i := 0 defer fmt.Println("deferred i:", i) // i is read right now, value is 0
i = 42 fmt.Println("final i:", i)}// final i: 42// deferred i: 0If you want the deferred code to see the value at the end of the function, you need a closure instead, which captures the variable itself rather than a snapshot of it:
func process() { i := 0 defer func() { fmt.Println("closure i:", i) }() // reads i when it runs
i = 42 fmt.Println("final i:", i)}// final i: 42// closure i: 42Same keyword, two different capture rules depending on whether you pass arguments directly or wrap the call in a closure. I didn’t internalize the difference until a log line printed a stale value and I had to go find out why.
Named Returns Can Be Changed After return Runs
Go lets you name your return values in the function signature, and a deferred closure can still reach in and change them — after the return statement has already executed:
func doSomething() (result int) { defer func() { result *= 2 // runs after `return 21`, still rewrites the return value }()
result = 21 return}// returns 42There’s no TypeScript equivalent to reach for here; finally runs after return too, but it can’t touch the value already handed back to the caller. This pattern is exactly how Go’s panic recovery usually works — a deferred function calls recover() and assigns an error to a named return so the caller gets a clean error instead of a crash. Outside of that pattern, though, a bare return on a named-return function just hides which value is actually going out, so most Go style guides (and I agree now) treat it as something to avoid rather than a shortcut worth using by default.
A Named Function Type Is Documentation, Not a Stricter Contract
type Handler func(w http.ResponseWriter, r *http.Request)This reads a lot like a TypeScript type alias for a function signature, and it behaves the same way: it’s structural, not nominal. Any function with a matching signature satisfies Handler without saying so, the same way any matching arrow function satisfies a TS function type without an explicit annotation. The value of naming it is purely for readability when you reference the shape more than once, not for extra type safety.
Anonymous Functions Are for defer and Goroutines, Not Everyday Use
In TypeScript, inline arrow functions are the default — array methods, promise chains, event handlers all lean on them. In Go, an anonymous function that isn’t assigned to a name mostly shows up in two places: a defer call, or launching a goroutine. Anywhere else, Go code reaches for a named function instead. It’s a small thing, but it changed how much I reach for inline closures out of habit versus writing a name for something that’s going to be read more than once.
Takeaway
The two sharp edges are argument timing (defer snapshots arguments immediately, closures don’t) and named returns (a deferred closure can still rewrite the result after return). Everything else — function types, anonymous functions — is closer to TypeScript than it first looks, once you know Go structural-types functions the same way.