[Go语言入门] 09 Go语言切片详解

09 Go语言切片详解

Go数组的长度是固定的,不能动态扩展,在有些场景中不太适用。

所以,Go提供了一种建立在数组之上的更强大的类型,叫做切片。

切片表示一个拥有相同类型元素的可变长度的序列。切片类型写作[]T,其中T是切片元素的类型。

切片和数组的关系非常密切,实际上,切片是一个轻量的数据结构,它内部引用了一个底层数组,同时还有另外零个属性:长度和容量,分别用来指明数组中实际被引用的元素和数组最大可用空间。

Go的内置函数len()和cap()用来返回切片的长度和容量。


9.1 切片的创建和基本使用

声明切片变量
// 声明一个元素类型为T的切片。
// 注意: 声明切片时[]中没有指定长度,而声明数组时需要指定长度。这是他们的区别。
var identifier []T

上面的语句声明了一个变量identifier,他的值被初始化为nil切片,nil切片的长度和容量都是0。

这里先记住nil切片的概念,在后续的内容中将再次对nil切片做介绍。

示例:

var slice1 []int
fmt.Prinln(slice1==nil)		// 输出 true
fmt.Println(slice1)			// 输出 []
fmt.Println(len(slice1))	// 切片长度,输出 0
fmt.Println(cap(slice1))	// 切片容量,输出 0

创建切片

方式一(通过字面量创建切片):

[]T {value1, value2, value3, ...}

上面的语句根据{}中的元素个数来创建切片,切片的长度和容量等于{}中的元素个数。

特别的,如果{}中不含有任何元素,将会创建一个空切片,且长度和容量都是0。

这里先记住空切片的概念,在后续的内容中将再次空切片做介绍。

示例:

var s =  []int{1, 2, 3}
fmt.Println(s)				// [1 2 3]
fmt.Println(len(s))			// 3
fmt.Println(cap(s))			// 3

// 空切片,不含任何元素的切片
s = []int{}
fmt.Println(s)				// []
fmt.Println(len(s))			// 0
fmt.Println(cap(s))			// 0

方式二(使用make()函数创建切片):

// 创建长度和容量都为len的切片,元素类型为T,元素初始化为T型的零值。
make([]T, len)

// 创建长度为len,容量为cap的切片,元素类型为T,元素初始化为T型的零值。
make([]T, len, cap)

上面的语句根据len和cap的值来创建切片,如果len不是0,切片中的元素将会初始化为T型的零值。

特别的,如果使用len=0来创建切片,得到的也是空切片。

示例:

var s = make([]int, 3)
fmt.Println(s)			// [0 0 0]     可以看到每个元素都被初始化为0零值
fmt.Println(len(s))		// 3
fmt.Println(cap(s))		// 3

s = make([]int, 3, 6)	
fmt.Println(s)			// [0 0 0]
fmt.Println(len(s))		// 3
fmt.Println(cap(s))		// 6

// 空切片
s = make([]int, 0)
fmt.Println(s)			// []
fmt.Println(len(s))		// 0
fmt.Println(cap(s))		// 0

// 也是空切片
s = make([]int, 0, 6)
fmt.Println(s)			// []
fmt.Println(len(s))		// 0
fmt.Println(cap(s))		// 6

切片的长度、容量

长度:表示切片当前实际可被访问的元素个数。

容量:>=长度。是为向切片追加元素所预留的空间。如果向切片追加元素时容量>长度,那么直接使用原有的底层数组即可,只需要修改一下长度值。如果向切片追加元素时容量==长度,那么就需要重新分配一个更大的底层数组。

关于如何向切片中追加元素,在后续的内容中将会介绍。


访问切片元素

可以像数组一样,通过下标来访问切片的元素。

下标从0开始的且必须小于切片的长度,否则会引发越界异常。

读取切片元素:

var s = []int{1, 2, 3, 4, 5, 6}
fmt.Println(s[0])
fmt.Println(s[1])

修改切片元素:

var s = []int{1, 2, 3, 4, 5, 6}
s[0] = 100
s[1] = 200
fmt.Println(s[0])
fmt.Println(s[1])

