首页 > 其他分享 >Go语言精进之路读书笔记第19条——理解Go语言表达式的求值顺序

Go语言精进之路读书笔记第19条——理解Go语言表达式的求值顺序

时间:2024-02-07 18:12:59浏览次数:25  
标签:Println 读书笔记 int fmt var 求值 range func Go

第19条 了解Go语言控制语句惯用法及使用注意事项

19.1 使用if控制语句时应遵循"快乐路径"原则

  • 当出现错误时,快速返回;
  • 成功逻辑不要嵌入if-else语句中;
  • "快乐路径"当执行逻辑中代码布局上始终靠左,这样读者可以一眼看到该函数当正常逻辑流程;
  • "快乐路径"的返回值一般在函数最后一行。

19.2 for range的避"坑"指南

1. 迭代变量的重用

for range的惯用法是使用短变量声明方式(:=)在for的初始化语句中声明迭代变量(iteration variable)。但需要注意的是,这些迭代变量在for range的每次循环中都会被重用,而不是重新声明,可以将for range进行等价转换:

//使用了数组长度的省略语法(...),表示编译器自动计算数组的长度,因为数组的元素个数已经明确指定为5个,所以[...]的省略语法会被自动解释为[5]
var m = [...]int{1, 2, 3, 4, 5}
for i, v := range m {
    ...
}
//上述代码可等价转换为:
var m = [...]int{1, 2, 3, 4, 5}
{
    i, v := 0
    for i, v = range m {
        ...
    }
}

//我们看到,goroutine中输出的i、v值都是for range循环结束后的i、v最终值,而不是各个goroutine启动时的i、v值。
//这是因为goroutine执行的闭包函数引用了它的外层包裹函数中的变量i、v,这样变量i、v在主goroutine和新启动的goroutine之间实现了共享。
//而i、v值在整个循环过程中是重用的,仅有一份。
//在for range循环结束后,i=4,v=5,因此各个goroutine在等待3秒后进行输出的时候,输出的是i、v的最终值。
func demo1() {
    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 * 2)
}

//如果要修正Demo1中的问题,可以为闭包函数增加参数并在创建goroutine时将参数与i、v的当时值进行绑定。
//注意每次输出结果的行序可能不同,这是由于goroutine的调度顺序决定的。
func demo2() {
    var m = [...]int{1, 2, 3, 4, 5}

    for i, v := range m {
        go func(i, v int) {
            time.Sleep(time.Second * 3)
            fmt.Println(i, v)
        }(i, v)
    }

    time.Sleep(time.Second * 2)
}

2. 参与迭代的是range表达式的副本

for range语句中,range后面接受的表达式的类型可以是数组、指向数组的指针、切片、字符串、map和channel(至少需要有读权限)。

3. 不同类型range表达式的使用注意事项

对于range后面的其他表达式类型,例如string、map和channel,for range依旧会制作副本。

array & slice

//在arrayRangeExpression中,真正参与循环的是a的副本,而不是真正的a。
//Go中数组在内部表示为连续的字节序列,a的副本是Go临时分配的连续字节序列,与a完全不是一块内存区域。
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)
}

