如果连续拼接一组这样的操作,比如输入 [][]int, 输出 []string ():
package slice import ( "strconv" "unsafe" ) func SliceInt2String4(s [][]int) []string { res := make([]string, len(s)) for i, v := range s { if len(v) < 1 { res[i] = "" continue } res[i] += strconv.Itoa(v[0]) for j := 1; j < len(v); j++ { res[i] += "," + strconv.Itoa(v[j]) } } return res } func SliceInt2String5(s [][]int) []string { res := make([]string, len(s)) b := make([]byte, 0, 256) for i, v := range s { if len(v) < 1 { res[i] = "" continue } b = b[:0] b = append(b, strconv.Itoa(v[0])...) for j := 1; j < len(v); j++ { b = append(b, ',') b = append(b, strconv.Itoa(v[j])...) } res[i] = string(b) } return res }SliceInt2String5 中使用 b = b[:0] 来促使达到反复使用一块缓冲区,写了一个性能测试(),看一下效果吧:
goos: darwin goarch: amd64 pkg: github.com/thinkeridea/example/slice BenchmarkSliceInt2String4-8 300000 4420 ns/op 1440 B/op 82 allocs/op BenchmarkSliceInt2String5-8 1000000 1102 ns/op 432 B/op 10 allocs/op PASS ok github.com/thinkeridea/example/slice 8.364s较 + 版本提升接近4倍的性能,这是使用 slice 作为缓冲区极好的技巧,使用非常方便,并不用使用 builder 和 buffer, slice 操作非常的简单实用。
append 与 copy如果合并多个 slice 为一个,有三种方式来合并,主要合并差异来源于创建新 slice 的方法,使用 var news []int 或者 news:=make([]int, 0, len(s1)+len(s2)....) 的方式创建的新变量就需要使用 append 来合并,如果使用 news:=make([]int, len(s1)+len(s2)....) 就需要使用 copy 来合并。不同的方法也有差异,append 和 copy 在这个例子中主要差异在于 append 适用于零长度的初始化 slice, copy 适用于确定长度的 slice。
写了一个测试来看看两者的差异吧(源码在GitHub):
func BenchmarkExperiment3Append1(b *testing.B) { for i := 0; i < b.N; i++ { var s []int for j := 0; j < 20; j++ { s = append(s, []int{j, j + 1, j + 2, j + 3, j + 4}...) } } } func BenchmarkExperiment3Append2(b *testing.B) { for i := 0; i < b.N; i++ { s := make([]int, 0, 100) for j := 0; j < 20; j++ { s = append(s, []int{j, j + 1, j + 2, j + 3, j + 4}...) } } } func BenchmarkExperiment3Copy(b *testing.B) { for i := 0; i < b.N; i++ { s := make([]int, 100) n := 0 for j := 0; j < 20; j++ { n += copy(s[n:], []int{j, j + 1, j + 2, j + 3, j + 4}) } } }测试结果如下:
goos: darwin goarch: amd64 pkg: github.com/thinkeridea/example/slice BenchmarkExperiment3Append1-8 2000000 782 ns/op 3024 B/op 6 allocs/op BenchmarkExperiment3Append2-8 10000000 192 ns/op 0 B/op 0 allocs/op BenchmarkExperiment3Copy-8 10000000 217 ns/op 0 B/op 0 allocs/op PASS ok github.com/thinkeridea/example/slice 6.926s从结果上来看使用没有容量的 append 性能真的很糟糕,实际上不要对没有任何容量的 slice 进行 append 操作是最好的实践,在准备用 append 的时候应该预先给定一个容量,哪怕这个容量并不是确定的,像前面缓存连接字符串时一样,并不能明确使用的空间,先分配256个字节,这样的好处是可以减少系统调用分配内存的次数,即使空间不能用完,也不用太过担心浪费,append 本身扩容机制也会导致空间不是刚刚好用完的,而初始化的容量往往结合业务场景给的一个均值,这是很好的。
append 和 copy 在预先确定长度和容量时 append 效果更好一些,主要原因是 copy 需要一个变量来记录位置。 如果使用场景中没有强制限定长度,建议使用 append 因为 append 会根据实际情况再做内存分配,较 copy 也更加灵活一些, 而 copy 往往用在长度固定的地方,可以防止数据长度溢出的问题,例如标准库中 strings.Repeat 函数,它采用指数增长的方式快速填充指定数量的字符,但是如果使用 append 就会发生多余的内存分配,导致长度溢出。
func Repeat(s string, count int) string { b := make([]byte, len(s)*count) bp := copy(b, s) for bp < len(b) { copy(b[bp:], b[:bp]) bp *= 2 } return string(b) } csv reader 中的一个例子官方标准库 csv 的读取性能极高,其中 reader 里面有使用 slice 极好的例子,以下是简略的代码,如果想要全面了解程序需要去看标准库的源码:
func (r *Reader) readRecord(dst []string) ([]string, error) { line, errRead = r.readLine() if errRead == io.EOF { return nil, errRead } r.recordBuffer = r.recordBuffer[:0] r.fieldIndexes = r.fieldIndexes[:0] parseField: for { if r.TrimLeadingSpace { line = bytes.TrimLeftFunc(line, unicode.IsSpace) } i := bytes.IndexRune(line, r.Comma) field := line if i >= 0 { field = field[:i] } else { field = field[:len(field)-lengthNL(field)] } r.recordBuffer = append(r.recordBuffer, field...) r.fieldIndexes = append(r.fieldIndexes, len(r.recordBuffer)) if i >= 0 { line = line[i+commaLen:] continue parseField } break parseField } if err == nil { err = errRead } // Create a single string and create slices out of it. // This pins the memory of the fields together, but allocates once. str := string(r.recordBuffer) // Convert to string once to batch allocations dst = dst[:0] if cap(dst) < len(r.fieldIndexes) { dst = make([]string, len(r.fieldIndexes)) } dst = dst[:len(r.fieldIndexes)] var preIdx int for i, idx := range r.fieldIndexes { dst[i] = str[preIdx:idx] preIdx = idx } return dst, err }这里删除了极多的代码,但是能看懂大意,其中 line 是一段 bufio 中的一段引用,所以这块数据不能返回给用户,也不能进行并发读取操作。