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.
Search across all documentation pages
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.
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.
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:
if err := ...; err != nil immediately after calls that return errors.switch when comparing one expression to many constants.switch true (or bare conditions) for ordered boolean cases.interface{} or generic constraint.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:
p := e.Payload.(type) binds a typed variable per case.default handles unexpected dynamic types with %T in errors.switch with no tag expression chains comparisons like if/else if.if/switch.fallthrough appears.switch can include an init statement: switch err := f(); err != nil { case ... } (uncommon but valid).| 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 |
| Rule | Behavior |
|---|---|
| Default | Stop after first matching case |
fallthrough | Continue to next case unconditionally |
| Last case | fallthrough into default allowed but rare |
// 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 switcherr after the if block when declared in init. Fix: keep handling inside the block or declare err outside intentionally.break bugs from C do not apply, but accidental fallthrough causes double execution. Fix: remove fallthrough unless deliberate.x.(type) only works on interface operands. Fix: pass any or a small interface wrapper.switch v := i.(type) { case *T: } may match a typed nil pointer. Fix: check v == nil inside the case.case 1, 2:.| 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 |
It keeps the error variable scoped to the check.
Readers see acquisition and validation together.
Almost never in application code.
Prefer shared helper calls from multiple cases instead.
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.
Type switches handle many types in one construct.
Type assertion v.(T) targets a single type and returns ok.
For interfaces, the dynamic value may be copied depending on type.
Large values - switch on pointer or index instead.
Yes - compare with errors.Is in tagless cases:
case errors.Is(err, os.ErrNotExist):
An older spelling of tagless switch.
switch { case cond: is the idiomatic modern form.
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.
Yes but useless - usually a sign to delete or add handling.
Linters may flag empty cases.
Treat runes as integers with character constants:
case '+', '-':
if and switch snippetsif opensifStack 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).
Reviewed by Chris St. John·Last updated Jul 18, 2026