原文链接:https://blog.thinkeridea.com/201901/go/slice_de_yi_xie_shi_yong_ji_qiao.html
slice 是 Go 语言十分重要的数据类型,它承载着很多使命,从语言层面来看是 Go 语言的内置数据类型,从数据结构来看是动态长度的顺序链表,由于 Go 不能直接操作内存(通过系统调用可以实现,但是语言本身并不支持),往往 slice 也可以用来帮助开发者申请大块内存实现缓冲、缓存等功能。
在 Go 语言项目中大量的使用 slice, 我总结三年来对 slice 的一些操作技巧,以方便可以高效的使用 slice, 并使用 slice 解决一些棘手的问题。
slice 的基本操作先熟悉一些 slice 的基本的操作, 对最常规的 : 操作就可玩出很多花样。
s=ss[:] 引用一个切片或数组
s=s[:0] 清空切片
s=s[:10] s=s[10:] s=s[10:20] 截取接片
s=ss[0:10:20] 从切片或数组引用指定长度和容量的切片
下标索引操作的一些误区 s[i:l:c] i 是起始偏移的起始位置,l 是起始偏移的长度结束位置, l-i 就是新 slice 的长度, c 是起始偏移的容量结束位置,c-i 就是新 slice 的容量。其中 i 、l 、c 并不是当前 slice 的索引,而是引用底层数组相对当前 slice 起始位置的偏移量,所以是可超出当前 slice 的长度的, 但不能超出当前 slice 的容量,如下操作是合法的:
package main import ( "fmt" ) func main() { s := make([]int, 100) s[20] = 100 s1 := s[10:10] s2 := s1[10:20] fmt.Println(s1) fmt.Println(s2) }其中 s1 是 [];s2 是 [100 0 0 0 0 0 0 0 0 0], 这里并不会发生下标越界的情况,一个更好的例子在
创建 slice
创建切片的方法有很多,下面罗列一些常规的:
var s []int 创建 nil切片
s := make([]int, 0, 0) 、 s=[]int{} 创建无容量空切片
s:= make([]int, 0, 100) 创建有容量空切片
s:=make([]int, 100) 创建零值切片
s:=array[:] 引用数组创建切片
内置函数
len(s) 获取切片的长度
cap(s) 获取切片的容量
append(s, ...) 向切片追加内容
copy(s, s1) 向切片拷贝内容
一个缓冲的简单示例遇到过很多拼接字符串的方法,各种各样的都有 fmt builder buffer + 等等,实际上 builder 和 buffer 都是使用 []byte 的切片作为缓冲来实现的,fmt 往往性能最差,原因是它主要功能不是连接字符串而是格式化数据会用到反射等等操作。+ 操作在大量拼接时性能也是很差, 不过小字符串少量拼接效果很理想,builder 往往性能不如 buffer 特别是在较短字符串拼接上,实际 builder 和 buffer 实现原理非常类似,builder 在转成字符串时使用了 unsafe 减少了一次内存分配,因为小字符串因为扩容机制不如 buffer 灵活,所以性能有所不如,大字符串降低一次大的内存分配就显得很明显了。
经常遇到一个需求就是拼接 []int 中个各个元素,很多种实现都有人用,都是需要遍历转换 int 到 string,但是拼接方法千奇百怪,以下提供两种方法对比(源码在GitHub)。
package slice import ( "strconv" "unsafe" ) func SliceInt2String1(s []int) string { if len(s) < 1 { return "" } ss := strconv.Itoa(s[0]) for i := 1; i < len(s); i++ { ss += "," + strconv.Itoa(s[i]) } return ss } func SliceInt2String2(s []int) string { if len(s) < 1 { return "" } b := make([]byte, 0, 256) b = append(b, strconv.Itoa(s[0])...) for i := 1; i < len(s); i++ { b = append(b, ',') b = append(b, strconv.Itoa(s[i])...) } return string(b) } func SliceInt2String3(s []int) string { if len(s) < 1 { return "" } b := make([]byte, 0, 256) b = append(b, strconv.Itoa(s[0])...) for i := 1; i < len(s); i++ { b = append(b, ',') b = append(b, strconv.Itoa(s[i])...) } return *(*string)(unsafe.Pointer(&b)) }SliceInt2String1 使用原始的 + 操作,因为是较小的字符串拼接,使用 + 主要是因为在小字符串拼接性能优于其它几种方法,SliceInt2String2 与 SliceInt2String3 都使用了一个 256 容量的 []byte 作为缓冲, 唯一的区别是在返回时一个使用 string 转换类型,一个使用 unsafe 转换类型。
写了一个性能测试(源码在GitHub),看一下效果吧:
goos: darwin goarch: amd64 pkg: github.com/thinkeridea/example/slice BenchmarkSliceInt2String1-8 3000000 461 ns/op 144 B/op 9 allocs/op BenchmarkSliceInt2String2-8 20000000 117 ns/op 32 B/op 1 allocs/op BenchmarkSliceInt2String3-8 10000000 144 ns/op 256 B/op 1 allocs/op PASS ok github.com/thinkeridea/example/slice 5.928s明显可以看得出 SliceInt2String2 的性能是 SliceInt2String1 7倍左右,提升很明显,SliceInt2String2 与 SliceInt2String3 差异很小,主要是因为使用 unsafe 转换类型导致大内存无法释放,实际这个测试中连接字符串只需要 32 个字节,使用 unsafe 却导致 256 个字节无法被释放,这也正是 builder 和 buffer 的差别,所以小字符串拼接 buffer 性能往往更好。在这里简单的通过 []byte 减少内存分配次数来实现缓冲。