网站首页 > 知识剖析 正文
简介
Go语言的slice(切片,后面统一使用slice)类型为处理一组同类型数据提供了便捷的方法。它与其它编程语言的“数组”有些类似,但相对而言包含了更多的特性。通过这篇文章,我们来看一看slice到底是什么,如何去使用它。
Arrays(数组)
Slice是建立在arrary(数组)类型上的一种抽象,要理解slice,我们必须先了解一下数组。
array的定义中包含一个数组长度和元素类型字段。举个例子,类型[4]int 表示一个有四个整型数的数组。数组的类型是固定的,它的长度也是类型的一部分,因此 [4]int 和 [5]int 是两个完全不同的数据类型。我们可以使用下标对数组进行检索,表达式s[n] 即数组的第n个元素(从0开始)。
var a [4]int
a[0] = 1
i := a[0]
// i == 1
使用者不需要对数组进行显式初始化,一个零值的数组的所有元素默认被初始化为0:
// a[2] == 0, the zero value of the int type
类型 [4]int 的一个数组在内存中表现为四个连续存放的整型数:
Go语言中,数组是“值”,一个数组变量代表整个数组;注意,与C语言不同,它不是指向数组首元素的指针。这意味着,当你把一个数组变量进行传递或赋值时,你会得到它的一份拷贝。为了避免拷贝,可以传递指向该数组的指针。你可以把数组当成一种结构体(struct),只是通过下标而不是字段名获取元素,或者当成一个固定大小的组合值。
我们可以使用下面这种方式定义一个数组:
b := [2]string{"Penn", "Teller"}
不指定元素个数也可以,编译器会自动计算:
b := [...]string{"Penn", "Teller"}
在上面两个例子中,b 的类型都是[2]string。
Slices(切片)
数组有一些应用场景,但是不太灵活,所以在go语言的代码中不经常出现。但是切片可以随处可见。切片建立在数组之上,但是功能和易用上都更胜一筹。
切片的类型规格是 []T,这里 T 是元素的类型。不像数组,切片没有特定的长度。
切片变量的定义和数组有些类似,但是不用定义长度:
letters := []string{"a", "b", "c","d"}
切片也可以使用make函数进行创建,make的语言规格如下:
func make([]T, len, cap) []T
这里 T 表示将被创建切片的元素类型。Make函数接受三个参数:类型、长度(length)、容量(capacity)。第三个参数是可选的,如果不设置,则与“长度”一致。被调用时,make分配一个数组,然后返回一个指向该数组的slice。
var s []byte
s = make([]byte, 5, 5)
// s == []byte{0, 0, 0, 0, 0}
下面这行代码实现了同样的效果:
s := make([]byte, 5)
我们可以使用len和cap函数分别查看切片的长度和容量:
len(s) == 5
cap(s) == 5
下两个环节我们会讨论长度和容量的关系。
切片的零值是 nil。使用len和cap函数时,返回值都是0。
还有一种创建切片的方式:slicing(切割)。切割操作时通过一个半开的域来定义的,语法上表现为使用冒号分开的两个下标。举个例子,表达式 b[1:4] 会创建一个包含b第1、2、3位置三个元素的切片,新切片的长度是3:
b := []byte{'g', 'o', 'l', 'a', 'n', 'g'}
// b[1:4] == []byte{'o', 'l', 'a'}, sharing the same storage as b
起始和结束的下标都是可选的,默认值分别是0和原始切片的长度:
// b[:2] == []byte{'g', 'o'}
// b[2:] == []byte{'l', 'a', 'n', 'g'}
// b[:] == b
基于数组创建切片的语法类似:
x := [3]string{"Лайка", "Белка", "Стрелка"}
s := x[:] // a slice referencing the storage of x
slice的内部机制
切片是数组片段的描述符,它包含一个指向数组的指针、片段的长度、容量(片段的最大长度)。
之前通过make([]byte,5)创建的变量s的内存结构如下:
长度(length)即切片中元素的个数
容量(capacity)是slice基于的数组的元素个数(从slice指针指向的第一个元素开始计算)。
后面我们还会讲几个例子,长度和容量的差别会越来越清晰。
我们对 s 进行切割,观察数据结构的变化,以及与底层数组关系的变化。
s = s[2:4]
切割并不会拷贝原切片的数据,而是创建一个新的切片,新切片指向原切片底层的数组。所以切割操作的效率非常高,因此带来的一个副作用是修改新切片元素的值时,也会修改老切片的值:
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'}
之前,我们对 s 进行切割后,s 的长度已经小于容量(见上图)。通过切割我们可以把 s的长度调整成和容量一致。
s = s[:cap(s)]
增长切片(通过copy和append函数)
如果要增加一个切片变量的长度,你必须创建一个新的、更大切片变量,然后将原切片的内容拷贝过去。这项技术时从其它语言的动态数组学来的。下一个例子中,我们将通过创建一个新切片t 来将原切片 s 的容量扩大一倍,然后将 s 的内容拷贝到 t,最后将 t 赋值给 s。
// +1 以免 cap(s) == 0
t := make([]byte, len(s), (cap(s)+1)*2)
for i := range s {
t[i] = s[i]
}
s = t
遍历赋值的操作可以使用内置的copy函数实现。这个函数正如其名,将数据从一个切片拷贝到另一个切片,返回拷贝元素的数量。
func copy(dst, src []T) int
copy 函数支持在不同长度的切片之间拷贝数据(以元素个数较少的为准)。另外,如果两个切片共享一个底层数组,即便两个切片的数据存在重叠部分,copy 函数也能正确处理。
使用 copy 函数,上面的代码可以简化为:
t := make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t
切片的一个常用操作是向末尾添加数据。AppendByte 函数支持向byte切片添加byte元素,必要时自动增长切片,返回更新过的切片。
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
}
你可以像下面这样使用AppendByte:
p := []byte{2, 3, 5}
p = AppendByte(p, 7, 11, 13)
// p == []byte{2, 3, 5, 7, 11, 13}
类似于AppendByte的函数非常实用,即便切片在不断增长,也能够完全应付得来。考虑到不同程序的具体情况,刚开始可能需要分配一个较小或较大的内存,或限制重新分配的大小。
但是大多数程序并不需要完全掌控这些细节,因此Go语言提供了内置的 append函数,该函数能够应付大多数情况下的需求。该函数的语言规格是:
func append(s []T, x ...T) []T
append 函数可以将元素x添加到切片 s,并在需要更大的容量时,增长切片。
a := make([]int, 1)
// a == []int{0}
a = append(a, 1, 2, 3)
// a == []int{0, 1, 2, 3}
如果要把一个切片追加到另一个切片的末尾,使用 ... 扩展参数列表:
a := []string{"John", "Paul"}
b := []string{"George", "Ringo", "Pete"}
a = append(a, b...) // 等价于 "append(a, b[0], b[1], b[2])"
// a == []string{"John", "Paul", "George", "Ringo", "Pete"}
由于切片的零值(nil)和零长度表现是一致的,你可以声明一个切片,然后在一个循环里对它赋值:
// Filter returns a new slice holding only
// the elements of s that satisfy f()
func Filter(s []int, fn func(int) bool) []int {
var p []int // == nil
for _, v := range s {
if fn(v) {
p = append(p, v)
}
}
return p
}
一个可能的“坑”
之前提到,重新切割不会拷贝底层的数组,所以整个数组会一致保留在内存中,知道没有变量去引用它。在极少数情况下,这可能会导致程序把一大整块数据都保留在内存中,而只用到极少的一部分。
举个例子,FindDigits函数加载一个文件到内存中,查询一组连续的数字,并作为一个新的slice返回。
var digitRegexp = regexp.MustCompile("[0-9]+")
func FindDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
return digitRegexp.Find(b)
}
这段代码表现很正常,但是返回了的 []byte 指向的数组包含整个文件的内容。由于切片指向原始数组,只要这个切片存在,gc就不会释放底层数组。极少有用的数据却把整个文件的内容卡在内存里。
为了修正这个问题,你可以把有用的数据存放到一个新的切片中,然后返回它:
func CopyDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
b = digitRegexp.Find(b)
c := make([]byte, len(b))
copy(c, b)
return c
}
这个函数更准确的版本可以借助于append实现,这里留给读者去思考。
扩展阅读
Effective Go包含slice和arrary的深入探讨,Go语言规格定义了slice和相关的辅助函数。
原作者:Andrew Gerrand,翻译:赵帅虎
相关链接:
原文链接:https://blog.golang.org/go-slices-usage-and-internals
Effective Go:http://golang.org/doc/effective_go.html
Effective Go slices:http://golang.org/doc/effective_go.html#slices
Go 语言规格:http://golang.org/doc/go_spec.html
Go 语言规格 slices: http://golang.org/doc/go_spec.html#Slice_types
猜你喜欢
- 2024-11-10 PHP数组学习笔记(1) php数组有哪几种类型
- 2024-11-10 Rust语言入门教程 数组和切片 rust语言例子
- 2024-11-10 javascript自学笔记:Array类型1 javascript自学笔记:array类型1怎么解决
- 2024-11-10 Array.from详解: 语法、功能与应用场景
- 2024-11-10 帮你精通JS:解析与盘点数组array的5类22种方法
- 2024-11-10 10 个实用的 JS 技巧 js常用方法大全
- 2024-11-10 WordPress 内置的数组处理相关函数大全
- 2024-11-10 3分钟短文 | PHP获取函数的代码片段,唯有反射最高效
- 2024-11-10 JS 中的类数组对象如何转换为数组?
- 2024-11-10 Go 中的循环是如何转为汇编的?看完你懂了吗?
- 最近发表
- 标签列表
-
- xml (46)
- css animation (57)
- array_slice (60)
- htmlspecialchars (54)
- position: absolute (54)
- datediff函数 (47)
- array_pop (49)
- jsmap (52)
- toggleclass (43)
- console.time (63)
- .sql (41)
- ahref (40)
- js json.parse (59)
- html复选框 (60)
- css 透明 (44)
- css 颜色 (47)
- php replace (41)
- css nth-child (48)
- min-height (40)
- xml schema (44)
- css 最后一个元素 (46)
- location.origin (44)
- table border (49)
- html tr (40)
- video controls (49)