Go(7)--语言陷阱

骑猪看日落 2021-08-29 15:09 469阅读 0赞

语言陷阱

    1. 多值赋值和短变量声明
    • 1.1 多值赋值
    • 1.2 短变量的声明和赋值
    1. range 复用临时变量
    1. defer 陷阱
    1. 切片困惑
    • 4.1 数组
    • 4.2 切片
    1. 值、指针和引用
    • 5.1 传值还是传引用
    • 5.2 函数名的意义
    1. 习惯用法
    • 6.1 干净
    • 6.2 comma, ok 表达式
    • 6.3 简写模式
    • 6.4 包中的函数或方法设计
    • 6.5 多值返回函数

1. 多值赋值和短变量声明

  1. Go 语言支持多值赋值,在函数或方法内部也支持短变量声明并赋值,同时 Go 语言依据类型字面量的值能够自动进行类型推断。

1.1 多值赋值

  1. 可以一次性声明多个变量,并可以在声明时赋值,而且可以省略类型,但需要遵守一定的规则。
  2. // 相同类型的变量可以在末尾带上类型
  3. var x, y int
  4. var x, y int = 1, 2
  5. // 如果不带类型,编译器可以直接进行类型推断
  6. var x, y = 1, 2
  7. var x, y = 1, "abc"
  8. // 不同类型的变量声明和隐式初始化可以使用如下语法
  9. var (
  10. x int
  11. y string
  12. )
  13. 如下都是非法的
  14. // 多值赋值语句中每个变量后面不能都带上类型
  15. var x int, y int = 1, 2
  16. var x int, y string = 1, "abc"
  17. var x int, y int
  18. var x int, y string
  19. **多值赋值的两种格式**
  20. 1)右边是一个多返回值得表达式,可以是返回多汁得函数调用,也可以是 `range``map``slice`等函数得操作,还可以是类型断言。
  21. // 函数调用
  22. x, y = f()
  23. // range 表达式
  24. for k, v := range map{
  25. }
  26. // type assertion
  27. v, ok := i.(xxxx)
  28. 2)赋值的左边操作数和右边的单一返回值的表达式个数一样,逐个从左向右依次对左边的操作数赋值。
  29. x, y, z = a, b, c
  30. **多值赋值的语义**
  1. 对左侧操作数中的表达式、索引值进行计算和确定,首先确定左侧的操作数的地址;然后对右侧的赋值表达式进行计算,如果发现右侧的表达式计算引用了左侧的变量,则创建临时变量进行值拷贝,最后完成计算
  2. 从左到右的顺序依次赋值

    package main

    import “fmt”

    func main() {

    1. x := []int{ 1, 2, 3}
    2. i := 0
    3. i, x[i] = 1, 2 // set i = 1, x[0] = 2
    4. fmt.Println(i, x) // 1 [2 2 3]
    5. x = []int{ 1, 2, 3}
    6. i = 0
    7. x[i], i = 2, 1 // set x[0] = 2, i = 1
    8. fmt.Println(i, x) // 1 [2 2 3]
    9. x = []int{ 1, 2, 3}
    10. i = 0
    11. x[i], i = 2, x[i] // set x[0] = 2, i = 1
    12. fmt.Println(i, x) // 1 [2 2 3]
    13. x[0], x[0] = 1, 2 // set x[0] = 1, then x[0] = 2
    14. fmt.Println(x[0]) // 2

    }

  3. 第 8 行先计算 x[i] 中的数组索引i的值,此时i=0,两个被赋值变量是ix[0],然后从左到右赋值操作i=1x[0]=2

  4. 第 13 行和第 8 行的逻辑一样
  5. 第 16 行先计算赋值语句左右两侧x[i]中的数组索引i的值,此时i=0,两个被赋值变量是ix[0],两个赋值变量分别是2x[0]。由于x[0]是左边的操作数,所以编译器创建一个临时变量tmp,将其赋值为x[0],然后从左到右依次赋值操作x[0]=2i=tmpi的值为1
  6. 第 22 行按照从左到右的执行顺序,先执行x[0]=1,然后执行x[0]=2,所以最后x[0]的值为2

