Polyglot
Languages Go conditionals
Go § conditionals

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/else for 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 switch for multi-way value dispatch (treated separately).
  • Avoid deep nesting — restructure into guard returns.
  • Don’t reach for IIFE — explicit if/else is 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.