• Go语言入门之数组切片


    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)
    
    

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

  • 相关阅读:
    定时任务调度(crond)
    JavaScript-4.正则表达式、BOM
    async 和await和promise
    如何将带GPS的网络化的软件定义无线电接收机应用于分布式和移动频谱监测?(一)
    位运算基础知识及性质(精简总结)
    ESP8266-Arduino编程实例-MLX90614红外测温传感器驱动
    我最喜欢的白版应用,AI加持的新功能开源!强烈推荐
    ES基本使用_2_整合SpringBoot
    聊聊编程中的 “魔数”
    「全域BI-运营」——助力双11店铺数据可视化
  • 原文地址:https://blog.csdn.net/clisks/article/details/140364337