No Nested Switch

ID

go.no_nested_switch

Severity

high

Remediation Complexity

medium

Remediation Risk

low

Remediation Effort

medium

Resource

Code Smell

Language

Go

Tags

complexity, control-flow, switch

Description

Reports a switch statement written inside the body of another switch. The inner switch is reported, since that is the one to extract.

Both expression switches and type switches count, as either the inner or the outer switch. A switch inside a function literal is not nested, and select is never reported — see Configuration.

Rationale

switch kind {
case "blob":
	switch encoding {          // FLAW — inner table hidden inside a case
	case "gzip":
		return "blob/gzip"
	default:
		return "blob/raw"
	}
default:
	return "unknown"
}

A switch is already a table the reader has to hold in mind while checking that every case is handled. Nesting a second one inside a case multiplies the paths through the function — two switches of five cases each is up to twenty-five outcomes — and each path has to be traced through two levels of case labels to be understood.

The inner table is also hidden. Nobody scanning the function for "what happens for `gzip`" expects to find a second dispatch buried under a case of the first, so the inner cases get missed in review and in testing, and a case added to the inner switch is invisible from the outer one.

Remediation

Lift the inner switch into a named function. The name documents the second dimension, and each table can then be read, reviewed and tested on its own.

func describe(kind string, encoding string) string {
	switch kind {
	case "blob":
		return describeBlob(encoding)
	default:
		return "unknown"
	}
}

func describeBlob(encoding string) string {
	switch encoding {
	case "gzip":
		return "blob/gzip"
	default:
		return "blob/raw"
	}
}

When the two switches dispatch on values that are really one composite decision, a single switch over a combined key or a lookup map keyed on the pair often replaces both.

Configuration

This rule has no configurable properties.

Nesting is lexical, not immediate: an inner switch wrapped in an if or a for inside the case still counts, because the reader still meets it while reading the outer case. In a three-deep stack the middle and the innermost switches are each reported against their own nearest enclosing switch — each is independently a level too many.

Two shapes are excluded by construction.

A switch inside a function literal. The search for an enclosing switch stops at the nearest function boundary, so this is not nested:

switch n {
case 1:
	go func() {
		switch m {          // OK — its own function body
		case 1:
			work()
		}
	}()
}

Putting the inner switch in its own function is the remediation this rule asks for, and it applies whether the function has a name or not.

select. A select inside a switch is ordinary concurrent code — it waits on channels rather than dispatching on a value — and its default means "do not block", not "everything else".

The same shape for if statements is covered by go.nested_if.

© 2026 Xygeni. All rights reserved