Go(7)--语言陷阱

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

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

1.1 多值赋值

      可以一次性声明多个变量,并可以在声明时赋值,而且可以省略类型,但需要遵守一定的规则。

// 相同类型的变量可以在末尾带上类型
var x, y int
var x, y int = 1, 2

// 如果不带类型,编译器可以直接进行类型推断
var x, y = 1, 2
var x, y = 1, "abc"

// 不同类型的变量声明和隐式初始化可以使用如下语法
var (
	x int
	y string
)

      如下都是非法的

// 多值赋值语句中每个变量后面不能都带上类型
var x int, y int = 1, 2
var x int, y string = 1, "abc"
var x int, y int
var x int, y string

      多值赋值的两种格式

      (1)右边是一个多返回值得表达式,可以是返回多汁得函数调用,也可以是 rangemapslice等函数得操作,还可以是类型断言。

// 函数调用
x, y = f()

// range 表达式
for k, v := range map{
}

// type assertion
v, ok := i.(xxxx)

      (2)赋值的左边操作数和右边的单一返回值的表达式个数一样,逐个从左向右依次对左边的操作数赋值。

x, y, z = a, b, c

      多值赋值的语义

  1. 对左侧操作数中的表达式、索引值进行计算和确定,首先确定左侧的操作数的地址;然后对右侧的赋值表达式进行计算,如果发现右侧的表达式计算引用了左侧的变量,则创建临时变量进行值拷贝,最后完成计算
  2. 从左到右的顺序依次赋值
package main

import "fmt"

func main() {
	x := []int{1, 2, 3}
	i := 0
	i, x[i] = 1, 2 		// set i = 1, x[0] = 2
	fmt.Println(i, x)   // 1 [2 2 3]

	x = []int{1, 2, 3}
	i = 0
	x[i], i = 2, 1      // set x[0] = 2, i = 1
	fmt.Println(i, x)	// 1 [2 2 3]

	x = []int{1, 2, 3}
	i = 0
	x[i], i = 2, x[i]	// set x[0] = 2, i = 1
	
	fmt.Println(i, x)	// 1 [2 2 3]

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

1.2 短变量的声明和赋值

      短变量的声明和赋值是指在 Go 函数或类型方法内部使用 “:=”声明并初始化变量,支持多值赋值,格式如下:

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

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

package main

var n int

func foo() (int, error){
	return 1, nil
}

func g()  {
	println(n)
}

func main() {
	// 此时 main 函数作用域里面没有 n
	// 所以创建新的局部变量 n
	n, _ := foo()

	// 访问的是全局变量n
	g()	// 0

	// 访问的是 main 函数作用域下的 n
	println(n)	// 1
}

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

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

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

  1. =不会声明并创建新变量,而是在当前赋值语句所在的作用域由内向外逐层去搜寻变量,如果没有搜索到相同的变量名,则编译错误。
  2. :=必须出现在函数或类型方法内部
  3. :=至少要创建一个局部变量并初始化

2. range 复用临时变量

package main

import "sync"

func main() {
	wg := sync.WaitGroup{}

	si := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

	for i := range si{
		wg.Add(1)
		go func() {
			println(i)
			wg.Done()
		}()
	}

	wg.Wait()
}

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

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

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

package main

import "sync"

func main() {
	wg := sync.WaitGroup{}

	si := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

	for i := range si{
		wg.Add(1)
		go func(a int) {
			println(a)
			wg.Done()
		}(i)
	}

	wg.Wait()
}

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

3. defer 陷阱

      defer带来的副作用有:一、对返回值的影响;二、对性能的影响。

      defer中如果引用了函数的返回值,则因引用形式不同会导致不同的结果。

package main

func f1()(r int)  {
	defer func() {
		r++
	}()
	return 0
}

func f2()(r int) {
	t := 5
	defer func() {
		t = t + 5
	}()
	return t
}

func f3()(r int)  {
	defer func(r int) {
		r = r + 5
	}(r)
	return 1
}

func main() {
	println("f1 = ", f1())
	println("f2 = ", f2())
	println("f3 = ", f3())
}

在这里插入图片描述

      对于有名函数,有如下特点:

  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)返回值r被初始化为0

      (2)引入局部变量t,并初始化为 5

      (3)复制t的值 5 到返回值 r 所在的栈区

      (4)defer语句后面的匿名函数是对局部变量t的闭包引用,t的值被设置为 10

      (5)函数返回,此时函数返回值栈区上的值仍然是 5

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

      (1)返回值r被初始化为0

      (2)复制 1 到函数返回值 r 所在的栈区

      (3)执行 deferdefer后匿名函数使用的是传参数调用,在注册defer函数时将函数返回值r作为实参传进去,由于函数调用是值拷贝,所以defer函数执行后只是形参值变为 5,对实参没有任何影响。

      (4)函数返回,此时函数返回值栈区上的值是 1