1.2 短变量的声明和赋值

  1. 短变量的声明和赋值是指在 Go 函数或类型方法内部使用 `:=`”声明并初始化变量,支持多值赋值,格式如下:
  2. a := va
  3. a, b := va, vb
  1. 使用“:=”操作符,变量的定义和初始化同时完成
  2. 变量名后不要跟任何类型名,Go 编译器完全靠右边的值进行推导
  3. 支持多值短变量声赋值
  4. 只能用在函数和类型方法的内部

    在多值短变量声明和赋值时,至少有一个变量是新创建的局部变量,其他的变量可以复用以前的变量,不是新创建的变量执行的仅仅是赋值。

    package main

    var n int

    func foo() (int, error){

    1. return 1, nil

    }

    func g() {

    1. println(n)

    }

    func main() {

    1. // 此时 main 函数作用域里面没有 n
    2. // 所以创建新的局部变量 n
    3. n, _ := foo()
    4. // 访问的是全局变量n
    5. g() // 0
    6. // 访问的是 main 函数作用域下的 n
    7. println(n) // 1

    }

    a, b := va, vb什么时候定义新变量,什么时候复用已存在变量有以下规则:

  5. 如果想通过编译,则ab中至少要有一个是新定义的局部变量。

  6. 如果在赋值语句a, b := va, vb所在的代码块中已经存在一个局部变量a,则赋值语句a, b := va, vb不会创建新变量a,而是直接使用va赋值给已经声明的局部变量a,但是会创建新变量b,并将vb赋值给b
  7. 如果在赋值语句a, b := va, vb所在的代码块中没有局部变量ab,但在全局命名空间有变量ab,则该语句会创建新的局部变量ab并使用vavb初始化它们。此时赋值语句所在的局部作用域类内,全局的ab被屏蔽。

    赋值操作符=:=的区别:

  8. =不会声明并创建新变量,而是在当前赋值语句所在的作用域由内向外逐层去搜寻变量,如果没有搜索到相同的变量名,则编译错误。

  9. :=必须出现在函数或类型方法内部
  10. :=至少要创建一个局部变量并初始化

2. range 复用临时变量

  1. package main
  2. import "sync"
  3. func main() {
  4. wg := sync.WaitGroup{ }
  5. si := []int{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
  6. for i := range si{
  7. wg.Add(1)
  8. go func() {
  9. println(i)
  10. wg.Done()
  11. }()
  12. }
  13. wg.Wait()
  14. }

在这里插入图片描述
通过输出可以看到,程序并没有如预期一样变量整个切片,原因如下:

  1. for range 下的迭代变量 i 的值是共用的
  2. main 函数所在的 goroutine 和后续启动的 goroutine 存在竞争关系

    正确的写法是使用函数参数做一次数据复制,而不是闭包。

    package main

    import “sync”

    func main() {

    1. wg := sync.WaitGroup{ }
    2. si := []int{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    3. for i := range si{
    4. wg.Add(1)
    5. go func(a int) {
    6. println(a)
    7. wg.Done()
    8. }(i)
    9. }
    10. wg.Wait()

    }

在这里插入图片描述
for 循环下调用并发时要复制迭代变量后再使用,不要直接引用 for 迭代变量。

3. defer 陷阱

  1. `defer`带来的副作用有:一、对返回值的影响;二、对性能的影响。
  2. `defer`中如果引用了函数的返回值,则因引用形式不同会导致不同的结果。
  3. package main
  4. func f1()(r int) {
  5. defer func() {
  6. r++
  7. }()
  8. return 0
  9. }
  10. func f2()(r int) {
  11. t := 5
  12. defer func() {
  13. t = t + 5
  14. }()
  15. return t
  16. }
  17. func f3()(r int) {
  18. defer func(r int) {
  19. r = r + 5
  20. }(r)
  21. return 1
  22. }
  23. func main() {
  24. println("f1 = ", f1())
  25. println("f2 = ", f2())
  26. println("f3 = ", f3())
  27. }

在这里插入图片描述

  1. 对于有名函数,有如下特点:
  1. 函数调用方负责开辟栈空间,包括形参和返回值的空间
  2. 有名的函数返回值相当于函数的局部变量,被初始化为类型的零值

    f1函数,defer语句后面的匿名函数是对函数返回值 r 的闭包引用,f1 函数的逻辑如下:

    (1)r 是函数的有名返回值,分配在栈上,其地址又被称为返回值所在栈区。首先r被初始化为0。

    (2)return 0会复制 0 到返回值栈区,返回值r被赋值为0

    (3)执行defer语句,由于匿名函数对返回值r是闭包引用,所以r++执行后,函数返回值被修改为1

    (4)defer语句执行完后RET返回,此时函数的返回值仍然为 1

在这里插入图片描述
函数f2的逻辑:

  1. 1)返回值`r`被初始化为0
  2. 2)引入局部变量`t`,并初始化为 5
  3. 3)复制`t`的值 5 到返回值 `r` 所在的栈区
  4. 4`defer`语句后面的匿名函数是对局部变量`t`的闭包引用,`t`的值被设置为 10
  5. 5)函数返回,此时函数返回值栈区上的值仍然是 5

