详解Go语言中的Slice

本文详细介绍了Go语言中的Slice,其作为动态数组,由指针、长度和容量三部分组成,允许灵活的容量调整。Slice可以进行切片操作,通过append函数可追加元素,并探讨了 Slice 的内存管理技巧,包括模拟栈操作和删除元素的方法。

 

什么是Slice

我们知道每种语言的底层数据结构中都会有数组这一结构,数组由连续的内存空间组成,因此系统很容易通过基址寄存器和偏移量来控制索引,从而读到数组的内容。

那么在使用一个数组之前,首先需要创建这个数组结构。于是我们通常需要向内存申请这么一块连续的固定大小的空间,再将数组的起始位置的地址返回给变量,这样,这个变量便持有了对这个数组的引用。

到这里我们知道了,我们创建一个数组需要向内存申请一块指定大小的空间,因此我们必须在创建数组时明确表明所创建数组的大小,即容量。那么如果我们在创建时确实无法明确数组的具体容量怎么办呢?

动态数组解决了这一问题。

什么是动态数组呢?顾名思义,随着我们程序的需求能够进行动态容量调整的数组,就是动态数组。

在Java中,有ArrayList,也就是数组列表。它的底层结构同样是数组,通过初始化一个较小的初始容量来创建初始数组,当数组的大小超过一定阈值时,便以2倍大小进行自动扩容。

而在Go语言中,就是Slice(切片)

Slice详解

Java中的ArrayList与普通数组array密不可分。slice同样离不开数组结构,slice的底层引用着一个数组对象。

它由3部分组成:

  • 指针——指向第一个slice元素对应的底层数组元素的地址
  • 长度
  • 容量 

多个slice可共享底层数据

slice可进行切片操作例如s[i:j],0<=i<=j<=cap(s)。创建一个新的slice,引用s切片的第i个元素到底j-1个元素。

若i或j被省略,则i索引以0代替,j索引以len(s)代替。

切片操作中,范围超过cap(s)上限将抛出panic异常,但超出len(s)则是拓展了slice。

由于我们创建的slice包含指向第一个slice元素的指针,它直接指向底层数组结构,在想函数传递slice时,复制该slice参数仅是对其底层数组创建一个新的别名,也就意味着我们能够在函数中直接操作原内存空间的数据。比如以下反转函数:

func reverse(a []int)  {
	for i, j := 0, len(a)-1;i<j;i,j = i+1,j-1  {
		a[i],a[j] = a[j],a[i]
	}
}

反转后:

a := [...]int{1,2,3,4,5}
reverse(a[:])
fmt.Println(a)  //[5 4 3 2 1]

append函数

append函数用于向slice追加元素。

例如appendInt函数,调用前首先检查slice底层是否有足够容量保存新添加的元素,有的话,直接扩展slice,将新添加的元素复制到新扩展的空间,返回slice。

若空间不足,则先分配一个足够大的slice用于保存结果。扩展数组时通常直接将当前长度翻倍,演示如下:

func main() {
    var x, y []int
    for i := 0; i < 10; i++ {
        y = appendInt(x, i)
        fmt.Printf("%d cap=%d\t%v\n", i, cap(y), y)
        x = y
    }
}

每次容量变化会导致重新分配内存和复制操作

0  cap=1    [0]
1  cap=2    [0 1]
2  cap=4    [0 1 2]
3  cap=4    [0 1 2 3]
4  cap=8    [0 1 2 3 4]
5  cap=8    [0 1 2 3 4 5]
6  cap=8    [0 1 2 3 4 5 6]
7  cap=8    [0 1 2 3 4 5 6 7]
8  cap=16   [0 1 2 3 4 5 6 7 8]
9  cap=16   [0 1 2 3 4 5 6 7 8 9]

内置的append函数可能使用比appendInt更复杂的内存扩展策略。因此,通常我们并不知道append调用是否导致了内存的重新分配,因此我们也不能确认新的slice和原始的slice是否引用的是相同的底层数组空间。同样,我们不能确认在原先的slice上的操作是否会影响到新的slice。因此,通常是将append返回的结果直接赋值给输入的slice变量:

runes = append(runes, r)

 

我们的appendInt函数每次只能向slice追加一个元素,但是内置的append函数则可以追加多个元素,甚至追加一个slice。

var x []int
x = append(x, 1)
x = append(x, 2, 3)
x = append(x, 4, 5, 6)
x = append(x, x...) // append the slice x
fmt.Println(x)      // "[1 2 3 4 5 6 1 2 3 4 5 6]"

 Slice内存技巧

  1. 原字符串切片内存上去除“”返回:
func nonEmpty(string []string) []string {
	i := 0
	for _,s := range string {
		if s != "" {
			string[i] = s
			i++
		}
	}
	return string[:i]
}
func main() {
	
	data := []string{"one", "", "three"}
	fmt.Printf("%q\n", nonEmpty(data)) // `["one" "three"]`
	fmt.Printf("%q\n", data)           // `["one" "three" "three"]`
}

nonempty函数也可以使用append函数实现:

func nonempty2(strings []string) []string {
    out := strings[:0] // zero-length slice of original
    for _, s := range strings {
        if s != "" {
            out = append(out, s)
        }
    }
    return out
}

无论如何实现,以这种方式重用一个slice一般都要求最多为每个输入值产生一个输出值,事实上很多这类算法都是用来过滤或合并序列中相邻的元素。这种slice用法是比较复杂的技巧,虽然使用到了slice的一些技巧,但是对于某些场合是比较清晰和有效的。

一个slice可以用来模拟一个stack。最初给定的空slice对应一个空的stack,然后可以使用append函数将新的值压入stack:

stack = append(stack, v) // push v

stack的顶部位置对应slice的最后一个元素:

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

通过收缩stack可以弹出栈顶的元素

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

要删除slice中间的某个元素并保存原有的元素顺序,可以通过内置的copy函数将后面的子slice向前依次移动一位完成:

func remove(slice []int, i int) []int {
    copy(slice[i:], slice[i+1:])
    return slice[:len(slice)-1]
}

func main() {
    s := []int{5, 6, 7, 8, 9}
    fmt.Println(remove(s, 2)) // "[5 6 8 9]"
}

如果删除元素后不用保持原来顺序的话,我们可以简单的用最后一个元素覆盖被删除的元素:

func remove(slice []int, i int) []int {
    slice[i] = slice[len(slice)-1]
    return slice[:len(slice)-1]
}

func main() {
    s := []int{5, 6, 7, 8, 9}
    fmt.Println(remove(s, 2)) // "[5 6 9 8]
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值