下标越界将会引发异常:

var s = []int{1, 2, 3, 4, 5, 6}   // len = 6
s[6] = 100			// 引发越界异常
fmt.Println(s[6]) 	// 引发越界异常

遍历切片

我们可以使用for循环来遍历切片:

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

for i:=0; i < len(s1); i++ {
    fmt.Println("index:", i, ", value:", s1[i])
}

还可以使用for-range循环来遍历切片:

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

for i, v := range s1 {
    fmt.Println("index:", i, ", value:", v)
}

注意:range返回的是切片元素的复制,不是对切片元素的引用,所以我们在for-range循环中修改v值并不会改变切片里的值:

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

for i, v := range s1 {
    v = 100+i
	_ = v
}

fmt.Println(s1) // 输出 [1 2 3 4 5 6 7 8 9 10]

如果需要修改切片元素的值,可使用普通的for循环来实现。


切片的比较

切片只支持和nil比较是否相等/不等,切片之间不可以比较。

s1 := []int{1, 2, 3, 4, 5}
s2 := []int{1, 2, 3, 4, 5}

fmt.Println(s1 == nil)
fmt.Println(s2 != nil)
// fmt.Println(s1 == s2)		// 编译错误:slice can only be compared to nil

9.2 切片的数据结构

数据结构

Go语言的切片是引用类型,也就是说在切片的数据结构中有一个指针指向了实际的数据。

切片的数据结构如下图:
在这里插入图片描述

由于切片是引用类型,所以当把切片复制给一个变量或传递给函数参数时,只是拷贝切片的数据结构部分,而不会拷贝指针指向的底层数组。


空切片和nil切片
  • 空切片的底层数组指针是一个有效的地址,地址指向了某个数组元素,空切片的len==0表示切片是空的。
  • nil切片的底层数组指针是nil值。
  • 空切片==nil的结果为false,nil切片==nil的结果为true。
  • nil切片表示不存在的切片,而空切片表示一个存在但不含有任何元素的切片

示例:

s1 := []int{}
var s2 []int
fmt.Println(s1 == nil)		// false
fmt.Println(s2 == nil)		// true

**注意:**在编程的时候,没有必要创建一个切片的指针来指向切片,因为切片本身已经是一个引用类型,它内部就包含指向实际数据的指针。


底层数组的地址、切片型变量的地址

假设有一个切片型的变量slice,那么:

  • 底层数组地址:slice。
  • 切片型变量的地址:&slice。

示例:

slice := []int{1, 2, 3, 4, 5}

// 输出切片底层数组的地址
fmt.Printf("slice: %p\n", slice)			// slice: 0xc00000c300
// 输出切片数据结构的地址
fmt.Printf("&slice: %p\n", &slice)			// &slice: 0xc0000044c0

// 而打印数组地址时,必须用&arr
arr := [...]int{1, 2, 3, 4, 5}
fmt.Printf("&arr: %p\n", &arr)				// &arr: 0xc00000c390
fmt.Printf("arr: %p\n", arr)				// arr: %!p([5]int=[1 2 3 4 5])

9.3 切片截取

切片的截取

可以使用[m:n]操作来截取切片,得到的新切片引用了原切片中从下标m到n(不含n)的元素。

m和n都可省略,如果省略m表示从下标0开始截取,如果省略n表示截取到最后一个元素。

m和n必须是原切片中的有效索引(即,<=原切片容量)。

示例:

numbers := []int{0,1,2,3,4,5,6,7,8} 
fmt.Println(numbers)					// [0 1 2 3 4 5 6 7 8]

n1 := numbers[1:4]
fmt.Println(n1)							// [1 2 3]

n2 := numbers[:4]
fmt.Println(n2)							// [0 1 2 3]

n3 := numbers[4:]
fmt.Println(n3)							// [4 5 6 7 8]

n4 := numbers[:]
fmt.Println(n4)							// [0 1 2 3 4 5 6 7 8]

当对切片执行截取操作后,得到的新切片与原切片共用一个底层数组。其数据结构图如下:
在这里插入图片描述

