聊聊Go语言的控制语句

在高级编程语言中,控制流语句(control-flow statement)是一类用于控制程序执行流程的语句,以下简称为控制语句。它们可以根据条件或循环执行相应的代码块,或者跳转到指定位置执行代码。

常见的控制语句包括:

  • 条件语句:根据条件执行不同的代码块,如if语句、switch语句等。

  • 循环语句:根据条件重复执行相应的代码块,如for语句、while语句等。

  • 跳转语句:跳转到指定位置执行代码,如break、goto语句。

  • 异常处理语句:处理程序运行过程中出现的异常,如try-catch语句、throw语句等。

控制语句是编程语言中实现程序逻辑的重要手段,它们可以帮助程序员实现复杂的算法和逻辑。不同的编程语言支持的控制语句的种类和用法可能会有所不同,但它们的基本作用都是相似的,即控制程序的执行流程。

Go语言中的控制语句语法在主流编程语言中算是极少的了!掐指算来,主要的包括if、for和switch。当然goto、defer、panic/recover语句也应归类于控制语句,并且后面这些控制语句也都是Go语言实现程序逻辑的重要手段。但后面这几个并非本篇讲述的重点,在这篇文章中,我将聚焦于Go的if、for和switch语句

1. if语句

首先我们先来看看if语句。

if语句用于根据一个条件执行相应的代码块,是Go语言中最常用的控制语句。

if语句的基本语法如下:

if condition {
    // code block
} else if condition {
    // code block
} else {
    // code block
}

关于if语句,我主要说下面三点:

1.1 隐式代码块(block)

我们看下面代码:

func bar() {
    if a := 1; false {
    } else if b := 2; false {
    } else if c := 3; false {
    } else {
        println(a, b, c)
    }
}

看完这段代码后,你觉得这段代码可以被正常编译吗?如果可以,那么它会输出什么信息呢?Go编译器告诉我们:上面这段可以正常编译并运行!但很多人会质疑:为何在第一个if语句中声明的变量a、第二个if中的变量b以及第三个if中的变量c,在最后的else语句中都可以有效访问呢

要想解答这个问题,必须要搞清楚if语句的隐式代码块和作用域规则。上述代码等价于下面代码:

func bar() {
    { // 等价于第一个if的隐式代码块
        a := 1 // 变量a作用域始于此
        if false {

        } else {
            { // 等价于第一个else if的隐式代码块
                b := 2 // 变量b的作用域始于此
                if false {

                } else {
                    { // 等价于第二个else if的隐式代码块
                        c := 3 // 变量c作用域始于此
                        if false {

                        } else {
                            println(a, b, c)
                        }
                        // 变量c的作用域终止于此
                    }
                }
                // 变量b的作用域终止于此
            }
        }
        // 变量a作用域终止于此
    }
}

通过这段展开后的代码,我们可以清楚地看到第一段代码中的最后的else语句实质上是一个最内层的else,变量a、b、c的作用域范围是可以覆盖到else的。

注:极客时间的《Go语言第一课》专栏[1]的第11讲[2]对代码块与作用域有着更为全面的讲解,欢迎大家订阅学习。

1.2 使用自用变量

在Go中使用if语句时,开发者常常纠结于到底使用下面哪种形式:

if a, ok := foo(); a < 10 && ok{ //使用if表达式自用变量

} 

vs.

a, ok := foo()
if a < 10 && ok {

}

这里建议采用第一种,即“使用if表达式自用变量”,而不是在if外部定义临时变量。因为前者除了简洁,可读性略好的优点外,还有一点优势,那就是将a放在if隐式代码块中,将变量a的作用域限制到最小范围,这样可以避免不同代码段中变量命名相同而引起的冲突问题。进而让代码实现更加清晰和易于理解。

上述代码还有一个可能让初学者疑惑的点,那就是a < 10 && ok的运算符优先级问题,是(a < 10) && ok 还是 a < (10 && ok),为了避免给后续代码阅读者带去理解上的困惑,建议使用小括号明确求值时的计算顺序。

