• Go if流程控制与快乐路径原则


    Go if流程控制与快乐路径原则

    一、流程控制基本介绍

    流程控制是每种编程语言控制逻辑走向和执行次序的重要部分,流程控制可以说是一门语言的“经脉”。

    那么 Go 语言对分支与循环两种控制结构的支持是怎么样的呢?针对程序的分支结构,Go 提供了 ifswitch-case 两种语句形式;我们就先从 Go 语言分支结构之一的 if 语句开始讲起。

    二、if 语句

    2.1 if 语句介绍

    if 语句是 Go 语言中提供的一种分支控制结构,它也是 Go 中最常用、最简单的分支控制结构。它会根据布尔表达式的值,在两个分支中选择一个执行。

    2.2 单分支结构的 if 语句形式

    单分支结构的if语句包含一个条件表达式和一个要执行的代码块。如果条件表达式的值为true,则执行代码块。如果条件表达式的值为false,则代码块将被跳过。以下是单分支结构的if语句的一般形式:

    if boolean_expression {
        // 新分支
    }
    
    // 原分支
    

    这个 if 语句中的代码执行流程就等价于下面这幅流程图:

    • boolean_expression是一个布尔表达式,通常返回truefalse
    • 如果boolean_expression的值为true,则执行// 当条件为真时执行的代码部分的代码块。
    • 如果boolean_expression的值为false,则代码块将被跳过,继续执行下一个语句。

    2.3 Go 的 if 语句的特点

    2.3.1 分支代码块左大括号与if同行

    if 语句的分支代码块的左大括号与 if 关键字在同一行上,这是 Go 代码风格的统一要求,gofmt 工具会帮助我们实现这一点;

    2.3.2 条件表达式不需要括号

    if 语句的布尔表达式整体不需要用括号包裹,这使得代码更加简洁。而且,if 关键字后面的条件判断表达式的求值结果必须是布尔类型,即要么是 true,要么是 false

    if runtime.GOOS == "darwin" {
        println("we are on MacOS")
    }
    

    如果判断的条件比较多,我们可以用多个逻辑操作符连接起多个条件判断表达式,比如这段代码就是用了多个逻辑操作符 && 来连接多个布尔表达式:

    	if (runtime.GOOS == "darwin") && (runtime.GOARCH == "amd64") &&
    		(runtime.Compiler != "gccgo") {
    		println("we are using standard go compiler on Mac os for amd64")
    	}
    

    上面示例代码中的每个布尔表达式都被小括号括上了,这是为了降低你在阅读和理解这段代码时,面对操作符优先级的心智负担。

    三、操作符

    3.1 逻辑操作符

    逻辑操作符除了上面的 && 之外,Go 还提供了另外两个逻辑操作符,如下表:

    逻辑操作符 含义 表达式求值举例
    && 逻辑与 a &&b:当ab都为true时,该表达式的求值 结果为true
    ` `
    ` ` 逻辑非

    3.2 操作符的优先级

    一元操作符,比如上面的逻辑非操作符,具有最高优先级,其他操作符的优先级如下:

    优先级(从高到低) 操作符列表
    5 *, /, %, <<, >>, &, &^
    4 +, -
    3 !=, ==, <, <=, >, >=
    2 &&
    1 ||
    • 优先级5的是乘、除、取模和位操作符
    • 优先级4的是加法和减法运算符
    • 优先级3的是关系和相等运算符
    • 优先级2的是逻辑与
    • 优先级最低的是逻辑或

    操作符优先级决定了操作数优先参与哪个操作符的求值运算,我们以下面代码中 if 语句的布尔表达式为例:

    func main() {
        a, b := false,true
        if a && b != true {
            println("(a && b) != true")
            return
        }
        println("a && (b != true) == false")
    }
    

    这段代码会输出得到的是 a && (b != true) == false。这是为什么呢?

    这段代码的关键就在于,if 后面的布尔表达式中的操作数 b 是先参与 && 的求值运算,还是先参与!= 的求值运算。根据前面的操作符优先级表,我们知道,!= 的优先级要高于 &&,因此操作数 b 先参与的是!= 的求值运算,这样 if 后的布尔表达式就等价于 a && (b != true)

    针对以上问题,推荐在 if 布尔表达式中,使用带有小括号的子布尔表达式来清晰地表达判断条件

    这样做不仅可以消除了自己记住操作符优先级的学习负担,当其他人阅读你的代码时,也可以很清晰地看出布尔表达式要表达的逻辑关系,这能让我们代码的可读性更好,更易于理解,不会因记错操作符优先级顺序而产生错误的理解。

    三、if 多(N)分支结构

    3.1 if else(分支结构)

    Go语言中if else(分支结构)条件判断的格式如下:

    if boolean_expression {
      // 分支1
    } else {
      // 分支2
    }
    

    3.2 if(N)分支结构(if ... else if ... else)

    if条件(N)分支结构格式如下:

    if boolean_expression1 {
      // 分支1
    } else if boolean_expression2 {
      // 分支2
    
    ... ...
    
    } else if boolean_expressionN {
      // 分支N
    } else {
      // 分支N+1
    }
    

    以下面是一个四分支的wei伪代码示例:

    if boolean_expression1 {
        // 分支1
    } else if boolean_expression2 {
        // 分支2
    } else if boolean_expression3 {
        // 分支3
    } else {
        // 分支4
    } 
    

    以下是一个示例,演示如何使用if-else结构来判断一个分数的等级:

    package main
    
    import "fmt"
    
    func main() {
        score := 85
    
        if score >= 90 {
            fmt.Println("A")
        } else if score >= 80 {
            fmt.Println("B")
        } else if score >= 70 {
            fmt.Println("C")
        } else {
            fmt.Println("D")
        }
    }
    

    四、if 语句的自用变量

    无论是单分支、二分支还是多分支结构,我们都可以在 if 后的布尔表达式前,进行一些变量的声明,在 if 布尔表达式前声明的变量,叫 if 语句的自用变量。顾名思义,这些变量只可以在 if 语句的代码块范围内使用,比如下面代码中的变量 a、b 和 c:

    func main() {
        if a, c := f(), h(); a > 0 {
            println(a)
        } else if b := f(); b > 0 {
            println(a, b)
        } else {
            println(a, b, c)
        }
    }
    

    我们可以看到自用变量声明的位置是在每个 if 语句的后面,布尔表达式的前面,而且,由于声明本身是一个语句,所以我们需要把它和后面的布尔表达式通过分号分隔开。

    在 if 语句中声明自用变量是 Go 语言的一个惯用法,这种使用方式直观上可以让开发者有一种代码行数减少的感觉,提高可读性。同时,由于这些变量是 if 语句自用变量,它的作用域仅限于 if 语句的各层隐式代码块中,if 语句外部无法访问和更改这些变量,这就让这些变量具有一定隔离性,这样你在阅读和理解 if 语句的代码时也可以更聚焦。

    五、if 语句的“快乐路径”原则

    上面我们已经学了 if 分支控制结构的三种形式了,从可读性上来看,单分支结构要优于二分支结构,二分支结构又优于多分支结构。那么显然,我们在日常编码中要减少多分支结构,甚至是二分支结构的使用,这会有助于我们编写出优雅、简洁、易读易维护且不易错的代码。

    首先,我们来看一段伪代码段1:

    //伪代码段1:
    
    func doSomething() error {
      if errorCondition1 {
        // some error logic
        ... ...
        return err1
      }
      
      // some success logic
      ... ...
    
      if errorCondition2 {
        // some error logic
        ... ...
        return err2
      }
    
      // some success logic
      ... ...
      return nil
    }
    
    

    我们看到单分支控制结构的伪代码段 1 有这几个特点:

    • 没有使用 else 分支,失败就立即返回;
    • “成功”逻辑始终“居左”并延续到函数结尾,没有被嵌入到 if 的布尔表达式为 true 的代码分支中;
    • 整个代码段布局扁平,没有深度的缩进;
    • 代码的可读性很高

    我们来看一段伪代码段2:

    // 伪代码段2:
    
    func doSomething() error {
      if successCondition1 {
        // some success logic
        ... ...
    
        if successCondition2 {
          // some success logic
          ... ...
    
          return nil
        } else {
          // some error logic
          ... ...
          return err2
        }
      } else {
        // some error logic
        ... ...
        return err1
      }
    }
    

    伪代码段 2 实现了同样逻辑码段 1,就使用了带有嵌套的二分支结构,它的特点如下:

    • 整个代码段呈现为“锯齿状”,有深度缩进;
    • “成功”逻辑被嵌入到 if 的布尔表达式为 true 的代码分支中;

    很明显,伪代码段 1 的逻辑更容易理解,也更简洁。Go 社区把这种 if 语句的使用方式称为 if 语句的“快乐路径(Happy Path)”原则,所谓“快乐路径”也就是成功逻辑的代码执行路径,它的特点是这样的:

    • 仅使用单分支控制结构;

    • 当布尔表达式求值为 false 时,也就是出现错误时,在单分支中快速返回;

    • 正常逻辑在代码布局上始终“靠左”,这样读者可以从上到下一眼看到该函数正常逻辑的全貌;

    • 函数执行到最后一行代表一种成功状态。

    Go 社区推荐 Gopher 们在使用 if 语句时尽量符合这些原则,如果你的函数实现代码不符合“快乐路径”原则,你可以按下面步骤进行重构:

    • 尝试将“正常逻辑”提取出来,放到“快乐路径”中;

    • 如果无法做到上一点,很可能是函数内的逻辑过于复杂,可以将深度缩进到 else 分支中的代码析出到一个函数中,再对原函数实施“快乐路径”原则。

  • 相关阅读:
    嵌入式分享合集4
    WPF向Avalonia迁移(二、一些可能使用到的库)
    「uniapp」 H5高德地图定位、周边、路线规划、多点绘制、位置点击监听
    苹果双系统和虚拟机哪个好用?
    通关剑指 Offer——剑指 Offer II 028. 展平多级双向链表
    缓存与数据库双写一致性几种策略分析
    【机器学习】随机森林(Random Forest,简称 RF):预测光伏电站功率 || 缺失数据处理 || 重复行处理...
    【环境栏Composer】Composer常见问题(持续更新)
    Springboot笔记-有header的post请求、get请求
    初识C++|类与对象(上)
  • 原文地址:https://www.cnblogs.com/taoxiaoxin/p/17758434.html