go语言中slice原理和坑。

本文探讨了Go语言中Slice的数据结构和动态扩容原理,指出Slice在实践中的潜在问题,特别是扩容陷阱,通过示例代码解释了为何在某些情况下连续append会导致容量变化,并提醒开发者在与前端交互时注意Slice为空与null的区别,以避免bug。

slice 原理

Slice数据结构和原理

  • 1:相对于数组,Slice的长度是动态可变的。如下:
func CreatSlice() {
	s := make([]int, len(), cap())
	var s1 []int
}
func CreatArr() {
	var a [length]int
}

可以很清楚的看到,数组的长度是在编译时静态计算的,并且数组无法在运行时动态扩缩容量的。

  • 2:在go的/src/runtime/slice.go中可以看到如下:
type slice struct {
    array unsafe.Pointer
    len int
    cap int
}

此外无论是数组还是Slice都是按照值传递。并且!!!!goland中只有值传递!!!

  • 3:slice和数组的传递性能区别。
    talk is cheap, show code
func CallSlice(s []int) {

}
func CallArr(s [10000]int) {

}

func BenchMarkCallSlice(b *testing.B) {
    s := make([]int, 10000)
    for i := 0; i < b.N; i++ {
        CallSlice(s)
    }
}

func BenchMarkCallArr(b *testing.B) {
    var a [10000]int
    for i := 0; i < b.N; i++ {
        CallArr(a)
    }
}

结果如下

Slice的实践

  • 1:slice扩容过程中的坑。
       var s []int
       fmt.Println(len(s),cap(s))//0,0

这个就很简单的可以理解了,增加一个元素,容量和长度增加1个

	s = append(s, 0)
	fmt.Println(len(s), cap(s)) //1,1

	//s = append(s, 1, 2)
	//fmt.Println(len(s), cap(s)) //3,3

	s = append(s, 1)
        fmt.Println(len(s), cap(s)) //2,2
	s = append(s, 2)
	fmt.Println(len(s), cap(s)) //3,4

但是这个注释掉的和下面的有什么区别?为什么长度一样,容量不一样?

因为切片的每次扩容是前面扩容过的一倍,注释掉的代码就是一下append了两个,所以容量也是增加两个(内存优化)。

但是我们连续两次的append,第一次append后的容量为2,是1的二倍,然后再次append,再次翻倍,这也就是为什么为4。

最后:

	for i := 3; i < 1025; i++ {
		s = append(s, i)
	}
	fmt.Println(len(s), cap(s)) //1025,1280

为什么容量会为1280,而不是2048呢?talk is cheap, show code。我们可以在runtime/slice中找到growslice的方法,如下。

newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
		newcap = cap
	} else {
		if old.cap < 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
			}
		}
	}

之后我们就能看到当容量大于等于1024情况下,增长倍数近似看为1.25倍。

扩容陷阱

先看三个例子

demo1

func main() {
	var s []int
	for i := 0; i < 3; i++ {
		s = append(s, i)
	}
	modifySlice(s)
	fmt.Println(s) //[1024,1,2]
}

func modifySlice(s []int) {
	s[0] = 1024
}

demo2

func main() {
	var s []int
	for i := 0; i < 3; i++ {
		s = append(s, i)
	}
	modifySlice(s)
	fmt.Println(s) //[1024,1,2]
}

func modifySlice(s []int) {
	s = append(s, 2048)
	s[0] = 1024
}

demo3

func main() {
	var s []int
	for i := 0; i < 3; i++ {
		s = append(s, i)
	}
	modifySlice(s)
	fmt.Println(s) //[0 1 2]
}

func modifySlice(s []int) {
	s = append(s, 2048)
	s = append(s, 4096)
	s[0] = 1024
}


demo4

func main() {
	var s []int
	for i := 0; i < 3; i++ {
		s = append(s, i)
	}
	modifySlice(s)
	fmt.Println(s) //[1024 1 2]
}

func modifySlice(s []int) {
	s[0] = 1024
	s = append(s, 2048)
	s = append(s, 4096)
}

重点来说demo2和demo3

首先demo2,在modifySlice的过程中,其实是传递的一个slice的一个结构体,虽然共享了底层的存储,但是在modifySlice里的操作,比如扩容,在外层是看不到的,所以也就是为什么2048无法打印出来。但是s[0]是直接操作了原有的结构体,所以,s[0]是可以更改的。

接下来是demo3,在第一次append的时候,len和cap都为4了,如果再次进行append会导致扩容,这个时候就导致两个s的底层存储空间发生变化。所以无法进行更改了。

常见问题

如下:

	var s []int
	b, _ := json.Marshal(s)
    fmt.Println(string(b)) //null 
	s := []int{}
	b, _ := json.Marshal(s)
	fmt.Println(string(b)) //[]

主要是和前端交互判断好是null还是[],否则容易产生bug。

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Carpe-Wang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值