在这里插入图片描述

4. 切片困惑

4.1 数组

      Go中的数组是一种基本类型,数组的类型不仅包括其元素类型,也包括其大小,[2]int 和[5]int 是两个完全不同的数组类型。

      创建数组

  1. 声明时通过字面量进行初始化
  2. 直接声明,不显式地进行初始化
package main

import "fmt"

func main() {
	// 指定大小的显式初始化
	a := [3]int{1, 2, 3}

	// 通过 ... 由后面的元素个数推断数组大小
	b := [...]int{1, 2, 3}

	// 指定大小,并通过索引值初始化,未显示初始化的元素被置为“零值”
	c := [3]int{1:1, 2:3}

	// 指定大小,但不显式初始化,数组元素全被置为“零值”
	var d [3]int

	fmt.Printf("len = %d, value = %v\n", len(a), a)	// len = 3, value = [1 2 3]
	fmt.Printf("len = %d, value = %v\n", len(b), b)	// len = 3, value = [1 2 3]
	fmt.Printf("len = %d, value = %v\n", len(c), c)	// len = 3, value = [0 1 3]
	fmt.Printf("len = %d, value = %v\n", len(d), d)	// len = 3, value = [0 0 0]
}

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

  1. 数组间的直接赋值
  2. 数组作为函数参数
  3. 数组内嵌到struct
package main

import "fmt"

func f(a [3]int)  {
	a[2] = 10
	fmt.Printf("%p, %v\n", &a, a)
}

func main() {
	a := [3]int{1, 2, 3}
	// 直接赋值是值拷贝
	b := a

	// 修改 a 元素值并不影响 b
	a[2] = 4

	fmt.Printf("%p, %v\n", &a, a)	// 0xc00005e120, [1 2 4]
	fmt.Printf("%p, %v\n", &b, b)	// 0xc00005e140, [1 2 3]

	// 数组作为函数参数仍然是值拷贝
	f(a)	// 0xc0000601c0, [1 2 10]

	c := struct {
		s [3]int
	}{
		s: a,
	}

	// 结构是值拷贝,内部的数组也是值拷贝
	d := c

	// 修改 c 中的数组元素值并不影响 a
	c.s[2] = 30

	// 修改 d 中的数组元素值并不影响 c
	d.s[2] = 20

	fmt.Printf("%p, %v\n", &a, a)	// 0xc00000c400, [1 2 4]
	fmt.Printf("%p, %v\n", &c, c)	// 0xc00000c4e0, {[1 2 30]}
	fmt.Printf("%p, %v\n", &d, d)	// 0xc00000c500, {[1 2 20]}

}

4.2 切片

      切片创建

  1. 通过数组创建

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

  1. make

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

  1. 直接声明

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

	var a []int	// a is nil
	var a []int = []int{1, 2, 3, 4}

      切片数据结构

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

type slice struct{
	array unsafe.Pointer
	len int
	cat int
}

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

      nil 切片和空切片

      make([]int, 0)var a []int创建的切片是有区别的。前者的切片指针有分配,后者的内部指针为0。

package main

import (
	"fmt"
	"reflect"
	"unsafe"
)

func main() {
	var a []int

	b := make([]int, 0)

	if a == nil{
		fmt.Println("a is nil")
	}else{
		fmt.Println("a is not nil")
	}

	// 虽然 b 的底层数组大小为0,但切片并不是 nil
	if b == nil{
		fmt.Println("b is nil")
	}else{
		fmt.Println("b is not nil")
	}

	// 使用反射中的 SliceHeader 来获取切片运行时的数据结构
	as := (*reflect.SliceHeader)(unsafe.Pointer(&a))
	bs := (*reflect.SliceHeader)(unsafe.Pointer(&b))

	fmt.Printf("len = %d, cap = %d, type=%d\n", len(a), cap(a), as.Data)
	fmt.Printf("len = %d, cap = %d, type=%d\n", len(b), cap(b), bs.Data)

}