在这里插入图片描述
函数f3的逻辑:

  1. 1)返回值`r`被初始化为0
  2. 2)复制 1 到函数返回值 r 所在的栈区
  3. 3)执行 `defer``defer`后匿名函数使用的是传参数调用,在注册`defer`函数时将函数返回值`r`作为实参传进去,由于函数调用是值拷贝,所以`defer`函数执行后只是形参值变为 5,对实参没有任何影响。
  4. 4)函数返回,此时函数返回值栈区上的值是 1

在这里插入图片描述

4. 切片困惑

4.1 数组

  1. `Go`中的数组是一种基本类型,数组的类型不仅包括其元素类型,也包括其大小,`[2]`int `[5]`int 是两个完全不同的数组类型。
  2. **创建数组**
  1. 声明时通过字面量进行初始化
  2. 直接声明,不显式地进行初始化

    package main

    import “fmt”

    func main() {

    1. // 指定大小的显式初始化
    2. a := [3]int{ 1, 2, 3}
    3. // 通过 ... 由后面的元素个数推断数组大小
    4. b := [...]int{ 1, 2, 3}
    5. // 指定大小,并通过索引值初始化,未显示初始化的元素被置为“零值”
    6. c := [3]int{ 1:1, 2:3}
    7. // 指定大小,但不显式初始化,数组元素全被置为“零值”
    8. var d [3]int
    9. fmt.Printf("len = %d, value = %v\n", len(a), a) // len = 3, value = [1 2 3]
    10. fmt.Printf("len = %d, value = %v\n", len(b), b) // len = 3, value = [1 2 3]
    11. fmt.Printf("len = %d, value = %v\n", len(c), c) // len = 3, value = [0 1 3]
    12. fmt.Printf("len = %d, value = %v\n", len(d), d) // len = 3, value = [0 0 0]

    }

    在 Go 中,数组的一切传递都是值拷贝,体现在以下三个方面:

  3. 数组间的直接赋值

  4. 数组作为函数参数
  5. 数组内嵌到struct

    package main

    import “fmt”

    func f(a [3]int) {

    1. a[2] = 10
    2. fmt.Printf("%p, %v\n", &a, a)

    }

    func main() {

    1. a := [3]int{ 1, 2, 3}
    2. // 直接赋值是值拷贝
    3. b := a
    4. // 修改 a 元素值并不影响 b
    5. a[2] = 4
    6. fmt.Printf("%p, %v\n", &a, a) // 0xc00005e120, [1 2 4]
    7. fmt.Printf("%p, %v\n", &b, b) // 0xc00005e140, [1 2 3]
    8. // 数组作为函数参数仍然是值拷贝
    9. f(a) // 0xc0000601c0, [1 2 10]
    10. c := struct {
    11. s [3]int
    12. }{
    13. s: a,
    14. }
    15. // 结构是值拷贝,内部的数组也是值拷贝
    16. d := c
    17. // 修改 c 中的数组元素值并不影响 a
    18. c.s[2] = 30
    19. // 修改 d 中的数组元素值并不影响 c
    20. d.s[2] = 20
    21. fmt.Printf("%p, %v\n", &a, a) // 0xc00000c400, [1 2 4]
    22. fmt.Printf("%p, %v\n", &c, c) // 0xc00000c4e0, {[1 2 30]}
    23. fmt.Printf("%p, %v\n", &d, d) // 0xc00000c500, {[1 2 20]}

    }

4.2 切片

  1. **切片创建**
  1. 通过数组创建

    array[m:n]创建一个包含[m,n)个元素的切片。

  2. make

    通过内置的make函数创建,make([]T, len, cap)中的 T是元素类型,len 是长度,cap 是底层数组的容量,cap是可选参数。

  3. 直接声明

    可以直接声明一个切片,也可以在声明切片的过程中使用字面量进行初始化,直接声明但不进行初始化的切片其值为nil

    var a []int // a is nil

    1. var a []int = []int{ 1, 2, 3, 4}

    切片数据结构

    切片是一种类型的引用类型,原因是其存放数据的数组是通过指针间接引用的。所以切片名作为函数参数和指针传递是一样的效果。

    type slice struct{

    1. array unsafe.Pointer
    2. len int
    3. cat int

    }

