数组
是一组连续内存空间
存储的具有相同类型
的数据,是一种线性结构
。在Go语言中,
数组
的长度是固定
的。数组是值传递,有较大的内存开销,可以使用指针解决
数组声明
var name [size]type
name
:数组声明及使用时的变量名。size
:数组的元素数量,可以是一个表达式,但最终通过编译期计算的结果必须是整型数值,不能含有到运行时才能确认大小的数值。type
:可以是任意基本类型。案例:
var arr [3]int
数组比较:
如果两个数组类型的元素类型与数组长度都是一样时,那么这两个数组类型是等价的,如果有一个属性不同,它们就是两个不同的数组类型。
// 1.正常赋值
var arr [3]int = [3]int{1, 2, 3}
// 2.赋值短缺,短缺处默认为0
var arr1 [3]int = [3]int{1, 2}
// 3.简化赋值
arr2 := [3]int{1, 2, 3}
// 4.根据赋值数决定长度,表示数组的长度是根据初始化值的个数来计算
arr3 := [...]int{1, 2, 3}
// 5.指定赋值,给索引为2的赋值 ,所以结果是 0,0,3
arr4 := [3]int{2: 3}
// 6.索引赋值
var arr5 [3]int
arr5[0] = 1
arr5[1] = 2
arr5[2] = 3
// 1.正常索引取值
var arr [3]int = [3]int{1, 2, 3}
fmt.Println(arr[2])
// 2.for range取值
for index, value := range arr {
fmt.Printf("索引:%d,值:%d \n", index, value)
}
// 声明一个二维数组,存储3*4数量的字段
var array [3][4]int
fmt.Println(array)
切片是对数组的一个连续片段的引用,所以切片是一个
引用类型
在Go语言中,数组的值传递和固定长度的设定限制了数组的更好使用。针对这些问题,切片应用而生,切片基于数组实现,但它提供了一种动态调整大小的能力,使得数据的存储和管理更加灵活。
切片与数组区别
切片的底层是基于数组,本质上是对数组的封装
type slice struct {
array unsafe.Pointer
len int
cap int
}
array
:是指向底层数组的指针。len
:切片的长度,即切片中当前元素的个数。使用 len()
获取长度。cap
:切片的容量,是底层数组的长度,cap
值永远大于等于 len
值。使用 cap()
获取容量。Go语言中切片的内部结构包含
地址
、大小
和容量
,切片一般用于快速地操作一块数据集合。
案例见初始化方式1和2
- nil slice的底层数组指针是nil,是不分配内存的。
- empty slice底层数组指针指向一个长度为0的数组,是一个空数组,有内存。
所以判断切片是否有数据应该通过len(s) == 0
来判断
方式1: 普通声明
var name []type // 此方式声明的切片和长度均为0,值为nil,不需要分配内存
方式2: 字面量
创建方式:切片在编译器创建一个数组,再基于数组创建切片
a := []int{} // 空切片且需要内存分配 切片的长度为0 , 但值并不是nil而是空值
b := []int{1,2} // 声明一个长度为2的切片
方式3: 内置函数make
创建时直接调用了
makeslice
方法,这是运行时的方法,直接传入参数,返回新建切片的指针
a := make([]int , len , cap) // 其中len为切片的初始长度,cap为切片预分配的容量
a := make([]int , len) // 如未指定cap,则其长度与len一致
用
make
函数生成切片会发生内存分配操作;如果有显示声明开始和结束位置的切片,则只是将新的切片结构指向已经分配好的内存区域。
方式4: 从切片或数组中切取
array := [5]int{1,2,3,4,5}
s1 := array[1:3] // 截取规则,左截右不截,即左开右闭。输出结果2,3
s2 := array[:3] // 从连续区域开头到给定的结束位置。输出结果1,2,3
s3 := array[1:] // 从给定开始位置到连续区域末尾结束。输出结果2,3,4,5
s4 := array[:] // 打印整个数组或切片。输出结果1,2,3,4,5
s5 := array[0:0] // 空切片,用于切片复位
// 完整切片表达式:[low:high:max]
// 其中cap = max - low
// 只有low可以省略,默认0,max不能超过数组长度
s6 := array[:3:5] // 输出结果1,2,3
注意:
go语言中,append常用于向切片中添加元素
append()
函数会将元素追加到切片的末尾,并返回一个新的切片,原始切片并没有被修改
语法如下:
// 第一个表示原切片,第二个代表要追加的元素
append(slice []T, elements ...T) []T
注意: 使用append
添加时需要将返回值重新赋给原切片,才可以保证原切片添加成功
s := make([]int, 2, 4)
s = append(s, 6, 7, 8, 9, 10, 11, 12)
append原理
我们都知道切片的底层是数组,append在追加元素时会判断切片的容量是否足够
如果容量足够,append会在底层数组中直接添加元素
反之,append会创建一个新的底层数组,将原底层数组数据复制过去,之后在新数组中进行添加。
append用法
var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素
a = append(a, []int{1,2,3}...) // 追加一个切片
如何删除切片内的一个元素i?
a = append(s[0:i], a[i+1:]...)
切片通常使用
append
对切片新增元素
growslice()
,接下来的处理都在此中进行nextslicecap()
处理扩容后容量大小func nextslicecap(newLen, oldCap int) int {
newcap := oldCap
// 计算出旧容量的2倍
doublecap := newcap + newcap
// 期望容量大于旧容量的两倍,返回期望容量
if newLen > doublecap {
return newLen
}
// 定义常量256,小于256返回2倍旧容量,不满足则继续执行for
const threshold = 256
if oldCap < threshold {
return doublecap
}
// 循环处理,直到达到期望容量
for {
// 每次扩容的公式 右移两次相当于除以4
newcap += (newcap + 3*threshold) >> 2
// 如果处理结果大于期望容量,跳出
if uint(newcap) >= uint(newLen) {
break
}
}
// 来判断是否溢出,如果溢出超过整型最大值,返回期望容量
if newcap <= 0 {
return newLen
}
return newcap
}
roundupsize()
对容量进行内存对齐
这个内存对齐公式比较复杂,了解即可
为什么要内存对齐?
- 因为CPU访问的规则,未对齐的内存,会造成CPU多次访问,耗费性能
// 第一个参数可以简单理解为容量大小
// 第二个参数是et.PtrBytes == 0的结果,et为切片元素类型
func roundupsize(size uintptr, noscan bool) uintptr {
// 大小小于32768(32K)
if size < _MaxSmallSize {
// 大小小于等于1024-8
if size <= smallSizeMax-8 {
// 其中divRoundUp返回一个return (n + a - 1) / a n是第一个参数,a是第二个
// 其他的都是常量数组,有兴趣可以看看源码
return uintptr(class_to_size[size_to_class8[divRoundUp(size, smallSizeDiv)]])
} else {
return uintptr(class_to_size[size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]])
}
}
// _PageSize简单理解为8192
if size+_PageSize < size {
return size
}
return alignUp(size, _PageSize)
}
经过源码理解后,我们看一个例子,此时s的容量会是多少
s := make([]int, 2, 4) // 一个4容量2长度的数组
s = append(s, 6, 7, 8, 9, 10, 11, 12) // 我给它7个数据
经过分析发现,我们往其中加两个数据后才会触发扩容,扩容后期望的容量为9,但是9大于4的二倍,所以直接返回9,但是还要经过一个内存对齐,因此最后返回的结果是10
newcap += (newcap + 3*256) / 4
这个公式扩容Go语言的内置函数
copy()
可以将一个数组切片复制到另一个数组切片中如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。
copy( destSlice, srcSlice []T) int
slice1 := []int{1, 2, 3}
slice2 := []int{4, 5, 6, 7, 8}
copy(slice2, slice1) // 只会复制slice1的3个元素到slice2的前3个位置
copy(slice1, slice2) // 只会复制slice2的前3个元素到slice1中
// 修改slice1中的数据不对对slice2产生影响,因为copy属于深拷贝
注意:copy属于深拷贝(浅拷贝是只copy地址,深拷贝是值copy)
可以通过反射
reflect.DeepEqual
进行比较
func main() {
a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
b := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
c := []int{10, 2, 3, 4, 5, 6, 7, 8, 9, 1}
sliceCompare(a, b)
sliceCompare(a, c)
}
func sliceCompare(s1 []int, s2 []int) {
ok := reflect.DeepEqual(s1, s2)
if ok {
fmt.Println("切片相等")
} else {
fmt.Println("切片不等")
}
}
// go1.20新特性
slice1 := make([]int, 2, 8)
arr := [2]int(slice1)
fmt.Println(arr)
切片的更多操作请参考:切片底层实现