npx skills add https://github.com/cxuu/golang-skills --skill go-control-flow来源 : Effective Go。Go 的控制结构虽然与 C 语言相关,但在重要方面存在差异。理解这些差异对于编写地道的 Go 代码至关重要。
Go 没有 do 或 while 循环,只有通用的 for 循环。条件周围没有括号,且循环体必须始终用大括号界定。
Go 的 if 语句要求使用大括号,并且条件周围没有括号:
if x > 0 {
return y
}
if 和 switch 可以接受一个可选的初始化语句。这通常用于将变量作用域限定在条件块内:
// 良好实践:err 的作用域限定在 if 块内
if err := file.Chmod(0664); err != nil {
log.Print(err)
return err
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
当 if 语句体以 break、continue、goto 或 return 结束时,可以省略不必要的 else。这可以使成功路径保持不缩进:
// 良好实践:没有 else,成功路径位于左边界
f, err := os.Open(name)
if err != nil {
return err
}
codeUsing(f)
// 不良实践:else 子句掩盖了正常流程
f, err := os.Open(name)
if err != nil {
return err
} else {
codeUsing(f) // 不必要的缩进
}
当成功路径顺着页面往下流动,并在错误出现时立即处理时,代码的可读性会更好:
// 良好实践:卫语句尽早消除错误
f, err := os.Open(name)
if err != nil {
return err
}
d, err := f.Stat()
if err != nil {
f.Close()
return err
}
codeUsing(f, d)
短变量声明 := 允许在特定条件下在同一作用域内重新声明变量:
f, err := os.Open(name) // 声明 f 和 err
// ...
d, err := f.Stat() // 声明 d,重新赋值 err(不是新的 err)
即使变量 v 已经声明过,它仍然可以出现在 := 声明中,前提是:
v 在同一作用域内v这个实用的规则使得可以方便地在一系列操作中重用单个 err 变量。
// 良好实践:在多次调用中重用 err
data, err := fetchData()
if err != nil {
return err
}
result, err := processData(data) // err 被重新赋值,result 被声明
if err != nil {
return err
}
警告:如果 v 在外部作用域中声明,:= 会创建一个新的变量并遮蔽它:
// 不良实践:意外的变量遮蔽
var err error
if condition {
x, err := someFunc() // 这个 err 遮蔽了外部的 err!
// 外部的 err 仍然为 nil
}
Go 将 for 和 while 统一为单一结构,有三种形式:
// C 风格 for 循环(唯一使用分号的形式)
for init; condition; post { }
// While 风格(仅条件)
for condition { }
// 无限循环
for { }
使用 range 来遍历数组、切片、字符串、映射和通道:
// 使用键和值进行迭代
for key, value := range oldMap {
newMap[key] = value
}
// 仅键/索引(丢弃第二个变量)
for key := range m {
if key.expired() {
delete(m, key)
}
}
// 仅值(使用空白标识符表示索引)
for _, value := range array {
sum += value
}
对于字符串,range 遍历的是 UTF-8 编码的符文(不是字节),会自动处理多字节字符。
Go 没有逗号操作符。对于多个循环变量,请使用并行赋值:
// 反转切片
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
a[i], a[j] = a[j], a[i]
}
注意:++ 和 -- 是语句,不是表达式,因此不能在并行赋值中使用。
Go 的 switch 比 C 语言更灵活:
break)如果 switch 没有表达式,则相当于在 true 上进行切换。这是编写清晰的 if-else-if 链的地道方式:
// 良好实践:无表达式的 switch 用于范围判断
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
}
多个 case 可以用逗号组合(不需要贯穿):
func shouldEscape(c byte) bool {
switch c {
case ' ', '?', '&', '=', '#', '+', '%':
return true
}
return false
}
默认情况下,break 会终止 switch。要跳出外层的循环,请使用标签:
Loop:
for n := 0; n < len(src); n += size {
switch {
case src[n] < sizeOne:
break // 仅跳出 switch
case src[n] < sizeTwo:
if n+1 >= len(src) {
break Loop // 跳出 for 循环
}
}
}
类型 switch 使用 .(type) 来发现接口值的动态类型:
switch v := value.(type) {
case nil:
fmt.Println("value is nil")
case int:
fmt.Printf("integer: %d\n", v) // v 是 int 类型
case string:
fmt.Printf("string: %q\n", v) // v 是 string 类型
case bool:
fmt.Printf("boolean: %t\n", v) // v 是 bool 类型
default:
fmt.Printf("unexpected type %T\n", v)
}
重用变量名(v := value.(type))是地道的写法,因为该变量在每个 case 子句中具有不同的类型。
当一个 case 列出多个类型时(case int, int64:),变量具有接口类型。
空白标识符 _ 用于丢弃值。就像写入 /dev/null。
从多值表达式中丢弃不需要的值:
// 只需要错误
if _, err := os.Stat(path); os.IsNotExist(err) {
fmt.Printf("%s does not exist\n", path)
}
// 只需要值(丢弃 ok)
value := cache[key] // 更简单:直接使用单值形式
_, present := cache[key] // 当你只需要检查存在性时
切勿随意丢弃错误:
// 不良实践:忽略错误,如果路径不存在程序会崩溃
fi, _ := os.Stat(path)
if fi.IsDir() { // 如果路径不存在,解引用 nil 指针
// ...
}
在活跃开发期间临时消除编译器错误:
import (
"fmt"
"io"
)
var _ = fmt.Printf // 消除未使用导入的警告(提交前删除)
var _ io.Reader
func main() {
fd, _ := os.Open("test.go")
_ = fd // 消除未使用变量的警告
}
仅为了包的 init() 副作用而导入:
import _ "net/http/pprof" // 注册 HTTP 处理器
import _ "image/png" // 注册 PNG 解码器
这清楚地表明该包仅因副作用而被导入——在此文件中没有可用的名称。
在编译时验证类型是否实现了某个接口:
// 验证 *MyType 实现了 io.Writer
var _ io.Writer = (*MyType)(nil)
// 验证 MyHandler 实现了 http.Handler
var _ http.Handler = MyHandler{}
如果类型没有实现该接口,这会在编译时失败,从而及早捕获错误。
| 模式 | Go 惯用法 |
|---|---|
| If 初始化 | if err := f(); err != nil { } |
| 提前返回 | 当 if 体返回时省略 else |
| 重新声明 | 如果在同一作用域且有新变量,:= 会重新赋值 |
| C 风格 for 循环 | for i := 0; i < n; i++ { } |
| While 风格 | for condition { } |
| 无限循环 | for { } |
| 带键和值的 Range | for k, v := range m { } |
| 仅值的 Range | for _, v := range slice { } |
| 仅键的 Range | for k := range m { } |
| 并行赋值 | i, j = i+1, j-1 |
| 无表达式的 Switch | switch { case cond: } |
| 逗号分隔的 Case | case 'a', 'b', 'c': |
| 无贯穿 | 默认行为(如果需要,使用显式 fallthrough) |
| 在 Switch 中跳出循环 | break Label |
| 类型 Switch | switch v := x.(type) { } |
| 丢弃值 | _, err := f() |
| 副作用导入 | import _ "pkg" |
| 接口检查 | var _ Interface = (*Type)(nil) |
每周安装量
130
仓库
GitHub 星标数
34
首次出现
2026 年 1 月 27 日
安全审计
安装于
github-copilot123
gemini-cli122
codex122
kimi-cli121
amp121
opencode121
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.
Go's if requires braces and has no parentheses around the condition:
if x > 0 {
return y
}
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
}
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
}
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)
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:
vvThis 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
}
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 { }
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
}
For strings, range iterates over UTF-8 encoded runes (not bytes), handling multi-byte characters automatically.
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.
Go's switch is more flexible than C's:
break in each case)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
}
Multiple cases can be combined with commas (no fall through needed):
func shouldEscape(c byte) bool {
switch c {
case ' ', '?', '&', '=', '#', '+', '%':
return true
}
return false
}
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
}
}
}
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 _ discards values. It's like writing to /dev/null.
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
// ...
}
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 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.
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.
| 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 { } |
Weekly Installs
130
Repository
GitHub Stars
34
First Seen
Jan 27, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
github-copilot123
gemini-cli122
codex122
kimi-cli121
amp121
opencode121
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
106,200 周安装
| 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) |