golang学习-切片
Go切片
前面我们学习了数组,数组是固定长度,可以容纳相同数据类型的元素的集合。当长度固定时,使用还是带来了一些限制,比如:申请的长度太大浪费内存,太小又不够用。
鉴于上述原因,就有了go语言的切片(slice),可以把slice理解为,可变长度的数组,其实它底层就是使用数组实现的,增加了自动扩容功能。Slice是一个拥有相同类型元素的可变长度的序列。
go语言slice的用法
声明一个slice和声明一个数组类似,只要不添加长度就可以了
var identifier []type
slice是引用类型,可以使用make
函数来创建slice:
var slice1 []type = make([]type , len)
也可以简写为
slice1 := make([]type , len)
也可以指定容量,其中capacity为可选参数。
make([]T , length , capacity)
这里len
是数组的长度也是slice的初始长度。
go语言slice实例
package main
import(
"fmt"
)
func main(){
var slice1 []int
var slice2 []string
fmt.Printf("%T\n",slice1) //[]int
fmt.Printf("%T\n",slice2) //[]string
fmt.Println(slice1 == nil) //true
fmt.Println(slice2 == nil) //true
slice1 := make([]int, 2)
fmt.Printf("slice1: %v\n", slice1)
}
go语言slice的长度和容量
slice拥有自己的长度和容量,我们可以通过内置的len()
函数求长度,使用内置的cap()
函数求slice的容量。
实例
package main
import (
"fmt"
)
func test3() {
var name = []string{"ljy", "23"}
var numbers = []int{1, 2, 3}
fmt.Printf("len:%d cap:%d\n", len(name), cap(name))
fmt.Printf("len:%d cap:%d\n", len(numbers), cap(numbers))
s1 := make([]string, 2, 3)
fmt.Printf("len:%d cap:%d\n", len(s1), cap(s1))
}
go语言slice的初始化
slice的初始化方法很多,可以直接初始化,也可以使用数组初始化等。
直接初始化
package main
import (
"fmt"
)
// 直接初始化slice
func main() {
s1 := []int{1, 2, 3}
fmt.Printf("s1: %v\n", s1) //s1: [1 2 3]
}
使用数组初始化
package main
import (
"fmt"
)
// 使用数组初始化slice
func main() {
arr := [...]int{1, 2, 3}
s1 := arr[:] //取数组所有元素
fmt.Printf("s1: %v\n", s1) //s1: [1 2 3]
fmt.Printf("s1: %T\n", s1) //s1: []int
}
使用数组部分元素初始化
slice的底层就是一个数组,所以我们可以基于数组通过slice表达式得到slice。slice表达式中的low和high表示一个索引范围(左包含,右不包含),得到的slice长度=high-low,容量等于得到的slice的底层数组的容量。
package main
import (
"fmt"
)
// 使用数组部分元素初始化
func main() {
arr := [...]int{1, 2, 3, 4, 5, 6}
s1 := arr[2:5]
fmt.Printf("s1: %v\n", s1) //s1: [3 4 5]
s2 := arr[2:]
fmt.Printf("s2: %v\n", s2) //s2: [3 4 5 6]
s3 := arr[:3]
fmt.Printf("s3: %v\n", s3) //s3: [1 2 3]
}
go语言slice的遍历
for遍历
package main
import (
"fmt"
)
// for循环遍历
func main() {
var s = []int{1, 2, 3, 4, 5}
for i := 0; i < len(s); i++ {
fmt.Printf("s[i]: %v\t", s[i])
}
}
for range遍历
package main
import (
"fmt"
)
// for range遍历
func main() {
var s = []int{1, 2, 3, 4, 5}
for _, v := range s {
fmt.Printf("v: %v\t", v)
}
}
go语言slice的添加和删除
slice是一个动态数组,可以使用append()
函数添加元素,go语言中没有删除slice元素的专用方法,我们可以使用slice本身的特性来删除元素。由于,slice是引用类型,通过赋值的方式,会修改原有内容,go提供了copy()
函数来拷贝slice。
添加元素
package main
import (
"fmt"
)
// 添加元素
func main() {
s := []int{}
s = append(s, 1)
s = append(s, 2) // 添加单个元素
s = append(s, 3, 4, 5) // 添加多个元素
fmt.Printf("s: %v\n", s)
s1 := []int{3, 4, 5}
s2 := []int{6, 7}
s1 = append(s1, s2...) // 添加另外一个slice
fmt.Printf("s1: %v\n", s1)
}
删除元素
package main
import (
"fmt"
)
// 删除元素
func main() {
s := []int{1, 2, 3, 4, 5}
fmt.Printf("s: %v\n", s)
s = append(s[:2], s[3:]...)
fmt.Printf("s: %v\n", s)
}
公式:要从slicea中删除索引为
index
的元素,操作方法是a = append(a[:index],a[index+1:]...)
拷贝slice
package main
import (
"fmt"
)
// 拷贝slice
func test11() {
s := []int{1, 2, 3}
s1 := make([]int, 3)
fmt.Printf("s1: %v\n", s1)
copy(s1, s)
fmt.Printf("s1: %v\n", s1)
}
slice底层探究
那么我们首先就要从slice
的底层结构看起
slice底层结构
type slice struct {
array unsafe.Pointer //数组首地址指针
len int //长度
cap int //容量
}
我们从slice底层结构可以看出,slice结构体有三个属性,分别是unsafe.Pointer
、len
、cap
,他们分别对应slice对应数组的指针、slice的长度、slice的容量。在得知slice底层结构后,我们来看第一个问题。
通过截取数组创建的slice
package main
import (
"fmt"
)
// slice在内存中的布局
func test12() {
arr := [...]int{1, 2, 3, 4, 5}
slice := arr[2:3]
fmt.Printf("arr[2]: %p\n", &arr[2]) //arr[2]: 0xc00000c460
fmt.Printf("slice[0]: %p\n", &slice[0]) //slice[0]: 0xc00000c460
// 这也就表明了实际上slice是在操作原有数组
// 下面我们修改slice,然后观察数组的值,从而进行判断
slice[0] = 10 //10
fmt.Printf("slice[0]: %v\n", slice[0]) //10
fmt.Printf("arr[2]: %v\n", arr[2]) //10
// 以上便是证明slice操作原数组的验证
// 那么slice一直都在操作原数组么?什么时候会有变化?
// 我们可以先查看slice的unsafe.Pointer、cap和len
fmt.Printf("slice: %p %v %v\n", &slice[0], len(slice), cap(slice)) // slice: 0xc00000c460 2 3
// 下面我们尝试向slice中添加元素
slice = append(slice, 91)
// 可以看出只有len有变化,是因为向slice中添加了元素
fmt.Printf("slice: %p %v %v\n", &slice[0], len(slice), cap(slice)) // slice: 0xc00000c460 3 3
// 继续尝试添加元素
slice = append(slice, 92)
// 此时我们可以看到,slice的unsafe.Pointer并不再指向截取arr的地址,而是一个新的地址
fmt.Printf("slice: %p %v %v\n", &slice[0], len(slice), cap(slice)) // slice: 0xc00000c480 4 6
// 上述尝试中,我们在修改slice中的元素后,会对应修改arr中的元素,那么我们添加元素是否会影响arr呢?
for _, v := range arr {
fmt.Printf("v: %v\t", v) // v: 1 v: 2 v: 10 v: 91 v: 92
}
// 此时我们可以看到,追加元素同样会影响arr,追加元素在slice不用扩容的情况下,会默认覆盖arr对应的元素
}
对应内存结构图
通过make方式创建的slice
package main
import (
"fmt"
)
func test13() {
slice := make([]int, 0, 3)
// slice: [] slice address: 0xc000004078 len: 0 cap: 3
fmt.Printf("slice: %v slice address: %p len: %v cap: %v\n", slice, &slice, len(slice), cap(slice))
slice = append(slice, 1)
// slice: [1] slice Pointer: 0xc00000a120 slice address: 0xc000004078 len: 1 cap: 3
fmt.Printf("slice: %v slice Pointer: %p slice address: %p len: %v cap: %v\n", slice, &slice[0], &slice, len(slice), cap(slice))
slice = append(slice, 2)
slice = append(slice, 3)
slice = append(slice, 4)
// slice: [1 2 3 4] slice Pointer: 0xc00000c450 slice address: 0xc000004078 len: 4 cap: 6
fmt.Printf("slice: %v slice Pointer: %p slice address: %p len: %v cap: %v\n", slice, &slice[0], &slice, len(slice), cap(slice))
}
分别观察两种创建slice方式操作,他们都有一个共同点,那就是在slice进行扩容操作后,unsafe.Poniter
地址会发生改变,那么这是为什么?
我们观察runtime.growslice()
方法的源码
func growslice(et *_type, old slice, cap int) slice {
...
...
if cap < old.cap {
panic(errorString("growslice: cap out of range"))
}
if et.size == 0 {
// append should not create a slice with nil pointer but non-zero len.
// We assume that append doesn't need to preserve old.array in this case.
return slice{unsafe.Pointer(&zerobase), old.len, cap}
}
newcap := old.cap// 1
doublecap := newcap + newcap// 1+1 = 2 为什么不直接*2, 而是使用加法?
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 // 1.25倍
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
...
switch {
case et.size == 1:
lenmem = uintptr(old.len)
newlenmem = uintptr(cap)
capmem = roundupsize(uintptr(newcap))
overflow = uintptr(newcap) > maxAlloc
newcap = int(capmem)
case et.size == sys.PtrSize:
lenmem = uintptr(old.len) * sys.PtrSize
newlenmem = uintptr(cap) * sys.PtrSize
capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
newcap = int(capmem / sys.PtrSize)
case isPowerOfTwo(et.size):
var shift uintptr
if sys.PtrSize == 8 {
// Mask shift for better code generation.
shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
} else {
shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
}
lenmem = uintptr(old.len) << shift
newlenmem = uintptr(cap) << shift
capmem = roundupsize(uintptr(newcap) << shift)
overflow = uintptr(newcap) > (maxAlloc >> shift)
newcap = int(capmem >> shift)
default:
lenmem = uintptr(old.len) * et.size
newlenmem = uintptr(cap) * et.size
capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
capmem = roundupsize(capmem)
newcap = int(capmem / et.size)
}
...
memmove(p, old.array, lenmem)
}
//
func roundupsize(size uintptr) uintptr {
if size < _MaxSmallSize { // 32768
if size <= smallSizeMax-8 {
return uintptr(class_to_size[size_to_class8[divRoundUp(size, smallSizeDiv)]]) // 申请的内存块个数
} else {
return uintptr(class_to_size[size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]]) 申请的内存块个数
}
}
if size+_PageSize < size {
return size
}
return alignUp(size, _PageSize)
}
// alignUp rounds n up to a multiple of a. a must be a power of 2.
func alignUp(n, a uintptr) uintptr {
return (n + a - 1) &^ (a - 1)
}
const _MaxSmallSize = 32768
const smallSizeDiv = 8
const smallSizeMax = 1024
const largeSizeDiv = 128
可以看出以下两点:
- 当slice进行扩容时,如果cap<1024,新slice的cap变为原来的两倍;如果cap>102,新slice变为原来的1.25倍。
- roundupsize时内存对齐的过程,golnag中内存分配是根据对象大小来分配不同的mspan,为了避免有过多的内存碎片,因此slice在扩容的过程中需要对扩容后的cap容量进行内存对齐的操作,这也就解释了为什么slice在扩容后
unsafe.Poniter
会发生改变。
总结:
- slice的确是一个引用类型
- slice从底层来说,其实就是一个数据结构(struce结构体)
- slice在扩容后,
unsafe.Pointer
会发生改变,原因是为了避免有过多的内存碎片。