go-control-flow
npx skills add https://github.com/cxuu/golang-skills --skill go-control-flow
Agent 安装分布
Skill 文档
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:
- The declaration is in the same scope as the existing
v - The value is assignable to
v - 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
breakin 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
| Pattern | Go Idiom |
|---|---|
| If initialization | if err := f(); err != nil { } |
| Early return | Omit else when if body returns |
| Redeclaration | := reassigns if same scope + new var |
| C-style for | for i := 0; i < n; i++ { } |
| While-style | for condition { } |
| Infinite loop | for { } |
| Range with key+value | for k, v := range m { } |
| Range value only | for _, v := range slice { } |
| Range key only | for k := range m { } |
| Parallel assignment | i, j = i+1, j-1 |
| Expression-less switch | switch { case cond: } |
| Comma cases | case 'a', 'b', 'c': |
| No fallthrough | Default behavior (explicit fallthrough if needed) |
| Break from loop in switch | break Label |
| Type switch | switch v := x.(type) { } |
| Discard value | _, err := f() |
| Side-effect import | import _ "pkg" |
| Interface check | var _ 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