Go语言精进之路读书笔记第19条——理解Go语言表达式的求值顺序
第19条 了解Go语言控制语句惯用法及使用注意事项
19.1 使用if控制语句时应遵循"快乐路径"原则
- 当出现错误时,快速返回;
- 成功逻辑不要嵌入if-else语句中;
- "快乐路径"当执行逻辑中代码布局上始终靠左,这样读者可以一眼看到该函数当正常逻辑流程;
- "快乐路径"的返回值一般在函数最后一行。
19.2 for range的避"坑"指南
1. 迭代变量的重用
for range的惯用法是使用短变量声明方式(:=)在for的初始化语句中声明迭代变量(iteration variable)。但需要注意的是,这些迭代变量在for range的每次循环中都会被重用,而不是重新声明,可以将for range进行等价转换:
//使用了数组长度的省略语法(...),表示编译器自动计算数组的长度,因为数组的元素个数已经明确指定为5个,所以[...]的省略语法会被自动解释为[5]
var m = [...]int{1, 2, 3, 4, 5}
for i, v := range m {
...
}
//上述代码可等价转换为:
var m = [...]int{1, 2, 3, 4, 5}
{
i, v := 0
for i, v = range m {
...
}
}
//我们看到,goroutine中输出的i、v值都是for range循环结束后的i、v最终值,而不是各个goroutine启动时的i、v值。
//这是因为goroutine执行的闭包函数引用了它的外层包裹函数中的变量i、v,这样变量i、v在主goroutine和新启动的goroutine之间实现了共享。
//而i、v值在整个循环过程中是重用的,仅有一份。
//在for range循环结束后,i=4,v=5,因此各个goroutine在等待3秒后进行输出的时候,输出的是i、v的最终值。
func demo1() {
var m = [...]int{1, 2, 3, 4, 5}
for i, v := range m {
go func() {
time.Sleep(time.Second * 3)
fmt.Println(i, v)
}()
}
time.Sleep(time.Second * 2)
}
//如果要修正Demo1中的问题,可以为闭包函数增加参数并在创建goroutine时将参数与i、v的当时值进行绑定。
//注意每次输出结果的行序可能不同,这是由于goroutine的调度顺序决定的。
func demo2() {
var m = [...]int{1, 2, 3, 4, 5}
for i, v := range m {
go func(i, v int) {
time.Sleep(time.Second * 3)
fmt.Println(i, v)
}(i, v)
}
time.Sleep(time.Second * 2)
}
补充:Go1.22版本将修复for range循环变量的问题
2. 参与迭代的是range表达式的副本
for range语句中,range后面接受的表达式的类型可以是数组、指向数组的指针、切片、字符串、map和channel(至少需要有读权限)。
3. 不同类型range表达式的使用注意事项
对于range后面的其他表达式类型,例如string、map和channel,for range依旧会制作副本。
array & slice
//在arrayRangeExpression中,真正参与循环的是a的副本,而不是真正的a。
//Go中数组在内部表示为连续的字节序列,a的副本是Go临时分配的连续字节序列,与a完全不是一块内存区域。
func arrayRangeExpression() {
var a = [5]int{1, 2, 3, 4, 5}
var r [5]int
fmt.Println("arrayRangeExpression result:")
fmt.Println("a = ", a)
for i, v := range a {
if i == 0 {
a[1] = 12
a[2] = 13
}
r[i] = v
}
fmt.Println("r = ", r)
fmt.Println("a = ", a)
}
//在pointerToArrayRangeExpression中,&a的副本依旧是一个指向原数组a的指针,因此后续所有循环中均是&a指向的原数组在参与。
func pointerToArrayRangeExpression() {
var a = [5]int{1, 2, 3, 4, 5}
var r [5]int
fmt.Println("pointerToArrayRangeExpression result:")
fmt.Println("a = ", a)
for i, v := range &a {
if i == 0 {
a[1] = 12
a[2] = 13
}
r[i] = v
}
fmt.Println("r = ", r)
fmt.Println("a = ", a)
}
//在sliceRangeExpression中,切片在Go内部表示为一个结构体,由(*T, len, cap)三元组组成,
//其中*T指向切片对应的底层数组的指针,len是切片当前长度,cap为切片的容量。
//在进行range表达式复制时,它实际上复制的是一个切片,也就是表示切片的那个特结构体,而结构体中的*T依旧指向原切片对应的底层数组。
func sliceRangeExpression() {
var a = [5]int{1, 2, 3, 4, 5}
var r [5]int
fmt.Println("sliceRangeExpression result:")
fmt.Println("a = ", a)
for i, v := range a[:] {
if i == 0 {
a[1] = 12
a[2] = 13
}
r[i] = v
}
fmt.Println("r = ", r)
fmt.Println("a = ", a)
}
//在sliceLenChangeRangeExpression中,切片副本中的len字段没有改变,依旧是5,因此for range只会循环5次。
func sliceLenChangeRangeExpression() {
var a = []int{1, 2, 3, 4, 5}
var r = make([]int, 0)
fmt.Println("sliceLenChangeRangeExpression result:")
fmt.Println("a = ", a)
for i, v := range a {
if i == 0 {
a = append(a, 6, 7)
}
r = append(r, v)
}
fmt.Println("r = ", r)
fmt.Println("a = ", a)
}
string
//string在Go运行时内部表示为struct {*byte, len},并且string本身是不可改变的。
//string每次循环的单位是一个rune,返回的第一个值为迭代字符码点的第一字节的位置。
func stringDemo1() {
var s = "中国人"
for i, v := range s {
fmt.Printf("%d %s %s 0x%x\n", i, string(v), reflect.TypeOf(v), v)
}
fmt.Println("")
// 0 中 int32 0x4e2d
// 3 国 int32 0x56fd
// 6 人 int32 0x4eba
}
//如果作为range表达式的字符串s中存在非法UTF8字节序列,那么v将返回0xfffd这个特殊值,并且值下一轮循环中,v将仅前进一字节。
func stringDemo2() {
//byte sequence of s: 0xe4 0xb8 0xad 0xe5 0x9b 0xbd 0xe4 0xba 0xba
var sl = []byte{0xe4, 0xb8, 0xad, 0xe5, 0x9b, 0xbd, 0xe4, 0xba, 0xba}
for _, v := range sl {
fmt.Printf("0x%x ", v)
}
fmt.Println()
sl[3] = 0xd0
sl[4] = 0xd6
sl[5] = 0xb9
for i, v := range string(sl) {
fmt.Printf("%d %x\n", i, v)
}
}
map
//map在Go运行时内部表示为一个hmap的描述符结构指针,因此该指针的副本也指向同一个hmap描述符,
//这样for range对map副本的操作即对源map的操作。
//for range无法保证每次迭代的元素次序是一致的。
//同时,如果在循环的过程中对map进行修改,那么这样修改的结果是否会影响后续迭代过程也是不确认的。
//第二个循环中输出的counter可能为2,也可能为3
//第三个循环中输出的counter可能为4,也可能为3
func mapDemo() {
var m = map[string]int{
"tony": 21,
"tom": 22,
"jim": 23,
}
for k, v := range m {
fmt.Println(k, v)
}
fmt.Println()
counter := 0
for k, v := range m {
if counter == 0 {
delete(m, "tony")
}
counter++
fmt.Println(k, v)
}
fmt.Println("counter is ", counter)
fmt.Println()
m["tony"] = 21
counter = 0
for k, v := range m {
if counter == 0 {
m["lucy"] = 24
}
counter++
fmt.Println(k, v)
}
fmt.Println("counter is ", counter)
}
channel
func recvFromUnbufferedChannel() {
var c = make(chan int)
go func() {
time.Sleep(time.Second * 3)
c <- 1
c <- 2
c <- 3
close(c)
}()
for v := range c {
fmt.Println(v)
}
}
func recvFromNilChannel() {
var c chan int
// 程序将一直阻塞在这里
for v := range c {
fmt.Println(v)
}
}
19.3 break跳到哪里去了
Go语言规范中明确规定break语句(不接label的情况下)结束执行并跳出的是同一函数内break语句所在的最内层的for、switch或select的执行。
//break实际上跳出了select语句,但并没有跳出外层但for循环
func breakDemo1() {
exit := make(chan interface{})
go func() {
for {
select {
case <-time.After(time.Second):
fmt.Println("tick")
case <-exit:
fmt.Println("exiting...")
break
}
}
fmt.Println("exit!")
}()
time.Sleep(3 * time.Second)
exit <- struct{}{}
// wait child goroutine exit
time.Sleep(3 * time.Second)
}
func breakDemo2() {
exit := make(chan interface{})
go func() {
loop:
for {
select {
case <-time.After(time.Second):
fmt.Println("tick")
case <-exit:
fmt.Println("exiting...")
break loop
}
}
fmt.Println("exit!")
}()
time.Sleep(3 * time.Second)
exit <- struct{}{}
// wait child goroutine exit
time.Sleep(3 * time.Second)
}
19.4 尽量用case表达式列表替代fallthrough
switch n {
case 1: fallthrough
case 3: fallthrough
case 5: fallthrough
case 7:
odd()
case 2: fallthrough
case 4: fallthrough
case 6: fallthrough
case 8:
even()
default:
unknown()
}
// 改为使用case表达式列表
switch n {
case 1, 3, 5, 7:
odd()
case 2, 4, 6, 8:
even()
default:
unknown()
}