go 语言基本上继承了 c 语言的逻辑判断语句,但是仍有些不同,比如:
- 在循环语句中,仅保留了
for
语句,do-while,while 均不支持,这也是满足了 go 小而美的原则 -- 单一功能仅保留一种方式。 - break continue 后面添加 label 功能
- switch 中 case 语句不会自动执行下一个 case,需要显示的使用
fallthrough
,当然了在 select 中的 case,并没有 fallthrough 功能 - switch 中 case 支持表达式列表
- switch 增加 type 模式,让类型也可以作为选择的条件
- 跟 c 语言最大的不同,增加了 select - case 功能。
go 语言中的控制语句基础语法:
if conditon {
} else if condition{
} else {
}
// 比如:
if a > 0 {
println("a > 0")
}else if a < 12 {
println("a > 0 并且 < 12")
}else {
println("a > 12 或者是非正数")
}
go 语言推崇的控制语句是 “快乐路径”:
- 当出现错误时,直接返回
- 成功的逻辑不要放在 if 语句中
- 返回值通常在此函数的最后一行
err, value := handle()
if err != nil {
return err
}
return value
for 循环的基础语言和 c 类似,for range 语句中,range 后面接数组,指向数组的指针,切片,字符串,和拥有读权限的 channel
for i:= 0;i< 10;i++ {}
for k,v := range sliceValue{}
其中普通循环语句中,i 值属于整个 loop 所有,这一点需要好好注意,我们在作用域那一章节中也提到过。
for-range 语句中,有两点需要注意,首先跟普通循环一样,变量属于整个 loop 所有。
其次,k,v 变量是切片或者 map 的 index 和 value 复制体,当遇到比如 slice 更改数据的时候,切勿直接更改 k v,可使用 arr[k] = v+1
这种方式直接改变切片本身,另外,当使用 for-range 语句时,如果第二个变量没有使用的价值,可以不写,并且无需使用占位符 _
:
for k := range sliceValue{}
// 如果是省略第一个,那么还是需要占位符的
for _,v := range sliceValue{}
切片在运行时,len 是会变化的,因为确定切片的 len 是 runtime 的责任,而数组的 len 是在编译期确定的,for - range 后面的数组或者切片,真正处理的其实是这个数据的复制,下面看一个 bug
a := []int{1, 2, 3, 4, 5}
var number int
for i := range a {
number++
if i == 0 {
a = append(a, 6, 7)
}
}
println(number) // 5
这里 number 输出的是 5,因为我们 append 添加的长度是原切片的长度,但是循环体中存储的 len,还是最初的长度 5,所以只能循环 5 次。
for-range 中还有一个容易出 bug 的事情,比如 range 后面跟一个数组,因为 range 的时候实际上是数组的复制品,看下面这段代码:
package main
import "fmt"
func main() {
var a = [5]int{1, 2, 3, 4, 5}
var r [5]int
fmt.Println(a)
for i, v := range a {
if i == 0 {
a[1] = 12
a[2] = 13
}
r[i] = v
}
fmt.Println(r, a)
}
本来期望的 r 值是 1 12 13 4 5,因为在 i == 0 时就更改了数组 a 的值,但是最终输出的却是 1 2 3 4 5,原因很简单,因为 v 读取的是 a 这个数组的复制品,也就是说,实际上这个代码的隐藏含义是 range a'
这里的 a'
就是 a 的复制品,所以更改了 a,a 的复制品也不会被改变,解决方法就是取这个数组的切片就可以了,这样即便是复制了,也只是复制了一份儿指针而已。
如果 for-range 中后面接的是 string,每次迭代不是按照 byte 来进行的,而是按照 rune 来进行,比如 "你好"
每次的迭代输出的就是你和好,而不是 byte
for range 后面是 map 时,无法保证 map 的输出顺序,但是 map 和 slice 一样都是胖指针,所以时可以对 map 进行直接操作的。如果在循环体中新创建一个 map 项,那么这个项目在 range 时有可能会被输出,不能保证一定。
channel 的本质也是一个胖指针,所以它也可以直接被操作本体,channel 在 range 时是阻塞式的读取,如果不关闭 channel,这个 range 会一直阻塞。
var c chan int
for v := range c {
}
这段代码就会造成一直阻塞,进而触发系统的 Panic
在 go 语言的 switch 和 select 的 case 中,我们经常会使用 break 来跳出循环,默认跳出的就是最小的那个循环单位,比如下面这个例子
func a(){
for {
select {
case <- time.After(time.Second):
break
}
}
}
这段代码是有 bug 的,因为它无法跳出 for 这层循环,只能跳出 select 这里。那么我们该怎么做呢?这个时候应该使用 label 了,所谓 label 就是标签的意思,意思就是指定好了 break 的位置,它的作用就是这个。
func a(){
Now:
for {
select {
case <- time.After(time.Second):
break Now
}
}
}
这个时候就可以 break 到 Now 指定的层级了。
我们应避免使用 fallthrough 来执行多条件表达式,比如这样的代码
switch v.(type){
case int: fallthrough
case int8: fallthrough
println("yes")
}
这种代码也很烂,我们可以直接使用多个 case 并列的方式:
package main
func main() {
var i int16 = 12
a(i)
}
func a(v any) {
switch v.(type) {
case int, int8:
println("yes")
default:
println("no")
}
}
这段代码使用的就是判断 type 的断言模式,固定用法就是 value.(type)
前面是要判断的 value,后面是固定用法 type,必须是这个单词才行。
我再带你回忆一下普通的断言,a.(int)
除了在 switch 中的断言方式,普通的断言就跟这段代码是一致的,一个 any 类型 (interface {}) 后面跟具体的类型。
经过上面的初步介绍,接下来,我们深入看一下 for range 的一些底层原理
第一段代码:
func main() {
arr := []int {1,2,3}
for _,v := range arr {
arr = append(arr,v)
}
fmt.Println(arr)
}
这段代码 for 循环不会一直循环,原因是,arr 会在 range 一个复制一份儿,这个复制体的 len 在最初的 range 中的开头已经确定是 3,后面继续追加的 arr,并不会改变这个最初读取的 len == 3
这个结果。
不过,如果你使用的是传统的循环,那么这种写法就会出现 bug:
package main
import "fmt"
func main() {
n := 12
for i := 0; i < n; i++ {
n++
fmt.Println("i")
}
}
使用这种传统的 for 循环,因为 n 在循环体和循环内部都是同一个,所以循环不会结束
因此你应该将这种代码改写为 for - range 模式:
go 1.22 增加了对于整数的 for range,之前只有 chan slice map,不过整数的 for range 前面只有一个变量,并且跟其他 for range 一致,n 为复制值,并且 for i := range n,i 也是复制值。这跟其他的 for range 保持一致
package main
import "fmt"
func main() {
n := 12
for range n {
n++
fmt.Println("i")
}
}
第二段代码:
arr := []int{1,2,3}
result := [] *int{}
for ,v := range arr {
result = append(result, &v)
}
这段代码是存在 bug 的,&v,首先,根据作用域可知道,这个 v 是 loop 级作用域,那么这个 result 中存在的&v 就是同一个值,所以这个代码是错误的。
改正的方式就是放入正确的切片中数据的指针即可:
arr := []int{1,2,3}
result := [] *int{}
for k := range arr {
result = append(result, &arr[k])
}
第三段代码:
for i,_ := range result {
result[i] = 0
}
这是一段对 int 切片进行归零的方法,很多人会觉得这要循环一次会非常浪费时间,其实不会,因为在编译器中,它不会真的循环,编译器会优化这个操作,直接给它内存清零。