Conditionals
Go’s conditional construct is if/else. The form requires braces (no single-statement form), the condition must be a bool (no truthiness coercion), and the construct admits an initialisation clause — a statement run before the condition is tested, with its bindings scoped to the if and any else. Go does not have a ternary operator; the conventional substitute is an explicit if/else block. For value-driven dispatch, switch (treated separately) is the conventional choice. The combination — strict bool conditions, mandatory braces, scoped initialisation, no ternary — is the substance of Go’s selection surface.
if/else
The principal form:
if condition {
// body
} else if other {
// body
} else {
// body
}
Examples:
if x > 0 {
fmt.Println("positive")
} else if x < 0 {
fmt.Println("negative")
} else {
fmt.Println("zero")
}
The condition must be a bool — Go does not coerce other types:
n := 5
if n { // ERROR: int is not bool
}
if n != 0 { // OK
}
if n > 0 { // OK
}
The strictness eliminates the C-family truthiness pitfalls. The conventional defence: explicit comparisons.
Mandatory braces
The braces are required; there is no single-statement form:
if cond { doSomething() } // OK; one-liner
if cond { // OK; multi-line
doSomething()
}
if cond
doSomething() // ERROR
The discipline eliminates the dangling-else ambiguity and produces consistent formatting.
The initialisation clause
if admits an initialisation clause — a statement run before the condition:
if v := compute(); v > 0 {
fmt.Println("got positive:", v)
} else {
fmt.Println("got non-positive:", v)
}
// v not accessible here
The variable v is scoped to the if and its else branches. The pattern is one of Go’s most distinctive idioms; treated as the conventional Go form for “compute, check, use”:
// Common:
if err := step(); err != nil {
return fmt.Errorf("step failed: %w", err)
}
// Common:
if val, ok := m[key]; ok {
process(val)
}
// Common:
if file, err := os.Open(path); err == nil {
defer file.Close()
/* use file */
}
The initialiser runs once; the condition is then evaluated. If the condition is true, the body runs; otherwise the else (if present) runs.
else if
For multiple conditions:
if x < 0 {
fmt.Println("negative")
} else if x == 0 {
fmt.Println("zero")
} else if x < 100 {
fmt.Println("small")
} else {
fmt.Println("large")
}
For substantial multi-way branching, switch is conventionally clearer (treated in Switch and pattern dispatch).
No ternary
Go does not have a ternary operator. The conventional substitute is if/else:
// Cannot:
// max := a > b ? a : b
// Conventional Go:
var max int
if a > b {
max = a
} else {
max = b
}
For inline value-conditioning, an immediately-invoked function admits compactness:
max := func() int {
if a > b { return a }
return b
}()
The form is rare in idiomatic Go; an explicit if/else is the conventional choice.
The deliberate omission is documented in the Go FAQ:
The reason
?:is absent from Go is that the language’s designers had seen the operation used too often to create impenetrable expressions.
Common patterns
Guard-style early return
func process(input string) (Result, error) {
if input == "" {
return Result{}, fmt.Errorf("empty input")
}
if len(input) > maxLength {
return Result{}, fmt.Errorf("input too long")
}
// main body
return Result{Data: parse(input)}, nil
}
The pattern reduces nesting; the conventional Go style favours early returns for precondition validation.
Error checking with init
if err := step(); err != nil {
return err
}
The pattern is the conventional Go error-handling form. The error is scoped to the if, avoiding pollution of the surrounding scope.
For chained operations:
if err := step1(); err != nil {
return err
}
if err := step2(); err != nil {
return err
}
if err := step3(); err != nil {
return err
}
Map lookup
if v, ok := m[key]; ok {
process(v)
} else {
handleAbsent()
}
The two-value form admits distinguishing “key not present” from “value is the zero value”.
Type assertion
if s, ok := value.(string); ok {
process(s)
} else {
return fmt.Errorf("expected string, got %T", value)
}
The form admits safe type extraction with branching on success or failure.
Pre-conditioned action
if file, err := os.Open(path); err == nil {
defer file.Close()
return process(file)
}
return fmt.Errorf("could not open %s", path)
The init-clause admits scoping resources to the conditional block.
Validation chain
func validateUser(u User) error {
if u.Name == "" {
return fmt.Errorf("name is required")
}
if u.Age < 0 {
return fmt.Errorf("age must be non-negative")
}
if u.Email != "" && !isValidEmail(u.Email) {
return fmt.Errorf("invalid email")
}
return nil
}
The form is conventional for validation routines.
Nested conditional
if user.IsAdmin {
if user.HasPermission("delete") {
return doDelete()
}
return ErrInsufficientPermission
}
return ErrNotAdmin
For deep nesting, conventionally restructure into early returns:
if !user.IsAdmin {
return ErrNotAdmin
}
if !user.HasPermission("delete") {
return ErrInsufficientPermission
}
return doDelete()
The pattern admits clearer code paths.
Conditional assignment
status := "inactive"
if user.Active {
status = "active"
}
// With map fallback:
greeting := defaultGreeting
if g, ok := greetings[locale]; ok {
greeting = g
}
Combined conditions
if user != nil && user.IsActive && user.HasRole("admin") {
grantAccess()
}
The && and || operators short-circuit; the example admits safely accessing user.IsActive because evaluation stops if user == nil.
Truthiness
Go does not admit truthiness; if requires a bool:
if "": // ERROR (and not even valid syntax)
if 0: // ERROR
if []int{}: // ERROR
// Explicit comparisons:
if s != "" {} // non-empty string
if n != 0 {} // non-zero
if len(s) > 0 {} // non-empty slice
if p != nil {} // non-nil pointer/slice/map/etc.
The strictness produces clearer code; the conventional explicit comparisons are conventionally preferred.
A note on the absence of if as expression
Go’s if is a statement, not an expression — it does not produce a value:
// Cannot:
// max := if a > b { a } else { b }
// Conventional Go:
var max int
if a > b {
max = a
} else {
max = b
}
Languages where if is an expression (Rust, Kotlin, Scala) admit substantial conciseness; Go’s statement-only form trades that for explicitness.
A note on the conventional discipline
The contemporary Go conditional advice:
- Use
if/elsefor boolean dispatch. - Use the init clause (
if x := f(); cond) when the value is needed only in the conditional. - Use early returns for precondition validation.
- Use the comma-ok idiom for map lookup, type assertion, channel receive.
- Use
switchfor multi-way value dispatch (treated separately). - Avoid deep nesting — restructure into guard returns.
- Don’t reach for IIFE — explicit
if/elseis conventional.
The combination — if/else with mandatory braces, init clause for scoped bindings, no ternary, no truthiness — is the substance of Go’s selection surface. The discipline produces clear, explicit conditional code.