Go 知识slice

这篇具有很好参考价值的文章主要介绍了Go 知识slice。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1. 什么是slice

slice 是动态数组,依托数组实现,可以方便的进行扩容和传递,实际中比数组使用更加频繁。

2. slice 基础

2.1 定义

  • 变量声明
    var s []int
  • 字面量
    s1 := []int{},s2 := []int{1,2,3}
  • 内置函数make
    s1 := make([]int, 12) 指定长度 len
    s2 := make([]int, 10, 20) 指定长度 len 和空间 cap
  • 切片
    array := [5]int{1, 2, 3, 4, 5}
    s1 := array[0:2] // 从数组切片 [0,2) 长度2,下标 0, 1
    s2 := s1[0:1] // 从slice切片 [0,1) 长度 1 ,下标 0
    s3 := s2[:] // 从slice 切片,如果开始位置等于0,结束位置等于len,那么可以省略
    

2.2 实现原理

slice 的定义在src/runtime/slice.go里面
Go 知识slice,golang,go,切片,slice,go细节

可以看到 slice 有 len 和 cap 参数,里面记录了 array 数组的长度和空间。
所以 len(slice) 和 cap(slice) 的时间复杂度都是O(1)

2.2.1 make 创建

使用 make 创建会申请分配数组空间,然后将len和cap初始化赋值。
如果 make 没有指定 cap ,只指定了 len ,那么cap = len 。

2.2.2 切片 创建

使用切片创建的时候,slice 和原始数组共用底层函数。

array := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s := array[5:7]

在这种情况下 s 的底层数组 array = &array[0+init],len=2,cap=len(array) - init=5
为什么 slice 的cap=10呢?
因为 slice 和 数组是共用底层数据的,此时计数的单位是从数组的0开始计数,但是操作 slice 的时候,会进行初始偏移量的处理。
比如 s[0] = array[0+5]
len(s) = 2
cap(s) = 10 -5
Go 知识slice,golang,go,切片,slice,go细节

这是最危险的情况,此时如果给 s 进行追加元素,会修改 array 的元素。

array := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
ts := array[5:7]
fmt.Println(array)
fmt.Println(ts)
ts = append(ts, -1)
fmt.Println(array)
fmt.Println(ts)

使用程序验证
Go 知识slice,golang,go,切片,slice,go细节

如何解决这种问题呢?
答案是指定cap,这样在追加元素的时候,就会触发扩容,这样 slice 和数组就分离了
上述代码可以修改为:

array := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
//ts := array[5:7]
//fmt.Printf("len=%d,cap=%d\n", len(ts), cap(ts))
//fmt.Println(array)
//fmt.Println(ts)
//ts = append(ts, -1)
//fmt.Println(array)
//fmt.Println(ts)

nts := array[5:7:7]
fmt.Println(array)
fmt.Println(nts)
nts = append(nts, -1)
fmt.Println(array)
fmt.Println(nts)

Go 知识slice,golang,go,切片,slice,go细节

2.3 操作

2.3.1 append 追加

s := make([]int, 0) // 初始化长度为0的切片,注意空间不一定为0
s = append(s, 1) // 添加一个元素,长度为1
s = append(s, 2, 3, 4) // 添加多个元素
s = append(s, []{5, 6, 7}...} // 添加切片 

当切片空间不足的时候,会先扩容在追加.
原理:
首先 append 的函数定义

func append(slice []Type, elems ...Type) []Type

可以看出 append 接收一个切片和不定数量的元素,返回切片,需要注意的是切片的元素类型和需要添加的元素类型要相同。
而添加切片的时候,用到了切片展开 ...
用于将一个切片展开为一个个的元素。

2.3.2 表达式切片

表达式切片的格式 slice[low:high]
如果是数组0<=low<=gigh<=len(array)
如果是slice0<=low<=high<=cap(slice)

arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[:] // 从数据转为 slice 省略了开始和结尾
s2 := arr[0:len(arr)] // 从数组转为 slice 指定了开始和结束
s3 := arr[2:4] // 从数组切片 由2开始到4结束,[2,4) => [3, 4] 取下标为 2, 3 的元素
s4 := arr[:4] // 从数组切片 由0开始到4结束,[0,4) => [1, 2, 3, 4] 取下标为 0, 1, 2, 3 的元素
s5 := arr[3:] // 从数组切片 由3开始到最后一个元素结束,[3, len(arr)) => [4, 5] 取下标为 3, 4 的元素

需要注意的是,表达式切片后产生的切片底层数组是共享的,所以非常容易出错。

2.3.3 扩展表达式

