目录
(2)for range (slice和map)时遇到的“坑”
一.(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中去,减少内存的占用