1.3 happy path原则

Go语言中,if语句使用的一个惯例就是遵循happy path(快乐路径)原则,所谓happy path是指通过将缩进程度降到最低,避免if语句或else-if语句的过度嵌套,使代码更易读和可维护。遵循快乐路径原则可以让你的代码变得更容易阅读和理解,执行的流程也变得更加清晰。

happy path意味着代码要尽量左对齐,减少嵌套,如下图所示:

2e47efbc7a12e5522b409876ab1f91d6.png

注:上图中原始素材来自于网络。

在编码实践中,要满足happy path有几个技巧:

  • 减少else、else if的使用;

  • 避免if语句的嵌套使用;

  • 快速返回。在if语句的body中使用return从函数中返回,而不是继续后续的处理。

注:极客时间的《Go语言第一课》专栏[3]的第18讲[4]对if语句做了更为全面的讲解,欢迎大家订阅学习。

2. for语句

印象中,for语句在使用频度方面是仅次于的if语句的控制流语句了。这里谈谈Go对于循环语句的支持的特点。

2.1 仅此一种for循环

Go信奉“做一件事只有一种方法”,不知道这是不是Go仅提供一种形式for语句的最初原因(相较于其他主流编程语言提供while、loop、do...while等)。

Go经典的for语句有如下一些典型使用形式:

// 最常规的for循环
for i := 0; i < 10; i++ {
    fmt.Println(i)
}

// 模拟while循环
i := 0
for i < 10 {
    fmt.Println(i)
    i++
}

// 死循环
for {
    // do something
}

2.2 for range不是可有可无

如果说go只有for语句,也不够准确,go还有一个for range变体。不过这个for range变体不是可有可无的,有些遍历没有for range无法完成,比如:

// 遍历map
for k, v := range aMap {
}

// 遍历string中的字符(非字节遍历)
for i, r := range s {
  // rune
}

2.3 带label与不带label的continue和break

在Go语言中,for循环语句中可以使用带label的continue和break语句,也可以使用我们通常认知中的不带label的continue和break语句。不过它们之间的差别应该牢记:

  • 不带label的continue和break语句

不带label的continue和break语句只能用于当前for循环语句中,它们的作用范围仅限于当前循环体内部。当执行continue语句时,会跳过本次循环,直接进入下一次循环;当执行break语句时,会结束当前循环,直接跳出循环体。

  • 带label的continue和break语句

带label的continue和break语句可以用于多层嵌套的for循环语句中,它们可以跳出指定的循环体。当执行带label的continue语句时,会跳过指定的循环体中的本次循环,直接进入下一次循环;当执行带label的break语句时,会结束指定的循环体,直接跳出循环。

下面是一个使用带label的break语句的示例:

package main

import "fmt"

func main() {
    outerLoop:
    for i := 1; i <= 3; i++ {
        for j := 1; j <= 3; j++ {
            if i == 2 && j == 2 {
                // 跳出指定循环体
                fmt.Println("跳出外层循环")
                break outerLoop
            }
            fmt.Printf("i=%d, j=%d\n", i, j)
        }
    }
}

在这个例子中,我们使用带label的break语句跳出了外层循环,从而避免了继续执行外层循环。如果使用不带label的break语句,仅会跳出内层循环,而不会跳出外层循环。

2.4 坑

虽然Go只有一种for语句形式,但可能遇到的“坑”却并不少,这里列出一些典型的“坑”:

  • 循环变量重用

看一下下面代码:

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

你预期的输出是什么呢?实际输出是什么呢?在go playground中执行一下,得到如下结果:

4 5
4 5
4 5
4 5
4 5

为什么会输出这个结果呢?我将上述代码做一个等价变换你就明白了:

