最近在翻阅Go部分源代码,略有涉及到数组(array)和切片(slice)的实现,本文出自Arrays, slices (and strings): The mechanics of 'append'(https://blog.golang.org/slices) 的中文翻译版本,我并无彻底按照原文翻译,部份内容我从新作了解释以及加入我的理解,还有部份内容我作了适当的删除和简化,若是不当之处多多指正。 golang
·介绍
·数组
·Slice:Slice Header
·函数传递Slice
·Slice 指针:方法接收器
·make
·copy
·append:示例编程
编程语言中最多见的一个概念是数组,看起来是彷佛很简单,但在将数组添加到编程语言时必须考虑许多问题,例如:数组
这些问题的答案会影响数组是编程语言的众多特性之一仍是其核心设计。 数据结构
在Go早期的开发阶段,设计数组前大约花了一年的时间来解决这些问题,关键之一是引入Slice:在一个固定大小的数组上有一个灵活且可扩展的数据结构。app
数组是Go中重要模块,像其余基础模块同样数组隐藏在一些可见组件之下。在谈及功能更增强大、突出的切片以前先简单说说数组。 编程语言
在Go程序中常常看不到数组,由于数组的大小是数组类型的组成部分,这点限制了书面表达能力。函数
var buffer [256]byte
以上定义了数组变量buffer
,类型是[256]byte
,类型中描述了大小是256,从这里能够理解:[256]byte
和[512]byte
是不一样的数组类型。 翻译
与数组有关的数据是元素,数组在内存中的样子以下:设计
buffer:byte byte byte ...... byte byte byte
这个变量拥有256字节的数据,除此别无其它。能够经过下标访问其元素:buffer[0],buffer[1]
等,若是索引值超过256访问数组元素会引发panic。指针
恐怕这里有个疑问:Slice的应用场景是什么?只有理解Slice是什么和Slice能作什么才能准确使用。
Slice被描述为:与Slice自己分开存储的数组的连续部分的数据结构,Slice不是数组,它描述一个数组。
能够用以下方式定义Slice变量:
var slice []byte = buffer[100:150] var slice = buffer[100:150] slice := buffer[100:150]
因此Slice到底是什么?在Go源代码目录下reflectvalue.go这个文件中找到sliceHeader
的定义:
type sliceHeader struct { Data unsafe.Pointer Len int Cap int }
因而咱们能够暂且对Slice作以下的理解(伪代码,暂时忽略Cap变量):
slice := sliceHeader{ Len: 50, Data: &buffer[100], }
正如上,在数组上构建了一个Slice,一样能够在Slice上构建Slice:
slice2 := slice[5:10]
根据咱们对Slice的理解,slice2的范围是[5, 10),放到原始数组上即[105, 110),那么slice2的结构应该是这样子:
slice2 := sliceHeader{ Len: 5, Data: &buffer[105], }
以上能够知道:slice和slice2仍然指向同一个底层buffer数组。
如今尝试从新构建slice
:从新截取一个Slice,并把新的Slice做为结果返回给原始Slice结构。
slice = slice[5:10]
这种状况下,slice看起来和slice2同样,再截取一次:
slice = slice[1:len(slice) - 1]
对应的sliceHeader:
slice = sliceHeader{ Len: 8, Data: &buffer[101] }
能够联想到Slice的应用场景之一:截取。
理解Slice包含原始数组指针同时它又是一个值这点很重要,Slice是一个包含了指针和长度的struct。
考虑以下的代码:
func AddOneToEachElement(slice []byte) { for i := range slice { slice[i]++ } } func main() { slice := buffer[10:20] for i := 0; i < len(slice); i++ { slice[i] = byte(i) } fmt.Println("before", slice) AddOneToEachElement(slice) fmt.Println("after", slice) }
Slice的传递规则是值传递,值传递过程当中拷贝的是sliceHeader
结构,并未改变内部指针,该Slice和原Slice都指向同一个数组,当函数返回时候,原数组元素已被修改。
func SubtractOneFromLength(slice []byte) []byte { slice = slice[0 : len(slice)-1] return slice } func main() { slice := buffer[10:20] for i := 0; i < len(slice); i++ { slice[i] = byte(i) } fmt.Println("Before: len(slice) =", len(slice)) newSlice := SubtractOneFromLength(slice) fmt.Println("After: len(slice) =", len(slice)) fmt.Println("After: len(newSlice) =", len(newSlice)) }
上面的代码意图对slice进行截取,但因为Slice是值传递,由于进入SubtractOneFromLength
只是slice的一个拷贝值,因此先后slice的长度都不变。若是某个函数想修改Slice长度,一个可行的方法是把新的Slice做为结果参数返回。
另外一个修改Slice的方法是以指针方式传递,上一节的代码能够改为这种:
func PtrSubtractOneFromLength(slicePtr *[]byte) { slice := *slicePtr *slicePtr = slice[0 : len(slice)-1] } func main() { slice := buffer[10:20] for i := 0; i < len(slice); i++ { slice[i] = byte(i) } fmt.Println("Before: len(slice) =", len(slice)) PtrSubtractOneFromLength(&slice) fmt.Println("After: len(slice) =", len(slice)) }
这种方法有点累赘,多了一个临时变量作中转,对于要修改Slice的函数来讲,使用指针传递也是比较常见的方式。还有一种方式:
type path []byte func (p *path) TruncateAtFinalSlash() { i := bytes.LastIndex(*p, []byte("/")) if i >= 0 { *p = (*p)[0:i] } } func (p path) ToUpper() { for i, b := range p { if 'a' <= b && b <= 'z' { p[i] = b + 'A' - 'a' } } } func main() { pathName := path("/usr/bin/tso") pathName.TruncateAtFinalSlash() fmt.Printf("%s\n", pathName) pathName1 := path("/usr/bin/tso") pathName1.ToUpper() fmt.Printf("%s\n", pathName1) } // output: /usr/bin /USR/BIN/TSO
若是咱们将TruncateAtFinalSlash
改成value receiver会发现并无改变原数组,而ToUpper
不管是value receiver仍是point receiver都会改变原数组。这也是Slice有趣的一点,Slice在函数传递中是值传递(拷贝变量值,内部指针仍旧指向原数组),若TruncateAtFinalSlash
为value receiver,在进行p = (p)[0:i]
操做时,p将会是一个新的Slice而不是pathName,而在ToUpper
中,以Slice方式操做底层原数组,不管是哪一种receiver都将改变原数组。
正如前面所说,sliceHeader
中还有一个Cap
变量,这个变量存储了Slice的容量,记录数组实际使用了多少的空间,这是Len
能达到的最大值。看看这样的代码:
func main() { var array [10]int for i := 0; i < 10; i++ { array[i] = i } slice := array[6:10] fmt.Printf("%v, %v, %v, %p\n", slice, cap(slice), len(slice), &slice[0]) slice = append(slice, 11) fmt.Printf("%v, %v, %v, %p\n", slice, cap(slice), len(slice), &slice[0]) slice[0] = 12 fmt.Printf("%v, %v, %v, %p\n", slice, cap(slice), len(slice), &slice[0]) fmt.Println(array) } // out put [6 7 8 9], 4, 4, 0xc00006a0d0 [6 7 8 9 11], 8, 5, 0xc00007c100 [12 7 8 9 11], 8, 5, 0xc00007c100 [0 1 2 3 4 5 6 7 8 9]
因而咱们知道,当向Slice追加元素致使Cap
大于Len
时会建立一个Cap大于原数组的新数组(首元素地址不一致),并将值拷贝进新数组,以后再改变Slice元素值时改变的是新建立的数组(切断与原数组的引用关系)。
根据Slice的定义:Cap
限制了Slice的增加,当想增大Slice到大于自己容量时,推荐的作法是建立新的数组,而后把Slice数据拷贝到新数组。使用make
建立一个新的数据并建立一个Slice。make
有三个参数:Slice类型,初始长度和容量,用于存储Slice数据的数组长度,默认状况下,
func main() { slice := make([]int, 10, 15) fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice)) newSlice := make([]int, len(slice), 2*cap(slice)) for i := range slice { newSlice[i] = slice[i] } slice = newSlice fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice)) } // output len: 10, cap: 15 len: 10, cap: 30
make
建立了一个新的Slice,而后将原数据拷贝至新的Slice(所指向的数组)。
Go有个内建的copy
函数,参数是两个Slice,将第二个Slice的数据拷贝到第一个Slice中:
func main() { slice := make([]int, 10, 15) newSlice := make([]int, len(slice), 2*cap(slice)) copy(newSlice, slice) }
对于Slice的copy
而言,有一点比较绕口:copy复制的元素数量是两个Slice中长度最小的那个,必定程度上节约效率。
有一种常见的状况:原Slice和目的Slice出现交叉(在C++中咱们常叫地址重叠),但copy操做任然能正常进行,这意味着copy
能够用于单个Slice移动元素。
// 向Slice指定有效位置插入元素,Slice必须有空间能够增长新的元素 func Insert(slice []int, index, value int) []int { // 增长1个元素的空间 slice = slice[0:len(slice)+1] // 使用copy移动Slice前半部分 copy(slice[index + 1:], slice[index:]) // 存放新的值 slice[index] = value return slice }
Insert
函数完成向Slice中插入值,值得注意的是,函数必须返回Slice(前面有奖过为何)。
这里留一个问题:如何自实现Slice append
函数?