这是我参与「第三届青训营 -后端场」笔记创作活动的的第一篇笔记
字符串和切片是Go中常用的数据结构,关于这两种数据结构的使用也是十分有讲究的,使用不当的话可能会造成性能损失,接下来就来分析一下这两种数据结构如何使用才能更好的提升性能。
字符串
常见的字符串拼接方式
使用+
func plusConcat(n int ,str string) string{
s :=""
for i:=0;i<n;i++{
s +=str
}
return s
}
使用fmt.Sprintf
func sprintfConcat(n int, str string)string{
s:=""
for i:=0;i<n;i++{
s = fmt.Sprintf("%s%s",s,str)
}
return s
}
使用strings.Builder
func builderConcat(n int, str string)string{
var builder strings.Builder
for i:=0;i<n;i++{
builder.WriteString(str)
}
return builder.String()
}
使用bytes.Buffer
func bufferConcat(n int, str string)string{
buf:=new (bytes.Buffer)
for i:=0;i<n;i++{
buf.WriteString(str)
}
return buf.String()
}
bytes.Buffer vs strings.Builder
底层都是[]byte数组,但是后者的性能略快,一个重要的区别在于bytes.Buffer转化为字符串时重新申请了一块空间,存放生成的字符串变量,而strings.Builder直接将底层的 []byte转换成了字符串类型。
为什么这两种方法会比使用+更快呢? 这是因为切片[]byte的内存是以倍数申请的。例如,初始化大小为0,当第一次写入6byte的字符串时,则会申请大小为8byte的内存,第二次写入10byte时,内存大小不够,那么会增加一倍,也就是16byte,以此类推....。在实际的过程中,超过1024byte之后,内存不会以倍数增加。
而直接使用+拼接的话,每拼接一个字符串都需要申请一块新的内存空间。加入每次拼接10byte的字符串,拼接1w次,那么总共需要申请 10 + 102 + 103 + .... + 10*100000 的内存空间。
使用[]bytes
func byteConcat(n int, str string)string{
buf := make([]byte,0)
for i:=0;i<n;i++{
buf = append(buf,str...)
}
return string(buf)
}
如果我们可以提前预知切片的大小的话,那么我们可以在创建切片的时候顺便给切片赋值一个容量大小,这样以后每次容量不够的时候就不用频繁的去申请内存空间了,有利于性能的提高。
关于切片的性能陷阱
存在什么问题?
如果我们原有的切片的基础上进行切片的话,不会创建新的底层数组,而是使用原有的切片的底层数组,这样一来这一片内存区域就会被一直占用着,知道没有变量引用该函数组。 如果存在一种情况,就是原切片有一个很大的数组组成,但是我们只在该切片上使用一小片,但是底层数组却一直占用着大片的内存区域,得不到释放。
那么有什么解决方法呢?
答案是使用copy重新创建一个切片。如下:
func smallSlice(num []int) []int{
return num[len(num)-2:]
}
func smallSliceCopy(num []int)[]int{
res := make([]int,2)
copy(res,[num:len(num)-2:])
return res
}
- 第一个函数直接在原切片的基础上进行切片,底层数组无法释放。
- 第二个函数创建了一个新的切片,并将num的最后两个元素拷贝到新切片。