Go字符串以及切片的性能 | 青训营笔记

180 阅读3分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第一篇笔记

字符串和切片是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的最后两个元素拷贝到新切片。