1.使用if控制语句时应遵循"快乐路径"原则:
对比下面两段伪代码:
//伪代码段1
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
}
}
伪代码段1:
1).没有使用else.失败就立即返回.
2).成功逻辑始终居左并延续到函数结尾.没有被嵌入if语句.
3).整个代码段布局扁平.没有深度缩进.
4).代码逻辑一目了然.可读性好.
伪代码段2:
1).整个代码呈锯齿状.有深度缩进.
2).成功逻辑被嵌入if-else代码块中.
3).代码逻辑曲折宛转.可读性较差.
代码段1的if控制语句使用方法符合Go语言惯用的"快乐路径"原则.
"快乐路径"原则
1).当出现错误时,快速返回.
2).成功逻辑不要嵌入if-else语句中.
3).快乐路径的执行逻辑在代码布局上始终靠左.这样可以一眼看到该函数的正常逻辑流程.
4).返回值一般在函数最后一行.
2.for range的闭坑指南:
1).小心迭代变量的重用:
for range的惯用法是使用短变量声明方式(:=)在for的initStmt中声明迭代变量.需要注意的是.这些迭代变量在for range的每次循环中都会被重用.而不是重新声明.
func main() {
var m = [...]int{1,2,3,4,5,6,7,8,9}
for i, v := range m {
...
}
}
上述代码可等价转换为:
func main() {
var m = [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
{
i, v := 0, 0
for i, v = range m {
...
}
}
}
可以清晰看到迭代变量的重用.
2).注意参与迭代的是range表达式的副本:
for range语句中.range后面接受的表达式类型可以是数组 指向数组的指针 切片 字符串 map和channel(至少具有读权限).
func main() {
arrayRangeExpression()
}
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)
}
期待输出:
a = [1 2 3 4 5]
r = [1 12 13 4 5]
a = [1 12 13 4 5]
实际运行结果:
a = [1 2 3 4 5]
r = [1 2 3 4 5]
a = [1 12 13 4 5]
原以为在第一次循环过程中.也就是i=0时.对a的修改(a[1]=12,a[2]=13)会在第二次 第三次循环中被v取出.但结果却是v取出的值依旧是a被修改之前的值.出现这个结果的原因是参与循环的是range表达式的副本.
Go中数组在内部表示为连续的字节序列.虽然长度是Go数组类型的一部分.但长度并不包含在数组类型在Go运行时的内部表示中.数组长度是由编译器编译器计算出来的.这个例子中.对range表达式的复制即对一个数组的复制.是Go临时分配的连续字节序列.与原数组完全不是一块内存区域.因为无论原数组如何修改.循环的副本依旧保持原值.
func main() {
arrayRangeExpression()
}
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)
}
执行结果:
这个例子使用指针作为range的表达式.结果是符合预期的了.
在Go中.大多数应用数组的场景都可以用切片替代.
func main() {
arrayRangeExpression()
}
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内部表示为一个结构体.由(T,len,cap)三元组组成.其中T指向切片对应的底层数组的指针.len是切片当前长度.cap为切片的容量.在进行range表达式复制时.它实际上复制的是一个切片.也就是表示切片的那个结构体.表示切片副本的结构体中的T依旧指向原切片对应的底层数组.因此对切片副本的修改也都会反映到底层数组a上.v从切片副本结构体中的T指向的底层数组中获取数组元素.也就得到了被修改后的元素值.
切片与数组还有一个不同点.就是其len在运行时可以被改变.而数组的长度可以认为是一个常量.不可改变.len的变化的切片对for range有何影响呢.看下面的例子.
func main() {
arrayRangeExpression()
}
func arrayRangeExpression() {
var a = []int{1, 2, 3, 4, 5}
var r = make([]int, 0)
fmt.Println("arrayRangeExpression result:")
fmt.Println("a = ", a)
for i, v := range a[:] {
if i == 0 {
a = append(a, 6, 7)
}
r = append(r, v)
}
fmt.Println("r = ", r)
fmt.Println("a = ", a)
}
执行结果:
在这个例子中.原切片a在for range的循环过程中被附加了两个元素6和7.其中len由5增加为7.但是对r没有任何影响.原因在于a的副本内部表示len并没有改变.依旧是5.所以for range只会循环5次.也就是只获取到了对应底层数组的前5个元素.
3).其他range表达式类型的使用注意事项.
对于range后面的其他表达式类型.比如string map 和channel.for range依旧会复制副本.
string:
当string作为range表达式的类型时.由于string在Go运行时内部表示为struct{*byte,len}.并且string本身是不可改变的.因此其行为和消耗与切片作为range表达式时类似.不过for range对于string来说.每次循环的单位是一个rune.而不是一个byte.返回的第一个值为迭代字符码点的第一字节位置.
func main() {
var s = "中国人"
for i, v := range s {
fmt.Printf("%d %s 0x%x\n", i, string(v), v)
}
}
执行结果:
如果作为range表达式的字符串s中存在非法UTF8字节序列.那么v将返回0xfffd这个特殊值.并且在下一轮中.v将仅前进一字节.
func main() {
var s1 = []byte{0xe4, 0xb8, 0xad, 0xe5, 0x9b, 0xbd, 0xe4, 0xba, 0xba}
for _, v := range s1 {
fmt.Printf("0x%x", v)
}
fmt.Println("\n")
//故意构造非法UTF8字节序列
s1[3] = 0xd0
s1[4] = 0xd6
s1[5] = 0xb9
for i, v := range string(s1) {
fmt.Printf("%d %x\n", i, v)
}
}
执行结果:
第二次循环时.由于以s1[3]开始的字节序列并非一个合法的UTF8字符.因此v的值为0xfffd.并且下一轮(第三轮)循环从i=4开始,第三轮循环找到了一个合法的UTF8字节序列0xd6,0xb9.即码点为0b59的UTF8字符.这是一个希伯来语字符.接下来第四轮循环.程序又回归正常.
map:
当map作为range表达式时.会得到一个map内部表示的副本.map在go运行时内部表示为一个hmap的描述符结构指针,因此该指针的副本也指向同一个hmap描述符.这样for range对map副本的操作即对源map的操作.
关于元素map的迭代.for range无法保证每次迭代元素的次序是一致的,如果在循环中对map进行修改.这样修改的结果是否对后面的迭代过程影响也是不确定的.
func main() {
var m = map[string]int{
"tony": 21,
"tom": 22,
"jim": 23,
}
counter := 0
for k, v := range m {
if counter == 0 {
delete(m, "tony")
}
counter++
fmt.Println(k, v)
}
fmt.Println("counter is ", counter)
}
执行结果:
或
channel:
对于channel来说.channel在Go运行时内部表示为一个channel描述符的指针.因此channel的指针副本也指向原channel.
当channel作为range表达式类型时.for range最终以阻塞读的方式阻塞在channel表达式上.即便是带缓冲的channel亦是如此.当channel无数据时.for range也会阻塞在channel上.直到channel关闭.
func main() {
var c = make(chan int)
go func() {
time.Sleep(time.Second * 3)
c <- 1
c <- 2
c <- 3
close(c)
}()
for v := range c {
fmt.Println(v)
}
}
执行结果:
func main() {
var c chan int
//会一直阻塞在这里.
for v := range c {
fmt.Println(v)
}
}
执行结果:
3.break:
func main() {
exit := make(chan interface{})
go func() {
for {
select {
case <-time.After(time.Second):
fmt.Println("timeout")
case <-exit:
fmt.Println("exit...")
break
}
}
fmt.Println("exit")
}()
time.Sleep(time.Second * 3)
exit <- struct{}{}
time.Sleep(time.Second * 3)
}
3秒后.主goroutine给子goroutine发一个退出信号(通过channel).子goroutine收到信号后通过break退出循环,主goroutine在发出信号后等待goroutine退出.等待时间为三秒.
执行结果:
从结果可以看出子goroutinebreal并未退出外层for循环.而是继续打印timeout.
这是Go break语法的一个小坑.Go语言规范中明确规定break语句(不接label的情况下)结束执行并跳出的是同一函数内的break语句所在的最内层的for switch或select的执行.上面例子虽然跳出了select语句.但并没有跳出外层的for循环.
func main() {
exit := make(chan interface{})
go func() {
loop:
for {
select {
case <-time.After(time.Second):
fmt.Println("timeout")
case <-exit:
fmt.Println("exit...")
break loop
}
}
fmt.Println("exit")
}()
time.Sleep(time.Second * 3)
exit <- struct{}{}
time.Sleep(time.Second * 3)
}
执行结果:
何处?几叶萧萧雨。湿尽檐花,花底无人语。
掩屏山,玉炉寒。谁见两眉愁聚倚阑干。 纳兰
语雀地址www.yuque.com/itbosunmian…?
《Go.》 密码:xbkk 欢迎大家访问.提意见.
如果大家喜欢我的分享的话.可以关注我的微信公众号
念何架构之路