Golang slice

本文深入探讨了Go语言中切片的内部结构,包括其作为引用类型的特点,如何与数组关联,以及切片的初始化、扩容、拷贝和比较。切片通过SliceHeader结构体表示,底层基于数组,允许动态调整长度。通过字面量或make关键字可以创建切片,append操作可能导致扩容。切片拷贝是浅拷贝,而copy函数实现深拷贝。文章还讨论了切片与数组的区别,以及在性能上的考量。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

切片是引用类型,不支持直接比较,只能和nil比较
1:底层数据结构 

type Slice struct {
	Elem *Type // element type
}

cmd/compile/internal/types.NewSlice  

func NewSlice(elem *Type) *Type {
	if t := elem.Cache.slice; t != nil {
		if t.Elem() != elem {
			Fatalf("elem mismatch")
		}
		return t
	}

	t := New(TSLICE)
	t.Extra = Slice{Elem: elem}
	elem.Cache.slice = t
	return t
}

Extra 字段是一个只包含切片内元素类型的结构。也就是说切片内元素的类型都是在编译期间确定的。编译器确定了类型之后,会将类型存储在 Extra 字段中帮助程序在运行时动态获取。

2 SliceHeader

编译期间的slice是上述的形式,但是运行时切片可以由如下的结构体表示

type SliceHeader struct {
	Data uintptr    //是指向数组的指针;
	Len  int        //当前切片的长度;
	Cap  int        //是当前切片的容量即 Data 数组的大小
}    

切片的底层是数组,切片其实是引入了一个抽象层,提供了对数组中部分连续片段的引用,而作为数组的引用,我们可以在运行区间可以修改它的长度和范围。当切片底层的数组长度不足时就会触发扩容,切片指向的数组可能会发生变化,不过在上层看来切片是没有变化的,上层只需要与切片打交道不需要关心数组的变化。

3初始化:

arr[0:3] or slice[0:3]     //通过下标的方式获得数组或者切片的一部分;
slice := []int{1, 2, 3}    //使用字面量初始化新的切片;
slice := make([]int, 10)   //使用关键字 make 创建切片:

使用下标初始化切片不会拷贝原数组或者原切片中的数据,它只会创建一个指向原数组的切片结构体,所以修改新切片的数据也会修改原切片

数组和切片比较:

编译器在编译期间简化了获取数组大小、读写数组中的元素等操作:因为数组的内存固定且连续,多数操作都会直接读写内存的特定位置。但是切片是运行时才会确定内容的结构,所有操作还需要依赖 Go 语言的运行时

4:字面量初始化

当我们使用字面量 []int{1, 2, 3} 创建新的切片时,cmd/compile/internal/gc.slicelit 函数会在编译期间将它展开成如下所示的代码片段:

var vstat [3]int
vstat[0] = 1
vstat[1] = 2
vstat[2] = 3
var vauto *[3]int = new([3]int)
*vauto = vstat        //值拷贝  不是 vauto = &vstat 
slice := vauto[:]

 slice := vauto[:]  执行完之后,改变vstat的值  不会影响到 slice了。。。如果是vauto = &vstat  那么new出来的内存也泄露了,改变了vstat的只  slice 也改变了。。。

  1. 根据切片中的元素数量对底层数组的大小进行推断并创建一个数组
  2. 将这些字面量元素存储到初始化的数组中
  3. 创建一个同样指向 [3]int 类型的数组指针
  4. 将静态存储区的数组 vstat 赋值给 vauto 指针所在的地址
  5. 通过 [:] 操作获取一个底层使用 vauto 的切片

 从上边最后一条我们也能看出 [:] 操作是创建切片最底层的一种方法。

5:make方式初始化slice

首先检查切片的大小和容量是否足够小,然后切片是否发生了逃逸,最终在堆上初始化

当切片发生逃逸或者非常大时,运行时需要 runtime.makeslice 在堆上初始化切片,如果当前的切片不会发生逃逸并且切片非常小的时候,make([]int, 3, 4) 会被直接转换成如下所示的代码:

var arr [4]int
n := arr[:3]

6:append 扩容

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 {
			for 0 < newcap && newcap < cap {
				newcap += newcap / 4
			}
			if newcap <= 0 {
				newcap = cap
			}
		}
	}

 在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:

如果期望容量大于当前容量的两倍就会使用期望容量;

如果当前切片的长度小于 1024 就会将容量翻倍;

如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量

 

var arr []int64
arr = append(arr, 1, 2, 3, 4, 5)

当我们执行上述代码时,会触发 runtime.growslice 函数扩容 arr 切片并传入期望的新容量 5,这时期望分配的内存大小为 40 字节;不过因为切片中的元素大小等于sys.PtrSize,所以运行时会调用 runtime.roundupsize 向上取整内存的大小到 48 字节,所以新切片的容量为 48 / 8 = 6。

7:拷贝切片

copy(a,b) 把b拷贝到a里边,如果copy是编译期间 不是在运行时调用的,copy会被转换成下边的代码

n := len(a)
if n > len(b) {
    n = len(b)
}
if a.ptr != b.ptr {
    memmove(a.ptr, b.ptr, n*sizeof(elem(a))) 
}

 如果拷贝是在运行时发生的,例如:go copy(a,b),编译器会使用 runtime.slicecopy 替换运行期间调用的 copy,该函数的实现很简单:

func slicecopy(to, fm slice, width uintptr) int {
	if fm.len == 0 || to.len == 0 {
		return 0
	}
	n := fm.len
	if to.len < n {
		n = to.len
	}
	if width == 0 {
		return n
	}
	...

	size := uintptr(n) * width
	if size == 1 {
		*(*byte)(to.array) = *(*byte)(fm.array)
	} else {
		memmove(to.array, fm.array, size)
	}
	return n
}

