美文网首页
go优化——容易犯错点记载

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

作者: chase_lwf | 来源:发表于2020-07-12 15:42 被阅读0次

    内容

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

    • 后调用的 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赋值给一个新的变量,像注释的那样

    引用:

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

    相关文章

      网友评论

          本文标题:go优化——容易犯错点记载

          本文链接:https://www.haomeiwen.com/subject/zrnvqktx.html