if with Initialization & switch Patterns
Go tightens conditional code by letting if and switch run a one-shot init statement before the branch, and by offering both value switches and type switches on interfaces.
Mastering these forms keeps error handling flat and replaces long if/else chains with clearer discrimination.
Summary
An init statement on if or switch scopes temporary variables to the block.
Expression switches compare one value against cases without implicit fallthrough.
Tagless switches chain boolean cases like else if.
Type switches inspect dynamic types inside interfaces.
fallthrough is explicit and uncommon.
Recipe
Quick-reference recipe card - copy-paste ready.
package main
import (
"fmt"
"os"
)
func main() {
if f, err := os.Open("config.txt"); err != nil {
fmt.Println(err)
return
} else {
defer f.Close()
fmt.Println("opened", f.Name())
}
code := 404
switch code {
case 200:
fmt.Println("ok")
case 404:
fmt.Println("not found")
default:
fmt.Println("other")
}
}When to reach for this:
- Use
if err := ...; err != nilimmediately after calls that return errors. - Use value
switchwhen comparing one expression to many constants. - Use tagless
switch true(or bare conditions) for ordered boolean cases. - Use type switches when behavior depends on the concrete type inside an
interface{}or generic constraint.
Working Example
package main
import (
"encoding/json"
"fmt"
)
type Event struct {
Kind string
Payload any
}
func handle(e Event) error {
switch p := e.Payload.(type) {
case nil:
return fmt.Errorf("missing payload for %s", e.Kind)
case json.RawMessage:
fmt.Println("raw json bytes", len(p))
case map[string]any:
fmt.Println("object keys", len(p))
case string:
fmt.Println("string", p)
default:
return fmt.Errorf("unsupported payload %T", p)
}
return nil
}
func classify(score int) string {
switch {
case score >= 90:
return "A"
case score >= 80:
return "B"
default:
return "C"
}
}
func main() {
events := []Event{
{Kind: "text", Payload: "hello"},
{Kind: "obj", Payload: map[string]any{"k": 1}},
}
for _, e := range events {
if err := handle(e); err != nil {
fmt.Println(err)
}
}
fmt.Println(classify(85))
}What this demonstrates:
- Type switch with
p := e.Payload.(type)binds a typed variable per case. defaulthandles unexpected dynamic types with%Tin errors.- Tagless
switchwith no tag expression chains comparisons likeif/else if. - Error checks stay at the call site with early handling in the loop.
Deep Dive
How It Works
- Init runs once before the condition; variables declared in init are block-scoped to the
if/switch. - Value switches evaluate cases top to bottom; the first true case runs and then exits unless
fallthroughappears. - Type switches require the operand to be an interface; each case must match interface assignability rules.
- Tagless switches evaluate cases as boolean expressions in order - useful for ranges and compound conditions.
switchcan include an init statement:switch err := f(); err != nil { case ... }(uncommon but valid).
Switch Forms at a Glance
| Form | Syntax | Matches on |
|---|---|---|
| Value | switch x { case v: | equality on comparable x |
| Tagless | switch { case cond: | boolean cond |
| Type | switch v := i.(type) { case T: | dynamic type of interface i |
| With init | switch x := f(); x { | value of x after init |
fallthrough Rules
| Rule | Behavior |
|---|---|
| Default | Stop after first matching case |
fallthrough | Continue to next case unconditionally |
| Last case | fallthrough into default allowed but rare |
Go Notes
// Comma-ok type assert in if - alternative to type switch for one type
if s, ok := v.(string); ok {
fmt.Println(s)
}
// switch on strings with duplicate cases is a compile error
// case-insensitive match: normalize before switch or use tagless switchGotchas
- Leaking init variables - Using
errafter theifblock when declared in init. Fix: keep handling inside the block or declareerroutside intentionally. - Assuming switch fallthrough - Missing
breakbugs from C do not apply, but accidentalfallthroughcauses double execution. Fix: removefallthroughunless deliberate. - Type switch on non-interface -
x.(type)only works on interface operands. Fix: passanyor a small interface wrapper. - Nil interface vs typed nil -
switch v := i.(type) { case *T: }may match a typed nil pointer. Fix: checkv == nilinside the case. - Duplicate case expressions - Compile error if two cases overlap on values. Fix: combine with comma lists
case 1, 2:. - Tagless switch order matters - First true case wins; put more specific conditions first.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
if/else chain | Few branches with unrelated conditions | Many constants on one value |
Map lookup handlers[key] | Dispatch table for strings | Need exhaustiveness compile checks |
Type assertion v.(T) | Single known type | Many types to discriminate |
| Generics + constraints | Compile-time type sets | Runtime heterogeneous JSON blobs |
errors.Is / errors.As | Error chain inspection | Non-error interface values |
FAQs
Why put err in the if init statement?
It keeps the error variable scoped to the check.
Readers see acquisition and validation together.
When is fallthrough appropriate?
Almost never in application code.
Prefer shared helper calls from multiple cases instead.
Can switch cases use expressions?
Cases must be compile-time comparable constants or boolean expressions (tagless form).
You cannot switch on a slice directly - use index or length patterns instead.
How is a type switch different from type assertion?
Type switches handle many types in one construct.
Type assertion v.(T) targets a single type and returns ok.
Does switch copy the switched value?
For interfaces, the dynamic value may be copied depending on type.
Large values - switch on pointer or index instead.
Can I switch on errors?
Yes - compare with errors.Is in tagless cases:
case errors.Is(err, os.ErrNotExist):
What is switch true?
An older spelling of tagless switch.
switch { case cond: is the idiomatic modern form.
Can init declare multiple variables?
Yes - if a, err := f(); err != nil works like a short variable declaration.
At least one name must be new in some contexts with := reuse rules.
Are empty switch bodies allowed?
Yes but useless - usually a sign to delete or add handling.
Linters may flag empty cases.
How do I switch on runes?
Treat runes as integers with character constants:
case '+', '-':
Related
- Control Flow Basics - first
ifandswitchsnippets - Go Control Flow: Expression-Oriented Design - expression-oriented mental model
- defer, panic & recover - pairing defer with successful
ifopens - Nil Interface vs Nil Pointer - typed nil in type switches
- Error Handling Basics - error checks beside init-style
if
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).