代码地址:go控制流
Go 控制流
这一章要记住什么
这一章主要讲四个点:
- Go 的循环只有
for,没有while。 if可以带初始化语句,变量作用域只在if内。switch默认每个case自动结束,不需要手写break。for range很方便,但要注意循环变量、切片长度、map 顺序这些细节。
1. if 和 if 初始化语句
代码里:
if localStr == "case3" {
fmt.Println("into true logic")
} else {
fmt.Println("into false logic")
}
这是最普通的条件判断。
更常见的是带初始化语句:
if num, ok := dic["apple"]; ok {
fmt.Printf("apple num %d\n", num)
}
num, ok := dic["apple"] 先执行。
ok 表示这个 key 是否存在。
dic["apple"]
|
v
返回两个值:num, ok
|
v
ok == true -> key 存在
ok == false -> key 不存在
总结一下
if 初始化; 条件 {} 很适合处理 map 查询、函数返回值检查这类场景。
初始化出来的变量只在这个 if 里有效。
2. switch
代码里:
switch localStr {
case "case1":
fmt.Println("case1")
case "case2":
fmt.Println("case2")
case "case3":
fmt.Println("case3")
default:
fmt.Println("default")
}
Go 的 switch 匹配到一个 case 后,默认不会继续往下执行。
localStr = "case3"
|
v
case1? no
case2? no
case3? yes
|
v
执行 case3,然后结束 switch
总结一下
Go 的 switch 默认自带 break 效果,不需要每个分支都写 break。
3. for 的几种写法
代码注释里总结了三种:
for i := 0; i < 5; i++ {}
for condition {}
for {}
可以对应理解成:
传统 for:
初始化 -> 条件判断 -> 循环体 -> 后置语句
类似 while:
条件判断 -> 循环体
死循环:
一直执行,通常配合 break
总结一下
Go 只有 for 一个循环关键字,但它可以覆盖 C/C++ 里的 for、while 和死循环。
4. for range 的循环变量
当前代码里重点放在闭包捕获循环变量:
var funcs []func()
for i := 0; i < 3; i++ {
funcs = append(funcs, func() {
fmt.Println(i)
})
}
匿名函数捕获了外层的 i。
在老版本 Go 中,循环变量可能复用同一块内存,所以最后执行这些函数时,容易拿到同一个最终值。
循环阶段:
i = 0 -> 保存函数,函数记住 i
i = 1 -> 保存函数,函数还是记住同一个 i
i = 2 -> 保存函数,函数还是记住同一个 i
执行阶段:
循环结束后 i 已经变了
多个函数读到同一个 i
常见修复方式:
for i := 0; i < 3; i++ {
i := i
funcs = append(funcs, func() {
fmt.Println(i)
})
}
每轮循环:
外层 i -> 拷贝出一个新的内层 i
匿名函数捕获的是这一轮自己的 i
总结一下
循环里创建闭包时,要确认闭包捕获的是不是每轮独立的变量。
写 i := i 或者通过函数参数传进去,是很常见的保护写法。
5. for range 的几个细节
代码注释里还提到了几个常见点:
- range 遍历切片时,循环开始前会先确定长度。
- range 得到的值变量是元素副本,改它不会改原切片。
- range 遍历 map 的顺序是随机的。
- range 遍历字符串时,拿到的是 rune,不是单纯的字节。
range slice:
每轮拿到 index 和 value
value 是元素副本
如果要改原切片:
用 slice[index] 修改
总结一下
range 很好用,但不要误以为它拿到的一定是原始元素本身。
需要修改原数据时,优先用索引。
易错点
if num, ok := dic[key]; ok {}里的num和ok只在 if 内有效。- Go 的
switch默认不会贯穿到下一个 case。 for range的 value 通常是副本。- map 遍历顺序不固定。
- 循环里创建闭包时,要注意捕获变量的问题。
快问快答
Q1:Go 有 while 吗?
答:
没有。Go 只有 for,可以用 for condition {} 表达 while 的效果。
Q2:map 查询为什么常写 num, ok := dic[key]?
答:
因为 value 的零值可能也是合法值,必须用 ok 判断 key 是否真的存在。
Q3:range 遍历切片时,修改 value 会影响原切片吗?
答:
不会。value 通常是元素副本。要改原切片,应该用索引修改 slice[i]。
Q4:map 遍历顺序稳定吗?
答:
不稳定。不能依赖 map 的遍历顺序,需要稳定顺序时,先取 key 排序再遍历。
一句话总结
Go 控制流语法不多,但 if 初始化、switch 默认 break、for range 副本和闭包 这些细节很容易考。
for range 有什么注意的坑点?怎么解决?
1. 变量作用域的版本差异
arr := [2]int{1, 2}
res := []*int{}
for _, v := range arr {
res = append(res, &v)
}
1.22 版本之前,整个循环中的 v 的地址不变->相当于一个公用垃圾桶,res中添加的都是同一个地址 &v,解引用打印出都是一样的 2
1.22 版本之后,每轮循环都会创建一个新的变量 v,->相当于一个新的一次性纸杯,这下打印的就是 1 2
为了兼容老版本:
-
引入同名或者异名的局部变量强制拷贝
res2 := []*int{} for _, v := range arr { r := v // 或者写成显式的作用域遮蔽:v := v res2 = append(res2, &r) } -
直接用索引访问原始内存地址
总结:
- 老版本:整个循环从头到尾就一个变量,一个内存地址,每轮循环都往这个内存中塞入新值,新值覆盖旧值
- 新版本:把变量声明放到循环体内部,每轮循环给一个全新的、独立的变量和内存地址,不会存在新值覆盖旧值的现象
2.循环动态切片追加的死循判定
v := []int{1, 2, 3}
// 在循环体内不断往切片里 append 元素,这个循环会演变成死循环吗?
for i := range v {
v = append(v, i)
}
// 不会,在循环开始之前,记录当前切片的 cap,此时循环次数就是当前切片的 cap,次数锁死了
// 编译期展开的底层伪代码:
rangeSlice := v
length := len(rangeSlice) // 💡 重点:在循环开始前,长度就已经被锁死在临时的寄存器/栈变量中了!
for i := 0; i < length; i++ {
v = append(v, i) // 后面修改的只是 v 的 len 属性,但控制循环次数的是固定好的 length!
}
3. 闭包捕获局部变量的逃逸分析
var funcs []func()
for i := 0; i < 3; i++ {
funcs = append(funcs, func() {
fmt.Println(i) // 匿名函数捕获了外部的 i
})
}
for _, f := range funcs { f() }
1.22 版本之前,i 只有一块内存,闭包捕获的是同一个 i 的地址,最后打印三个 3 (第四轮,i加到3,循环不成立,退出)
1.22 版本之后,每轮 i 的地址相互隔离, 打印出 0 1 2
兼容老版本解法:
-
利用同名变量进行作用域遮蔽
var funcs2 []func() for i := 0; i < 3; i++ { i := i // 💡 重点:在当前循环代码块的局部栈帧内,强行分配一个独立的同名变量 funcs2 = append(funcs2, func() { fmt.Println(i) // 此时闭包捕获的是全新独立的、长在不同内存地址上的局部 i }) } -
使用函数的参数强制拷贝
var funcs3 []func() for i := 0; i < 3; i++ { funcs3 = append(funcs3, func(val int) { fmt.Println(val) }(i)) // 立即执行func(val) 匿名函数,发生拷贝 }
总结:
- 闭包底层是一个包含函数指针+捕获变量引用的结构体
- 1.22 版本前,所有结构体都抓住同一个
i的地址,循环结束,i存的是最新值,遍历执行匿名函数,都去了同一个堆内存上找值- 1.22 版本后,
i变量地址隔离,每个闭包抓到的地址是不同的
4. 迭代遍历的值拷贝和副本隔离机制
slice := []int{1, 2, 3}
for _, v := range slice {
v *= 10 // ❌ 徒劳无功!它仅仅把那个叫 v 的临时副本乘以了 10
}
在for _, v := range slice执行时,底层执行了一次值拷贝 v = slice[index],所以修改的是这个拷贝的副本,而不是slice本身
总结:在 for range 里直接修改迭代变量的值,原切片会变吗?为什么?
for range展开后,遍历获取元素的方法是先通过索引拿到值,再通过内存复制把数据拷贝给v,v拥有全新的、独立的栈内存,改动v影响不了原切片的数据
5. map的随机遍历和排序输出
dic := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
for k, v := range dic { fmt.Printf("%s:%d ", k, v) }
每次遍历出来的顺序可能不一样
why?
for range map底层会转成调用mapiterinit函数初始化迭代器,使用了一个随机数种子来及挑选遍历的起始桶和桶内偏移量,是防止开发者依赖特定顺序导致代码在底层哈希扩容时发生错乱,所以不是物理随机,底层的数据存放是有序的,只是在代码层重新洗了一次牌
6. 字符串按照字节遍历和按照 Rune 遍历
str := "hello 世界"
// 🎯 核心特性一:for range 遍历字符串 (按 UTF-8 字符/rune 代码点解码)
// 如果遇到多字节的中文字符,它会自动向后嗅探,合并计算,绝不乱码。
fmt.Println("🔍 尝试 for range 遍历 (Rune 视角):")
for i, r := range str {
// 注意看打印出的 index 下标:0,1,2,3,4,5(空),6('世'),9('界')
// 下标直接从 6 跳到了 9!因为一个 UTF-8 的“世”字在内存里足足占了 3 个字节!
fmt.Printf(" index: %d, rune字符: %c\n", i, r)
}
// 🎯 核心特性二:常规经典 for 循环遍历 (纯粹的底层 Byte 物理裸内存字节流视角)
fmt.Println("\n🔍 尝试常规经典 for 循环遍历 (Byte 字节流视角):")
// len(str)拿到的是字符串在内存中的总字节数!
for i := 0; i < len(str); i++ {
// 乱码横飞现场:中文的 3 个字节被拆开单独打印成了十六进制。根本拼不出完整的“世界”!
fmt.Printf(" index: %d, byte裸字节(十六进制): %x\n", i, str[i])
}
为什么用 for range 遍历包含中文的字符串,它的 index 会不连续?
Go 默认是 UTF-8编码,英文占1字节,汉字占3字节,使用
for range遍历,底层会调用decoderune函数,遇到中文编码时,会自动向后强行找3个字节,同时合并并解码成一个单独的rune代码点,下一次迭代的起始索引就跨越3个字节
总结
1. 变量作用域的版本差异
1.22 之前是一块公用内存轮番卸货,新值洗掉旧值;1.22 之后是每轮流水线各自独立打包,变量生而隔离。
2. 循环动态切片追加的死循环判定
控制循环次数的是启动那一刻被编译器‘拍照锁死’的临时长度,后面原切片再怎么追加扩容,都惊动不了早已经注定好的退场时刻。
3. 闭包捕获局部变量的逃逸分析
闭包在 1.22 之前抓到的是同一个‘变量的地址钱包’,循环结束后钱包里只剩最后的尾款;而 1.22 之后抓到的是每一轮‘当场结算的现金分身’。
4. 迭代遍历的值拷贝和副本隔离机制
for range的v只是舞台上的一个临时‘替身’,通过内存复制接纳了原数据的投影,你对替身动刀,根本伤不到原切片的真身
5. map 的随机遍历和排序输出
底层数据在哈希桶里其实死死固定,但 Go 官方为了打消开发者对特定顺序的依赖幻想,故意在每次初始化迭代器时,都用一枚随机数种子重新洗了一次牌
6. 字符串按照字节遍历和按照 Rune 遍历
传统
for循环是盲人摸象的‘单字节裸内存’视角,而for range则是内置了 UTF-8 解码器的‘多字节智能嗅探’字符视角
Go 的
for range本质上是一个由编译器在前端自动展开的‘数据快照语法糖’。它在循环开始的刹那间,就已经通过‘浅拷贝’锁死了遍历目标的生命周期与边界;我们应对所有坑点的核心法门,就是始终保持清醒:你当前正在操作的,究竟是那张被编译器固化下来的‘历史快照’,还是时刻动态变化着的‘原始内存’

483

被折叠的 条评论
为什么被折叠?



