golang中的字符串拼接

go中常见的字符串拼接方法

假设我们现在要实现这样一个拼接函数: 将字符串重复n次拼接起来,返回一个新字符串。

方法一:使用+运算符

func simpleSplice(s string, n int) string {
	newStr := ""
	for i := 0; i < n; i++ {
		newStr += s
	}
	return newStr
}

方法二:使用Sprintf

func sprintfSplice(s string, n int) string {
	newStr := ""
	for i := 0; i < n; i++ {
		newStr = fmt.Sprintf("%s%s", newStr, s)
	}
	return newStr
}

方法三:使用[]byte

func bytesSplice(s string, n int) string {
	newStr := []byte{}
	for i := 0; i < n; i++ {
		newStr = append(newStr, []byte(s)...)
	}
	return string(newStr)
}

方法四:使用bytes.Buffer

func bufferSplice(s string, n int) string {
	buffer := bytes.Buffer{}
	for i := 0; i < n; i++ {
		buffer.WriteString(s)
	}
	return buffer.String()
}

方法五:使用strings.Builder

func builderSplice(s string, n int) string {
	builder := strings.Builder{}
	for i := 0; i < n; i++ {
		builder.WriteString(s)
	}
	return builder.String()
}

性能测试

我们对上面五种方法进行benchmark测试。

测试函数类似这样,生成100长度的随机字符串,然后拼接100次。

func BenchmarkBytesSplice3(b *testing.B) {
	for i := 0; i < b.N; i++ {
		bytesSplice3(genStr(100), 100)
	}
}

结果如下:

BenchmarkSimpleSplice
BenchmarkSimpleSplice-12     	    9901	    123436 ns/op
BenchmarkSprintfSplice
BenchmarkSprintfSplice-12    	    8151	    144824 ns/op
BenchmarkBytesSplice
BenchmarkBytesSplice-12      	   62271	     19435 ns/op
BenchmarkBuilderSplice
BenchmarkBuilderSplice-12    	   93918	     11890 ns/op
BenchmarkBufferSplice
BenchmarkBufferSplice-12     	   97413	     11816 ns/op

以上仅测试了一次, 不要求严谨, 只是为了说明问题

可以看见使用+运算符拼接和Sprintf的性能是最差的。

+运算符/Sprintf

+运算符和Sprintf性能差的原因其实差不多: 每次都会创建一个新的字符串,然后将原来的字符串和新的字符串拼接起来,这样就会产生很多的临时字符串,这些临时字符串会占用很多的内存,而且还会增加GC的负担。

bytes/strings.Builder/buffer

这三者都是利用[]byte实现的功能,所以从当前这个测试中来看差别不大(对比上面那两个来说~)。

我们查看buffer的源码发现, 他每次写入之前会计算好所需的空间, 然后将其copy[]byte中。

Builder中虽然使用append追加的数据, 但是使用了unsafe方法直接操作内存指针。

所以操作[]byte来实现拼接是最快, 而bufferBuilder带来的内存优化有多少呢?

内存占用

我们调大需要测试的字符串长度, 这样会更明显: 字符串长度1000, 拼接1000次。

func BenchmarkBytesSplice3(b *testing.B) {
	for i := 0; i < b.N; i++ {
		bytesSplice3(genStr(1000), 1000)
	}
}

运行结果

BenchmarkBytesSplice3-12            2421            521164 ns/op         2033674 B/op       1003 allocs/op
BenchmarkBuilderSplice-12           1135            986065 ns/op         5238292 B/op         26 allocs/op
BenchmarkBufferSplice-12            2733            451784 ns/op         3105806 B/op         14 allocs/op

BytesSplice3为什么会有1003 allocs ? 明明已经预分配内存了

func bytesSplice3(s string, n int) string {
    // 预分配内存   
	newStr := make([]byte, 0, len(s)*n)
	for i := 0; i < n; i++ {
        // 问题在这里, 每次都会创建一个新的[]byte
        // 其实这个动作可以省略掉
		newStr = append(newStr, []byte(s)...)
        // 更换成这种写法试试
        // newStr = append(newStr, s...)
	}
	return *(*string)(unsafe.Pointer(&newStr))
}

更换后:

BenchmarkBytesSplice3-12            8913            135231 ns/op         1009667 B/op          3 allocs/op
BenchmarkBuilderSplice-12           1530            756342 ns/op         5238292 B/op         26 allocs/op
BenchmarkBufferSplice-12            3198            381564 ns/op         3105808 B/op         14 allocs/op

总结

在可预知拼接结果长度的情况下, 使用make([]byte, 0, len(s)*n)这样的方式来预分配内存是最合适的。 其他情况下, 使用strings.Builder, buffer都是可以的。

你可能感兴趣的:(go,golang,开发语言,后端)