因此,当修改新切片的元素值时,原切片中对应位置的元素值也被改变。

numbers := []int{0,1,2,3,4,5,6,7,8} 
fmt.Println(numbers)					// [0 1 2 3 4 5 6 7 8]

n1 := numbers[1:4]
fmt.Println(n1)							// [1 2 3]

// 修改新切片元素值
n1[0] = 100
n1[1] = 200

fmt.Println(n1)							// [100 200 3]
fmt.Println(numbers)					// [0 100 200 3 4 5 6 7 8]


数组也支持截取

对数组执行截取操作,得到的结果是一个切片:

numbers := [...]int{0,1,2,3,4,5,6,7,8} 
fmt.Println(numbers)					// [0 1 2 3 4 5 6 7 8]

n1 := numbers[1:4]
fmt.Println(n1)							// [1 2 3]

n2 := numbers[:4]
fmt.Println(n2)							// [0 1 2 3]

n3 := numbers[4:]
fmt.Println(n3)							// [4 5 6 7 8]

n4 := numbers[:]
fmt.Println(n4)							// [0 1 2 3 4 5 6 7 8]

// 修改新切片元素值将会改变原数组中的元素值
n1[0] = 100
n1[1] = 200
fmt.Println(n1)							// [100 200 3]
fmt.Println(numbers)					// [0 100 200 3 4 5 6 7 8]
fmt.Println(n2)							// [0 100 200 3]
fmt.Println(n3)							// [4 5 6 7 8]
fmt.Println(n4)							// [0 100 200 3 4 5 6 7 8]

截取时的长度和容量

切片截取生成的新切片的长度和容量是多少?可按这个公式来计算:

假设底层数组的容量是k,对其进行切片[m:n]得到的新切片的长度为n-m,容量为k-m。

验证上面的公式:

numbers := make([]int, 6, 9)
n1 := numbers[1:3]
fmt.Printf("len=%d, cap=%d\n", len(n1), cap(n1))

根据上面的公式,n1长度=3-1=2,n1容量=9-1=8。执行上面的代码打印出:

len=2, cap=8

确实和公式计算出的结果一样。


截取时能否指定新切片的容量?

可以。Go语言还提供了一种使用3个索引的截取操作,第3个用来限定新切片的容量,语法格式为[m:n:k]。

通过这个操作截取得到的新切片长度=n-m,容量=k-m。当然,此处的k必须<=原切片容量,否则引发越界异常。

示例:

numbers := make([]int, 6, 9)

n1 := numbers[1:3:5]
fmt.Printf("len=%d, cap=%d\n", len(n1), cap(n1))		// len=2, cap=4

n2 := numbers[1:3:9]
fmt.Printf("len=%d, cap=%d\n", len(n2), cap(n2))		// len=2, cap=8

n3 := n1[0:2:4]
fmt.Printf("len=%d, cap=%d\n", len(n3), cap(n3))		// len=2, cap=4

n4 := n1[0:2:5]			// 越界异常:5 超过n1的容量了
fmt.Printf("len=%d, cap=%d\n", len(n4), cap(n4))

扩展切片长度(长度不超过容量)

截取的时候通常比原切片的长度小。但如果截取时指定的索引超出原切片的长度,得到的新切片将引用原切片中的未被使用的部分。

通过这种方法我们可以改变切片的长度,但长度不能超过容量,做法如下:

// 设slice是切片,new_len值不大于slice的容量,但大于其长度

slice = slice[0:new_len] // 执行后slice将具有新的长度,新增加的元素的值不是零值,而是底层数组中该元素的原有值。

切片可以通过这种方式改变长度,直到占满了整个切片的容量。


移除切片中元素

为了从slice的中间移除一个元素,并保留剩余元素的顺序,可以使用函数copy来将高位索引的元素向前移动来覆盖被移除元素所在位置:

// i必须不能越界, 即 0 <= i < len(slice)
func remove(slice[]int, i int) []int {
    copy(slice[i:], slice[i+1:])
    return slice[:len(slice)-1]
}

func main() {
    s := []int{1, 2, 3, 4, 5}
    s = remove(s, 2)
    fmt.Println(s)		// 输出 [1 2 3 5]
}