func main() {
    var m = []int{1, 2, 3, 4, 5}  
             
    {
      i, v := 0, 0
        for i, v = range m {
            go func() {
                time.Sleep(time.Second * 3)
                fmt.Println(i, v)
            }()
        }
    }

    time.Sleep(time.Second * 10)
}

我们看到:i, v两个变量不是在每次循环时重新声明,而是在整个循环过程中只定义了一份,这就是为何所有goroutine输出的都是“4 5”的原因。Go团队针对这个问题正在设计优化方法[5],在后续的Go版本中,这个坑可能会被自然“修复”。

  • range表达式副本

我们再来看一段代码:

func main() {
    var a = [5]int{1, 2, 3, 4, 5}
    var r [5]int

    fmt.Println("original a =", a)

    for i, v := range a {
        if i == 0 {
            a[1] = 12
            a[2] = 13
        }
        r[i] = v
    }

    fmt.Println("after for range loop, r =", r)
    fmt.Println("after for range loop, a =", a)
}

在你的预期中,上面程序的输出结果是这样的:

original a = [1 2 3 4 5]
after for range loop, r = [1 12 13 4 5]
after for range loop, a = [1 12 13 4 5]

不过实际运行一下,你会看到真正的输出是这样的:

original a = [1 2 3 4 5]
after for range loop, r = [1 2 3 4 5]
after for range loop, a = [1 12 13 4 5]

究其原因,是因为参数range循环的是a的副本,我们用a'来表示,将上面代码等价变换为下面后,就更容易理解了:

for i, v := range a' { //a'是a的一个值拷贝
    if i == 0 {
        a[1] = 12
        a[2] = 13
    }
    r[i] = v
}

这样变换后,我们知道for range遍历的是a的副本,对a的修改不会影响后续的遍历。

因此,当使用数组、切片作为range后的待遍历的容器集合时,要十分小心。

  • break未跳出for

当for与switch语句联合使用时,也要注意避坑,看一下下面代码:

func main() {
    var sl = []int{5, 19, 6, 3, 8, 12}
    var firstEven int = -1

    // find first even number of the interger slice
    for i := 0; i < len(sl); i++ {
        switch sl[i] % 2 {
        case 0:
            firstEven = sl[i]
            break
        case 1:
            // do nothing
        }        
    }         
    println(firstEven) 
}

执行这个代码,输出结果为12,与我们预期的第一个偶数6不符。原因是什么呢?从输出结果为12来看,应该是break并未跳出for循环,导致循环继续进行到最后。

记住:Go语言规范中明确规定,不带label的break语句中断执行并跳出的,是同一函数内break语句所在的最内层的for、switch或select。所以,上面这个例子的break语句实际上只跳出了switch语句,并没有跳出外层的for循环,这也就是程序未按我们预期执行的原因。

注:极客时间的《Go语言第一课》专栏[6]的第19讲[7]对for语句做了更为全面的讲解,欢迎大家订阅学习。

3. switch语句

最后聊聊switch语句。在Go语言中,switch语句也是一种常用的控制流语句,它可以根据不同的条件执行不同的代码块:

switch expression {
case value1:
    // 执行代码块1
case value2:
    // 执行代码块2
default:
    // 执行默认代码块
}

由于Go switch语句执行语义不会默认执行下一个case,因此上述switch语句等价于一个多个if-else的语句,但从可读性上来说,比多层的if else更易理解,可读性更好。在这样的场景下,我们是推荐使用switch替代多个if-else语句的。

3.1 case语句求值顺序

switch语句通常会有很多表达式,这些表达式的求值顺序是有明确规定的,即从switch表达式开始求值,然后各个case语句的求值顺序是从上到下,从左到右的。记住这个顺序,有助于你分析switch语句的执行语义。

3.2 switch case的灵活性

Go switch语句在语法语义方面相对于其先祖C语言的Switch语句来说,做了很多优化,结果是更加灵活,坑几乎填平,主要的优化包括:

  • switch支持任何值的case比较,而不像C语言只能用int或枚举

  • 支持case表达式列表

package main

import "fmt"

