Missing Switch Default
ID |
go.missing_switch_default |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
Go |
Tags |
control-flow, reliability, switch |
Description
Reports a switch that dispatches on a value and has no default clause.
Only a switch with a switch expression qualifies. An expression-less switch, a bool switch that
handles both true and false, a switch with no clauses, a switch whose every case terminates with
a statement after it, and a type switch are all excluded — see Configuration. select is a
different construct and is never reported.
Rationale
func tally(level string, c *counters) {
switch level { // FLAW — an unknown level is silently ignored
case "warn":
c.warn++
case "fail":
c.fail++
}
}
Without a default, a value matching no case falls straight through and the switch does nothing.
Go’s switch has no fallthrough by default and no exhaustiveness check, so this is entirely silent:
no compiler warning, no panic, no log line. The caller sees a function that returned normally and
did not do its job.
The failure mode that matters is the one that arrives later. When a value is added to whatever the
switch dispatches on — a new status, a new message kind, a new enum constant — every switch over it
that lacks a default starts quietly ignoring the new value, and the bug surfaces far from the code
that caused it. A default is the one place written to catch that.
It also bites on the return path. A switch with no default inside a function that returns a
value will fall out of the switch and return the zero value — an empty string, 0, nil — which is
frequently indistinguishable from a legitimate result.
Remediation
Add a default that handles the remaining values, rejects them, or declares them impossible.
// Handle the rest.
switch level {
case "warn":
c.warn++
case "fail":
c.fail++
default:
c.other++
}
// Or reject them, so the caller finds out.
switch level {
case "warn", "fail":
return nil
default:
return fmt.Errorf("unknown level %q", level)
}
// Or state that the case cannot happen, and fail loudly if it does.
default:
panic(fmt.Sprintf("unhandled level %q", level))
Placing default first or last keeps it where readers look for it — see
go.switch_default_first_or_last.
Configuration
| Property | Meaning | Default |
|---|---|---|
|
Also report a type switch ( |
|
Four further shapes are excluded by construction.
The expression-less switch. switch { case a > b: … } is Go’s if / else if ladder written
as a switch, not a dispatch on a value. Its missing catch-all is the concern of
go.else_if_without_else; reporting it here would report one defect twice, differing only in
syntax.
A bool switch covering both values. When the cases are exactly true and false, they already
enumerate every value the type has and a default would be unreachable. A case that merely passes a
boolean to a call — case allowed(true): — says nothing about exhaustiveness and is still reported.
A switch with no clauses. switch n {} is an empty statement with nothing to dispatch.
A switch whose every case terminates, with a statement after it. When every case ends in
return, break, continue, goto, panic(…) or os.Exit(…), the statement following the
switch is reached only when no case matched — it is the default, written unindented:
func (m mode) String() string {
switch m {
case active:
return "active"
case inactive:
return "inactive"
}
return "unknown" // OK — this is the default
}
That is the idiomatic Go early return, the shape go.superfluous_else actively asks for and the one
go.else_if_without_else exempts for the same reason; all three decide what "terminates" means the
same way.
The statement does not have to be the switch’s own sibling. The catch-all is as often written after
the construct that holds the switch — a switch that is the last statement of an if block, or of a
case of an outer switch, with the return at the end of the function:
func serviceName(group, version string) (string, error) {
switch group {
case "extensions":
return "legacy", nil
case "networking.k8s.io":
switch version { // OK — the default is the return below
case "v1beta1":
return "beta", nil
case "v1":
return "stable", nil
}
}
return "", errors.New("unresolved")
}
An enclosing for counts too: falling out of the switch runs the next iteration, which is where the
unmatched value goes even when nothing follows the loop. A statement following an enclosing function
literal does not count — it runs after the whole call that took the literal, not after the unmatched
value inside it.
A catch-all is required: a terminating switch with nothing after it anywhere in its function has none, and an unmatched value really does fall out of the dispatch, so that shape is still reported.