go优化——容易犯错点记载
内容
1 切片与数组
2 defer
3 make与new
4 方法与函数
5 闭包
6 循环
1 切片和数组
- 数组和结构体都是值变量,即:如果把一个数组变量和结构体变量赋值给另外的变量,是拷贝了一份值,两者的修改互不影响;
2 . go通过切片生成另外一个切片时,两个切片共享同一个底层数组,对其中一个修改元素时,两个都会改变;
例如:
a=[]int{1,2,3}
b:=a[:]
b[0] = 2
printLn(a[0]) —》输出:2
- 如果要拷贝一个切片,使用copy函数,copy之后对新切片的修改不会影响到老切片「元素是指针除外,指针引用关系」如:
a := []int{1, 2, 3}
b := []int{4, 5, 6, 7}
copy(a, b) // 把b的前三个元素内容复制到a
fmt.Println(a) //[4 5 6]
a := []int{1, 2, 3}
b := []int{4, 5, 6, 7}
copy(b, a)
fmt.Println(b)// [1 2 3 7]
如果:a和b的长度不一样,会按照长度较小的那个的元素进行复制
- 特别注意初始化切片时,如果指定了切片的长度,go会用nil来填充这个切片, 如果是基本类型则用基本类型零值填充,此后对切片通过append操作时,会在后面进行填充;所以在初始化一个切片并且指定了容量时,要注意长度初始化为0;
a := []int{1,2,3}
b := make([]int, 0, len(a)) //Yes
B := make([]int, len(a), len(a)) //No
B1 := make([]int, 0) //Yes
b1 := make([]int, 4) //No
- 切片的append
可以使用append方法往一个切片中追加元素,如:
a := []int{1, 2, 3}
b := []int{4, 5, 6, 7}
a = append(a, b...)
fmt.Println(a) //[1 2 3 4 5 6 7]
注意:如果b和a共享一个底层数组,并且满足:1 b的容量小于a; 2 b占用的底层数组后面的位置够追加新元素时,追加元素到b时,实际会修改他们共享的底层的元素,即a元素也会被修改,不会开辟新的数组空间;
a := []int{1, 2, 3,4}
b := a[:2]
b = append(b, 4)
fmt.Println(a) // [1 2 4 4]
切片扩容长度规则:
如果切片的容量小于1024个元素,那么扩容的时候slice的cap就翻番,乘以2;
一旦元素个数超过1024个元素,增长因子就变成1.25,即每次增加原来容量的四分之一;
6 slice是值拷贝
即:在方法间传递切片是拷贝了slice 结构体, chan和map 也是一样的,值拷贝,看下面两个例子:
1 方法间传递切片,拷贝了一份切片struct
func main(){
a := []int{1,2,3,4,5}
fmt.Printf("%p\n", &a)
TestSlice(a)
}
func TestSlice(a []int){
fmt.Printf("%p\n", &a)
}
输出:
0xc00000e880
0xc0001b6000
2 切片赋值, append了之后切片底层指向的数组进行了扩容,新开辟了一份内存,
append之后编译器会根据是否是赋值给原有的切片变量做不同的逻辑,如果append了之后赋值给一个新的变量即b,
那么很好理解,拷贝了一份切片 struct,所以和原来a的地址不一样;但是如果是赋值给老的变量,即append之后还是赋值给a,编译器做了优化逻辑,没有重新拷贝;
a := []int{1,2,3,4,5}
fmt.Printf("%p\n", &a)
a = append(a, 6)
fmt.Printf("%p\n", &a)
b := append(a, 7)
fmt.Printf("%p\n", &b)
输出:
0xc0000c67c0
0xc0000c67c0
0xc00000e060
2 defer
1.对于defer,当代码运行到defer语句时,defer后要运行的函数的入参此时已经确定了(即:defer函数的入参函数此时就被执行了,而不是到了调用是才被执行),defer下面的语句对该参数做的修改对于函数无效;;
2.如果同一个函数中有多个defer,被推迟的函数按照先进后出的顺序执行(压栈出栈),即:最后一个defer会被第一个执行;
func main(){
b()
}
func un(s string) {
fmt.Println("leaving:", s)
}
func trace(s string) string {
fmt.Println("enter:", s)
return s
}
func b() {
defer un(trace("b"))
fmt.Println("in:", "b")
a()
}
func a() {
defer un(trace("a"))
fmt.Println("in:", "a")
}
打印:
enter: b
in: b
enter: a
in: a
leaving: a
leaving: b
3.defer 原理
- 编译期;
- 将
defer
关键字被转换runtime.deferproc
; - 在调用
defer
关键字的函数返回之前插入runtime.deferreturn
;
- 将
- 运行时:
-
runtime.deferproc
会将一个新的runtime._defer
结构体追加到当前 Goroutine 的链表头; -
runtime.deferreturn
会从 Goroutine 的链表中取出runtime._defer
结构并依次执行;
-
- 后调用的
defer
函数会先执行:- 后调用的
defer
函数会被追加到 Goroutine_defer
链表的最前面; - 运行
runtime._defer
时是从前到后依次执行;
- 后调用的
- 函数的参数会被预先计算;
- 调用
runtime.deferproc
函数创建新的延迟调用时就会立刻拷贝函数的参数,函数的参数不会等到真正执行时计算;
- 调用
3 make与new
make和new的区别:make只能用于slice map和channel,返回的是该类型初始化后的引用,用于出初始它们内部的数据结构,并准备好将要使用的值;new(T)返回的是指向该类型的指针。
type File struct{
name string
}
&File{} 《=》 new(File)
```
make只用于映射、切片和管道,并且不返回指针,如果要得到指针请使用new
```
var p *[]int = new([]int) // 得到指针
var v []int = make([]int, 0, 100)
var p *[]int = new([]int)
*p = make([]int, 100)
4 方法和函数
1、函数中如果入参是值参数,那么该函数只能接收值入参;如果方法中的接收器是值接收器,那么该方法可以接收值接收器和指针接收器,即:可以通过该类型接收器的值变量和指针变量调用方法;
2、函数中如果入参是指针参数,那么只能接收指针参数;如果方法中申明的接收器是指针接收器,那么该方法可以接收值接收器和指针接收器;
5 闭包
闭包:闭包是函数加运行环境组成的实体,简单来说就是一个函数引用了外层函数的变量,这个函数就是闭包,在go中通过一个函数返回一个匿名函数,这个匿名函数如果引用了外出函数,那么这个匿名函数就是一个闭包,例如下面这样:
func Test(i int) func() int {
return func() int {
i ++
return i
}
}
go闭包容易遇到的坑:
- 1 for循环中使用闭包
- 2 函数切片添加闭包
- 3 defer使用闭包
例子一 for 循环使用闭包
func main(){
s := []int{1,2,3}
for _, v := range s {
go func() {
fmt.Println(v)
}()
}
time.Sleep(time.Second * 10)
}
输出:
3
3
3
闭包引用的都是变量v,循环完毕后v指向的是3,所以输出都是3, 正确使用应该是:拷贝表里v,传入到闭包中
s := []int{1,2,3}
for _, v := range s {
go func(a int) {
fmt.Println(a)
}(v)
}
time.Sleep(time.Second * 10)
例子二 函数切片中添加闭包函数
func main(){
f := make([]func(), 0)
s := []int{1,2,3}
for _, v := range s {
f = append(f, func() {
fmt.Println(v)
})
}
for _, v := range f {
v()
}
}
输出:
3
3
3
同理:闭包引用了外层变量v,随着v的变化,闭包引用的变量值也在改变,最终都是指向3,正确做法:变量进行拷贝
for _, v := range s {
vBak := v
f = append(f, func() {
fmt.Println(vBak)
})
}
例子三: defer 闭包函数中对外层的变量引用,会在引用的这个变量做了所有运算后,取最终指向的值
func main(){
a := 1
defer func() {
fmt.Println(a)
}()
a++
}
输出:
2
6 循环
请看下面这个例子:
func main() {
type people struct {name string}
peopleList := make([]people, 0, 2)
p1 := people{
name: "lisi",
}
p2 := people{
name: "zhangsan",
}
peopleList = append(peopleList, p1)
peopleList = append(peopleList, p2)
peopleAddrList := make([]*people, 0, 2)
for _,p := range peopleList {
peopleAddrList = append(peopleAddrList, &p)
// newP := p
// peopleAddrList = append(peopleAddrList, &newP)
}
for i := range peopleAddrList {
fmt.Println(*peopleAddrList[i])
}
}
输出:
{zhangsan}
{zhangsan}
我们本意是想循环一个结构体切片,然后获取这个切片的各个元素的结构体指针,然后把各个元素结构体的指正放到另外一个结构体指针切片中,但是结果却是新的切片里元素全部是都是老切片的最后一个元素,出现这种情况的原因是:在循环过程中,循环变量p是一个临时变量,在循环内一直是引用这个变量来存储遍历的值,所以在循环结束后,&p会指向老切片的最后一个值,而peopleAddrList里的元素全部都是&p,自然peopleAddrList列表元素全部都是peopleList的最后一个值了;
正确使用:
- 可以在循环内进行把p赋值给一个新的变量,像注释的那样
引用:
- 《go语言实现与设计》https://draveness.me/golang/
- 《effective go》 https://www.kancloud.cn/kancloud/effective/72214