Get the FREE Ultimate OpenClaw Setup Guide →

go-control-flow

Scanned
npx machina-cli add skill cxuu/golang-skills/go-control-flow --openclaw
Files (1)
SKILL.md
8.8 KB

Go Control Flow

Source: Effective Go. Go's control structures are related to C but differ in important ways. Understanding these differences is essential for writing idiomatic Go code.

Go has no do or while loop—only a generalized for. There are no parentheses around conditions, and bodies must always be brace-delimited.


If Statements

Basic Form

Go's if requires braces and has no parentheses around the condition:

if x > 0 {
    return y
}

If with Initialization

if and switch accept an optional initialization statement. This is common for scoping variables to the conditional block:

// Good: err scoped to if block
if err := file.Chmod(0664); err != nil {
    log.Print(err)
    return err
}

Omit Else for Early Returns

When an if body ends with break, continue, goto, or return, omit the unnecessary else. This keeps the success path unindented:

// Good: no else, success path at left margin
f, err := os.Open(name)
if err != nil {
    return err
}
codeUsing(f)
// Bad: else clause buries normal flow
f, err := os.Open(name)
if err != nil {
    return err
} else {
    codeUsing(f)  // unnecessarily indented
}

Guard Clauses for Error Handling

Code reads well when the success path flows down the page, eliminating errors as they arise:

// Good: guard clauses eliminate errors early
f, err := os.Open(name)
if err != nil {
    return err
}
d, err := f.Stat()
if err != nil {
    f.Close()
    return err
}
codeUsing(f, d)

Redeclaration and Reassignment

The := short declaration allows redeclaring variables in the same scope under specific conditions:

f, err := os.Open(name)  // declares f and err
// ...
d, err := f.Stat()       // declares d, reassigns err (not a new err)

A variable v may appear in a := declaration even if already declared, provided:

  1. The declaration is in the same scope as the existing v
  2. The value is assignable to v
  3. At least one other variable is newly created by the declaration

This pragmatic rule makes it easy to reuse a single err variable through a chain of operations.

// Good: err reused across multiple calls
data, err := fetchData()
if err != nil {
    return err
}
result, err := processData(data)  // err reassigned, result declared
if err != nil {
    return err
}

Warning: If v is declared in an outer scope, := creates a new variable that shadows it:

// Bad: accidental shadowing
var err error
if condition {
    x, err := someFunc()  // this err shadows the outer err!
    // outer err remains nil
}

For Loops

Go unifies for and while into a single construct with three forms:

// C-style for (only form with semicolons)
for init; condition; post { }

// While-style (condition only)
for condition { }

// Infinite loop
for { }

Range Clause

Use range to iterate over arrays, slices, strings, maps, and channels:

// Iterate with key and value
for key, value := range oldMap {
    newMap[key] = value
}

// Key/index only (drop the second variable)
for key := range m {
    if key.expired() {
        delete(m, key)
    }
}

// Value only (use blank identifier for index)
for _, value := range array {
    sum += value
}

Range Over Strings

For strings, range iterates over UTF-8 encoded runes (not bytes), handling multi-byte characters automatically.

Parallel Assignment in For

Go has no comma operator. Use parallel assignment for multiple loop variables:

// Reverse a slice
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
    a[i], a[j] = a[j], a[i]
}

Note: ++ and -- are statements, not expressions, so they cannot be used in parallel assignment.


Switch

Go's switch is more flexible than C's:

  • Expressions need not be constants or integers
  • Cases are evaluated top to bottom until a match
  • No automatic fall through (no need for break in each case)

Expression-less Switch

If the switch has no expression, it switches on true. This is idiomatic for writing clean if-else-if chains:

// Good: expression-less switch for ranges
func unhex(c byte) byte {
    switch {
    case '0' <= c && c <= '9':
        return c - '0'
    case 'a' <= c && c <= 'f':
        return c - 'a' + 10
    case 'A' <= c && c <= 'F':
        return c - 'A' + 10
    }
    return 0
}

Comma-Separated Cases

Multiple cases can be combined with commas (no fall through needed):

func shouldEscape(c byte) bool {
    switch c {
    case ' ', '?', '&', '=', '#', '+', '%':
        return true
    }
    return false
}

Break with Labels

break terminates the switch by default. To break out of an enclosing loop, use a label:

Loop:
    for n := 0; n < len(src); n += size {
        switch {
        case src[n] < sizeOne:
            break        // breaks switch only
        case src[n] < sizeTwo:
            if n+1 >= len(src) {
                break Loop   // breaks out of for loop
            }
        }
    }

Type Switch

A type switch discovers the dynamic type of an interface value using .(type):

switch v := value.(type) {
case nil:
    fmt.Println("value is nil")
case int:
    fmt.Printf("integer: %d\n", v)      // v is int
case string:
    fmt.Printf("string: %q\n", v)       // v is string
case bool:
    fmt.Printf("boolean: %t\n", v)      // v is bool
default:
    fmt.Printf("unexpected type %T\n", v)
}

