Currently available · INDIA
← Writing
Languages·Aug 13, 2026·4 min read

Why Go Doesn't Have While Loops

if, switch, for, break, continue, labels, and defer — and why Go replaced while, do-while, and foreach with a single loop word.

0

Open a Go file looking for while. You won’t find it.

That’s not an accident. C, Java, and JavaScript give you while, do-while, for, and often foreach. Go kept one loop: for. Leave pieces out and it becomes “while.” Leave them all out and it runs forever. Add range and it walks a collection.

Fewer loop words. Same jobs. One shape to remember.

The rest of control flow follows the same taste: required braces, no ternary, switch that doesn’t fall through, and defer so cleanup sits next to setup.

Also read: Why Go Doesn't Let Variables Stay Uninitialized and Why Simple Code Beats Clever Code.

Did you miss while when you first wrote Go?

Remember this one line

for is the only loop. Condition-only for is while. Empty for is forever. range is foreach.

if, switch, break, continue, labels, and defer are the rest of the steering wheel.

if, switch, for, break, labels, defer

1 of 6

if

if is a fork in the road. Go keeps it boring: no parentheses, braces required, and you can declare a value right in the condition.

How you write it. if err != nil { return err }. Optional init: if v, err := load(); err != nil { ... }. There is no ternary ? : — write a normal if.

Why it sticks. Putting a short declaration in the if keeps err and v close to the check. That is the Go style: fail fast, keep the happy path unindented.

Don’t get tripped. A variable declared in if v, err := ... lives only in that if/else. Need it later? Declare it above. Also: braces are mandatory even for one line. That’s a feature — it stops dangling-else bugs.

Example

if n := len(items); n == 0 {
    return errors.New("empty")
}
if err != nil {
    return err
}

How other loops map onto for

Compare

Other languages

while (err == nil) {
    err = step()
}

Go

for err == nil {
    err = step()
}

Drop the init and the post. Keep the condition. That’s while — spelled for.

Why this design choice exists

Two questions

The design choice underneath the syntax.

Why doesn’t Go have while loops?

Because while is just for with the extra parts left blank — and Go would rather teach one loop than three.

Why it helps. C-family languages give you while, do-while, for, and often foreach. Each has a slightly different story. Go’s bet: one keyword, optional pieces. Condition only? That’s while. Nothing at all? That’s forever. range? That’s foreach. You learn for once.

But wait. while (running) reads like English. for running looks like a typo if you grew up with while. One keyword can feel like a costume party where every loop wears the same name.

Readability here is team-scale, not sentence-scale. A codebase with one loop word greps cleanly and onboards faster. The “missing while” is the same design as the missing ternary: fewer ways to say the same thing.

In real code

Fail fast, keep the happy path flat

go
f, err := os.Open(path)
if err != nil {
    return err
}
defer f.Close()

return parse(f)

No else wrapping the whole function. Check, return, continue. defer promises the close even if parse fails.

Sticky idea: Setup and cleanup share a zip code. The rest of the function can forget about the file.

switch instead of an else if ladder

go
switch {
case code >= 500:
    return "server"
case code >= 400:
    return "client"
default:
    return "ok"
}

No expression on switch means each case is a boolean. Cases don’t fall through — you can read one without skimming the next.

Sticky idea: A tagless switch is a labeled list of conditions, not a fall-through trap.

Waiting is still a loop — just spelled for

go
for attempts < max && err != nil {
    err = send()
    attempts++
}

That’s a while loop. Go didn’t delete the idea. It deleted the extra keyword.

Sticky idea: If you can say it with for, Go already has a word for it.

Nested loops: aim break with a label

go
Found:
for r, row := range grid {
    for c, cell := range row {
        if cell == needle {
            fmt.Println(r, c)
            break Found
        }
    }
}

A bare break would only leave the inner loop. The label names the loop you actually want to exit.

Sticky idea: Labels are a last resort for nested break — not a second goto.

Don’t defer inside a tight loop

go
// piles up until the function returns — usually wrong
for _, path := range paths {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close()
    read(f)
}

defer is tied to the function, not the block. Close in the loop, or wrap each file in a small helper so defer runs when that helper returns.

Sticky idea: defer means “when this function exits,” not “when this } is hit.”

Try this

  1. Rewrite a while from another language as for condition { }. Read it out loud.
  2. Find an else if chain and try a tagless switch.
  3. Search for break inside switch inside for. Does it leave the loop you think it does?
  4. Find defer in a loop. If the function is long-lived, that’s a leak — fix it.
  5. Walk a slice with for _, v := range and try mutating v. Notice the slice doesn’t change.

Take these home

  1. No while — condition-only for is the same idea.
  2. if has no parens and no ternary — braces always; declare in the condition when it stays local.
  3. switch does not fall throughfallthrough is opt-in and rare.
  4. break / continue target the inner loop or switch — labels when nested loops would lie.
  5. defer runs at function return, LIFO — keep it next to setup, out of hot loops.
  6. One loop keyword is governance: fewer dialects, same power.

Other languages hang extra loop words on the wall like spare keys. Go leaves one key on the hook: for. You still get while, foreach, and forever — you just stop arguing about which key to pick.

/ Stay in the loop

Get the next one in
your inbox.

New essays and field notes sent only when there's something worth sending. No tracking, no spam, easy unsubscribe.

Join · No spam · One-click unsubscribe