Go语言入门之数组切片

1 篇文章 0 订阅
1 篇文章 0 订阅

Go语言入门之数组切片

1.数组的定义

数组是一组连续内存空间存储的具有相同类型的数据,是一种线性结构

在Go语言中,数组的长度是固定的。

数组是值传递,有较大的内存开销,可以使用指针解决

数组声明

var name [size]type
  • name:数组声明及使用时的变量名。
  • size:数组的元素数量,可以是一个表达式,但最终通过编译期计算的结果必须是整型数值,不能含有到运行时才能确认大小的数值。
  • type:可以是任意基本类型。

案例:

var arr [3]int

数组比较:

如果两个数组类型的元素类型数组长度都是一样时,那么这两个数组类型是等价的,如果有一个属性不同,它们就是两个不同的数组类型。

2.数组的使用

(1)数组赋值(初始化)

// 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

(2)数组取值

// 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.多维数组

// 声明一个二维数组,存储3*4数量的字段
var array [3][4]int
fmt.Println(array)

4.切片(slice)

切片是对数组的一个连续片段的引用,所以切片是一个引用类型

(1)切片与数组

在Go语言中,数组的值传递和固定长度的设定限制了数组的更好使用。针对这些问题,切片应用而生,切片基于数组实现,但它提供了一种动态调整大小的能力,使得数据的存储和管理更加灵活。

切片与数组区别

  • 长度方面:数组固定长度,不可变;切片动态长度,自增减
  • 类型:数组值类型,传参复制数组;切片引用类型,传参只传引用
  • 内存分配:数组元素连续存储于内存;切片基于数组创建,底层数组相同
  • 长度容量:数组长度为类型的一部分;切片长度为元素个数,容量为切片开始位置到底层数组末尾的元素数量

(2)切片的底层结构

切片的底层是基于数组,本质上是对数组的封装

type slice struct {
    array unsafe.Pointer
    len int
    cap int
}
  • array:是指向底层数组的指针。
  • len:切片的长度,即切片中当前元素的个数。使用 len()获取长度。
  • cap:切片的容量,是底层数组的长度,cap 值永远大于等于 len 值。使用 cap() 获取容量。

Go语言中切片的内部结构包含地址大小容量,切片一般用于快速地操作一块数据集合。

切片中nil与空值的区别

案例见初始化方式1和2

  • nil slice的底层数组指针是nil,是不分配内存的。
  • empty slice底层数组指针指向一个长度为0的数组,是一个空数组,有内存。

所以判断切片是否有数据应该通过len(s) == 0来判断

(3)切片的初始化

方式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


注意

  • 用切片截取字符串时,返回值仍为字符串
  • 使用切片截取数组或切片,修改截取到的值,可能会影响到原数组或切片的值。

(4)切片append用法

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:]...)

(5)切片的扩容机制

切片通常使用append对切片新增元素

扩容机制源码解析
  • 1.首先进入growslice(),接下来的处理都在此中进行
  • 2.接着进入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
}

  • 3.之后根据不同的元素类型会有不同处理,核心是通过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)
}

  • 4.最后才会返回最终的扩容大小

经过源码理解后,我们看一个例子,此时s的容量会是多少

s := make([]int, 2, 4)  // 一个4容量2长度的数组
s = append(s, 6, 7, 8, 9, 10, 11, 12) // 我给它7个数据

经过分析发现,我们往其中加两个数据后才会触发扩容,扩容后期望的容量为9,但是9大于4的二倍,所以直接返回9,但是还要经过一个内存对齐,因此最后返回的结果是10

扩容机制总结简述
  • 容量小于256时,两倍扩容
  • 容量大于等于256时,按照newcap += (newcap + 3*256) / 4这个公式扩容
  • 实际的容量,在上述的基础上,还会进行内存对齐

(6)切片复制

Go语言的内置函数 copy() 可以将一个数组切片复制到另一个数组切片中

如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。

  • 函数格式:copy( destSlice, srcSlice []T) int
  • 函数返回值:实际发生复制的元素个数
  • 复制要求:目标切片必须分配过空间且足够承载复制的元素个数,2者类型必须一致
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)

(7)切片的比较

可以通过反射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("切片不等")
	}
}

(8)切片转数组

// go1.20新特性
slice1 := make([]int, 2, 8)
arr := [2]int(slice1)
fmt.Println(arr)

切片的更多操作请参考:切片底层实现

  • 15
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值