func main() {
    num := 3
    switch num {
    case 1, 3, 5: // case支持表达式列表
        fmt.Println("奇数")
    case 2, 4, 6:
        fmt.Println("偶数")
    default:
        fmt.Println("其他")
    }
}
  • 不会默认执行下一个case语句

C语言中那种默认执行下一个case语句的执行语义导致我们需要在每个case中都使用break跳出switch,Go修复了这个语义,看下面这个例子:

package main

import "fmt"

func main() {
    num := 2
    switch num {
    case 1:
        fmt.Println("第一个 case 块")
    case 2:
        fmt.Println("第二个 case 块")
    case 3:
        fmt.Println("第三个 case 块")
    }
}

这个例子只会输出“第二个 case 块”,不会执行case 3中的代码。

如果要显式告知执行下一个case代码块,需要使用fallthrough。显然Go将常见执行逻辑作为默认语义,即每个case执行完跳出;而C语言恰做反了。

3.3 type switch

这个是其他语言所没有的,又或者说是Go特有的,type switch是针对接口类型表达式的特殊语法,语法格式也比较固定:

var x interface{} = 3
switch i := x.(type) {
case nil:
    // x 的类型为 nil
 println(i) // 输出x中存储的动态类型值
case int:
    // x 的类型为 int
case string:
    // x 的类型为 string
default:
    // x 的类型为其他类型
}

如果不需要接口变量中存储的动态类型值的话,也可以简化为:

var x interface{} = 3
switch x.(type) {
case nil:
    // x 的类型为 nil
case int:
    // x 的类型为 int
case string:
    // x 的类型为 string
default:
    // x 的类型为其他类型
}

注:极客时间的《Go语言第一课》专栏[8]的第20讲[9]对switch语句做了更为全面的讲解,欢迎大家订阅学习。

4. 小结

Go语言的控制流语句虽然种类不那么丰富,但足够帮助开发者实现各种不同类型的程序逻辑了。在编写代码时,需要根据具体的需求选择合适的控制语句,并注意遵循使用各种控制语句的惯例和规范,避免掉入各种“坑”中。


“Gopher部落”知识星球[10]旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2023年,Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码,关注代码质量并深入理解Go核心技术,并继续加强与星友的互动。欢迎大家加入!

a9386cab64331964cdc363e954c78acb.jpegfa6b816435036e0b7d2fff27c2fe1a62.png

13e47c679e18784128f366b5e8123364.png305b48ea1b81ef48df8e5a545fe1e000.jpeg

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址[11]:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 - https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx

  • 微博2:https://weibo.com/u/6484441286

  • 博客:tonybai.com

  • github: https://github.com/bigwhite

0a03cf4090cdf50e3c6dc3f5d70679e8.jpeg

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

参考资料

[1] 

《Go语言第一课》专栏: http://gk.link/a/10AVZ

[2] 

第11讲: https://time.geekbang.org/column/article/436915

[3] 

《Go语言第一课》专栏: http://gk.link/a/10AVZ

[4] 

第18讲: https://time.geekbang.org/column/article/447723

[5] 

Go团队针对这个问题正在设计优化方法: https://github.com/golang/go/discussions/56010

[6] 

《Go语言第一课》专栏: http://gk.link/a/10AVZ

[7] 

第19讲: https://time.geekbang.org/column/article/450944

[8] 

《Go语言第一课》专栏: http://gk.link/a/10AVZ

[9] 

第20讲: https://time.geekbang.org/column/article/455912

[10] 

“Gopher部落”知识星球: https://wx.zsxq.com/dweb2/index/group/51284458844544

[11] 

链接地址: https://m.do.co/c/bff6eed92687

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/25457.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

10. python字典

文章目录 一、什么是字典二、访问键-值对三、添加、修改键-值对四、删除键-值对4.1 语句del4.2 方法pop() 五、创建空字典六、遍历字典6.1方法items()6.2方法keys()6.3方法values() 七、嵌套7.1 字典列表7.2 在字典中存储列表7.3 在字典中存储字典 一、什么是字典 #创建一个字…

