3、Go流程控制:if-else条件判断、for循环、switch语句、defer延迟调用
流程控制,说白了就是让代码按你的想法走。Go语言在这块做得非常简洁,没有while,没有do-while,也没有三元表达式。刚开始从其他语言转过来时,我确实有点不习惯。但用久了你会发现,这种「少即是多」的设计,反而让代码更清晰。
3.1 if-else条件判断
Go的if语句有个很特别的地方——条件表达式不需要括号。我个人习惯直接写,不加括号,这样代码看着更清爽。
if score >= 90 {
fmt.Println("优秀")
} else if score >= 60 {
fmt.Println("及格")
} else {
fmt.Println("不及格")
}
嗯,这里要注意:else if 和 else 必须跟前面的右花括号在同一行。Go强制要求花括号的格式,不能换行。我第一次写Go时就被这个坑过,编译报错还找了半天原因。
if 的特殊用法:带初始化语句
Go的if可以像for一样,在条件前加一个初始化语句。这个特性我在项目中经常用,尤其是处理函数返回值时。
if err := doSomething(); err != nil {
fmt.Println("出错了:", err)
return
}
// 注意:err 的作用域只在 if 块内
我曾经在代码审查时看到有人把err定义在if外面,然后在if里面又重复判断。其实完全没必要,用这种写法既简洁又安全。
3.2 for循环
Go只有for这一种循环关键字。但它有三种形态,覆盖了所有场景。
3.2.1 经典for循环
跟C语言一样,三个部分:初始化、条件、后置。不过没有括号。
for i := 0; i < 10; i++ {
fmt.Println(i)
}
你想想看,为什么Go不提供while?其实for去掉初始化和后置,就是while了。
// 相当于 while
for sum < 1000 {
sum += sum
}
3.2.2 range循环
range是Go的杀手锏之一。遍历数组、切片、map、字符串、通道,都用它。
// 遍历切片
names := []string{"张三", "李四", "王五"}
for index, name := range names {
fmt.Printf("第%d个是%s\n", index+1, name)
}
// 遍历map
scores := map[string]int{"语文": 90, "数学": 95}
for subject, score := range scores {
fmt.Printf("%s: %d分\n", subject, score)
}
// 遍历字符串(按rune)
for i, ch := range "你好Go" {
fmt.Printf("位置%d: %c\n", i, ch)
}
避坑指南:range遍历map时,顺序是不确定的。我曾经在项目中写了一个依赖map顺序的逻辑,结果测试时偶尔通过偶尔失败。后来查了半天才发现是map遍历顺序的问题。如果你需要固定顺序,请先对key排序。
3.2.3 无限循环
有时候我们需要一直跑,直到某个条件触发。Go的写法很直接:
for {
// 一直执行
if shouldStop() {
break
}
}
我个人习惯在无限循环里加个sleep,防止CPU跑满。当然,如果是等待通道消息,那就不需要了。
3.3 switch语句
Go的switch比很多语言都灵活。它有两种形式:表达式switch和类型switch。
3.3.1 表达式switch
最常用的形式。注意:Go的switch默认每个case后面自动break,不需要手动写。如果你想要穿透,得用fallthrough关键字。
day := 3
switch day {
case 1:
fmt.Println("周一")
case 2:
fmt.Println("周二")
case 3:
fmt.Println("周三")
// 这里不会自动进入下一个case
default:
fmt.Println("其他")
}
为什么会这样设计?因为Go团队发现,大部分情况下break才是我们想要的。少数的穿透场景用fallthrough显式声明,反而更清晰。
switch还可以不带表达式,直接当if-else链用:
score := 85
switch {
case score >= 90:
fmt.Println("优秀")
case score >= 60:
fmt.Println("及格")
default:
fmt.Println("不及格")
}
3.3.2 类型switch
这个特性我在处理接口类型时经常用。它可以根据接口变量的实际类型来执行不同分支。
var x interface{} = 42
switch v := x.(type) {
case int:
fmt.Printf("整数: %d\n", v)
case string:
fmt.Printf("字符串: %s\n", v)
case bool:
fmt.Printf("布尔: %v\n", v)
default:
fmt.Printf("未知类型: %T\n", v)
}
注意:类型switch只能在接口类型上使用。如果你对一个具体类型做类型断言,编译会报错。我曾经看到有人对string类型做类型switch,结果编译不过,一脸懵。
3.4 defer延迟调用
defer是Go最优雅的特性之一。它保证函数返回前一定会执行。最常见的用途就是释放资源。
func readFile(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close() // 函数返回前自动关闭文件
// 处理文件...
return nil
}
我个人习惯把defer紧跟在资源获取之后。这样一眼就能看出资源什么时候释放,代码的可维护性大大提高。
3.4.1 defer的执行顺序
多个defer会按后进先出的顺序执行。说白了就是最后一个defer最先执行。
func main() {
defer fmt.Println("第一个defer")
defer fmt.Println("第二个defer")
defer fmt.Println("第三个defer")
}
// 输出:
// 第三个defer
// 第二个defer
// 第一个defer
为什么会这样?因为defer本质上是把函数调用压入栈中,函数返回时再从栈顶弹出执行。
3.4.2 defer的坑
defer有个经典陷阱:defer的参数在声明时求值。
func main() {
x := 10
defer fmt.Println(x) // 输出10,不是20
x = 20
}
我曾经在项目中写了一个defer来记录函数执行时间,结果发现时间总是0。后来才意识到,time.Now()在defer声明时就已经求值了。正确的做法是用闭包:
func main() {
start := time.Now()
defer func() {
fmt.Println("耗时:", time.Since(start))
}()
// 执行一些操作...
}
我的建议:defer虽然好用,但别滥用。在循环里用defer要特别小心,因为defer直到函数返回才执行。如果循环次数很多,defer会堆积大量未执行的函数,可能导致内存问题。这种情况下,我一般直接调用函数,不用defer。
3.5 小结
Go的流程控制就这些了。没有while,没有do-while,没有三元表达式。但就是这几个关键字,配合defer的优雅设计,已经能写出非常清晰的代码。你想想看,是不是越简单的设计,反而越不容易出错?
下一章我们会聊函数,包括多返回值、可变参数、闭包这些Go的特色。到时候见。