9.4 向函数传递切片

切片是引用类型,向函数传递切片类型时,仅仅会拷贝切片的数据结构部分,而不会拷贝底层数组,传入函数的切片和原切片共用同一个底层数组。因此在函数内修改切片元素值,会改变原切片中的元素值。

示例:

package main

import "fmt"

func main() {
    slice := []int{1, 2, 3, 4, 5}
    fmt.Printf("%p\n", slice)
    myFunction(slice)
    fmt.Println(slice)
}

func myFunction(slice []int) {	// 注意这里[]中没有长度。没有长度表示切片,有长度就是数组了。
    fmt.Printf("%p\n", slice)
    slice[1] = 10
}

// 执行后输出
// 0xc00000c300
// 0xc00000c300
// [1 10 3 4 5]

其实无论是值类型还是引用类型,在函数间传递的时候都是值传递。

只不过值类型变量的数据结构中直接存放数据,在传递的时候拷贝该数据结构,也就把整个值拷贝了。

引用类型变量的数据结构中存放的是指向数据的指针,在传递的时候只拷贝该数据结构,并不拷贝指针指向的数据。


同样的,如果向myFunction函数传入一个由数组截取而来的切片,在函数内修改切片元素值,原数组中的元素值也会改变:

package main

import "fmt"

func main() {
    arr := [...]int{1, 2, 3, 4, 5}
    fmt.Printf("%p\n", &arr)
    myFunction(arr[:])
    fmt.Println(arr)
}

func myFunction(slice []int) {	// 注意这里[]中没有长度。没有长度表示切片,有长度就是数组了。
    fmt.Printf("%p\n", slice)
    slice[1] = 10
}

// 执行后输出
// 0xc00000c300
// 0xc00000c300
// [1 10 3 4 5]

9.5 切片扩展

虽然不能访问长度之外的元素,但可以向切片追加元素来扩展其长度。在扩展长度的时候,如果容量不够,也会自动扩展容量。append()函数用来实现该功能。

append()函数的作用:向切片追加元素,原切片不变,返回一个具有新长度的新切片。


append()的执行过程:

首先检查原切片是否有足够容量来存放追加的元素。

=> 如果原切片容量足够容纳追加的元素,将会定义一个与原切片共用底层数组的具有新长度的新切片,然后将待追加的元素复制到底层数组中,并返回这个新切片。

=> 如果原切片容量不够容纳追加的元素,将会创建一个拥有全新的底层数组的新切片,新底层数组的容量足够容纳原切片的元素和待追加的元素,然后将原切片中的元素复制到新底层数组,再将待追加的元素复制到新底层数组后面。


以上过程可用伪代码描述:

func append(slice []T, ele T)  {
    var sliceNew []T
    lenNew := len(slice) + 1
    if lenNew <= cap(slice) {
        // slice容量足够,只需扩展其长度
        sliceNew = slice[:lenNew]
    } else {
        // slice容量不够,需要扩展容量,此处采用将容量扩展一倍的策略,Go语言实际的扩展策略有所不同。
        capNew := lenNew
        if capNew < 2*len(slice) {
            capNew = 2 * len(slice)
        }
        sliceNew = make([]T, lenNew, capNew)
        copy(sliceNew, slice)
    }
    sliceNew[len(slice)] = ele
    return sliceNew
 }

通常情况下,我们并不清楚一个append()调用会不会导致分配新的底层数组,所以我们不能假设原始的slice和调用append()之后返回的结果slice是否指向的是同一个底层数组。所以,我们也无法假设对旧slice上的元素操作会不会影响西悉尼的slice元素。所以,我们通常将append()调用结果再次赋值给原slice:

slice := []int{1, 2, 3}
slice = append(slice, 4)

记住:不仅仅是在调用append函数的情况下需要更新slice变量,对于任何函数,只要有可能改变slice的长度或者容量,或者是使得slice指向不同的底层数组,都需要更新slice变量。


使用示例:

package main

import "fmt"

