04、流程控制
1.条件语句
if
if age >= 18 {
fmt.Println("成年")
}
if-else
if age >= 18 {
fmt.Println("成年")
} else {
fmt.Println("未成年")
}
else if多分支
score := 85
if score >= 90 {
fmt.Println("A")
} else if score >= 80 {
fmt.Println("B")
} else if score >= 60 {
fmt.Println("C")
} else {
fmt.Println("D")
}
Go 标准错误处理(高频)
file, err := os.Open("test.txt")
if err != nil { // 常规写法
log.Fatal(err)
}
// 简化:一行声明+判断
if file, err := os.Open("test.txt"); err != nil {
log.Fatal("打开失败:", err)
}
defer file.Close()
特殊限制
不支持数字 / 字符串直接当布尔
// 全部报错!Go 严格要求 bool
if 1 {}
if "abc" {}
if 0 {}
不能省略大括号
// 错误,哪怕一行代码也要 {}
if a > 10
fmt.Println("xxx")
条件不能写赋值 =,只能判等 ==
if a = 10 {} // 编译报错,禁止在if里赋值
实战完整示例
package main
import "fmt"
func main() {
score := 78
// 1. 基础多分支
if score >= 90 {
fmt.Println("优秀")
} else if score >= 80 {
fmt.Println("良好")
} else if score >= 60 {
fmt.Println("及格")
} else {
fmt.Println("不及格")
} // 及格
// 2. 短变量声明if
if num := 15; num%2 == 0 {
fmt.Println(num, "偶数")
} else {
fmt.Println(num, "奇数")
} // 15 奇数
// 3. 逻辑运算
age := 22
vip := true
if age >= 18 && vip {
fmt.Println("VIP成人通道")
} // VIP成人通道
}
2.选择语句
switch
package main
import "fmt"
func main() {
num := 2
switch num {
case 1:
fmt.Println("数字1")
case 2:
fmt.Println("数字2")
default:
fmt.Println("其他数字")
}
}
// 输出:数字2
case 多值
month := 3
switch month {
case 1, 3, 5, 7, 8, 10, 12:
fmt.Println("大月31天")
case 4, 6, 9, 11:
fmt.Println("小月30天")
case 2:
fmt.Println("28/29天")
default:
fmt.Println("非法月份")
}
fallthrough 强制穿透
\\若需要执行完当前 case 后继续执行下一个 case,使用 fallthrough(只能写在 case 末尾)
a := 1
switch a {
case 1:
fmt.Println("匹配1")
fallthrough // 强制进入下一个case,不判断条件
case 2:
fmt.Println("匹配2")
case 3:
fmt.Println("匹配3")
}
// 输出:
// 匹配1
// 匹配2
注意:
- Go 的
switch默认每一个 case 自带 break,不会自动穿透;想要继续执行下一个 case 代码,手动写fallthrough。 fallthrough不会校验下一个 case 条件,直接执行;- 必须放在 case 块末尾,但不能放在最后一个 case、default 后面;
- Go 不支持跨多层穿透,只能穿透紧邻下一条。
例:权限分级(游客 < 用户 < 管理员)
auth := "user"
switch auth {
case "guest":
fmt.Println("基础浏览权限")
fallthrough // 游客也要拥有用户通用权限逻辑
case "user":
fmt.Println("发帖、评论权限")
fallthrough // 用户顺带拥有管理员基础页面
case "admin":
fmt.Println("后台管理、数据修改权限")
}
无表达式 switch
switch 后不写表达式,等价于 switch true,每个 case 写布尔判断,替代替代 if-else if :
score := 85
switch {
case score >= 90:
fmt.Println("优秀")
case score >= 80:
fmt.Println("良好")
case score >= 60:
fmt.Println("及格")
default:
fmt.Println("不及格")
}
// 输出:良好
优势:代码更简洁,多层 if-else 首选。
switch 带初始化语句
switch 开头可定义临时变量,作用域仅 switch 内部:
switch age := 20; {
case age >= 18:
fmt.Println("成年")
default:
fmt.Println("未成年")
}
类型 switch
Go 独有,专门判断 interface{} 变量的底层类型,语法:变量.(type),仅能在 switch 中使用
package main
import "fmt"
func checkType(v interface{}) {
switch val := v.(type) { // val接收对应类型的值
case int:
fmt.Printf("int类型:%d\n", val)
case string:
fmt.Printf("string类型:%s\n", val)
case bool:
fmt.Printf("bool类型:%t\n", val)
default:
fmt.Printf("未知类型:%T\n", val)
}
}
func main() {
checkType(100)
checkType("hello")
checkType(true)
}
select 语句
用于多个channel 的并发读写操作
作用:同时等待多个 channel,哪个 channel 先就绪就执行哪个分支。
基础语法:
select {
case ch1 <- val: // 发送操作:ch1可写入则执行
case res := <-ch2: // 接收操作:ch2有数据可读则执行
case <-ch3: // 只接收、丢弃数据
default: // 无channel就绪时立刻执行,非阻塞
}
规则:
- 所有 case 必须是channel 收发表达式;
- 同一时刻多个 channel 同时就绪:随机选一个执行;
- 没有
default时:select 阻塞,直到任意一个 case channel 就绪; - 有
default:永不阻塞,无就绪 channel 直接走 default。
四大基础场景示例
阻塞等待多个 channel(无 default)
goroutine 发数据,select 等待任意一个通道返回:
package main
import "fmt"
import "time"
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "来自通道1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "来自通道2"
}()
// 阻塞,等最先就绪的通道
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
}
}
// 1秒后输出:来自通道1
非阻塞 select(带 default)
没有通道可读写,立刻执行 default,不会卡住 goroutine:
ch := make(chan int, 1)
select {
case val := <-ch:
fmt.Println("读到数据", val)
default:
fmt.Println("通道无数据,非阻塞退出")
}
select 发送分支 case
监听通道是否可写入(缓冲 chan 满了就阻塞,无缓冲 chan 需对面 goroutine 接收):
ch := make(chan int, 1)
select {
case ch <- 100:
fmt.Println("写入成功")
default:
fmt.Println("通道已满,无法写入")
}
超时控制(最常用业务场景)
搭配 time.After() 实现操作限时,避免永久阻塞:
ch := make(chan string)
select {
case res := <-ch:
fmt.Println("收到:", res)
case <-time.After(3 * time.Second):
fmt.Println("3秒超时,放弃等待")
}
select 关键特性详解
多通道同时就绪:随机选择
若多个 channel 同时有数据,Go 运行时随机公平挑选一个 case,不会从上到下优先匹配:
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
ch1 <- 1
ch2 <- 2
// 每次运行随机输出1或2
select {
case <-ch1:
fmt.Println("ch1")
case <-ch2:
fmt.Println("ch2")
}
nil channel 永久阻塞
nil chan 的收发操作永远阻塞,可用来临时屏蔽某个分支:
var ch chan int // nil
select {
case <-ch: // 永远不会命中
fmt.Println("ok")
case <-time.After(time.Second):
fmt.Println("超时")
}
循环 + select 持续监听(消息循环标准写法)
后台协程持续处理多通道消息,服务、消费者通用模板:
func worker(ch1, ch2 <-chan int, stop <-chan struct{}) {
for {
select {
case v := <-ch1:
fmt.Println("处理ch1数据", v)
case v := <-ch2:
fmt.Println("处理ch2数据", v)
case <-stop:
fmt.Println("收到停止信号,退出循环")
return
}
}
}
关闭 channel 不会阻塞,会立刻返回零值
通道 close 后,<-ch 会立即执行分支,不会阻塞:
ch := make(chan int)
close(ch)
select {
case v := <-ch:
fmt.Println("读到关闭通道零值:", v) // v=0
}
经典业务高频使用场景
接口 / 请求超时
RPC、HTTP 调用、数据库操作,用 time.After 限制最大等待时间,防止 goroutine 泄漏。
多数据源监听
同时监听消息队列、定时任务、停止信号、心跳通道。
优雅退出控制
单独一个 stop chan,收到信号立刻终止循环,安全释放资源。
限流非阻塞收发
带 default 的 select 实现:通道满了不阻塞,直接丢弃 / 返回繁忙。
定时心跳保活
搭配 time.Tick 定时发送心跳包。
select vs switch 核心区别
| 特性 | switch | select |
|---|---|---|
| 分支条件 | 任意表达式、常量、布尔 | 只能是 channel 收发 |
| 匹配规则 | 从上到下顺序匹配 | 就绪通道随机选 |
| 阻塞行为 | 永不阻塞 | 可以阻塞(无 default) |
| 独有能力 | 类型断言、多值匹配 | 多路 IO 监听、超时控制 |
示例:
package main
import (
"fmt"
)
func main(){
a := make(chan int, 1024)
b := make(chan int, 1024)
for i := 0; i < 10; i++ {
fmt.Printf("第%d 次,", i)
a <- 1
b <- 1
select {
case <-a:
fmt.Println("from a")
case <-b:
fmt.Println("from b")
}
}
}
/ *
返回(每次返回结果不同):
第0 次,from a
第1 次,from b
第2 次,from a
第3 次,from a
第4 次,from b
第5 次,from b
第6 次,from b
第7 次,from a
第8 次,from a
第9 次,from a
* /
上述代码的意思是,同时在a 和b 中选择,哪个有内容就从哪个读,由于channel 的读写操作是阻塞操作,使用select 语句可以避免单个channel 的阻塞。此外select 同样可以使用default 代码块,用于避免所有channel 同时阻塞。具体内容可见并发编程。
3.循环语句
for
格式:
for 初始化; 条件; 自增 {
循环体
}
示例
package main
import "fmt"
func main() {
// i:=0 初始化; i<10 条件; i++ 自增
for i := 0; i < 10; i++ {
fmt.Println(i)
}
}
组件省略规则
三个部分均可省略分号,但分号不能乱删:
1.省略初始化:i 提前定义
i := 0
for ; i < 5; i++ {
fmt.Println(i)
}
2.省略自增:循环体内手动更新
for i := 0; i < 5; {
fmt.Println(i)
i++
}
3.只保留条件(等价 while)
i := 0
for i < 5 {
fmt.Println(i)
i++
}
三部分全部省略,等价 while(true),一般搭配 break 跳出:
i := 0
for {
if i >= 3 {
break // 跳出当前for循环
}
fmt.Println(i)
i++
}
for range
专门遍历:字符串、数组、切片、map、通道
语法:
for 索引/键, 值 := range 容器 {
}
1.遍历切片 / 数组
s := []int{10,20,30}
for idx, val := range s {
fmt.Printf("下标:%d, 值:%d\n", idx, val)
}
2.遍历字符串
str := "golang语言"
for i, r := range str {
fmt.Printf("索引%d, 字符%c\n", i, r)
}
3.遍历 map
m := map[string]int{"a":1, "b":2}
for k, v := range m {
fmt.Println(k, v)
}
4.遍历通道 channel
通道关闭前持续读取,关闭自动退出循环
ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch)
for v := range ch {
fmt.Println(v)
}
continue
终止本次循环,直接进入下一轮条件判断
// 只打印偶数
for i := 0; i < 10; i++ {
if i%2 != 0 {
continue
}
fmt.Println(i) // 0 2 4 6 8
}
break
直接终止当前一层for 循环
for i := 0; i < 10; i++ {
if i == 5 {
break
}
fmt.Println(i) //0 1 2 3 4
}
Label
多层循环精准跳出,break/continue 默认只作用内层,用标签指定外层循环。
语法
标签名:
for {
for {
break 标签名 // 直接跳出外层循环
}
}
示例:双层循环一键跳出外层
outer: // 定义外层标签
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
break outer // 直接终止外层循环
}
fmt.Printf("i=%d,j=%d\n",i,j)
}
}
示例:continue + 标签:跳到外层下一轮
outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
continue outer
}
fmt.Println(i,j)
}
}
goto
日常业务 99% 场景不需要 goto
//循环重试
RETRY:
err := connectDB()
if err != nil {
time.Sleep(time.Second)
goto RETRY
}
循环常见实操案例:
1.倒序循环
for i := 9; i >= 0; i-- {
fmt.Println(i)
}
2.求和 1~100
sum := 0
for i := 1; i <= 100; i++ {
sum += i
}
fmt.Println(sum)
3.过滤 range(忽略索引)
arr := []string{"A","B","C"}
for _, v := range arr {
fmt.Println(v)
}
4、延迟语句
defer
func main() {
fmt.Println("1")
defer fmt.Println("defer执行")
fmt.Println("2")
}
// 输出:
// 1
// 2
// defer执行
规则 1:defer 注册顺序「后进先出」栈结构
func main() {
defer fmt.Println("A")
defer fmt.Println("B")
defer fmt.Println("C")
}
// 输出:C B A
规则 2:defer 的参数立即求值,函数体延迟执行
func main() {
x := 10
defer fmt.Println(x) // x此时=10,值已复制
x = 20
}
// 输出:10,不是20
想要延迟取变量最新值两种方案
传指针
x := 10
defer func(v *int) { fmt.Println(*v) }(&x)
x = 20
// 输出20
闭包无参(内部直接捕获变量)
x := 10
defer func() { fmt.Println(x) }()
x = 20
// 输出20
三种常见使用场景
1.资源释放(最常用):文件、网络连接
打开资源后立刻 defer 关闭,避免遗忘释放
func readFile() {
f, err := os.Open("test.txt")
if err != nil {
return
}
defer f.Close() // 函数退出自动关文件
// 读写文件逻辑
}
2.锁释放
var mu sync.Mutex
func do() {
mu.Lock()
defer mu.Unlock() // 无论中间是否return/panic,都会解锁
// 临界区操作
}
3.捕获 panic、异常恢复 recover
recover() 只能在 defer 内部生效,捕获 panic 避免程序崩溃
func test() {
defer func() {
if err := recover(); err != nil {
fmt.Println("捕获panic:", err)
}
}()
panic("程序崩溃了")
}
/*
panic()主动触发程序异常,终止当前正常流程,逐层向上执行defer,若无 recover 捕获,程序直接退出打印堆栈。
panic() 主动抛出崩溃,触发所有 defer 执行;
recover() 仅在 defer 闭包内生效,捕获当前 goroutine 异常;
*/
注意:
循环内慎用 defer,尽量内层包装函数,每次循环立刻执行释放
defer 仅作用于当前函数,跨函数无效
开销:少量 defer 无影响,极高并发循环不建议滥用