语言陷阱
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)右边是一个多返回值得表达式,可以是返回多汁得函数调用,也可以是 range
对map
、slice
等函数得操作,还可以是类型断言。
// 函数调用
x, y = f()
// range 表达式
for k, v := range map{
}
// type assertion
v, ok := i.(xxxx)
(2)赋值的左边操作数和右边的单一返回值的表达式个数一样,逐个从左向右依次对左边的操作数赋值。
x, y, z = a, b, c
多值赋值的语义
- 对左侧操作数中的表达式、索引值进行计算和确定,首先确定左侧的操作数的地址;然后对右侧的赋值表达式进行计算,如果发现右侧的表达式计算引用了左侧的变量,则创建临时变量进行值拷贝,最后完成计算。
- 从左到右的顺序依次赋值
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
}
- 第 8 行先计算
x[i]
中的数组索引i
的值,此时i=0
,两个被赋值变量是i
和x[0]
,然后从左到右赋值操作i=1
,x[0]=2
- 第 13 行和第 8 行的逻辑一样
- 第 16 行先计算赋值语句左右两侧
x[i]
中的数组索引i
的值,此时i=0
,两个被赋值变量是i
和x[0]
,两个赋值变量分别是2
、x[0]
。由于x[0]
是左边的操作数,所以编译器创建一个临时变量tmp
,将其赋值为x[0]
,然后从左到右依次赋值操作x[0]=2
,i=tmp
,i
的值为1 - 第 22 行按照从左到右的执行顺序,先执行
x[0]=1
,然后执行x[0]=2
,所以最后x[0]
的值为2
1.2 短变量的声明和赋值
短变量的声明和赋值是指在 Go 函数或类型方法内部使用 “:=
”声明并初始化变量,支持多值赋值,格式如下:
a := va
a, b := va, vb
- 使用“
:=
”操作符,变量的定义和初始化同时完成 - 变量名后不要跟任何类型名,Go 编译器完全靠右边的值进行推导
- 支持多值短变量声赋值
- 只能用在函数和类型方法的内部
在多值短变量声明和赋值时,至少有一个变量是新创建的局部变量,其他的变量可以复用以前的变量,不是新创建的变量执行的仅仅是赋值。
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
什么时候定义新变量,什么时候复用已存在变量有以下规则:
- 如果想通过编译,则
a
和b
中至少要有一个是新定义的局部变量。 - 如果在赋值语句
a, b := va, vb
所在的代码块中已经存在一个局部变量a
,则赋值语句a, b := va, vb
不会创建新变量a
,而是直接使用va
赋值给已经声明的局部变量a
,但是会创建新变量b
,并将vb
赋值给b
。 - 如果在赋值语句
a, b := va, vb
所在的代码块中没有局部变量a
和b
,但在全局命名空间有变量a
和b
,则该语句会创建新的局部变量a
和b
并使用va
、vb
初始化它们。此时赋值语句所在的局部作用域类内,全局的a
和b
被屏蔽。
赋值操作符=
和:=
的区别:
=
不会声明并创建新变量,而是在当前赋值语句所在的作用域由内向外逐层去搜寻变量,如果没有搜索到相同的变量名,则编译错误。:=
必须出现在函数或类型方法内部:=
至少要创建一个局部变量并初始化
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()
}
通过输出可以看到,程序并没有如预期一样变量整个切片,原因如下:
for range
下的迭代变量i
的值是共用的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())
}
对于有名函数,有如下特点:
- 函数调用方负责开辟栈空间,包括形参和返回值的空间
- 有名的函数返回值相当于函数的局部变量,被初始化为类型的零值
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)执行 defer
,defer
后匿名函数使用的是传参数调用,在注册defer
函数时将函数返回值r
作为实参传进去,由于函数调用是值拷贝,所以defer
函数执行后只是形参值变为 5,对实参没有任何影响。
(4)函数返回,此时函数返回值栈区上的值是 1
4. 切片困惑
4.1 数组
Go
中的数组是一种基本类型,数组的类型不仅包括其元素类型,也包括其大小,[2]
int 和[5]
int 是两个完全不同的数组类型。
创建数组
- 声明时通过字面量进行初始化
- 直接声明,不显式地进行初始化
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 中,数组的一切传递都是值拷贝,体现在以下三个方面:
- 数组间的直接赋值
- 数组作为函数参数
- 数组内嵌到
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 切片
切片创建
- 通过数组创建
array[m:n]
创建一个包含[m,n)个元素的切片。
- make
通过内置的make
函数创建,make([]T, len, cap)
中的 T
是元素类型,len
是长度,cap
是底层数组的容量,cap
是可选参数。
- 直接声明
可以直接声明一个切片,也可以在声明切片的过程中使用字面量进行初始化,直接声明但不进行初始化的切片其值为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
操作可能引发如下两种情况:
append
追加的元素没有超过底层数组的容量,则会直接操作共享底层数组,如果其他切片有引用数组被覆盖的元素,则也会导致其他切片的值也隐式地发生变化。append
追加的元素超过底层数组的容量,则会重新申请数组,并将原来数组值赋值到新数组。
5. 值、指针和引用
5.1 传值还是传引用
Go 语言只有一种参数传递规则,那就是值拷贝。
5.2 函数名的意义
Go 的函数名和匿名函数字面量的值有 3 层含义:
- 类型信息,表明其数据类型是函数类型
- 函数名代表函数的执行代码的起始位置
- 可以通过函数名进行函数调用,函数调用格式为
func_name(param_list)
。在底层执行层面包含以下 4 部分内容。
- 准备好参数
- 修改 PC 值,跳转到函数代码起始位置开始执行
- 赋值值到函数的返回值栈区
- 通过 RET 返回函数调用的下一条指令处继续执行
6. 习惯用法
6.1 干净
- 编译器不能通过未使用的局部变量(包括未使用的标签)
import
未使用的包同样通不过编译- 所有的控制结构、函数和方法定义的“
{
”放到结尾,而不能另起一行。 - 提供
go fmt
工具格式化代码,使所有的代码风格保持统一
6.2 comma, ok 表达式
常见的几个comma, ok
表达式如下:
- 获取
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 ")
}
}
- 读取
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)
}
}
- 类型断言
类型断言通常可以使用comma, ok
语句来确定接口是否绑定了某个实例类型,或者判断接口绑定的实例类型是否实现另一个接口。
6.3 简写模式
Go 语言很多重复的引用或声明可以使用“()
”进行简写
import
多个包
// 推荐写法
import(
"bufio"
"bytes"
)
// 不推荐写法
import "bufio"
import "bytes"
- 多个变量声明
包中多个相关全局变量声明时,监视使用"()
"进行合并声明
// 推荐写法
var(
a int
b string
c float
)
// 不推荐写法
var a int
var b string
var float
6.4 包中的函数或方法设计
很多包开发者会在内部实现两个"同名
"的函数或方法,一个首字母大写,用于导出API供外部使用;一个首字母小写,用于实现具体逻辑。一般首字母大写的函数调用首字母小写的函数,同时包装一些功能;首字母小写的函数负责更多的底层细节。
6.5 多值返回函数
多值返回函数里如果有error
或bool
类型的返回值,则应该将error
或bool
作为最后一个返回值。