电商服务智能解决方案

互联网时代&#xff0c;智能客服已成为电商企业客户服务、管理和运营的标配。面临大量客户咨询、订单流程等业务&#xff0c;传统人工客服工作时间有限、人员流动性大、人工成本持续上涨等&#xff0c;已经无法满足电商企业“数智化”转型的需求&#xff0c;这也促使AI成为电商…

SpringSecurity入门

简介 官网地址&#xff1a;https://spring.io/projects/spring-security#overview Spring家族当中&#xff0c;一个安全管理框架 Shiro也是一个安全框架&#xff0c;提供了很多安全功能。Shiro比较老&#xff0c;旧的项目当中&#xff0c;可能还在使用。上手还挺简单 在新项…

Zemax Lumerical | 二维光栅出瞳扩展系统优化

简介 本文提出并演示了一种以二维光栅耦出的光瞳扩展&#xff08;EPE&#xff09;系统优化和公差分析的仿真方法。 在这个工作流程中&#xff0c;我们将使用3个软件进行不同的工作 &#xff0c;以实现优化系统的大目标。首先&#xff0c;我们使用 Lumerical 构建光栅模型并使用…

(双指针 ) 18. 四数之和 ——【Leetcode每日一题】

❓18. 四数之和 难度&#xff1a;中等 给你一个由 n 个整数组成的数组 nums &#xff0c;和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] &#xff08;若两个四元组元素一一对应&#xff0c;则认为两个四元组重…

不愧是阿里,扣的真细。

铜三铁四已经过去了&#xff0c;今天的行情虽然没有以前好&#xff0c;但是相比去年来说也算是好了一些了。有一些人已经在这个招聘季拿到了不错的Offer了。 今天给大家分享一份面经&#xff0c;今天这位朋友的背景是Java五年本&#xff0c;2023年前被毕业后投入了面试大军怀抱…

融合改进Sine混沌映射的新型粒子群优化算法(NIPSO)-附代码

融合改进Sine混沌映射的新型粒子群优化算法(NIPSO) 文章目录 融合改进Sine混沌映射的新型粒子群优化算法(NIPSO)1.粒子群优化算法2. 改进粒子群优化算法2.1 改进的 Sine 混沌映射2.2 粒子群改进 3.实验结果4.参考文献5.Matlab代码6.Python代码 摘要&#xff1a;为了应对传统粒子…

OpenGl之摄像机

文章目录 摄像机/观察空间摄像机位置摄像机方向右轴上轴 Look At自由移动移动速度鼠标输入缩放摄像机源码 OpenGL本身没有摄像机(Camera)的概念&#xff0c;但我们可以通过把场景中的所有物体往相反方向移动的方式来模拟出摄像机&#xff0c;产生一种我们在移动的感觉&#xff…

第12届蓝桥杯Scratch省赛真题集锦

编程题 第 1 题 问答题 下雨 题目说明 编程实现: 下雨。 具体要求: 1).点击绿旗,角色与背景如下图所示呈现在对应位置; 2).小猫说:“快下雨了,赶快回家”,小狗说:“我再玩一会”; 3).开始下雨,雨滴持续下落, 4).小猫躲在亭子里,雨滴在小猫和亭子后落下, 5).小狗在雨中…

java-基础语法(二)

java-基础语法(二) 一、流程控制语句 1.1 流程控制语句分类 顺序结构 分支结构(if, switch) 循环 结构(for, while, do…while) 1.2 顺序结构 顺序结构执行流程图&#xff1a; 1.3 分支结构之if语句 if语句格式1 格式&#xff1a;if (关系表达式) {语句体; }执行流程&…

【Jenkins+Ant+Jmeter】持续集成接口测试平台搭建

