What Go's Zero-Value Maps and Single for Loop Taught Me
Go collapses for, for-of, for-in, and forEach into one for, and a missing map key returns a zero value instead of undefined. Both are smaller languages on purpose.
JavaScript gives you a for, a for...of, a for...in, .forEach, .map, and a Map that returns undefined on a miss. Go gives you one loop keyword and one map syntax. Fewer tools, but each one asks you to be explicit about something JS lets you skip.
A Missing Map Key Returns a Zero Value, Not undefined
counts := map[string]int{}fmt.Println(counts["missing"]) // 0 — indistinguishable from a real zeroA Go map never returns “nothing.” It returns the zero value for whatever type the map holds — 0 for int, "" for string, nil for a slice or pointer. That’s fine until zero is also a value you’d store on purpose, which is exactly the counting-map case above. JS sidesteps this because Map.get returns undefined on a miss, so === undefined tells you presence directly. Go’s answer is the second return value on a map read:
v, ok := counts["missing"]fmt.Println(v, ok) // 0 falseok is the only reliable way to ask “was this key actually set” once zero is a legitimate value. I got away without it for a while, right up until a counter that legitimately hit zero looked identical to a key that was never touched.
Go Doesn’t Have for-of, for-in, or forEach — Just for
for ... range is the one form, and it covers what JS splits across several: slices, arrays, strings, maps, channels.
for i, v := range items { fmt.Println(i, v)}
for key, val := range myMap { fmt.Println(key, val)}The one thing this form doesn’t give you that a JS Map does: order. Ranging over a Go map visits keys in a randomized order, on purpose — the runtime deliberately shuffles it so you can’t accidentally depend on insertion order the way you can with a JS Map or Object.entries. If you need a stable order, you sort the keys yourself first. It’s the kind of thing that only shows up as a bug once your test data happens to come back “in order” and production data doesn’t.
The Three-Part for Is for When You’re Not Going Start to Finish
for-range is the default, and I reach for the classic three-part for only when the iteration itself isn’t a straight walk from the first element to the last — going backward, skipping a stride, or stopping early on something other than the range’s own bounds.
for i := len(items) - 1; i >= 0; i-- { fmt.Println(items[i])}This part isn’t really a departure from JS — it’s the same for (let i = arr.length - 1; i >= 0; i--) you’d write there too. The difference is just that JS developers reach for for...of and the three-part form about equally often, where in Go the three-part form is clearly the exception.
Labeled Loops Exist, but I’ve Used Them Once
outer:for i := 0; i < 3; i++ { for j := 0; j < 3; j++ { if j == 1 { continue outer } fmt.Println(i, j) }}continue outer skips to the next iteration of the outer loop, not the inner one. JS has the same labeled-loop syntax and it’s just as rarely used there — this isn’t a Go-specific surprise so much as confirmation that both languages consider it a tool for the rare nested-search-with-early-exit case, not everyday control flow.
Takeaway
Go trades JS’s many specialized loop and lookup forms for fewer, more explicit ones: one for, one map syntax with an optional second return value. The randomized map order and the zero-value-on-miss behavior are the two that actually cost me time — worth checking for both the first time you touch a map or a range loop in a new codebase.