func main() {
    // 向空切片中append
    s1 := []int{}
    fmt.Printf("len = %d, cap = %d\n", len(s1), cap(s1))		// len = 0, cap = 0
    fmt.Println("s1 = ", s1)									// s1 =  []
    s1 = append(s1, 1)
    s1 = append(s1, 2)
    s1 = append(s1, 3)
    fmt.Printf("len = %d, cap = %d\n", len(s1), cap(s1))		// len = 3, cap = 4
    fmt.Println("s1 = ", s1)									// s1 =  [1 2 3]
    
    // 向非空切片中append
    s2 := []int{1, 2, 3}
	fmt.Printf("len = %d, cap = %d\n", len(s2), cap(s2))		// len = 3, cap = 3
    fmt.Println("s2 = ", s2)									// s2 =  [1 2 3]
    s2 = append(s2, 5)
    s2 = append(s2, 5)
    s2 = append(s2, 5)
    s2 = append(s2, 5)
	fmt.Printf("len = %d, cap = %d\n", len(s2), cap(s2))		// len = 7, cap = 12
    fmt.Println("s2 = ", s2)									// s2 =  [1 2 3 5 5 5 5]
    
    // Go支持向nil切片中append
    var s3 []int
    fmt.Printf("len = %d, cap = %d\n", len(s3), cap(s3))		// len = 0, cap = 0
    fmt.Println("s3 = ", s3)									// s3 =  []
    s3 = append(s3, 1)
    fmt.Printf("len = %d, cap = %d\n", len(s3), cap(s3))		// len = 1, cap = 1
    fmt.Println("s3 = ", s3)									// s3 =  [1]
    
    // 验证append返回的是一个新切片
    s4 := make([]int, 3, 6)
    fmt.Printf("len = %d, cap = %d\n", len(s4), cap(s4))		// len = 3, cap = 6
    fmt.Println("s4 = ", s4)									// s4 =  [0 0 0]
    s5 := append(s4, 1)
    fmt.Printf("len = %d, cap = %d\n", len(s4), cap(s4))		// len = 3, cap = 6
    fmt.Println("s4 = ", s4)									// s4 =  [0 0 0]    !!!原切片没有变
    fmt.Printf("len = %d, cap = %d\n", len(s5), cap(s5))		// len = 4, cap = 6
    fmt.Println("s5 = ", s5)									// s5 =  [0 0 0 1]
    
}

9.6 字符串与切片

对字符串做截取操作得到的结果是一个字符串,而不是切片类型。而且Go语言的字符串是不可变的。

str := "Hello World"
substr := str[0:5]
fmt.Printf("type:%T, value: %s\n", substr, substr)		// 输出 type:string, value: Hello

//str[0] = 'h'		// 编译错误:cannot assign to str[0]
//substr[0] = 'h'		// 编译错误:cannot assign to substr[0]

可以从字符串生成[]byte或[]rune类型的切片:

str := "Hello 世界"
b := []byte(str)
r := []rune(str)
fmt.Println(b)		// 输出 [72 101 108 108 111 32 228 184 150 231 149 140]
fmt.Println(r)		// 输出 [72 101 108 108 111 32 19990 30028]

可以通过代码 len([]rune(s)) 来获得字符串中字符的数量,但使用 utf8.RuneCountInString(s) 效率会更高一点。


如果想要修改字符串中的某些字符,那么可以先把字符串转为[]byte或[]rune类型切片,修改切片中的数据,最后再把切片转为字符串。

示例:

str := "Hello 世界"

b := []byte(str)
b[0] = 'h'
str2 := string(b)
fmt.Println(str2)				// 输出 hello World
fmt.Println(str)				// 输出 Hello World		// 发现原字符串并没有变

r := []rune(str)
r[6] = '天'
str3 := string(r)
fmt.Println(str3)				// 输出 Hello 天界
fmt.Println(str)				// 输出 Hello 世界		// 原字符串并没有变

9.7 通过切片实现栈

// push
stack = append(stack, v)

// empty
empty := len(stack) == 0

// top
top := stack[len(stack)-1]

// pop
stack = stack[:len(stack)-1]


Copyright@2022 , 359152155@qq.com

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

时空旅客er

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值