扩展表达式是为了解决新旧变量共用底层数组,导致相互影响的问题。只要限制了新slice的空间容量的限制,那么当修改的时候,还是会修改底层数组,也就是原数组的对应的元素也会发生变化,当时当发生扩容的时候,就不会完全修改超出预期的元素。
扩展表达式的格式slice[low:high:cap]需要注意的是,cap是在原数组或者slice的基础上计算的cap值。
0<=low<=high<=cap<=cap(slice)
比如

array := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
nts := array[5:7:7]
fmt.Println(array)
fmt.Println(nts)
fmt.Printf("len=%d, cap=%d\n", len(nts), cap(nts))
nts = append(nts, -1)
fmt.Println(array)
fmt.Println(nts)
fmt.Printf("len=%d, cap=%d\n", len(nts), cap(nts))

Go 知识slice,golang,go,切片,slice,go细节

2.3.4 扩容

在slice扩容的过程中,会重新申请一个更大容量的底层数组,然后将len拷贝过去,将新的cap赋值。
扩容的规则:

  • 如果原slice的cap<=1024,那么新slice的newCap = oldCap*2
  • 如果原slice的cap > 1024,那么新slice的newCap = oldCap*1.25

2.3.5 拷贝

在前面说到,使用表达式切片,会导致底层数组共享的问题,使用扩展表达式,只是解决了新slice的越界修改的问题,最基本的数据隔离还是没有实现,修改新slice的数据,还是会影响到原slice的数据。
那么,如何解决呢,只能是主动显示的拷贝,产生新的底层数组,实现新旧slice的数据隔离。
使用内置函数copy就能实现拷贝,但是需要注意,拷贝的过程中不会主动扩容。
意思是指假设新的slice的容量是5,旧的slice的容量是3,那么只会拷贝3个元素,实际上这种场景也到符合预期。
但是当newCap=5,oldCap=10,那么只会拷贝5个元素,并不会进行扩容。

3. 测试一下

3.1 len && cap

func TestSliceCap(t *testing.T) {
    var arr [10]int
    var sl = arr[5:6]
    sl = append(sl, 7)
    fmt.Printf("len=%d\n", len(sl))
    fmt.Printf("cap=%d\n", cap(sl))
}

len=2
cap=5

3.2 append && 扩容

func TestSliceCap(t *testing.T) {
	s1 := []int{1, 2}
	s2 := s1
	s2 = append(s2, 3)
	add(s1)
	add(s2)
	fmt.Println(s1, s2)
}
func add(s []int) {
	s = append(s, 0)
	for i := range s {
		s[i]++
	}
}

Go 知识slice,golang,go,切片,slice,go细节

解析:
s1 是slice
s2 等于s1,此时s1和s2相同
s2 = append(s2, 3) 发生了扩容,此时s2和s1是两个底层数组
add(s1)此时将s1传入了add函数,在add函数中进行append,进行了扩容,此时add函数内的s表示的底层数组也和s1不同了
add函数对newS1进行了+1操作,但是因为add函数没有返回,所以s1函数还是旧的底层数组,也就是[1 2]
add(s2)将s2传入了add函数,但是s2在s1的基础上进行了扩容,因为s1的cap<=1024,所以s2的cap=cap(s1)*2=4,
当进行append的时候,此时还不需要扩容,所以newS2=s2,add函数对元素+1也就修改了s2,也就是s2是[2 3 4 1]
打印的时候因为s2的len(s2)=3,所以没有打印s2[3]
相当于我们越界修改了len(s2)之外的数据,但是对于程序,s2[3]是不可见的.

3.3 切片表达式

func TestSliceExpress(t *testing.T) {
	orderLen := 5
	order := make([]int, 2*orderLen)
	lowOrder := order[:orderLen:orderLen]
	highOrder := order[orderLen:][:orderLen:orderLen]
	fmt.Printf("len(low)=%d, cap(low)=%d\n", len(lowOrder), cap(lowOrder))
	fmt.Printf("len(high)=%d,cap(high)=%d\n", len(highOrder), cap(highOrder))
}

Go 知识slice,golang,go,切片,slice,go细节

上述方式可以实现将切片一分为二,并且不会发生越界问题,怎么实现的呢?
首先创建order,len=10,cap=10
因为orderLen=5,所以lowOrder采用扩展表达式切片:lowOrder,[0,5),而且指定cap=5,这样lowOrder就不会出现越界问题,当len=5并且需要扩容的时候,会进行拷贝。
highOrder相当于进行了两次切片:
第一次 order[orderLen:]表示从5开始切分,剩余的元素都要,但是没有设置cap
第二次 在第一次的基础上,进行了指定长度,但是因为第一次产生的变量中已经存在了init,所以第二次是从0开始。需要注意,上述所有操作都是基于order这个slice进行的,没有产生新的底层数组。
为了便于理解,我们加一个中间变量:

func TestSliceExpress(t *testing.T) {
	orderLen := 5
	order := make([]int, 2*orderLen)
	lowOrder := order[:orderLen:orderLen]
	midOrder := order[orderLen:]
	highOrder := midOrder[:orderLen:orderLen]
	fmt.Printf("len(low)=%d, cap(low)=%d\n", len(lowOrder), cap(lowOrder))
	fmt.Printf("len(mid)=%d,cap(mid)=%d\n", len(midOrder), cap(midOrder))
	fmt.Printf("len(high)=%d,cap(high)=%d\n", len(highOrder), cap(highOrder))
}
lowOrder = &order[0], len=5, cap=5
midOrder = &order[5], len=5, cap=5
highOrder = &midOrder[0] = &order[5], len=5, cap=5

Go 知识slice,golang,go,切片,slice,go细节文章来源地址https://www.toymoban.com/news/detail-813876.html

到了这里,关于Go 知识slice的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Golang】Golang进阶系列教程--Go 语言数组和切片的区别

    在 Go 语言中,数组和切片看起来很像,但其实它们又有很多的不同之处,这篇文章就来说说它们到底有哪些不同。 数组和切片是两个常用的数据结构。它们都可以用于存储一组相同类型的元素,但在底层实现和使用方式上存在一些重要的区别。 Go 中数组的长度是不可改变的

    2024年02月15日
    浏览(57)
  • 【Golang】Golang进阶系列教程--Go 语言切片是如何扩容的?

    在 Go 语言中,有一个很常用的数据结构,那就是切片(Slice)。 切片是一个拥有相同类型元素的可变长度的序列,它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。 切片是一种引用类型,它有三个属性:指针,长度和容量。 底层源码定义如下: 指针: 指向

    2024年02月14日
    浏览(65)
  • Golang学习——string和slice切片

    1个字节可以表示256个数字,2个字节可以表示65536个数字 字符集:字符编号对照表。收录很多数字,给它们一一编号。 字符集的更迭 全球化统一标准——通用字符集 于1990年开始研发并于1994年正式公布,实现了跨语言跨平台的文本转换与处理。 字符集促成了字符与二进制的

    2024年02月15日
    浏览(39)
  • 一、Go基础知识2、iota、匿名变量与变量作用域的小细节

    1、iota是特殊常量,可以理解为是一个可被编译器修改的常量。 2、iota中有一个计数器,会自动加1,自增类型默认是int类型。 3、如果中断了iota则必须显示恢复。参考示例二。 4、iota简化了const类型的定义。 5、每次出现const的时候,iota归零。 示例一: 运行结果: 两个const打

    2024年02月05日
    浏览(36)
  • go语言(六)----slice

    1、slice 固定数组 2、动态数组 动态数组在传参上是引用传递,而且在不同元素长度的动态数组他们的形参也是一致的。

    2024年01月19日
    浏览(35)
  • go slice使用

    在go中, slice 是一种动态数组类型,其底层实现中使用了数组。 slice 有以下特点: * slice 本身并不是数组,它只是一个引用类型,包含了一个指向底层数组的指针,以及长度和容量。 * slice 的长度可以动态扩展或缩减,通过 append 和 copy 操作可以增加或删除 slice 中的元素。

    2023年04月27日
    浏览(34)
  • Go语言-Slice详解

    Go语言中的slice表示一个具有相同类型元素的可变长序列,语言本身提供了两个操作方法: 创建:make([]T,len,cap) 追加: append(slice, T ...) 同时slice支持随机访问。本篇文章主要对slice的具体实现进行总结。 go语言的slice有三个主要的属性: 指针:slice的首地址指针 长度:slice中元素

    2024年02月10日
    浏览(37)
  • go数据类型-slice底层

    有上篇string为基础了,能猜到slice肯定也有一个对应的struct。 切片的本质是对数组的引用 len 表示当前已经存储的个数,cap表示容量。 根据数组创建 字面量:编译时插入创建数组的代码 make:运行时创建数组 扩容时,编译时转为调用 runtime.growslice() 有兴趣的可以看下源码,方

    2024年02月05日
    浏览(35)
  • go语言Array 与 Slice

    有的语言会把数组用作常用的基本的数据结构,比如 JavaScript,而 Golang 中的数组(Array),更倾向定位于一种底层的数据结构,记录的是一段连续的内存空间数据。但是在 Go 语言中平时直接用数组的时候不多,大多数场景下我们都会直接选用更加灵活的切片(Slice) 声明与初始化

    2024年02月07日
    浏览(37)
  • go语言(七)----slice的声明方式

    1、声明方式一 2、声明方式二 3、声明方式三 二、切片容量的追加 1、切片的长度和容量不同,长度表示左指针到右指针之间的距离。容量表示左指针到底层数组末尾的距离。 2、切片的扩容机制,append的时候,如果长度增加后超过容量,则将容量增加2倍。 3、切片的截取

    2024年01月21日
    浏览(41)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包