skip to content

When would you replace a long if/else-if chain in Go with a tagless `switch`?

level: middleimportance: nice to knowfreq 40%

answer

  1. the missing expression is true
  2. peer conditions in one column
  3. no break, and no implicit fallthrough
  4. default is the catch-all
  5. it does not change indentation

basics

~20 s

A Go switch with no expression after the keyword compares each case against true, so every case is an ordinary condition. Use it when a function classifies: the conditions align in one column and default names the catch-all.

solid answer

~50 s

`switch { case a: ...; case b: ... }` is shorthand for `switch true`, so each case is a boolean expression evaluated top to bottom with the first match winning. I reach for it when the function is genuinely classifying: mapping a set of conditions onto a set of results. The conditions then sit in one column, one per line, adding or reordering one is a single-line change, and `default` names the catch-all explicitly. Go does not fall through between cases, so there is no `break` noise, and each case body is its own scope. What it does not do is reduce indentation - in gofmt'd code an `else if` chain is already flat. So if the real problem is that one branch is exceptional and the rest of the function is the normal path, an early return is the better shape and a switch is only decoration.

code

go · 11 lines
go
func validate(r createRequest) error {
	switch {
	case r.Name == "":
		return errors.New("name is required")
	case len(r.Name) > 64:
		return errors.New("name is too long")
	case r.Age < 0:
		return errors.New("age must not be negative")
	}
	return nil
}

go deeper

for a junior

Know that a Go switch can be written with nothing after the keyword and that each case is then just a condition. Recognise the form when you meet it in a code review.

for a middle

Explain the mechanics precisely: it means switch true, cases evaluate in source order, first match wins, there is no implicit fallthrough, and each case body has its own scope.

for a senior

Show judgment about when it is the wrong tool - a classifying function benefits, but a function with one exceptional branch reads better as a guard clause and an early return - and name the overlapping-case ordering hazard.

for a principal

Treat it as a shape convention to settle once: when the codebase prefers a switch for classification, when a lookup table beats both forms, and whether order-dependent cases need a comment rule.

## What a tagless switch is Go's `switch` comes in three forms. The familiar one compares a value against each case: `switch code { case 200: ... }`. The type switch inspects the dynamic type held by an interface. The third form omits the value entirely: ``` switch { case r.Name == "": return errors.New("name is required") case len(r.Name) > 64: return errors.New("name is too long") case r.Age < 0: return errors.New("age must not be negative") } return nil ``` Omitting the expression is defined as `switch true`, so each case is compared against `true` - which means each case may be any boolean expression, and the cases have nothing to do with one another. That is what makes it a replacement for a chain of conditions rather than a lookup on one value. ## The semantics worth stating precisely - **Order is source order.** Cases are evaluated top to bottom and the first one that is true runs. Nothing is reordered, and no jump table is built for a tagless switch, so a case that overlaps an earlier one is dead. - **There is no implicit fallthrough.** When a case body finishes, control leaves the switch. No `break` is needed, and writing one is redundant. The explicit `fallthrough` keyword exists, must be the last statement in a case, and transfers control into the *next case's body without evaluating its condition* - which is why it is rare and deserves a comment. - **Each case body is its own scope.** Variables declared in one case are invisible to the others, so several cases can each declare `v` without collision. - **It accepts an init statement.** `switch v, err := parse(s); {` runs the statement first and scopes `v` and `err` to the whole switch, cases included. The tag is still omitted; the semicolon before the brace is what marks it. - **`default` may appear anywhere** among the cases, though convention is last, and it runs when no case matched. ## When it is the right shape The case for it is uniformity. A classifying function - one that maps conditions onto results - reads best when the conditions are a column of peers. Every condition starts in the same place, each occupies one line, and adding a fourth classification is one `case` line rather than an edit to a chain's tail. `default` names the fallback rather than leaving it implied by a trailing `else`. When the branch bodies are short and none of them is more "normal" than the others, this is the shape that says so. It also removes a small class of mistakes: with an `else if` chain it is easy to attach a condition to the wrong tail during an edit, because the syntax runs the conditions together. Cases cannot be accidentally chained. ## When it is the wrong shape The common misuse is reaching for a switch to *reduce nesting*. It does not. In gofmt'd Go, an `else if` chain is already flat: `} else if cond {` sits on the closing brace line, so ten conditions in a chain sit at the same column as the first `if`. Converting the chain to a switch changes the punctuation, not the depth. If a function is genuinely deep, the fix is early returns and extracting a helper. The second misuse is using it where the branches are not peers. If one condition is exceptional and everything after it is the normal path, a guard clause plus `return` communicates that ranking; a switch flattens the ranking away and tells the reader these are equal alternatives when they are not. A two-branch switch is almost always ceremony over an `if`. ## Order dependence is the hazard to name Because cases are conditions rather than distinct values, they can overlap, and correctness then depends on the order. `case n > 100` placed above `case n > 10` makes the second unreachable for large values in a way that is silent - no compiler error, no vet diagnostic, and the tests pass if nobody tries a value in the shadowed range. When you write a tagless switch whose cases overlap, ordering is a decision and deserves a comment, or a restructuring into non-overlapping ranges. ## A short answer for the interview Use it when you are classifying and the conditions are peers; keep the early return when one branch is the exceptional exit; and do not claim it as a fix for indentation, because in gofmt'd Go the chain it replaces was never indented in the first place.

  • Can a tagless switch declare a variable before its cases?
    Yes. `switch v, err := parse(s); {` runs the statement first and scopes `v` and `err` to the whole switch, cases included; the semicolon before the brace is what marks the tag as omitted. It is the same init-statement form an `if` takes, and it keeps a parsed value out of the surrounding function's scope while several cases inspect it.
  • What exactly does Go's `fallthrough` keyword do?
    It transfers control into the next case's body without evaluating that case's condition, and it must be the last statement in the case. It is rare and worth a comment, because readers arriving from C assume falling through is the default. In a tagless switch it is especially surprising, since the condition being skipped is an arbitrary expression.
  • What is the hazard specific to a tagless switch whose cases overlap?
    Silent order dependence. Cases are conditions, not distinct values, so `case n > 100` above `case n > 10` leaves the second unreachable for large values with no compiler or vet complaint. Either make the ranges non-overlapping, or treat the ordering as a decision and say so in a comment next to the cases.

saying these in an interview costs you the question

  • Says each case needs a break to stop falling through
  • Claims an else-if chain indents one level per condition
  • Thinks a tagless switch needs a boolean variable as its tag
  • Uses a switch where one guard clause and a return fit
  • Assumes overlapping cases are diagnosed by the compiler or go vet