• 《Go学习笔记 . 雨痕》流程控制(if、switch、for range、goto、continue、break)


    Go 精简(合并)了流控制语句,虽然某些时候不够便捷,但够用。

    if...else...

    条件表达式值必须是布尔类型,可省略括号,且左花括号不能另起一行。

    func main()  {
    	x := 3
    
    	if x > 5 {
    		println("a")
    	} else if x < 5 && x > 0 {
    		println("b")
    	} else {
    		println("z")
    	}
    }
    

    比较特别的是对初始化语句的支持,可定义块局部变量或执行初始化函数。

    func main()  {
    	x := 10
    
    	if xinit(); x == 0 { // 优先执行 xinit 函数
    		println("a")
    	}
    
    	if a, b := x + 1, x + 10; a < b { // 定义一个或多个局部变量(也可以是函数返回值)
    		println(a)
    	} else {
    		println(b)
    	}
    }

    局部变量的有效范围包含整个 if/else 块。

    尽可能减少代码块嵌套,让正常逻辑处于相同层次。

    import (
    	"errors"
    	"log"
    )
    
    func check(x int) error {
    	if x <= 0 {
    		return errors.New("x <= 0")
    	}
    
    	return nil
    }
    
    func main()  {
    	x := 10
    
    	if err := check(x); err == nil {
    		x++
    		println(x)
    	} else {
    		log.Fatal(err)
    	}
    }

    该示例中,if 块虽然承担了 2种 逻辑:错误处理 和 后续正常操作。基于重构原则,我们应该保持代码块功能的单一性。

    func check(x int) error {
    	if x <= 0 {
    		return errors.New("x <= 0")
    	}
    
    	return nil
    }
    
    func main() {
    	x := 10
    
    	if err := check(x); err != nil {
    		log.Fatalln(err)
    	}
    
    	x++
    	println(x)
    }

    如此,if 块仅完成条件检查 和 错误处理,相关正常逻辑保持在同一层次。当有人视图通过阅读这段代码来获知逻辑流程时,完全可忽略 if 块细节。同时,单一功能可提升代码可维护性,更利于拆分重构。

    当然,如须在多个条件块中使用局部变量,那么只能保留层次,或直接使用外部变量。

    func main() {
    	s := "9"
    
    	n, err := strconv.ParseInt(s, 10, 64) // 使用外部变量
    
    	if err != nil {
    		log.Fatalln(err)
    	} else if n < 0 || n > 10 { // 也可以考虑拆分成另一个独立 if 块
    		log.Fatalln("invalid number")
    	}
    
    	println(n) // 避免 if 局部变量将该逻辑放到 else 块
    }

    对应某些过于复杂的组合条件,建议将其重构为函数。

    func main() {
    	s := "9"
    
    	if n, err := strconv.ParseInt(s, 10, 64); err != nil || n < 0 || n > 10 || n % 2 != 0 {
    		log.Fatalln("invalid number")
    	}
    
    	println("ok")
    }

    函数调用虽然有一些性能损失,可却让主流程序变得更加清爽。况且,条件语句独立之后,更易于测试,同样会改善代码可维护性。

    func check(s string) error {
    	n, err := strconv.ParseInt(s, 10, 64)
    	if err != nil || n < 0 || n > 10 || n%2 != 0 {
    		return errors.New("invalid number")
    	}
    
    	return nil
    }
    
    func main() {
    	s := "9"
    
    	if err := check(s); err != nil {
    		log.Fatalln(err)
    	}
    
    	println("ok")
    }

    将 流程 和 布局 细节分离是很常见的做法,不同的变化因素被分隔在各自独立单元(函数或模块)内,可避免修改时造成关联错误,减少患“肥胖症”的函数数量。当然,代码单元测试也是主要原因之一。另一方面,该示例中的函数 check 仅被 if 块调用,也可将其作为局部函数,以避免扩大作用域,只是对测试的友好度会差一些。

    当前编译器只能说够用,须优化的地方太多,其中内联处理做得也差强人意,所以代码维护性 和 性能平衡需要投入更多心力。

    语言方面,最遗憾的是没有条件运算符 “a > b ? a : b”。有没有 lambda 无所谓,但没有这个却少份优雅。加上一大堆 err != nil 判断语句,对于有完美主义倾向的代码洁癖患者来说是种折磨。

    switch

    与 if 类似,switch 语句也用于选择执行,但具体使用场景会有所不同。

    1、表达式 switch 语句

    func main() {
    	a, b, c, x := 1, 2, 3, 2
    
    	switch x {				// 将 x 与 case 条件匹配
    	case a, b:				// 多个匹配条件命中其一即可(OR),变量
    		println("a | b")
    	case c:					// 单个匹配条件
    		println("c")
    	case 4:					// 常量
    		println("d")
    	default:
    		println("z")
    	}
    }

    输出:

    a | b

    条件表达式支持非常量值,这要比 C 更加灵活。相比 if 表达式,switch 值列表要更加简洁。编译器对 if、switch 生成的机器指令可能完全相同,所谓谁性能更好须看具体情况,不能作为主观判断条件。

    switch 同样支持初始化语句,按从上到下、从左到右顺序匹配 case 执行。只有全部匹配失败时,才会执行 default 块。

    func main() {
    	switch x := 5; x {
    	default:			// 编译器确保不会先执行 default 块
    		x += 100
    		println(x)
    	case 5:
    		x += 50
    		println(x)
    	}
    }

    输出:

    55

    考虑到 default 作用类似 else,建议将其放置在 switch 末尾。

    相邻的空 case 不构成多条件匹配。

    switch x {      // 单条件,内容为空。隐式 "case a: break;"
    case a:
    case b:
        println("b")
    }

    不能出现重复的 case 常量值。

    func main() {
    	switch x := 5; x {
    	case 5:
    		println("a")
    	case 6, 5:		// 错误:duplicate case 5 in switch
    		println("b")
    	}
    }

    无须显式执行 break 语句,case 执行完毕后自动中断。如须贯通后续 case (源码顺序),须执行 fallthrough,但不再匹配后续条件表达式。

    func main() {
    	switch x := 5; x {
    	default:
    		println(x)
    	case 5:
    		x += 10
    		println(x)
    
    		fallthrough		// 继续执行下一个 case,但不再匹配条件表达式
    	case 6:
    		x += 20
    		println(x)
    
    		//fallthrough	// 如果在此继续 fallthrough,不会执行 default,完全按照源码顺序
    						// 导致 "cannot fallthrough final case in switch" 错误
    	}
    }

    输出:

    15
    35

    注意:fallthrough 必须放在 case 块结尾,可使用 break 语句阻止。

    func main() {
    	switch x := 5; x {
    	case 5:
    		x += 10
    		println(x)
    
    		if x >= 15 {
    			break		// 终止,不再执行后续语句
    		}
    
    		fallthrough		// 必须是 case 块的最后一条语句
    	case 6:
    		x += 20
    		println(x)
    	}
    }

    输出:

    15

    某些时候,switch 还被用来替换 if 语句。被省略的 switch 条件表达式默认值为 true,继而与 case 比较表达式结果匹配。

    func main() {
    	switch x := 5; { // 相当于 "switch x :=5; true { ... }"
    	case x > 5:
    		println("a")
    	case x > 0 && x <= 5: // 不能写成 "case x > 0, x <= 5",因为多条件是 OR 关系
    		println("b")
    	default:
    		println("c")
    	}
    }
    

    输出:

    b

    2、类型 switch 语句

    类型 switch 语句 将对类型进行判定,而不是值。下面是一个简单的例子:

    var v interface{}
    // 省略了部分代码
    // v = 8
    // v = "wenjianbao"
    
    switch v.(type) {
    case string:
        fmt.Printf("The string is '%s'
    ", v.(string))
    case int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64:
        fmt.Printf("The interger is %d
    ", v)
    default:
        fmt.Printf("Unsupporte value.(type=%T)
    ", v)
    }

    类型 switch 语句的 switch 表达式会包含一个特殊的类型断言,例如 v.(type)。它虽然特殊,但是也要遵循类型断言的规则。其次,每个 case 表达式中包含的都是 类型字面量,而不是表达式。最后,fallthrough 语句不允许出现在类型 switch 语句中。

    类型 switch 语句的 switch 表达式还有一种变形写法,如下:

    var v interface{}
    // 省略了部分代码
    // v = 8
    // v = "wenjianbao"
    
    switch i := v.(type) {
    case string:
        fmt.Printf("The string is '%s'
    ", i)
    case int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64:
        fmt.Printf("The interger is %d
    ", i)
    default:
        fmt.Printf("Unsupporte value.(type=%T)
    ", i)
    }

    这里的 i := v.(type) 使经类型转换后的值得以保存。i 的类型一定会是 v 的值的实际类型。

    for

    仅有 for 一种循环语句,但常用方式都能支持。

    for i := 0; i < 3; i++ { // 初始化表达式支持函数调用或定义局部变量
    }
    for x < 10 { // 类似 "while x < 10 {}" 或 "for ; x < 10; {}"
        x++
    }
    for {		// 类似 "while true {}" 或 "for true {}"
    	break
    }

    初始化语句仅被执行一次。条件表达式中如有函数调用,须确认是否会重复执行。可能会被编译器优化掉,也可能是动态结果,须每次执行确认。

    func count() int {
    	print("count.")
    	return 3
    }
    
    func main() {
    	for i, c := 0, count(); i < c; i++ { // 初始化语句的 count() 函数仅执行一次
    		println("a", i)
    	}
    
    	c := 0
    	for c < count() { // 条件表达式中的 count 重复执行
    		println("b", c)
    		c++
    	}
    }
    

    输出:

    count.a 0
    a 1
    a 2
    count.b 0
    count.b 1
    count.b 2
    count.
    Process finished with exit code 0
    

    规避方式 就是在初始化表达式中定义局部变量保存 count 结果。

    可用 for ... range 完成数据迭代,支持 字符串、数组、数组指针、切片、字典、通道类型,返回 索引键值 数据。

    data teyp1st value2nd value 
    string index s[index] unicode, rune
    array/slice index v[index]  
    map key value  
    channel element    

    # 迭代字符串:

    func main() {
    	str := "hello world"
    
    	for index, ch := range str {
    		fmt.Printf("%d -- %c
    ", index, ch)
    	}
    }

    输出:

    0 -- h
    1 -- e
    2 -- l
    3 -- l
    4 -- o
    5 --  
    6 -- w
    7 -- o
    8 -- r
    9 -- l
    10 -- d

    # 迭代数组

    func main() {
    	data := [3]string{"a", "b", "c"}
    
    	for i, s := range data {
    		println(i, s)
    	}
    }

    输出:

    0 a
    1 b
    2 c

    没有相关接口实现自定义类型迭代,除非基础类型是上述类型之一。

    允许返回单值,或用 “_” 忽略。

    func main() {
    	data := [3]string{"a", "b", "c"}
    
    	for i := range data { // 只返回 1st value
    		println(i, data[i])
    	}
    
    	for _, s := range data { // 忽略 1st value
    		println(s)
    	}
    
    	for range data { // 仅迭代,不返回。可用来执行清空 channel 等操作
    	}
    }

    无论普通 for 循环,还是 range 迭代,其定义的局部变量都会 重复使用

    func main() {
    	data := [3]string{"a", "b", "c"}
    
    	for i, s := range data {
    		println(&i, &s)
    	}
    }

    输出:

    0xc42003bef0 0xc42003bf08
    0xc42003bef0 0xc42003bf08
    0xc42003bef0 0xc42003bf08

    这对 闭包 存在一些影响,相关详情,请阅读后续章节。

    注意,range 会 复制 目标数据。受直接影响的是 数组,可改用 数组指针切片 类型。

    func main() {
    	data := [3]int{10, 20, 30}
    
    	for i, x := range data { // 从 data 复制品中取值
    		if i == 0 {
    			data[0] += 100
    			data[1] += 200
    			data[2] += 300
    		}
    
    		fmt.Printf("x: %d, data: %d
    ", x, data[i])
    	}
    
    	for i, x := range data[:] { // 仅复制 slice,不包括 底层 array
    		if i == 0 {
    			data[0] += 100
    			data[1] += 200
    			data[2] += 300
    		}
    
    		fmt.Printf("x: %d, data: %d
    ", x , data[i])
    	}
    }

    输出:

    x: 10, data: 110
    x: 20, data: 220    // range 返回的依旧是复制值
    x: 30, data: 330
    
    x: 110, data: 210   // 当 i == 0 修改 data 时,x 已经取值,所以是 110
    x: 420, data: 420   // 复制的仅是 slice 自身,底层 array 依旧是原对象
    x: 630, data: 630

    相关数据类型中,字符串、切片基本结构是个很小的结构体,而 字典、通道 本身是指针封装,复制成本都很小,无须专门优化。

    如果 range 目标表达式是函数调用,也仅被执行一次。

    func data() []int {
    	println("origin data.")
    	return []int{10, 20, 30}
    }
    
    func main() {
    	for i, x := range data() {
    		println(i, x)
    	}
    }

    输出:

    origin data.
    0 10
    1 20
    2 30

    建议嵌套循环不要超过 2 层,否则会难以维护。必要时可剥离,重构为函数。

    使用 range 子句,有 3 点需要注意,如下:

    • 若对数组、切片 或 字符串值进行迭代,且 := 左边只有一个迭代变量时,一定要小心。这时只会得到其中元素的索引,而不是元素本身;这很可能并不是你想要的。
    • 迭代没有任何元素的数组值、为 nil 的切片值、为 nil 的字典值 或 为 "" 的字符串值,并不会执行 for 语句中的代码。for 语句在一开始就会直接结束执行。因为这些值的长度都为 0。
    • 迭代为 nil 的通道值 会让当前流程永远阻塞在 for 语句上!

    goto,continue,break

    对于 goto 的讨伐由来已久,仿佛它是“笨蛋”标签一般。可事实上,能在很多场合见到

    它的身影,就连 Go 源码里都有很多。

    $ cd go/src
    $ grep -r -n "goto"  *

    单就 Go 1.6 的源码统计结果,goto 语句就超过 1000 条有余。很惊讶,不是吗?虽然某些设计模式可用来消除 goto 语句,但在性能优先的场合,它能发挥积极作用。

    使用 goto 前,须先定义标签。标签区分大小写,且未使用的标签会引发编译错误。

    func main() {
    start:							// 错误:label start defined and note used
    	for i := 0; i < 3; i++ {
    		println(i)
    
    		if i > 1 {
    			goto exit
    		}
    	}
    exit:
    	println("exit.")
    }

    不能跳转到其他函数,或内层代码块内。

    func test() {
    test:
    	println("test")
    	println("test exit.")
    }
    
    func main() {
    	for i := 0; i < 3; i++ {
    	loop:
    		println(i)
    	}
    
    	goto test		// 错误:label test not defined
    	goto loop		// 错误:goto loop jumps into block
    }

    和 goto 定义跳转不同,break、continue 用于中断代码执行。

    • break:用于 switch、for、select 语句,终止整个语句块执行。
    • continue:仅用于 for 循环,终止后续逻辑,立即进入下一轮循环。
    func main() {
    	for i := 0; i < 10; i++ {
    		if i%2 == 0 {
    			continue		// 立即进入下一轮循环
    		}
    
    		if i > 5 {
    			break			// 立即终止整个 for 循环
    		}
    
    		println(i)
    	}
    }

    输出:

    1
    3
    5

    配合标签,break 和 continue 可在多层嵌套中指定目标层级。

    func main() {
    outer:
    	for x := 0; x < 5; x++ {
    		for y := 0; y < 10; y++ {
    			if y > 2 {
    				println()
    				continue outer
    			}
    
    			if x > 2 {
    				break outer
    			}
    
    			print(x, ":", y, " ")
    		}
    	}
    }
    

    输出:

    0:0 0:1 0:2 
    1:0 1:1 1:2 
    2:0 2:1 2:2 
    
  • 相关阅读:
    python pytesseract模块,报错
    CSS清除浮动_清除float浮动
    CSS中@import与link的具体区别
    js substr和substring的区别
    WebStorm 自定义字体+颜色+语法高亮+导入导出用户设置
    Jquery中的重置
    table标签中thead、tbody、tfoot的作用
    下拉列表框的几个属性
    使用GDI绘制文本
    使用GDI绘制一条直线
  • 原文地址:https://www.cnblogs.com/52php/p/6391537.html
Copyright © 2020-2023  润新知