Go语言控制语句惯用法以及注意事项

93 阅读3分钟

Go语言控制语句惯用法以及注意事项

快乐路径

func doSomething() error {
    if errorCondition1 {
        // 错误逻辑
        ...
        return err1
    }

    // 成功逻辑
    ...

    if errorCondition2 {
        // 错误逻辑
        ...
        return err2
    }

    // 成功逻辑
    ...
    return nil
}
// 伪代码段2
func doSomething() error {
    if successCondition1 {
        // 成功逻辑
        ...

        if successCondition2 {
            // 成功逻辑
            ...

            return nil
        } else {
            // 错误逻辑
            ...
            return err2
        }
    } else {
        // 错误逻辑
        ...
        return err1
    }
}
  • 没有使用else,遇到错误立即返回
  • 成功逻辑始终居左并延续到函数结尾,没有嵌入if语句中
  • 整个代码段布局扁平,没有深度缩进
  • 逻辑一目了然,可读性好

for range的避坑指南

注意参与迭代的是range表达式的副本

参与循环的是range表达式的副本。

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

Go中的数组在内部表示为连续的字节序列,长度是编译器在编译期计算出来的,对range表达式的复制即对一个数组的复制,使用数组指针可以避免这个坑,在Go的1.23版本中for range已经避免了这个问题,自动变为指针。

在Go中,大多数应用数组的场景都可以用切片替代。 切片在Go内部表示为一个结构体,由(* T, len, cap)三元组组成。其中T指向切片对应的底层数组的指针,len是切片当前长度,cap为切片的容量。在进行range表达式复制时,它实际上复制的是一个切片,也就是表示切片的那个结构体。表示切片副本的结构体中的T依旧指向原切片对应的底层数组,因此对切片副本的修改也都会反映到底层数组a上。而v从切片副本结构体中*T指向的底层数组中获取数组元素,也就得到了被修改后的元素值。

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

break跳到哪里去了

func main() {
    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)
}

子协程在收到channel信号后执行的break并未退出外层的for循环(没有输出exit),而是再次进入循环打印“tick”

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

修正后代码

func main() {
    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{}{}

    // 等待子goroutine退出
    time.Sleep(3 * time.Second)
}

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

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

小结:

  • 使用if语句时遵循“快乐路径”原则
  • 小心 for range的循环变量重用,明确真实参与循环的是range表达式的副本
  • 明确break和continue执行后的真实目的地
  • 使用fallthrough关键字前,考虑能否用更简洁、清晰的case表达式列表替代