无论是编译期间拷贝还是运行时拷贝,两种拷贝方式都会通过 runtime.memmove 将整块内存的内容拷贝到目标的内存区域中:

相比于依次拷贝元素,runtime.memmove 能够提供更好的性能。需要注意的是,整块拷贝内存仍然会占用非常多的资源,在大切片上执行拷贝操作时一定要注意对性能的影响。

-----------------------------------------------------------2021-06-08----------------------------------------------------------

切片是引用类型,都指向了底层的一个数组,切片就是一个框,框住了一块连续的内存

var name []T
name:表示变量名
T:表示切片中的元素类型

var a []string              //声明一个字符串切片
var b = []int{}             //声明一个整型切片并初始化
var c = []bool{false, true} //声明一个布尔切片并初始化
var d = []bool{false, true} //声明一个布尔切片并初始化
fmt.Println(a == nil)       //true
fmt.Println(b == nil)       //false
fmt.Println(c == nil)       //false
// fmt.Println(c == d)   //切片是引用类型,不支持直接比较,只能和nil比较

切片拥有自己的长度和容量,我们可以通过使用内置的len()函数求长度,使用内置的cap()函数求切片的容量

切片的长度就是它元素的个数,切片的容量是底层数组从切片的第一个元素到最后一个元素的数量

a := [5]int{55, 56, 57, 58, 59}
b := a[startIndex:endIndex]   //基于数组a创建切片 x,y可以省略不写  **左闭右开**
c := a[1:4]                     //基于数组a创建切片,包括元素a[1],a[2],a[3]   

array[low  :   high   :   max]

它会将得到的结果切片的容量设置为max-low

var numbers = [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
myslice := numbers[4:6:8]
fmt.Println(myslice, len(myslice), cap(myslice))
myslice = myslice[:cap(myslice)]
fmt.Println(myslice[3])
//[5 6] 2 4
//8
make([]T, size, cap)
T:切片的元素类型
size:切片中元素的数量
cap:切片的容量
**如果cap省略,表示cap和size大小一样**
a := make([]int, 2, 10)
fmt.Println(a)      //[0 0]
fmt.Println(len(a)) //2
fmt.Println(cap(a)) //10

切片是引用类型 ,切片不能直接比较

我们不能使用 == 操作符来判断两个切片是否含有全部相等元素。 切片唯一合法的比较操作是和nil比较。 一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量都是0。但是我们不能说一个长度和容量都是0的切片一定是nil。。。。

var s1 []int         //len(s1)=0;cap(s1)=0;s1==nil
s2 := []int{}        //len(s2)=0;cap(s2)=0;s2!=nil
s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil
//要检查切片是否为空,请始终使用`len(s) == 0`来判断,而不应该使用`s == nil`来判断

切片的赋值拷贝(浅拷贝)

s1 := make([]int, 3) //[0 0 0]
s2 := s1             //将s1直接赋值给s2,s1和s2共用一个底层数组
s2[0] = 100
fmt.Println(s1) //[100 0 0]
fmt.Println(s2) //[100 0 0]
//由于切片是引用类型,所以s1和s2其实都指向了同一块内存地址。修改s2的同时s1的值也会发生变化
fmt.Printf("%p %p\n",s1,&s1) //0xc0000120c0 0xc00000c060
fmt.Printf("%p %p\n",s2,&s2) //0xc0000120c0 0xc00000c080

切片的深拷贝 copy

copy(destSlice, srcSlice []T)
//srcSlice: 数据来源切片
//destSlice: 目标切片

// copy()复制切片
a := []int{1, 2, 3, 4, 5}
c := make([]int, 5, 5)
copy(c, a)     //使用copy()函数将切片a中的元素复制到切片c
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1 2 3 4 5]
c[0] = 1000
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1000 2 3 4 5]
//如果c := make([]int,0,5) copy(c, a) c里边是没有值的 因为c的size为0 不能满足拷贝要求

切片 append添加元素

var s []int
s = append(s, 1)        // [1]
s = append(s, 2, 3, 4)  // [1 2 3 4]
s2 := []int{5, 6, 7}  
s = append(s, s2...)    // [1 2 3 4 5 6 7]
//通过var声明的零值切片可以在append()函数直接使用,无需初始化
var s []int
s = append(s, 1, 2, 3)
//没有必要向下边这么干
s := []int{}  // 没有必要初始化
s = append(s, 1, 2, 3)
var s = make([]int)  // 没有必要初始化
s = append(s, 1, 2, 3)

扩容操作往往发生在append()函数调用时,所以我们通常都需要用原变量接收append函数的返回值

sli := make([]int,2,2)
fmt.Println(len(sli),cap(sli))									//2 2
fmt.Printf("%v %p %p %p\n",sli,sli,&sli,&sli[0])//[0 0] 0xc000014090 0xc00000c060 0xc000014090
ma := make(map[string][]int)
ma["sandy"] = sli
sli = append(sli,3)
fmt.Println(len(sli),cap(sli))								//3 4
fmt.Printf("%v %p %p %p\n",sli,sli,&sli,&sli[0])//[0 0 3] 0xc0000120e0 0xc00000c060 0xc0000120e0

扩容前后 sli变量的地址一直都是0xc00000c060,但是存放的值放生了变化从0xc000014090-->0xc0000120e0 ,所以说切片名是一个指针

有坑的题目

func main() {
	a := []int{1, 2, 3}
	for k, v := range a {
		if k == 0 {
			a[0], a[1] = 100, 200
			fmt.Print(a)
		}
		a[k] = 100 + v
	}
	fmt.Print(a)
}

/*
[100 200 3][101 300 103]
*/

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值