It's idiomatic to reuse the variable name (v := value.(type)) since the variable has a different type in each case clause.

When a case lists multiple types (case int, int64:), the variable has the interface type.


The Blank Identifier

The blank identifier _ discards values. It's like writing to /dev/null.

Multiple Assignment

Discard unwanted values from multi-value expressions:

// Only need the error
if _, err := os.Stat(path); os.IsNotExist(err) {
    fmt.Printf("%s does not exist\n", path)
}

// Only need the value (discard ok)
value := cache[key]  // simpler: just use single-value form
_, present := cache[key]  // when you only need presence check

Never discard errors carelessly:

// Bad: ignoring error will crash if path doesn't exist
fi, _ := os.Stat(path)
if fi.IsDir() {  // nil pointer dereference if path doesn't exist
    // ...
}

Unused Imports and Variables During Development

Silence compiler errors temporarily during active development:

import (
    "fmt"
    "io"
)

var _ = fmt.Printf  // silence unused import (remove before committing)
var _ io.Reader

func main() {
    fd, _ := os.Open("test.go")
    _ = fd  // silence unused variable
}

Import for Side Effect

Import a package only for its init() side effects:

import _ "net/http/pprof"  // registers HTTP handlers
import _ "image/png"       // registers PNG decoder

This makes clear the package is imported only for side effects—it has no usable name in this file.

Interface Compliance Check

Verify at compile time that a type implements an interface:

// Verify that *MyType implements io.Writer
var _ io.Writer = (*MyType)(nil)

// Verify that MyHandler implements http.Handler
var _ http.Handler = MyHandler{}

This fails at compile time if the type doesn't implement the interface, catching errors early.


Quick Reference

PatternGo Idiom
If initializationif err := f(); err != nil { }
Early returnOmit else when if body returns
Redeclaration:= reassigns if same scope + new var
C-style forfor i := 0; i < n; i++ { }
While-stylefor condition { }
Infinite loopfor { }
Range with key+valuefor k, v := range m { }
Range value onlyfor _, v := range slice { }
Range key onlyfor k := range m { }
Parallel assignmenti, j = i+1, j-1
Expression-less switchswitch { case cond: }
Comma casescase 'a', 'b', 'c':
No fallthroughDefault behavior (explicit fallthrough if needed)
Break from loop in switchbreak Label
Type switchswitch v := x.(type) { }
Discard value_, err := f()
Side-effect importimport _ "pkg"
Interface checkvar _ Interface = (*Type)(nil)

See Also

  • go-style-core: Core Go style principles and formatting
  • go-error-handling: Error handling patterns including guard clauses
  • go-naming: Naming conventions for loop variables and labels
  • go-concurrency: Goroutines, channels, and select statements
  • go-defensive: Defensive programming patterns

Source

git clone https://github.com/cxuu/golang-skills/blob/main/skills/go-control-flow/SKILL.mdView on GitHub

Overview

Go control flow idioms from Effective Go help you write clean, idiomatic conditionals, loops, and switches. It covers if with initialization, omitting else for early returns, various for-loop forms, range usage, switches without fallthrough, type switches, and blank identifier patterns. Mastering these patterns makes Go code more readable and maintainable.

How This Skill Works

Go uses a single for construct to cover all looping forms, with optional initialization in if and switch statements. This skill demonstrates guard clauses, range-based iteration, and type switches, showing how to structure conditionals and loops for clarity and correctness.

When to Use It

  • When you need to scope a variable to a conditional block using if with initialization
  • When you want to avoid nested else blocks by using early returns (guard clauses)
  • When iterating over maps, slices, strings, or channels with range
  • When you need to handle multiple concrete types behind an interface with a type switch
  • When you want to ignore certain values using the blank identifier in loops or assignments

Quick Start

  1. Step 1: Review the basic if with initialization, for loop forms, and range syntax
  2. Step 2: Implement a function using guard clauses and an early return pattern
  3. Step 3: Add a small type switch and a range-based loop to a sample struct

Best Practices

  • Use if with initialization to limit scope to the conditional block and keep code local
  • Omit else for early returns to keep the success path left-aligned and readable
  • Adopt guard clauses to handle errors early and avoid deep nesting
  • Prefer range-based loops for maps, slices, and strings; use proper key/value or index/value forms
  • Beware redeclaration rules with := to avoid shadowing outer variables and maintain clarity

Example Use Cases

  • Guarded error handling with if err := file.Chmod(...); err != nil { ... }
  • Omitting else after a successful operation to keep the main path simple
  • Iterating a map with for key, value := range m and updating a new map
  • Using a two-index for loop to reverse a slice: for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1
  • Switch on an interface with type assertions in a type switch

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers