slice与array的区别

目录

一.(slice和array)两者区别

二.关于slice的问题

         (1)理解slice的引用类型

         (2)for range (slice和map)时遇到的“坑”

         ##坑一

         ##坑二

         (3)slice是否线程安全?

         实现slice线程安全有两种方式

(4)slice的共享存储空间问题

(5)slice的扩容

          ##1 slice扩容方式 

         (6)内存泄漏问题

         Case1:

         Case2:

参考:


一.(slice和array)两者区别

       数组是一个固定长度的,初始化时候必须要指定长度,不指定长度的话就是切片了

       数组是值类型,将一个数组赋值给另一个数组时,传递的是一份深拷贝,赋值和函数传参操作都会复制整个数组数据,会占用额外的内存;切片是引用类型,将一个切片赋值给另一个切片时,传递的是一份浅拷贝,赋值和函数传参操作只会复制len和cap,但底层共用同一个数组,不会占用额外的内存。

        数组的长度=容量,数组一旦定义了长度就不可以改变,数组的空余位置用0填补,不允许数组越界。切片的内存会随着切片的长度的增加而进行判断扩容

二.关于slice的问题

        (1)理解slice的引用类型



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

 sa := a[2:7]

 sa = append(sa, 100)

 sb := sa[3:8]

 sb[0] = 99

 fmt.Println(a)  //输出:[1 2 3 4 5 99 7 100 9 0]

 fmt.Println(sa) //输出:[3 4 5 99 7 100]

 fmt.Println(sb) //输出:[99 7 100 9 0]

 a := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
 sa := a[2:7]
 sb := sa[3:8]
 fmt.Printf("%p\n", sa)   //输出:0xc084004290
 fmt.Println(&a[2], &sa[0])      //输出:0xc084004290 0xc084004290
 fmt.Printf("%p\n", sb)   //输出:0xc0840042a8
 fmt.Println(&a[5], &sb[0])      //输出:0xc0840042a8 0xc0840042a8

Slice是对源数组的一个引用,改变Slice中的元素的值,实质上就是改变源数组的元素的值。

可以看到,不管是append操作,还是赋值操作,都影响了源数组或者其他引用同一数组的Slice的元素。Slice进行数组引用的时候,其实是将指针指向了内存中具体元素的地址,如数组的内存地址,事实上是数组中第一个元素的内存地址,Slice也是如此。 

     (2)for range (slice和map)时遇到的“坑”

 ##坑一

	//for range 对切片是值得拷贝
	s := []int{1, 2, 3}
	for _, v := range s {
		if v == 1 {
			//v=10  //for range 是对切片s中的值进行拷贝,所以v=10不能改变原切片的内容
			s[0] = 10
		}
	}

 上面 v=10  是不能改变原切片的值的。因为for range 是对切片s中的值进行拷贝,所以v=10不能改变原切片的内容。

##坑二

	m := make(map[int]*int)
	for i, v := range s {
		//m[i] = &v 这样是不行的,
		//因为for range 迭代过程是根据slice中的变量遍历出来的新变量,遍历出来的值都是同一地址。所以应该用一个变量进行接受,这样每个不同值的地址就不同了
		n := v
		m[i] = &n //这样才是正确的姿势
	}
	fmt.Println(s)
	for _, v := range m {
		fmt.Println(*v)
	}

       for range 遍历切片的值,其内存地址都是相同的,因为for range 创建的是每个元素的拷贝,而不是返回每个元素的引用。不能通过遍历出来的值的内存地址作为指向每个元素的指针。需要一个变量进行接受在进行赋值给map才行 

     (3)slice是否线程安全?

/**
* 切片非并发安全
* 多次执行,每次得到的结果都不一样
* 可以考虑使用 channel 本身的特性 (阻塞) 来实现安全的并发读写
 */
func TestSliceConcurrencySafe(t *testing.T) {
    a := make([]int, 0)
    var wg sync.WaitGroup
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go func(i int) {
            a = append(a, i)
            wg.Done()
        }(i)
    }
    wg.Wait()
    t.Log(len(a)) 
    // not equal 10000
}

             slice不支持并发读写,所以并不是线程安全的,使用多个 goroutine 对类型为 slice 的变量进行操作,每次输出的值大概率都不会一样,与预期值不一致; slice在并发执行中不会报错,但是数据会丢失。

     实现slice线程安全有两种方式:

            ##1.通过加锁实现slice线程安全,适合对性能要求不高的场景。

func TestSliceConcurrencySafeByMutex(t *testing.T) {
    var lock sync.Mutex //互斥锁
    a := make([]int, 0)
    var wg sync.WaitGroup
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            lock.Lock()
            defer lock.Unlock()
            a = append(a, i)
        }(i)
    }
    wg.Wait()
    t.Log(len(a)) 
    // equal 10000
}

           ##2 .通过channel实现slice线程安全,适合对性能要求高的场景。 

func TestSliceConcurrencySafeByChanel(t *testing.T) {
    buffer := make(chan int)
    a := make([]int, 0)
    // 消费者
    go func() {
        for v := range buffer {
            a = append(a, v)
        }
    }()
    // 生产者
    var wg sync.WaitGroup
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            buffer <- i
        }(i)
    }
    wg.Wait()
    t.Log(len(a)) 
    // equal 10000
}

     (4)slice的共享存储空间问题

       多个切片如果共享同一个底层数组,这种情况下,对其中一个切片或者底层数组的更改,会影响到其他切片

/**
* 切片共享存储空间
 */
func TestSliceShareMemory(t *testing.T) {
    slice1 := []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"}
    Q2 := slice1[3:6]
    t.Log(Q2, len(Q2), cap(Q2)) 
    // [4 5 6] 3 9
    Q3 := slice1[5:8]
    t.Log(Q3, len(Q3), cap(Q3)) 
    // [6 7 8] 3 7
    Q3[0] = "Unkown"
    t.Log(Q2, Q3) 
    // [4 5 Unkown] [Unkown 7 8]

    a := []int{1, 2, 3, 4, 5}
    shadow := a[1:3]
    t.Log(shadow, a)             
    // [2 3] [1 2 3 4 5]
    shadow = append(shadow, 100) 
    // 会修改指向数组的所有切片
    t.Log(shadow, a)            
  // [2 3 100] [1 2 3 100 5]
}

     (5)slice的扩容

扩容会发生在slice append的时候,当slice的cap不足以容纳新元素,就会进行扩容

func growslice(et *_type, old slice, cap int) slice {
      // 省略一些判断...
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        if old.len < 1024 {
            newcap = doublecap
        } else {
            // Check 0 < newcap to detect overflow
            // and prevent an infinite loop.
            for 0 < newcap && newcap < cap {
                newcap += newcap / 4
            }
            // Set newcap to the requested cap when
            // the newcap calculation overflowed.
            if newcap <= 0 {
                newcap = cap
            }
        }
    }
    // 省略一些后续...
}

        ##1 slice扩容方式 

  • 如果新申请容量比两倍原有容量大,那么扩容后容量大小 等于 新申请容量
  • 如果原有 slice 长度小于 1024, 那么每次就扩容为原来的 2 倍
  • 如果原 slice 大于等于 1024, 那么每次扩容就扩为原来的 1.25 倍

        ##2  slice进行append添值时地址的变化

  •  如果使用append添加值时,添加后长度<=底层数组的长度,则地址不变
  •  如果使用append添加值时,添加后长度>底层数组的长度,则内存管理器就会重新开辟一个更大的内存空间,用于存储多出来的元素,并且会将原来的元素复制一份,放到这块新开辟的内存空间。所以内存地址就改变了。

     (6)内存泄漏问题

       由于slice的底层是数组,很可能数组很大,但slice所取的元素数量却很小,这就导致数组占用的绝大多数空间是被浪费的

       

Case1:

        比如下面的代码,如果传入的slice b是很大的,然后引用很小部分给全局量a,那么b未被引用的部分(下标1之后的数据)就不会被释放,造成了所谓的内存泄漏。

var a []int

func test(b []int) {
    a = b[:1] // 和b共用一个底层数组
    return
}

那么只要全局量a在,b就不会被回收。

如何避免?

在这样的场景下注意:如果我们只用到一个slice的一小部分,那么底层的整个数组也将继续保存在内存当中。当这个底层数组很大,或者这样的场景很多时,可能会造成内存急剧增加,造成崩溃。

所以在这样的场景下,我们可以将需要的分片复制到一个新的slice中去,减少内存的占用

var a []int

func test(b []int) {
    a = make([]int, 1)
    copy(a, b[:0])
    return
}

Case2:

比如下面的代码,返回的slice是很小一部分,这样该函数退出后,原来那个体积较大的底层数组也无法被回收

func test2() []int{
    s = make([]int, 0, 10000)
    for i := 0; i < 10000; i++ {
        s = append(s, p)
    }
    s2 := s[100:102]
    return s2
}

如何避免?

将需要的分片复制到一个新的slice中去,减少内存的占用

func test2() []int{
    s = make([]int, 0, 10000)
    for i := 0; i < 10000; i++ {
      // 一些计算...
        s = append(s, p)
    }
    s2 := make([]int, 2)
    copy(s2, s[100:102])
    return s2
}

三.slice总结

  • 创建切片时可根据实际需要预分配容量,尽量避免追加过程中进行扩容操作,有利于提升性能
  • 使用 append() 向切片追加元素时有可能触发扩容,扩容后将会生成新的切片
  • 使用 len()、cap()计算切片长度、容量时,时间复杂度均为 O(1),不需要遍历切片
  • 切片是非线程安全的,如果要实现线程安全,可以加锁或者使用Channel
  • 大数组作为函数参数时,会复制整个数组数据,消耗过多内存,建议使用切片或者指针
  • 切片作为函数参数时,可以改变切片指向的数组,不能改变切片本身len和cap;想要改变切片本身,可以将改变后的切片返回 或者 将切片指针作为函数参数。
  • 如果只用到大slice的一小部分,建议将需要的分片复制到一个新的slice中去,减少内存的占用

参考:

        彻底理解Golang Slice - 简书 (jianshu.com)  

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值