深色模式
Go语言函数与控制流
概述
Go 的控制流看起来不复杂,但风格很鲜明。它刻意收掉了一些语法糖,例如没有 while,switch 默认不贯穿,也不鼓励到处堆三元表达式和异常式跳转。结果就是,代码结构往往更直白,但也要求对函数返回、作用域和控制语句的边界有比较稳定的判断。
这一篇把函数和控制流放在一起讲,不是为了省篇幅,而是因为它们本来就绑得很紧。Go 里很多流程组织问题,最终都会落到这几件事上:函数怎么返回,错误怎么提早结束,defer 什么时候执行,if / for / switch 到底怎么选。
函数是流程组织的第一单位
Go 没有类方法那套入口结构时,函数就更像第一层组织单位。先看最普通的定义:
go
func Add(a, b int) int {
return a + b
}Go 的函数签名很看重清晰性,所以参数和返回值通常都写得比较直白。读函数时,最先看的通常不是函数体,而是签名本身告诉了什么输入输出关系。
多返回值是 Go 的常态
Go 很多 API 天然使用多返回值:
go
func Lookup(key string) (string, bool) {
value, ok := data[key]
return value, ok
}或者更常见的错误处理风格:
go
func ReadConfig(path string) ([]byte, error) {
return os.ReadFile(path)
}这意味着一个很重要的习惯:Go 不是靠异常把失败抛到别处,而是更倾向于在函数边界上把结果和失败状态一起交代清楚。
命名返回值能用,但别滥用
Go 支持命名返回值:
go
func Split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}这不是不能用,而是要看场景。对非常短、语义特别明确的函数,它还算直观;一旦函数稍微复杂一点,裸 return 往往会降低可读性。
defer 管的是“退出前要做什么”
defer 的意义不是“稍后执行”,而是“当前函数返回前执行”。
例如:
go
func ReadFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
return io.ReadAll(f)
}它特别适合放这类收尾动作:
CloseUnlock- 恢复状态
- 打点统计
defer 有两个容易误解的点
参数在 defer 声明时求值
go
x := 1
defer fmt.Println(x)
x = 2最终打印的是 1,不是 2。
执行顺序是后进先出
go
defer fmt.Println("A")
defer fmt.Println("B")
defer fmt.Println("C")输出顺序是 C、B、A。
if 在 Go 里通常负责把分支尽早收口
Go 的 if 没什么花样,但有一个很典型的使用习惯:把错误分支提早处理掉,让主流程保持平直。
go
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}Go 还支持在 if 前写一个短初始化语句:
go
if err := do(); err != nil {
return err
}这种写法的重点不是省行数,而是把变量作用域收在当前判断块里。
for 是 Go 唯一的循环语句
Go 没有单独的 while,循环统一由 for 承担。
最常见的几种写法是:
go
for i := 0; i < 10; i++ {
fmt.Println(i)
}go
for n < 10 {
n++
}go
for {
if done {
break
}
}遍历集合时还会经常看到 range:
go
for i, v := range nums {
fmt.Println(i, v)
}switch 是 Go 里很值得用的分支工具
Go 的 switch 和很多语言相比,有两个最明显的特点:
- 默认不会自动贯穿到下一个
case - 不一定非得带一个待比较的表达式
最普通的写法:
go
switch status {
case 200:
fmt.Println("ok")
case 404:
fmt.Println("not found")
default:
fmt.Println("unknown")
}多个值可以放在同一个 case:
go
switch ext {
case ".jpg", ".png", ".gif":
fmt.Println("image")
}不带表达式的 switch 很适合区间和条件判断
go
switch {
case score >= 90:
fmt.Println("A")
case score >= 80:
fmt.Println("B")
default:
fmt.Println("C")
}这种写法本质上是在按顺序匹配布尔条件。它在处理区间判断、优先级规则时,往往比一串 if else if 更整齐。
fallthrough 能用,但要慎用
Go 的 switch 默认只执行命中的那一支。如果一定要继续落到下一支,可以显式写:
go
switch n {
case 1:
fmt.Println("one")
fallthrough
case 2:
fmt.Println("two")
}但这通常不是主流写法。大多数场景下,显式表达条件比依赖 fallthrough 更清楚。
作用域要和控制块一起看
Go 的很多变量作用域都紧贴代码块本身。最常见的两个场景是:
if err := ...; err != nil {}里的变量只活在这个iffor或switch里的短变量声明,也只在对应块内有效
这使得 Go 代码很适合把临时变量限制在最小可见范围里。变量活得越短,后面排查时就越不容易被上下文污染。
一条够用的流程组织习惯
把 Go 代码写顺,通常靠的是下面这几条:
- 函数签名先把输入输出说清楚
- 错误尽早返回,不把主流程埋进嵌套里
defer负责退出前收尾for统一循环switch用来表达并列分支或条件阶梯
Go 的控制流不追求花样,它追求的是读代码时,视线能顺着往下走,不需要来回跳。
