Go的数组是值语义。一个数组变量表示整个数组,它不是指向第一个元素的指针(不像 C 语言的数组)。 当一个数组变量被赋值或者被传递的时候,实际上会复制整个数组。 (为了避免复制数组,你可以传递一个指向数组的指针,但是数组指针并不是数组。) 可以将数组看作一个特殊的struct,结构的字段名对应数组的索引,同时成员的数目固定。
b:=[...]string{"Penn",sans-serif;font-size:16px;">这两种写法,b
都是对应[2]string
类型。
切片
数组虽然有适用它们的地方,但是数组不够灵活,因此在Go代码中数组使用的并不多。 但是,切片则使用得相当广泛。切片基于数组构建,但是提供更强的功能和便利。
切片类型的写法是[]T
,T
是切片元素的类型。和数组不同的是,切片类型并没有给定固定的长度。
切片的字面值和数组字面值很像,不过切片没有指定元素个数:
letters:=[]string{"a","b","c","d"}
切片可以使用内置函数make
创建,函数签名为:
funcmake([]T,len,cap)[]T
其中T代表被创建的切片元素的类型。函数make
接受一个类型、一个长度和一个可选的容量参数。 调用make
时,内部会分配一个数组,然后返回数组对应的切片。
vars[]byte
s=make([]byte,5,5)
//s==[]byte{0,0}
当容量参数被忽略时,它默认为指定的长度。下面是简洁的写法:
s:=make([]byte,5)
可以使用内置函数len
和cap
获取切片的长度和容量信息。
len(s)==5
cap(s)==5
接下来的两个小节将讨论长度和容量之间的关系。
切片的零值为nil
。对于切片的零值,cap
都将返回0。
切片也可以基于现有的切片或数组生成。切分的范围由两个由冒号分割的索引对应的半开区间指定。 例如,表达式b[1:4]
创建的切片引用数组b
的第1到3个元素空间(对应切片的索引为0到2)。
b:=[]byte{'g','o','l','a','n','g'}
//b[1:4]==[]byte{'o','a'},sharingthesamestorageasb
切片的开始和结束的索引都是可选的;它们分别默认为零和数组的长度。
//b[:2]==[]byte{'g','o'}
//b[2:]==[]byte{'l','g'}
//b[:]==b
下面语法也是基于数组创建一个切片:
x:=[3]string{"Лайка","Белка","Стрелка"}
s:=x[:]//aslicereferencingthestorageofx
切片的内幕
一个切片是一个数组片段的描述。它包含了指向数组的指针,片段的长度, 和容量(片段的最大长度)。
前面使用make([]byte,5)
创建的切片变量s
的结构如下:
长度是切片引用的元素数目。容量是底层数组的元素数目(从切片指针开始)。 关于长度和容量和区域将在下一个例子说明。
我们继续对s
进行切片,观察切片的数据结构和它引用的底层数组:
s=s[2:4]
切片操作并不复制切片指向的元素。它创建一个新的切片并复用原来切片的底层数组。 这使得切片操作和数组索引一样高效。因此,通过一个新切片修改元素会影响到原始切片的对应元素。
d:=[]byte{'r','d'}
e:=d[2:]
//e==[]byte{'a','d'}
e[1]='m'
//e==[]byte{'a','m'}
//d==[]byte{'r','m'}
前面创建的切片s
长度小于它的容量。我们可以增长切片的长度为它的容量:
s=s[:cap(s)]
切片增长不能超出其容量。增长超出切片容量将会导致运行时异常,就像切片或数组的索引超 出范围引起异常一样。同样,不能使用小于零的索引去访问切片之前的元素。
切片的生长(copy and append 函数)
要增加切片的容量必须创建一个新的、更大容量的切片,然后将原有切片的内容复制到新的切片。 整个技术是一些支持动态数组语言的常见实现。下面的例子将切片s
容量翻倍,先创建一个2倍 容量的新切片t
,复制s
的元素到t
,然后将t
赋值给s
:
t:=make([]byte,len(s),(cap(s)+1)*2)//+1incasecap(s)==0
fori:=ranges{
t[i]=s[i]
}
s=t
循环中复制的操作可以由 copy 内置函数替代。copy 函数将源切片的元素复制到目的切片。 它返回复制元素的数目。
funccopy(dst,src[]T)int
copy
函数支持不同长度的切片之间的复制(它只复制较短切片的长度个元素)。 此外,copy
函数可以正确处理源和目的切片有重叠的情况。
使用copy
函数,我们可以简化上面的代码片段:
一个常见的操作是将数据追加到切片的尾部。下面的函数将元素追加到切片尾部, 必要的话会增加切片的容量,最后返回更新的切片:
funcAppendByte(slice[]byte,data...byte)[]byte{
m:=len(slice)
n:=m+len(data)
ifn>cap(slice){//ifnecessary,reallocate
//allocatedoublewhat'sneeded,forfuturegrowth.
newSlice:=make([]byte,(n+1)*2)
copy(newSlice,slice)
slice=newSlice
}
slice=slice[0:n]
copy(slice[m:n],data)
returnslice
}
下面是AppendByte
的一种用法:
p:=[]byte{2,3,5}
p=AppendByte(p,7,11,13)
//p==[]byte{2,13}
类似AppendByte
的函数比较实用,因为它提供了切片容量增长的完全控制。 根据程序的特点,可能希望分配较小的活较大的块,或则是超过某个大小再分配。
但大多数程序不需要完全的控制,因此Go提供了一个内置函数append
, 用于大多数场合;它的函数签名:
funcappend(s[]T,x...T)[]T
append函数将x
追加到切片s
的末尾,并且在必要的时候增加容量。
a:=make([]int,1)
//a==[]int{0}
a=append(a,1,2,3)
//a==[]int{0,3}
如果是要将一个切片追加到另一个切片尾部,需要使用...
语法将第2个参数展开为参数列表。
a:=[]string{"John","Paul"}
b:=[]string{"George","Ringo","Pete"}
a=append(a,b...)//equivalentto"append(a,b[0],b[1],b[2])"
//a==[]string{"John","Paul","George","Pete"}
由于切片的零值nil
用起来就像一个长度为零的切片,我们可以声明一个切片变量然后在循环 中向它追加数据:
//Filterreturnsanewsliceholdingonly
//theelementsofsthatsatisfyf()
funcFilter(s[]int,fnfunc(int)bool)[]int{
varp[]int//==nil
for_,v:=ranges{
iffn(v){
p=append(p,v)
}
}
returnp
}
可能的“陷阱”
正如前面所说,切片操作并不会复制底层的数组。整个数组将被保存在内存中,直到它不再被引用。 有时候可能会因为一个小的内存引用导致保存所有的数据。
例如,FindDigits
函数加载整个文件到内存,然后搜索第一个连续的数字,最后结果以切片方式返回。
vardigitRegexp=regexp.MustCompile("[0-9]+")
funcFindDigits(filenamestring)[]byte{
b,_:=ioutil.ReadFile(filename)
returndigitRegexp.Find(b)
}
这段代码的行为和描述类似,返回的[]byte
指向保存整个文件的数组。因为切片引用了原始的数组, 导致 GC 不能释放数组的空间;只用到少数几个字节却导致整个文件的内容都一直保存在内存里。
要修复整个问题,可以将感兴趣的数据复制到一个新的切片中:
funcCopyDigits(filenamestring)[]byte{
b,_:=ioutil.ReadFile(filename)
b=digitRegexp.Find(b)
c:=make([]byte,len(b))
copy(c,b)
returnc
}
可以使用append
实现一个更简洁的版本。
(编辑:李大同)
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!