数组与切片
数组与切片
本文为极客时间《Go语言核心36讲》的学习笔记,梳理了相关的知识点。
两者区别
Go 语言的切片类型属于引用类型,同属引用类型的还有字典类型、通道类型、函数类型等;而 Go 语言的数组类型则属于值类型,同属值类型的有基础数据类型以及结构体类型。简单的说,数组类型的长度是固定的,而切片类型是可变长的。数组的容量永远等于其长度,都是不可变的。
切片
切片的结构:
代码路径:runtime/slice.go
type slice struct {
array unsafe.Pointer //指向数组的指针
len int //当前切片的长度
cap int //当前切片的容量
}
切片的结构体由3部分构成,Pointer 是指向一个数组的指针,len 代表当前切片的长度,cap 是当前切片的容量。cap 总是大于等于 len 的,可以参考下面这段源码。
func makeslice(et *_type, len, cap int) unsafe.Pointer {
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
if overflow || mem > maxAlloc || len < 0 || len > cap {
// NOTE: Produce a 'len out of range' error instead of a
// 'cap out of range' error when someone does make([]T, bignumber).
// 'cap out of range' is true too, but since the cap is only being
// supplied implicitly, saying len is clearer.
// See golang.org/issue/4085.
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
return mallocgc(mem, et, true) //在堆上申请一块连续的内存
}
切片的扩容和缩容
切片的扩容
假设原有切片的容量是A,当切片需要扩容的时候: 第一,会先判定需要扩容的容量X,如果X大于两倍的A,那么直接扩容超过X。也就是说新的切片的容量将会是大于等于X。 第二,如果X小等于两倍的A,此时需要进行再一次的判断,旧容量A如果小于1024,直接扩容两倍的A,此时新的切片容量是2A(理论上)。 第三,如果X小等于两倍的A,且旧容量A大于1024,会循环扩容1.25倍,直到大于X为止。此时的新切片的容量是(n 1.25 A)
以上的计算是理论上的值,实际上的值可能会稍微大一些。
具体代码:/runtime/slice.go 中的 growslice方法。
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 //等价于 newcap = 1.25*newcap
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
在1.18版本之后,切片的扩容做了一些优化: 假设原有切片的容量是A,当切片需要扩容的时候: 第一,会先判定需要扩容的容量X,如果X大于两倍的A,那么直接扩容超过X。也就是说新的切片的容量将会是大于等于X。 第二,如果X小等于两倍的A,此时需要进行再一次的判断。如果A小于256,那么直接扩容到两倍的A,此时新的切片容量是2A(理论上)。 第三,如果X小等于两倍的A,且旧容量A大于256,会循环扩容(A+256_3)_0.25,直到大于X为止。此时的新切片的容量是(A+256_3)_0.25*n
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
const threshold = 256
if old.cap < threshold {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
// Transition from growing 2x for small slices
// to growing 1.25x for large slices. This formula
// gives a smooth-ish transition between the two.
newcap += (newcap + 3*threshold) / 4
}
if newcap <= 0 { //处理扩容时的边界情况
newcap = cap
}
}
}
切片扩容完成后,会再进行一次容量的调整,进而降低内存浪费,提高使用效率。
切片的缩容
Go 本身未找到缩容的地方。 在具体使用中,可以用copy的方法,创建新的切片和底层数组。并把原来的切片置nil。 其实不缩容也没关系,系统扫到这块内容没有人再使用了,就会触发GC。
切片的底层数组什么时候会替换
准确的说,一个切片不存在底层数组被替换的情况。当一个切片容量不够时,会给他创建一个新的切片,这个切片有自己的底层数组,自己的结构,自己的内存地址。 我们看到某个切片变量被扩容了,实际上是这个变量内容发生了变化。具体而言,该变量的内存地址不变,但是地址里的东西发生了变化。 真正会导致底层数据发生变化的只有扩容的时候。因为数组不能被扩容这个缘故,需要重新创建一个新的底层数组,并创建一个新的切片信息。
a := []int{1,2,3,4,5,6,}
println(a)
//a = a[:3] //裁剪三个
//println(a) //地址不会变化,容量不会变化,长度会变化
a = append(a,make([]int, 5)...) //增加5个元素
println(a) //地址 容量,长度 都会变
一些拓展问题
关于append的之后,切片的具体变化情况。
如果append时,引发了切片扩容,那么新的切片内容会发生变化,包括底层数组,长度。如果没有触发扩容,那么只有长度会发生变化。具体可以看代码:
s6 := make([]int, 0)
//s6 := make([]int, 1,10)
println(s6)
fmt.Printf("The capacity of s6: %d\n", cap(s6))
for i := 1; i <= 5; i++ {
s6 = append(s6, i)
println(s6) //一旦触发扩容,地址信息会变
fmt.Printf("s6(%d): len: %d, cap: %d\n", i, len(s6), cap(s6)) //长度在稳定增加,但是容量会跳着增加
}
fmt.Println()
range 循环时切片的具体变化
切片在range 循环时,value的赋值一样是值传递,本身的地址不变,内容会变。并且循环内对value 的修改,不会影响原来的切片内容。
var b = [6]int{1,2,3,4,5,6,}
for key, value := range b {
fmt.Printf("value的值:%d,value的地址:%x,切片该元素的地址:%x\n", value, &value, &b[key])
//Value的值不会变,value 的地址不变
//println(reflect.TypeOf(value))
value += 1 //验证下会不会改变原,需要再次以理解”值传递“的概念
b[key] += 1 //这样才会变
}
fmt.Printf("value的值:%d",b) //值没有加1
空切片 与nil切片
var a []int //nil切片,只定义了类型,slice.array内容指向nil。
println(a) //[0/0]0x0
b := make( []int , 0 ) //空切片,有类型,有地址
b := []int{ }
println(b) // [0/0]0xc00003e778
- nil切片被用在很多标准库和内置函数中,返回一个不存在的切片。
- 空切片是一个定义好的,分配好内存的切片,只是切片里面没有任何元素。
make 和 new 的区别
Make 是专门用来创建 slice、map、channel 的值的。它返回的是被创建的值,并且立即可用。 New 是申请一小块内存并标记它是用来存放某个值的。它返回的是指向这块内存的指针,而且这块内存并不会被初始化。或者说,对于一个引用类型的值,那块内存虽然已经有了,但还没法用(因为里面没有针对那个值的数据结构)。 所以,对于引用类型的值,不要用 new,能用 make 就用 make,不能用 make 就用复合字面量来创建。
引申
这里需要增加对于“值传递”,这个概念的理解。
func main(){
Cat1 := Cat{
Name: "abc1",
Age: 12,
}
Cat2 := Cat{
Name: "abc2",
Age: 13,
}
cats := make([]*Cat,2) //这里定义的是指针接口体
cats[0] = &Cat1
cats[1] = &Cat2
for _, cat := range cats {
fmt.Printf("Cat:%+v\n",cat)
cat.Age = 14 // 这里会改变数据
}
fmt.Printf("Cat:%+v\n",cats)
}
type Cat struct {
Name string
Age int64
}
func (c *Cat)String()string {
return fmt.Sprintf("%#v",c)
}
当我把切片改为:
cats := make([]Cat,2)
后面还会变么?
学习资料:
Go 语言切片的实现原理 | Go 语言设计与实现
package main
import (
"fmt"
)
func main() {
f1()
fmt.Println("===========")
f2()
}
func f1() {
intSlice := []int{1, 2, 3, 4, 5}
for index, value := range intSlice {
intSlice = append(intSlice, value*10)
fmt.Println(index, value)
}
}
func f2() {
m := map[int]int{
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
}
for k, v := range m {
m[k*10] = v * 10
fmt.Println(k, v)
}
}