在平时的开发过程中总会遇到各式各样的字符串拼接场景。本文就从本场景出发,探讨并实验哪一种拼接方法最为高效,以及其背后的底层原理实现。

字符串拼接定义

定义一个方法,实现字符串的拼接,函数签名原型如下:

type concatFn func(...string) string

即传入一系列的字符串,返回其拼接后的结果。

方式1:直接相加

func ConcatWithAdd(strs ...string) string {
 var r = ""
 for _, v := range strs {
  r += v
 }
 return r
}

StringHeader
type StringHeader struct {
    Len int
    Data uintptr
}

因此,在golang中,字符串是不可变的,这也导致了一个字符串的相加过程中会创建一个全新的变量来接收拼接后的结果。所以它在内存分配次数上、时间效率上都不是最优的选择。

方式2:使用fmt.Sprintf()

func ConcatWithSprintf(strs ...string) string {
 var r = ""
 for _, v := range strs {
  r = fmt.Sprintf("%s%s", r, v)
 }
 return r
}

fmt.Sprintf[]bytefmt.Spritf()

方式3:使用strings.Buider

func ConcatWithStringBuilder(strs ...string) string {
 var b strings.Builder
 for _, v := range strs {
  b.WriteString(v)
 }
 return b.String()
}

strings.Buider[]byte

方式4:使用strings.Join()

func ConcatWithStringsJoin(str ...string) string {
 return strings.Join(str, "")
}

strings.Join[]bytestrings.Builder

方式5:使用bytes.Buffer

func ConcatWithByteBuffer(strs ...string) string {
 var b bytes.Buffer
 for _, v := range strs {
  b.WriteString(v)
 }
 return b.String()
}

bytes.Buffer[]bytestrings.Builderb.String()strings.BuiderString()[]bytebytes.Bufferstrings.Buidler

性能测试

从以上的底层数据类型的分析,我们大致可以知道字符串拼接场景下不同拼接方式的优劣了。

package main

import (
 "strconv"
 "testing"
)

func prepare() []string {
 var r = []string{}
 for i := 0; i < 1000; i++ {
  r = append(r, strconv.Itoa(i))
 }
 return r
}

func BenchmarkConcatWithAdd(b *testing.B) {
 b.StopTimer()
 r := prepare()
 b.StartTimer()
 for i := 0; i < b.N; i++ {
  _ = ConcatWithAdd(r...)
 }
}

func BenchmarkConcatWithSprintf(b *testing.B) {
 b.StopTimer()
 r := prepare()
 b.StartTimer()
 for i := 0; i < b.N; i++ {
  _ = ConcatWithSprintf(r...)
 }
}

func BenchmarkConcatWithStringBuilder(b *testing.B) {
 b.StopTimer()
 r := prepare()
 b.StartTimer()
 for i := 0; i < b.N; i++ {
  _ = ConcatWithStringBuilder(r...)
 }
}

func BenchmarkConcatWithByteBuffer(b *testing.B) {
 b.StopTimer()
 r := prepare()
 b.StartTimer()
 for i := 0; i < b.N; i++ {
  _ = ConcatWithByteBuffer(r...)
 }
}

func BenchmarkConcatWithStringsJoin(b *testing.B) {
 b.StopTimer()
 r := prepare()
 b.StartTimer()
 for i := 0; i < b.N; i++ {
  _ = ConcatWithStringsJoin(r...)
 }
}

执行基准测试,可得:

Running tool: /usr/data/go1.17/go/bin/go test -benchmem -run=^$ -coverprofile=/tmp/vscode-goTqWRSX/go-code-cover -bench . demo

goos: linux
goarch: amd64
pkg: demo
cpu: Intel(R) Core(TM) i5-8500 CPU @ 3.00GHz
BenchmarkConcatWithAdd-4                  2076     597540 ns/op  1494032 B/op      999 allocs/op
BenchmarkConcatWithSprintf-4              1528    1122768 ns/op  1526913 B/op     2999 allocs/op
BenchmarkConcatWithStringBuilder-4      103244      16315 ns/op    10488 B/op       12 allocs/op
BenchmarkConcatWithByteBuffer-4          77482      27777 ns/op    12464 B/op        8 allocs/op
BenchmarkConcatWithStringsJoin-4         74232      23012 ns/op     3072 B/op        1 allocs/op
PASS
coverage: 100.0% of statements
ok   demo 9.102s

结论

fmt.Sprintf()strings.Builderbytes.Buffer[]bytefmt.Sprintfstrings.Joinstrings.Buidlerstrings.Buidlerbyte.Bufferbytes.Bufferstrings.Builder
strings.Builderstrings.Builder

最后,欢迎大家访问本文的网站版本:https://int64.ink/blog/golang字符串拼接的几种方式