//在pointerToArrayRangeExpression中,&a的副本依旧是一个指向原数组a的指针,因此后续所有循环中均是&a指向的原数组在参与。
func pointerToArrayRangeExpression() {
    var a = [5]int{1, 2, 3, 4, 5}
    var r [5]int

    fmt.Println("pointerToArrayRangeExpression 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)
}

//在sliceRangeExpression中,切片在Go内部表示为一个结构体,由(*T, len, cap)三元组组成,
//其中*T指向切片对应的底层数组的指针,len是切片当前长度,cap为切片的容量。
//在进行range表达式复制时,它实际上复制的是一个切片,也就是表示切片的那个特结构体,而结构体中的*T依旧指向原切片对应的底层数组。
func sliceRangeExpression() {
    var a = [5]int{1, 2, 3, 4, 5}
    var r [5]int

    fmt.Println("sliceRangeExpression 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)
}

//在sliceLenChangeRangeExpression中,切片副本中的len字段没有改变,依旧是5,因此for range只会循环5次。
func sliceLenChangeRangeExpression() {
    var a = []int{1, 2, 3, 4, 5}
    var r = make([]int, 0)

    fmt.Println("sliceLenChangeRangeExpression 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)
}

string

//string在Go运行时内部表示为struct {*byte, len},并且string本身是不可改变的。
//string每次循环的单位是一个rune,返回的第一个值为迭代字符码点的第一字节的位置。
func stringDemo1() {
    var s = "中国人"

    for i, v := range s {
        fmt.Printf("%d %s %s 0x%x\n", i, string(v), reflect.TypeOf(v), v)
    }
    fmt.Println("")

    // 0 中 int32 0x4e2d
    // 3 国 int32 0x56fd
    // 6 人 int32 0x4eba
}

//如果作为range表达式的字符串s中存在非法UTF8字节序列,那么v将返回0xfffd这个特殊值,并且值下一轮循环中,v将仅前进一字节。
func stringDemo2() {
    //byte sequence of s: 0xe4 0xb8 0xad 0xe5 0x9b 0xbd 0xe4 0xba 0xba
    var sl = []byte{0xe4, 0xb8, 0xad, 0xe5, 0x9b, 0xbd, 0xe4, 0xba, 0xba}
    for _, v := range sl {
        fmt.Printf("0x%x ", v)
    }
    fmt.Println()

    sl[3] = 0xd0
    sl[4] = 0xd6
    sl[5] = 0xb9

    for i, v := range string(sl) {
        fmt.Printf("%d %x\n", i, v)
    }
}

map

//map在Go运行时内部表示为一个hmap的描述符结构指针,因此该指针的副本也指向同一个hmap描述符,
//这样for range对map副本的操作即对源map的操作。
//for range无法保证每次迭代的元素次序是一致的。
//同时,如果在循环的过程中对map进行修改,那么这样修改的结果是否会影响后续迭代过程也是不确认的。
//第二个循环中输出的counter可能为2,也可能为3
//第三个循环中输出的counter可能为4,也可能为3
func mapDemo() {
    var m = map[string]int{
        "tony": 21,
        "tom":  22,
        "jim":  23,
    }

    for k, v := range m {
        fmt.Println(k, v)
    }
    fmt.Println()

    counter := 0
    for k, v := range m {
        if counter == 0 {
            delete(m, "tony")
        }
        counter++
        fmt.Println(k, v)
    }
    fmt.Println("counter is ", counter)
    fmt.Println()

    m["tony"] = 21
    counter = 0

    for k, v := range m {
        if counter == 0 {
            m["lucy"] = 24
        }
        counter++
        fmt.Println(k, v)
    }
    fmt.Println("counter is ", counter)
}

channel

func recvFromUnbufferedChannel() {
    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 recvFromNilChannel() {
    var c chan int

    // 程序将一直阻塞在这里
    for v := range c {
        fmt.Println(v)
    }

}

19.3 break跳到哪里去了

Go语言规范中明确规定break语句(不接label的情况下)结束执行并跳出的是同一函数内break语句所在的最内层的for、switch或select的执行。

//break实际上跳出了select语句,但并没有跳出外层但for循环
func breakDemo1() {
    exit := make(chan interface{})

    go func() {
        for {
            select {
            case <-time.After(time.Second):
                fmt.Println("tick")
            case <-exit:
                fmt.Println("exiting...")
                break
            }
        }
        fmt.Println("exit!")
    }()

    time.Sleep(3 * time.Second)
    exit <- struct{}{}

    // wait child goroutine exit
    time.Sleep(3 * time.Second)
}

func breakDemo2() {
    exit := make(chan interface{})

    go func() {
    loop:
        for {
            select {
            case <-time.After(time.Second):
                fmt.Println("tick")
            case <-exit:
                fmt.Println("exiting...")
                break loop
            }
        }
        fmt.Println("exit!")
    }()

    time.Sleep(3 * time.Second)
    exit <- struct{}{}

    // wait child goroutine exit
    time.Sleep(3 * time.Second)
}

19.4 尽量用case表达式列表替代fallthrough

switch n {
case 1: fallthrough
case 3: fallthrough
case 5: fallthrough
case 7:
    odd()
case 2: fallthrough
case 4: fallthrough
case 6: fallthrough
case 8:
    even()
default:
    unknown()
}
// 改为使用case表达式列表
switch n {
case 1, 3, 5, 7:
    odd()
case 2, 4, 6, 8:
    even()
default:
    unknown()
}

标签:Println,读书笔记,int,fmt,var,求值,range,func,Go
From: https://www.cnblogs.com/brynchen/p/18011161

相关文章

  • Go语言精进之路读书笔记第17条——理解Go语言表达式的求值顺序
    Go语言表达式支持在同一行声明和初始化多个变量支持在同一行对多个变量进行赋值(不同类型也可以)vara,b,c=5,"hello",3.45a,b,c:=5,"hello",3.45a,b,c=5,"hello",3.45RobPike练习题(规则见17.3赋值语句的求值)n0,n1=n0+n1,n0或者n0,n1=op(......
  • Go语言精进之路读书笔记第18条——理解Go语言代码块与作用域
    18.1Go代码块与作用域简介Go规范定义了如下几种隐式代码块。宇宙代(Universe)码块:所有Go源码都在该隐式代码块中,就相当于所有Go代码等最外层都存在一对大括号。包代码块:每个包都有一个包代码块,其中放置着该包都所有Go源码文件夹代码块:每个文件都有一个文件代码块,其中包含着该......
  • Go语言精进之路读书笔记第15条——了解string实现原理并高效使用
    15.1Go语言的字符串类型在Go语言中,无论是字符串常量、字符串变量还是代码中出现的字符串字面量,它们的类型都被统一设置为string特点string类型的数据是不可变的对string进行切片化后,Go编译器会为切片变量重新分配底层存储而不是共用string的底层存储string的底层的数据存......
  • Go语言精进之路读书笔记第16条——理解Go语言的包导入
    Go编译速度快的原因主要体现在以下三方面:Go要求每个源文件在开头处显式地列出所有依赖的包导入,这样Go编译器不必读取和处理整个文件就可以确定其依赖的包列表。Go要求包之间不能存在循环依赖。这样一个包的依赖关系便形成了一张有向无环图。由于无环,包可以被单独编译,也可以并行......
  • Django知识笔记1
    本文从分析现在流行的前后端分离Web应用模式说起,然后介绍如何设计RESTAPI,通过使用Django来实现一个RESTAPI为例,明确后端开发RESTAPI要做的最核心工作,然后介绍DjangoRESTframework能帮助我们简化开发RESTAPI的工作。Web应用模式在开发Web应用中,有两种应用模式:前后端不分离......
  • Go语言精进之路读书笔记第14条——了解map实现原理并高效使用
    14.1什么是mapmap对value的类型没有限制,但是对key的类型有严格要求:key的类型应该严格定义了作为“==”和“!=”两个操作符的操作数时的行为,因此func、map、slice、chan不能作为map的key类型。map类型不支持“零值可用”,未显式赋初值的map类型变量的零值为nil。对处于零值状态的......
  • golang类型转换模块之gconv
    gf框架提供了非常强大的类型转换包gconv,可以实现将任何数据类型转换为指定的数据类型,对常用基本数据类型之间的无缝转换,同时也支持任意类型到struct对象的属性赋值。由于gconv模块内部大量使用了断言而非反射(仅struct转换使用到了反射),因此执行的效率非常高。使用方式:import"g......
  • Go语言的For循环:语法全解析
    Go语言,作为一门旨在提供简洁、高效编程体验的编程语言,其循环结构的设计同样体现了这一理念。在Go中,for循环是唯一的循环语句,但它的灵活性足以应对各种迭代需求。本文将详细介绍Go语言中for循环的语法,通过示例展示其在实际编程中的应用。基本语法Go语言的for循环基本语法如下:for初......
  • 详解golang实现一个带时效的环形队列
    1.需求mysql执行时间超过100ms以上打warn日志,但是一分钟以内这种warn日志超过10条就需要告警。所以需求就是获得一分钟以内mysql的warn的个数。2.分析为什么使用环形队列而不使用slice?因为队列长度固定,所以可以一开始就分配好空间,不用自动扩容,环形的目的就是不用改变数组的值,只用移......
  • Go语言的100个错误使用场景(30-40)|数据类型与字符串使用
    目录前言4.控制结构4.1忽视元素在range循环中是拷贝(#30)4.2忽略在range循环中如何评估表达式(#31)4.3忽略在range中使用指针元素的影响(#32)4.4对map遍历的错误假设(#33)4.5忽略break的作用(#34)4.6在循环中使用defer(#35)5.字符串5.1不理解rune的概念(#36)5.2不准确的字......