在这里插入图片描述

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

在这里插入图片描述

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

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

      一个底层数组可以创建多个切片,这些切片共享底层数组,使用append扩展切片的过程中,可能修改底层数组的元素,间接地影响其他切片地值,也可能发生数组复制重建。

package main

import (
	"fmt"
	"reflect"
	"unsafe"
)

func main() {
	a := []int{0, 1, 2, 3, 4, 5, 6}
	b := a[0:4]

	as := (*reflect.SliceHeader)(unsafe.Pointer(&a))
	bs := (*reflect.SliceHeader)(unsafe.Pointer(&b))

	// a、b 共享底层数组
	fmt.Printf("a = %v, len = %d, cap = %d, type=%d\n", a, len(a), cap(a), as.Data)
	// a = [0 1 2 3 4 5 6], len = 7, cap = 7, type=824633795200

	fmt.Printf("b = %v, len = %d, cap = %d, type=%d\n", b, len(b), cap(b), bs.Data)
	// b = [0 1 2 3], len = 4, cap = 7, type=824633795200

	b = append(b, 10, 11, 12)
	// a、b 继续共享底层数组,修改 b 会影响共享的底层数组,间接影响 a
	fmt.Printf("a = %v, len = %d, cap = %d, type=%d\n", a, len(a), cap(a), as.Data)
	// a = [0 1 2 3 10 11 12], len = 7, cap = 7, type=824633795200

	fmt.Printf("b = %v, len = %d, cap = %d, type=%d\n", b, len(b), cap(b), bs.Data)
	// b = [0 1 2 3 10 11 12], len = 7, cap = 7, type=824633795200

	// len(b) = 7, 此时需要重新分配数组,并将原来数组值复制到新数组
	b = append(b, 13, 14)

	as = (*reflect.SliceHeader)(unsafe.Pointer(&a))
	bs = (*reflect.SliceHeader)(unsafe.Pointer(&b))

	// 此时 a、b 指向底层数组的指针已经不同了
	fmt.Printf("a = %v, len = %d, cap = %d, type=%d\n", a, len(a), cap(a), as.Data)
	// a = [0 1 2 3 10 11 12], len = 7, cap = 7, type=824633795200

	fmt.Printf("b = %v, len = %d, cap = %d, type=%d\n", b, len(b), cap(b), bs.Data)
	// b = [0 1 2 3 10 11 12 13 14], len = 9, cap = 14, type=824633778512

}

      多个切片共享一个底层数组,其中一个切片的append操作可能引发如下两种情况:

  1. append追加的元素没有超过底层数组的容量,则会直接操作共享底层数组,如果其他切片有引用数组被覆盖的元素,则也会导致其他切片的值也隐式地发生变化。
  2. append追加的元素超过底层数组的容量,则会重新申请数组,并将原来数组值赋值到新数组。

5. 值、指针和引用

5.1 传值还是传引用

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

5.2 函数名的意义

      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 表达式

      常见的几个comma, ok 表达式如下:

  1. 获取map的值

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

package main

func main() {
	m := make(map[string]string)
	
	v, ok := m["aaa"]
	
	if ok{
		println("m['aaa'] = ", v)
	}else{
		println("m['aaa'] is nil ")
	}
}

  1. 读取chan的值

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

package main

func main() {


	c := make(chan int)
	go func() {
		c <- 1
		c <- 2
		close(c)
	}()

	for{
		// 使用 comma, ok 判断通道是否关闭
		v, ok  := <- c
		if ok{
			println(v)
		}else{
			break
		}
	}

	// 使用 range 更加简洁
	for v := range c{
		println(v)
	}
}

  1. 类型断言

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

6.3 简写模式

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

  1. import多个包
// 推荐写法
import(
	"bufio"
	"bytes"
)

// 不推荐写法
import "bufio"
import "bytes"
  1. 多个变量声明

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

// 推荐写法
var(
	a int
	b string
	c float
)

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

6.4 包中的函数或方法设计

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

6.5 多值返回函数

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值