go slice

介绍

go slices是go的一种原生类型,可以存储数据序列。slices类似于其它语言的数组,不同的是,相比于其它语言的数组,slices多了一些特殊的特性。

go数组

go slices是建立在数组上的抽象概念,所以在了解slices之前,不妨先了解一下go数组

go数组声明如下

var a [4]int //声明数组时,需要指定数组长度和类型

以上声明表示a为一个长度为4,数组类型为int的数组。数组的长度是固定的,不可变的。这里需要注意的是,数组的长度也是数组类型的一部分,[4]int和[5]int是不同的类型。

数组访问是通过索引访问,a[n]表示访问a中从索引0开始的第n个元素

a[0] = 1  //赋值
i := a[0]  //取值 i == 1

数组无需显示初始化,数组在声明后,便可以视为一个长度为n,数组中所有元素为声明类型默认值的数组。

var a [4]int
fmt.Println(a)  // [0 0 0 0]

a数组在内存中表示的就是四个连续的int值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bk4s1vyR-1655217408163)(images/go slices/1655186312616.png)]

go的数组变量代表的是数组本身,而不是数组第一个元素的地址。在方法中,go数组参数传入的也是一个数组的拷贝,而非指向数组的指针。

如下所示,运行该方法并不会改变原有数组中a[0]的值

func change(a [4]int) { 
    a[0] = 100
}

var a [4]int
change(a)
fmt.Println(a) // [0 0 0 0]

如下所示,只有传入数组指针,才可以成功修改a[0]的值

func change(a *[4]int) { 
    a[0] = 100
}

var a [4]int
change(&a)
fmt.Println(a) // [100 0 0 0]

数组的声明也可以像下面这样

b := [2]string{"Penn", "Teller"}

或者这样

b := [...]string{"Penn", "Teller"}

这两个例子声明的数组b,本质上都是 [2]string 类型

slices

go数组在使用上并不太灵活,所以大多数情况下,代码中都使用的是slices,slices以数组为基础,提供了更强大的功能。

slice声明时无需指定数组长度,声明如下

var a []string

或者直接初始化

letters := []string{"a", "b", "c", "d"}

当然,make方法也可以创建slices

s := make([]byte, 5) //指定slices长度len和类型,cap大小默认和长度len一致

除了指定长度和类型,也可以指定cap大小

s = make([]byte, 5, 10)

这里可能有人会有疑惑,slices的len和cap有什么区别,后续我们会详细解释这个问题,接下来继续看看slices其他的创建和初始化操作

前面说到slices是基于数组的,那么根据数组生成slices也是理所当然的操作

x := [3]string{"Лайка", "Белка", "Стрелка"}
s := x[:] // a slice referencing the storage of x

创建新的slices可以针对现有的数组或者slices进行切片操作,具体操作方式为 arr[n:m]

将会得到一个包含数组arr(或者slices)下标[n,m)数据的新slices

其中n和m都可以省略,如果全部省略,那么新slices会获得原先数组或者slices的全部数据,只省略n的话,得到数据范围为[0,m),省略m的话,数据范围为[n,len(arr))

slice internals

slice是数组段的描述,它由指向数组的指针,slice长度(len),以及slice最大容量(cap)三部分构成

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GFxuHfds-1655217408164)(images/go slices/1655198298844.png)]

使用make创建出来的slice s,结构类似于下面这样

s := make([]byte, 5)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2cjfqtuZ-1655217408164)(images/go slices/1655198357671.png)]

slice长度len是slice引用的元素数量,slice的容量cap则是底层数组中可包含元素的数量(从slice最开始指向的元素开始计算)。

针对s重新切片,其底层变化如下图,可以看到,len变为2,而cap从数组下标2开始计算,为3。

s = s[2:4]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aP907QM0-1655217408165)(images/go slices/1655198849930.png)]

slice由于其结构的特殊性,所以在使用过程中并不像go数组那样传递拷贝的值,而是拷贝的指向底层数组的指针,因此,针对slice的操作都会影响底层数组的数据。这样也可以得到结论,通过对数组或者slice重新切片得到的slice进行操作,将会影响原先数组和slice

d := []byte{'r', 'o', 'a', 'd'}
e := d[2:]
// e == []byte{'a', 'd'}
e[1] = 'm'
// e == []byte{'a', 'm'}
// d == []byte{'r', 'o', 'a', 'm'}

slices的扩容

如果需要增加slice的容量,可以创建一个新的slice,使得新创建的slice容量是原先slice的两倍,并且将原先slice的底层数组的值赋给新的slice的底层数组,最后,将指向原先slice的指针指向新的slice

t := make([]byte, len(s), (cap(s)+1)*2) // +1 in case cap(s) == 0
for i := range s {
        t[i] = s[i]
}
s = t

在上面例子中,for循环的赋值还可以用copy方法来实现,copy方法如下

func copy(dst, src []T) int

使用copy方法,可以将上面的扩容操作简化

t := make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t

针对slice的常见操作为给slice后面追加新的值,下面用一个简单的函数来实现这一个功能,AppendByte可以给slice后面追加多个数据,在cap大小不够的情况下自动扩容,最后返回新的slice

func AppendByte(slice []byte, data ...byte) []byte {
    m := len(slice)
    n := m + len(data)
    if n > cap(slice) { // if necessary, reallocate
        // allocate double what's needed, for future growth.
        newSlice := make([]byte, (n+1)*2)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:n]
    copy(slice[m:n], data)
    return slice
}

p := []byte{2, 3, 5}
p = AppendByte(p, 7, 11, 13)
// p == []byte{2, 3, 5, 7, 11, 13}

当然,go语言也提供了append函数来操作slice

func append(s []T, x ...T) []T

append函数和AppendByte的使用方式相同,且都会自动扩展slice的cap

a := make([]int, 1)
// a == []int{0}
a = append(a, 1, 2, 3)
// a == []int{0, 1, 2, 3}

如果需要将两个slice合并的话,可以使用如下方式

a := []string{"John", "Paul"}
b := []string{"George", "Ringo", "Pete"}
a = append(a, b...) // equivalent to "append(a, b[0], b[1], b[2])"
// a == []string{"John", "Paul", "George", "Ringo", "Pete"}

这里要说明一下,slice的默认值为nil,但是和map不同的是,slice不需要显示的初始化,可以在声明之后直接使用append方法来为空的slice追加值而不需担心报错。

使用中可能会出现的坑

前面提到,slice使用过程中并不会拷贝整个数组,而仅是拷贝指向底层数组的指针,那么在使用过程中,如果底层数组过大,而我们使用的slice仅需极少的值的话,就会导致内存浪费

下面的例子展示了这样的一种情况,我们仅需文件中的数字,但是slice指向的底层数组会包含整个文件

var digitRegexp = regexp.MustCompile("[0-9]+")

func FindDigits(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    return digitRegexp.Find(b)
}

如果仅需数字的话,可以在获取到slice值时,进行一次copy操作,返回新创建的slice,这样,就避免了内存浪费

func CopyDigits(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    b = digitRegexp.Find(b)
    c := make([]byte, len(b))
    copy(c, b)
    return c
}

参考

Go Slices: usage and internals

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值