《Go语言精进之路》读书笔记 | 了解Go语言控制语句惯用法及使用注意事项

Posted COCOgsta

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Go语言精进之路》读书笔记 | 了解Go语言控制语句惯用法及使用注意事项相关的知识,希望对你有一定的参考价值。

书籍来源:《Go语言精进之路:从新手到高手的编程思想、方法和技巧》

一边学习一边整理读书笔记,并与大家分享,侵权即删,谢谢支持!

附上汇总贴:《Go语言精进之路》读书笔记 | 汇总_COCOgsta的博客-CSDN博客


要掌握一门编程语言,不光要掌握其各种语句的基本用法,还要掌握符合其语言思维的惯用法。在这一条中我们就来了解一下Go语言控制语句的惯用法及使用注意事项。

19.1 使用if控制语句时应遵循“快乐路径”原则

所谓“快乐路径”即成功逻辑的代码执行路径,这个原则要求:

当出现错误时,快速返回; 成功逻辑不要嵌入if-else语句中; “快乐路径”的执行逻辑在代码布局上始终靠左,这样读者可以一眼看到该函数的正常逻辑流程; “快乐路径”的返回值一般在函数最后一行。

如果你的函数实现代码不符合“快乐路径”原则,可以按下面的步骤进行重构:

尝试将“正常逻辑”提取出来,放到“快乐路径”中; 如果无法做到上一点,很可能是函数内的逻辑过于复杂,可以将深度缩进到if-else语句中的代码析出到一个函数中,再对原函数实施“快乐路径”原则的重构。

19.2 for range的避“坑”指南

for range的引入提升了Go语言的表达能力,在使用前需要搞清楚使用for range的一些注意事项。

  1. 小心迭代变量的重用

for range的惯用法是使用短变量声明方式(:=)在for的initStmt中声明迭代变量。但需要注意的是,这些迭代变量在for range的每次循环中都会被重用,而不是重新声明。我们可以将for range进行等价转换:

var m = [...]int1, 2, 3, 4, 5
for i, v := range m 
    ...

复制代码

上述代码可等价转换为:

var m = [...]int1, 2, 3, 4, 5

    i, v := 0
    for i, v = range m 
        ...
    

复制代码
  1. 注意参与迭代的是range表达式的副本

for range语句中,range后面接受的表达式的类型可以是数组、指向数组的指针、切片、字符串、map和channel(至少需具有读权限)。

参与循环的是range表达式的副本。也就是说在上面这个例子中,真正参与循环的是a的副本a',而不是真正的a。因此无论a被如何修改,其参与循环的副本a'依旧保持原值,因此从a'中取出的仍旧是a的原值,而非修改后的值。

试着使用数组指针&a,后续所有循环中均是&a指向的原数组亲自参与的,因此从&a指向的原数组中取出a修改后的值。

在Go中,大多数应用数组的场景都可以用切片替代。

在进行range表达式复制时,它实际上复制的是一个切片,也就是表示切片的那个结构体。表示切片副本的结构体中的T依旧指向原切片对应的底层数组,因此对切片副本的修改也都会反映到底层数组上。

切片与数组还有一个不同点,就是其len在运行时可以被改变,而数组的长度可认为是一个常量,不可改变。

range表达式的复制行为还会带来一些性能上的消耗,尤其是当range表达式的类型为数组时,range需要复制整个数组;而当range表达式类型为数组指针或切片时,这个消耗将小得多,因为仅仅需要复制一个指针或一个切片的内部表示(一个结构体)即可。

  1. 其他range表达式类型的使用注意事项

对于range后面的其他表达式类型,比如string、map和channel,for range依旧会制作副本。

(1)string

当string作为range表达式的类型时,由于string在Go运行时内部表示为struct *byte, len,并且string本身是不可改变的(immutable),因此其行为和消耗与切片作为range表达式时类似。不过for range对于string来说,每次循环的单位是一个rune,而不是一个byte。

(2)map

当map类型作为range表达式时,我们会得到一个map的内部表示的副本。map在Go运行时内部表示为一个hmap的描述符结构指针,因此该指针的副本也指向同一个hmap描述符,这样for range对map副本的操作即对源map的操作。

关于map的元素迭代,在前文中也提及过,for range无法保证每次迭代的元素次序是一致的。同时,如果在循环的过程中对map进行修改,那么这样修改的结果是否会影响后续迭代过程也是不确定的。

(3)channel

对于channel来说,channel在Go运行时内部表示为一个channel描述符的指针,因此channel的指针副本也指向原channel。

当channel作为range表达式类型时,for range最终以阻塞读的方式阻塞在channel表达式上,即便是带缓冲的channel亦是如此:当channel中无数据时,for range也会阻塞在channel上,直到channel关闭。

19.3 break跳到哪里去了

和大家习惯的C家族语言中的break不同,Go语言规范中明确规定break语句(不接label的情况下)结束执行并跳出的是同一函数内break语句所在的最内层的for、switch或select的执行。

要修正这一问题,可以利用Go语言为for提供的一项高级能力:break [label]。可以定义一个label——loop,该label附在for循环的外面,指代for循环的执行。代码执行到“break loop”时,程序将停止label loop所指代的for循环的执行。

带label的continue和break提升了Go语言的表达能力,可以让程序轻松拥有从深层循环中终止外层循环或跳转到外层循环继续执行的能力,使得Gopher无须为类似的逻辑设计复杂的程序结构或使用goto语句。

outerLoop:
    for i := 0; i < n; i++ 
        // ...
        for j := 0; j < m; j++ 
            // 当不满足某些条件时,直接终止最外层循环的执行
            break outerLoop

            // 当满足某些条件时,直接跳出内层循环,回到外层循环继续执行
            continue outerLoop
        
    
复制代码

19.4 尽量用case表达式列表替代fallthrough

选择switch-case语句默认是不“fall through”的,需要fall through的时候,可以使用关键字fallthrough显式实现。

不过在实际编码过程中,fallthrough的应用依然不多,而且Go的switch-case语句还提供了case表达式列表来支持多个分支表达式处理逻辑相同的情况:

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()


vs.

switch n 
case 1, 3, 5, 7:
    odd()
case 2, 4, 6, 8:
    even()
default:
    unknown()

复制代码

我们看到,通过case接表达式列表的方式要比使用fallthrough更加简洁和易读。因此,在程序中使用fallthrough关键字前,先想想能否使用更为简洁、清晰的case表达式列表替代。

以上是关于《Go语言精进之路》读书笔记 | 了解Go语言控制语句惯用法及使用注意事项的主要内容,如果未能解决你的问题,请参考以下文章

《Go语言精进之路》读书笔记 | 了解map实现原理并高效使用

《Go语言精进之路》读书笔记 | 了解string实现原理并高效使用

《Go语言精进之路》读书笔记 | 汇总

《Go语言精进之路》读书笔记 | 了解切片实现原理并高效使用

《Go语言精进之路》读书笔记 | 选择适当的Go语言版本

《Go语言精进之路》读书笔记 | 理解Go语言的设计哲学