Loading

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()
}
posted @ 2024-02-07 17:59  brynchen  阅读(5)  评论(0编辑  收藏  举报