在这里插入图片描述
len增长超过cap时,会申请一个更大容量的底层数组,并将数据从老数组赋值到新申请的数组中。

  1. **nil 切片和空切片**
  2. `make([]int, 0)` `var a []int`创建的切片是有区别的。前者的切片指针有分配,后者的内部指针为0
  3. package main
  4. import (
  5. "fmt"
  6. "reflect"
  7. "unsafe"
  8. )
  9. func main() {
  10. var a []int
  11. b := make([]int, 0)
  12. if a == nil{
  13. fmt.Println("a is nil")
  14. }else{
  15. fmt.Println("a is not nil")
  16. }
  17. // 虽然 b 的底层数组大小为0,但切片并不是 nil
  18. if b == nil{
  19. fmt.Println("b is nil")
  20. }else{
  21. fmt.Println("b is not nil")
  22. }
  23. // 使用反射中的 SliceHeader 来获取切片运行时的数据结构
  24. as := (*reflect.SliceHeader)(unsafe.Pointer(&a))
  25. bs := (*reflect.SliceHeader)(unsafe.Pointer(&b))
  26. fmt.Printf("len = %d, cap = %d, type=%d\n", len(a), cap(a), as.Data)
  27. fmt.Printf("len = %d, cap = %d, type=%d\n", len(b), cap(b), bs.Data)
  28. }

