go优化——容易犯错点记载

2020-07-12  本文已影响0人  chase_lwf

内容

1 切片与数组
2 defer
3 make与new
4 方法与函数
5 闭包
6 循环

1 切片和数组

  1. 数组和结构体都是值变量,即:如果把一个数组变量和结构体变量赋值给另外的变量,是拷贝了一份值,两者的修改互不影响;

2 . go通过切片生成另外一个切片时,两个切片共享同一个底层数组,对其中一个修改元素时,两个都会改变;

例如:
    a=[]int{1,2,3}
    b:=a[:]
    b[0] = 2
    printLn(a[0]) —》输出:2
  1. 如果要拷贝一个切片,使用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的长度不一样,会按照长度较小的那个的元素进行复制

  1. 特别注意初始化切片时,如果指定了切片的长度,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
  1. 切片的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

参考:https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-array-and-slice/#324-%E8%BF%BD%E5%8A%A0%E5%92%8C%E6%89%A9%E5%AE%B9

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 原理

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闭包容易遇到的坑:

例子一 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的最后一个值了;
正确使用:

引用:

  1. 《go语言实现与设计》https://draveness.me/golang/
  2. 《effective go》 https://www.kancloud.cn/kancloud/effective/72214
上一篇下一篇

猜你喜欢

热点阅读