一、环境准备&#xff1a; 1、JDK&#xff1a;Java Downloads | Oracle 2、Jmeter&#xff1a;Apache JMeter - Download Apache JMeter 3、Ant&#xff1a;Apache Ant - Binary Distributions 4、Jenkins&#xff1a;Jenkins 二、Jemter脚本准备&#xff1a; 1、脚本目录&a…

云服务器和专用服务器之间的区别

在当今数字化时代&#xff0c;服务器是构建和支持各种应用和服务的基础设施之一。随着技术的发展和需求的增加&#xff0c;出现了不同类型的服务器&#xff0c;其中最常见的是云服务器和专用服务器。本文将详细介绍云服务器和专用服务器之间的区别&#xff0c;以帮助您更好地了…

多线程安全的案例展示与解决方案

一、概念 1. 什么是线程安全 当多个线程访问一个对象时&#xff0c;如果不用考虑这些线程在运行时环境下的调度和交替执行&#xff0c;也不需要进行额外的同步&#xff0c;或者在调用方进行任何其他的协调操作&#xff0c;调用这个对象的行为都可以获得正确的结果&#xff0c…

【Linux】iptables防火墙

文章目录 一、Linux防火墙基础1.Linux防火墙概术2.netfilter/iptables3.四表五链4.规则链之间的匹配顺序 二、iptables 安装1.常用的控制类型2.常用的管理选项 三、示例演示1.添加新的规则2.查看规则列表3.删除规则4.清空规则 四、规则的匹配1.通用匹配2.隐含匹配3.显式匹配 一…

Mybatis generator

文章目录 依赖式使用引入依赖配置文件设置生成使用中出现的异常 Mybatis中javaType和jdbcType对应关系int、bigint、smallint 和 tinyint是使用整数数据的精确数字数据类型。 插件式使用添加依赖和插件创建逆向工程的配置文件执行MBG插件的generate目标执行结果 逆向工程&#…

shell SNAT与DNAT

文章目录 SNATSNAT原理与应用SNAT实验 DNATDNAT原理与应用DNAT实验 SNAT SNAT原理与应用 SNAT 应用环境&#xff1a;局域网主机共享单个公网IP地址接入Internet&#xff08;私有不能早Internet中正常路由&#xff09; SNAT原理&#xff1a;修改数据包的源地址。 SNAT转换前提…

C++进阶 —— lambda表达式(C++11新特性)

目录 一&#xff0c;模板函数sort 二&#xff0c;lambda表达式 一&#xff0c;模板函数sort 在C98中&#xff0c;如对一个数据集合中的元素进行排序&#xff0c;可使用模板函数sort&#xff0c;如元素为自定义类型&#xff0c;需定义排序时的比较规则&#xff1b;随着C的发展…

intel驱动程序和支持助理常见问题:不识别、无法检测等问题解决方法

起因&#xff1a; wifi驱动有点问题&#xff0c;于是想着更新一下官方的驱动&#xff0c;下载intel驱动程序和支持助理并安装完成后&#xff0c;打开成了这个样子&#xff0c;刷新多少次都没有用&#xff0c;就是不识别。 解决方法&#xff1a; 经过一波胡乱操作&#xff0…

【Linux入门】Linux权限及管理

【Linux入门】Linux权限及管理 目录 【Linux入门】Linux权限及管理Linux权限管理文件访问者的分类文件类型和访问权限&#xff08;事物属性&#xff09; 文件权限值的表示方法文件访问权限的相关设置方法目录的权限实现共享目录粘滞位目录权限总结 作者&#xff1a;爱写代码的刚…

算法基础学习笔记——⑫最小生成树\二分图\质数\约数

✨博主&#xff1a;命运之光 ✨专栏&#xff1a;算法基础学习 目录 ✨最小生成树 &#x1f353;朴素Prim &#x1f353;Kruskal算法 ✨二分图 &#x1f353;匈牙利算法 ✨质数 &#x1f353;&#xff08;1&#xff09;质数的判定——试除法 &#x1f353;&#xff08;2&…