在这里插入图片描述

  1. `var a []int`创建的切片是一个`nil`切片(底层数组没有分配,指针指向 `nil`

在这里插入图片描述

  1. `make([]int, 0)`创建的是一个空切片(底层数组指针非空,但底层数组是空的)。

在这里插入图片描述
多个切片引用同一个底层数组引发的混乱

  1. 一个底层数组可以创建多个切片,这些切片共享底层数组,使用`append`扩展切片的过程中,可能修改底层数组的元素,间接地影响其他切片地值,也可能发生数组复制重建。
  2. package main
  3. import (
  4. "fmt"
  5. "reflect"
  6. "unsafe"
  7. )
  8. func main() {
  9. a := []int{ 0, 1, 2, 3, 4, 5, 6}
  10. b := a[0:4]
  11. as := (*reflect.SliceHeader)(unsafe.Pointer(&a))
  12. bs := (*reflect.SliceHeader)(unsafe.Pointer(&b))
  13. // a、b 共享底层数组
  14. fmt.Printf("a = %v, len = %d, cap = %d, type=%d\n", a, len(a), cap(a), as.Data)
  15. // a = [0 1 2 3 4 5 6], len = 7, cap = 7, type=824633795200
  16. fmt.Printf("b = %v, len = %d, cap = %d, type=%d\n", b, len(b), cap(b), bs.Data)
  17. // b = [0 1 2 3], len = 4, cap = 7, type=824633795200
  18. b = append(b, 10, 11, 12)
  19. // a、b 继续共享底层数组,修改 b 会影响共享的底层数组,间接影响 a
  20. fmt.Printf("a = %v, len = %d, cap = %d, type=%d\n", a, len(a), cap(a), as.Data)
  21. // a = [0 1 2 3 10 11 12], len = 7, cap = 7, type=824633795200
  22. fmt.Printf("b = %v, len = %d, cap = %d, type=%d\n", b, len(b), cap(b), bs.Data)
  23. // b = [0 1 2 3 10 11 12], len = 7, cap = 7, type=824633795200
  24. // len(b) = 7, 此时需要重新分配数组,并将原来数组值复制到新数组
  25. b = append(b, 13, 14)
  26. as = (*reflect.SliceHeader)(unsafe.Pointer(&a))
  27. bs = (*reflect.SliceHeader)(unsafe.Pointer(&b))
  28. // 此时 a、b 指向底层数组的指针已经不同了
  29. fmt.Printf("a = %v, len = %d, cap = %d, type=%d\n", a, len(a), cap(a), as.Data)
  30. // a = [0 1 2 3 10 11 12], len = 7, cap = 7, type=824633795200
  31. fmt.Printf("b = %v, len = %d, cap = %d, type=%d\n", b, len(b), cap(b), bs.Data)
  32. // b = [0 1 2 3 10 11 12 13 14], len = 9, cap = 14, type=824633778512
  33. }
  34. 多个切片共享一个底层数组,其中一个切片的`append`操作可能引发如下两种情况:
  1. append追加的元素没有超过底层数组的容量,则会直接操作共享底层数组,如果其他切片有引用数组被覆盖的元素,则也会导致其他切片的值也隐式地发生变化。
  2. append追加的元素超过底层数组的容量,则会重新申请数组,并将原来数组值赋值到新数组。

5. 值、指针和引用

5.1 传值还是传引用

  1. Go 语言只有一种参数传递规则,那就是值拷贝。

5.2 函数名的意义

  1. Go 的函数名和匿名函数字面量的值有 3 层含义:
  1. 类型信息,表明其数据类型是函数类型
  2. 函数名代表函数的执行代码的起始位置
  3. 可以通过函数名进行函数调用,函数调用格式为func_name(param_list)。在底层执行层面包含以下 4 部分内容。

    • 准备好参数
    • 修改 PC 值,跳转到函数代码起始位置开始执行
    • 赋值值到函数的返回值栈区
    • 通过 RET 返回函数调用的下一条指令处继续执行

6. 习惯用法

6.1 干净

  1. 编译器不能通过未使用的局部变量(包括未使用的标签)
  2. import未使用的包同样通不过编译
  3. 所有的控制结构、函数和方法定义的“{”放到结尾,而不能另起一行。
  4. 提供go fmt工具格式化代码,使所有的代码风格保持统一

6.2 comma, ok 表达式

  1. 常见的几个`comma, ok` 表达式如下:
  1. 获取map的值

    获取map中不存在键的值不会发生异常,而是会返回值类型的零值,如果想确定map中是否存在key,则可以使用获取map值的comma, ok 语法。

    package main

    func main() {

    1. m := make(map[string]string)
    2. v, ok := m["aaa"]
    3. if ok{
    4. println("m['aaa'] = ", v)
    5. }else{
    6. println("m['aaa'] is nil ")
    7. }

    }

  2. 读取chan的值

    读取已经关闭的通道,不会阻塞,也不会引起panic,而是一直返回该通道的零值。怎么判断通道已经关闭?有两种方法,一种是读取通道的comma, ok表达式,如果通道已经关闭,则 ok 的返回值是 false,另一种就是通过range循环迭代。

    package main

    func main() {

  1. c := make(chan int)
  2. go func() {
  3. c <- 1
  4. c <- 2
  5. close(c)
  6. }()
  7. for{
  8. // 使用 comma, ok 判断通道是否关闭
  9. v, ok := <- c
  10. if ok{
  11. println(v)
  12. }else{
  13. break
  14. }
  15. }
  16. // 使用 range 更加简洁
  17. for v := range c{
  18. println(v)
  19. }
  20. }
  1. 类型断言

    类型断言通常可以使用comma, ok 语句来确定接口是否绑定了某个实例类型,或者判断接口绑定的实例类型是否实现另一个接口。
    在这里插入图片描述

6.3 简写模式

  1. Go 语言很多重复的引用或声明可以使用“`()`”进行简写
  1. import多个包

    // 推荐写法
    import(

    1. "bufio"
    2. "bytes"

    )

    // 不推荐写法
    import “bufio”
    import “bytes”

  2. 多个变量声明

    包中多个相关全局变量声明时,监视使用”()“进行合并声明

    // 推荐写法
    var(

    1. a int
    2. b string
    3. c float

    )

    // 不推荐写法
    var a int
    var b string
    var float

6.4 包中的函数或方法设计

  1. 很多包开发者会在内部实现两个"`同名`"的函数或方法,一个首字母大写,用于导出API供外部使用;一个首字母小写,用于实现具体逻辑。**一般首字母大写的函数调用首字母小写的函数,同时包装一些功能;首字母小写的函数负责更多的底层细节**。

6.5 多值返回函数

  1. 多值返回函数里如果有`error``bool`类型的返回值,则应该将`error``bool`作为最后一个返回值。

发表评论

表情:
评论列表 (有 0 条评论,469人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Go——Go语言简介

    Go语言的诞生背景 G0语言的诞生主要基于如下原因: 1. 摩尔定律接近失效后多核服务器已经成为主流,当前的编程语言对并发的支持不是很好,不能很好地发挥多核CPU的威