姜亚 發表於 2022-1-6 09:32:00

Go 字符串拼接6种,最快的方式 -- strings.builder

<p data-tool="mdnice编辑器">我们首先来了解一下<code>Go</code>语言中<code>string</code>类型的结构定义,先来看一下官方定义:</p>
<pre data-tool="mdnice编辑器"><code>//&nbsp;string&nbsp;is&nbsp;the&nbsp;set&nbsp;of&nbsp;all&nbsp;strings&nbsp;of&nbsp;8-bit&nbsp;bytes,&nbsp;conventionally&nbsp;but&nbsp;not<br>//&nbsp;necessarily&nbsp;representing&nbsp;UTF-8-encoded&nbsp;text.&nbsp;A&nbsp;string&nbsp;may&nbsp;be&nbsp;empty,&nbsp;but<br>//&nbsp;not&nbsp;nil.&nbsp;Values&nbsp;of&nbsp;string&nbsp;type&nbsp;are&nbsp;immutable.<br>type&nbsp;string&nbsp;string<br></code></pre>
<p data-tool="mdnice编辑器"><code>string</code>是一个<code>8</code>位字节的集合,通常但不一定代表UTF-8编码的文本。string可以为空,但是不能为nil。<strong>string的值是不能改变的</strong>。</p>
<p data-tool="mdnice编辑器"><code>string</code>类型本质也是一个结构体,定义如下:</p>
<pre data-tool="mdnice编辑器"><code>type&nbsp;stringStruct&nbsp;struct&nbsp;{<br>&nbsp;&nbsp;&nbsp;&nbsp;str&nbsp;unsafe.Pointer<br>&nbsp;&nbsp;&nbsp;&nbsp;len&nbsp;int<br>}<br></code></pre>
<p data-tool="mdnice编辑器"><code>stringStruct</code>和<code>slice</code>还是很相似的,<code>str</code>指针指向的是某个数组的首地址,<code>len</code>代表的就是数组长度。怎么和<code>slice</code>这么相似,底层指向的也是数组,是什么数组呢?我们看看他在实例化时调用的方法:</p>
<pre data-tool="mdnice编辑器"><code>//go:nosplit<br>func&nbsp;gostringnocopy(str&nbsp;*byte)&nbsp;string&nbsp;{<br>&nbsp;ss&nbsp;:=&nbsp;stringStruct{str:&nbsp;unsafe.Pointer(str),&nbsp;len:&nbsp;findnull(str)}<br>&nbsp;s&nbsp;:=&nbsp;*(*string)(unsafe.Pointer(&amp;ss))<br>&nbsp;return&nbsp;s<br>}<br></code></pre>
<p data-tool="mdnice编辑器">入参是一个<code>byte</code>类型的指针,从这我们可以看出<code>string</code>类型底层是一个<code>byte</code>类型的数组,所以我们可以画出这样一个图片:</p>
<p><img alt="图片" class="rich_pages wxw-img lazyload" data-ratio="0.3633136094674556" data-src="https://mmbiz.qpic.cn/mmbiz_png/CqB2u93NwB8jz2ZBH2msPUWSCLIugWVQ7mxjcKQibyiaUKMphn2pvbul5dCSKqZORuwbpEXJJicnDZHiaRbjMQg0eQ/640?wx_fmt=png&amp;tp=webp&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" data-type="png" data-w="845" data-fail="0">图片</p>
<p data-tool="mdnice编辑器"><code>string</code>类型本质上就是一个<code>byte</code>类型的数组,在<code>Go</code>语言中<code>string</code>类型被设计为不可变的,不仅是在<code>Go</code>语言,其他语言中<code>string</code>类型也是被设计为不可变的,这样的好处就是:在并发场景下,我们可以在不加锁的控制下,多次使用同一字符串,在保证高效共享的情况下而不用担心安全问题。</p>
<p data-tool="mdnice编辑器"><code>string</code>类型虽然是不能更改的,但是可以被替换,因为<code>stringStruct</code>中的<code>str</code>指针是可以改变的,只是指针指向的内容是不可以改变的,也就说每一个更改字符串,就需要重新分配一次内存,之前分配的空间会被<code>gc</code>回收。</p>
<p data-tool="mdnice编辑器">关于<code>string</code>类型的知识点就描述这么多,方便我们后面分析字符串拼接。</p>
<h2 data-tool="mdnice编辑器">字符串拼接的6种方式及原理</h2>
<h3 data-tool="mdnice编辑器">原生拼接方式"+"</h3>
<p data-tool="mdnice编辑器"><code>Go</code>语言原生支持使用<code>+</code>操作符直接对两个字符串进行拼接,使用例子如下:</p>
<pre data-tool="mdnice编辑器"><code>var&nbsp;s&nbsp;string<br>s&nbsp;+=&nbsp;"asong"<br>s&nbsp;+=&nbsp;"真帅"<br></code></pre>
<p data-tool="mdnice编辑器">这种方式使用起来最简单,基本所有语言都有提供这种方式,使用<code>+</code>操作符进行拼接时,会对字符串进行遍历,计算并开辟一个新的空间来存储原来的两个字符串。</p>
<h3 data-tool="mdnice编辑器">字符串格式化函数<code>fmt.Sprintf</code></h3>
<p data-tool="mdnice编辑器"><code>Go</code>语言中默认使用函数<code>fmt.Sprintf</code>进行字符串格式化,所以也可使用这种方式进行字符串拼接:</p>
<pre data-tool="mdnice编辑器"><code>str&nbsp;:=&nbsp;"asong"<br>str&nbsp;=&nbsp;fmt.Sprintf("%s%s",&nbsp;str,&nbsp;str)<br></code></pre>
<p data-tool="mdnice编辑器"><code>fmt.Sprintf</code>实现原理主要是使用到了反射,具体源码分析因为篇幅的原因就不在这里详细分析了,看到反射,就会产生性能的损耗,你们懂得!!!</p>
<h3 data-tool="mdnice编辑器">Strings.builder</h3>
<p data-tool="mdnice编辑器"><code>Go</code>语言提供了一个专门操作字符串的库<code>strings</code>,使用<code>strings.Builder</code>可以进行字符串拼接,提供了<code>writeString</code>方法拼接字符串,使用方式如下:</p>
<pre data-tool="mdnice编辑器"><code>var&nbsp;builder&nbsp;strings.Builder<br>builder.WriteString("asong")<br>builder.String()<br></code></pre>
<p data-tool="mdnice编辑器"><code>strings.builder</code>的实现原理很简单,结构如下:</p>
<pre data-tool="mdnice编辑器"><code>type&nbsp;Builder&nbsp;struct&nbsp;{<br>&nbsp;&nbsp;&nbsp;&nbsp;addr&nbsp;*Builder&nbsp;//&nbsp;of&nbsp;receiver,&nbsp;to&nbsp;detect&nbsp;copies&nbsp;by&nbsp;value<br>&nbsp;&nbsp;&nbsp;&nbsp;buf&nbsp;&nbsp;[]byte&nbsp;//&nbsp;1<br>}<br></code></pre>
<p data-tool="mdnice编辑器"><code>addr</code>字段主要是做<code>copycheck</code>,<code>buf</code>字段是一个<code>byte</code>类型的切片,这个就是用来存放字符串内容的,提供的<code>writeString()</code>方法就是像切片<code>buf</code>中追加数据:</p>
<pre data-tool="mdnice编辑器"><code>func&nbsp;(b&nbsp;*Builder)&nbsp;WriteString(s&nbsp;string)&nbsp;(int,&nbsp;error)&nbsp;{<br>&nbsp;b.copyCheck()<br>&nbsp;b.buf&nbsp;=&nbsp;append(b.buf,&nbsp;s...)<br>&nbsp;return&nbsp;len(s),&nbsp;nil<br>}<br></code></pre>
<p data-tool="mdnice编辑器">提供的<code>String</code>方法就是将<code>[]]byte</code>转换为<code>string</code>类型,这里为了避免内存拷贝的问题,使用了强制转换来避免内存拷贝:</p>
<pre data-tool="mdnice编辑器"><code>func&nbsp;(b&nbsp;*Builder)&nbsp;String()&nbsp;string&nbsp;{<br>&nbsp;return&nbsp;*(*string)(unsafe.Pointer(&amp;b.buf))<br>}<br></code></pre>
<h3 data-tool="mdnice编辑器">bytes.Buffer</h3>
<p data-tool="mdnice编辑器">因为<code>string</code>类型底层就是一个<code>byte</code>数组,所以我们就可以<code>Go</code>语言的<code>bytes.Buffer</code>进行字符串拼接。<code>bytes.Buffer</code>是一个一个缓冲<code>byte</code>类型的缓冲器,这个缓冲器里存放着都是<code>byte</code>。使用方式如下:</p>
<pre data-tool="mdnice编辑器"><code>buf&nbsp;:=&nbsp;new(bytes.Buffer)<br>buf.WriteString("asong")<br>buf.String()<br></code></pre>
<p data-tool="mdnice编辑器"><code>bytes.buffer</code>底层也是一个<code>[]byte</code>切片,结构体如下:</p>
<pre data-tool="mdnice编辑器"><code>type&nbsp;Buffer&nbsp;struct&nbsp;{<br>&nbsp;buf&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;[]byte&nbsp;//&nbsp;contents&nbsp;are&nbsp;the&nbsp;bytes&nbsp;buf<br>&nbsp;off&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;int&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;read&nbsp;at&nbsp;&amp;buf,&nbsp;write&nbsp;at&nbsp;&amp;buf<br>&nbsp;lastRead&nbsp;readOp&nbsp;//&nbsp;last&nbsp;read&nbsp;operation,&nbsp;so&nbsp;that&nbsp;Unread*&nbsp;can&nbsp;work&nbsp;correctly.<br>}<br></code></pre>
<p data-tool="mdnice编辑器">因为<code>bytes.Buffer</code>可以持续向<code>Buffer</code>尾部写入数据,从<code>Buffer</code>头部读取数据,所以<code>off</code>字段用来记录读取位置,再利用切片的<code>cap</code>特性来知道写入位置,这个不是本次的重点,重点看一下<code>WriteString</code>方法是如何拼接字符串的:</p>
<pre data-tool="mdnice编辑器"><code>func&nbsp;(b&nbsp;*Buffer)&nbsp;WriteString(s&nbsp;string)&nbsp;(n&nbsp;int,&nbsp;err&nbsp;error)&nbsp;{<br>&nbsp;b.lastRead&nbsp;=&nbsp;opInvalid<br>&nbsp;m,&nbsp;ok&nbsp;:=&nbsp;b.tryGrowByReslice(len(s))<br>&nbsp;if&nbsp;!ok&nbsp;{<br>&nbsp;&nbsp;m&nbsp;=&nbsp;b.grow(len(s))<br>&nbsp;}<br>&nbsp;return&nbsp;copy(b.buf,&nbsp;s),&nbsp;nil<br>}<br></code></pre>
<p data-tool="mdnice编辑器">切片在创建时并不会申请内存块,只有在往里写数据时才会申请,首次申请的大小即为写入数据的大小。如果写入的数据小于64字节,则按64字节申请。采用动态扩展<code>slice</code>的机制,字符串追加采用<code>copy</code>的方式将追加的部分拷贝到尾部,<code>copy</code>是内置的拷贝函数,可以减少内存分配。</p>
<p data-tool="mdnice编辑器">但是在将<code>[]byte</code>转换为<code>string</code>类型依旧使用了标准类型,所以会发生内存分配:</p>
<pre data-tool="mdnice编辑器"><code>func&nbsp;(b&nbsp;*Buffer)&nbsp;String()&nbsp;string&nbsp;{<br>&nbsp;if&nbsp;b&nbsp;==&nbsp;nil&nbsp;{<br>&nbsp;&nbsp;//&nbsp;Special&nbsp;case,&nbsp;useful&nbsp;in&nbsp;debugging.<br>&nbsp;&nbsp;return&nbsp;"&lt;nil&gt;"<br>&nbsp;}<br>&nbsp;return&nbsp;string(b.buf)<br>}<br></code></pre>
<h3 data-tool="mdnice编辑器">strings.join</h3>
<p data-tool="mdnice编辑器"><code>Strings.join</code>方法可以将一个<code>string</code>类型的切片拼接成一个字符串,可以定义连接操作符,使用如下:</p>
<pre data-tool="mdnice编辑器"><code>baseSlice&nbsp;:=&nbsp;[]string{"asong",&nbsp;"真帅"}<br>strings.Join(baseSlice,&nbsp;"")<br></code></pre>
<p data-tool="mdnice编辑器"><code>strings.join</code>也是基于<code>strings.builder</code>来实现的,代码如下:</p>
<pre data-tool="mdnice编辑器"><code>func&nbsp;Join(elems&nbsp;[]string,&nbsp;sep&nbsp;string)&nbsp;string&nbsp;{<br>&nbsp;switch&nbsp;len(elems)&nbsp;{<br>&nbsp;case&nbsp;0:<br>&nbsp;&nbsp;return&nbsp;""<br>&nbsp;case&nbsp;1:<br>&nbsp;&nbsp;return&nbsp;elems<br>&nbsp;}<br>&nbsp;n&nbsp;:=&nbsp;len(sep)&nbsp;*&nbsp;(len(elems)&nbsp;-&nbsp;1)<br>&nbsp;for&nbsp;i&nbsp;:=&nbsp;0;&nbsp;i&nbsp;&lt;&nbsp;len(elems);&nbsp;i++&nbsp;{<br>&nbsp;&nbsp;n&nbsp;+=&nbsp;len(elems)<br>&nbsp;}<br><br>&nbsp;var&nbsp;b&nbsp;Builder<br>&nbsp;b.Grow(n)<br>&nbsp;b.WriteString(elems)<br>&nbsp;for&nbsp;_,&nbsp;s&nbsp;:=&nbsp;range&nbsp;elems&nbsp;{<br>&nbsp;&nbsp;b.WriteString(sep)<br>&nbsp;&nbsp;b.WriteString(s)<br>&nbsp;}<br>&nbsp;return&nbsp;b.String()<br>}<br></code></pre>
<p data-tool="mdnice编辑器">唯一不同在于在<code>join</code>方法内调用了<code>b.Grow(n)</code>方法,这个是进行初步的容量分配,而前面计算的n的长度就是我们要拼接的slice的长度,因为我们传入切片长度固定,所以提前进行容量分配可以减少内存分配,很高效。</p>
<h3 data-tool="mdnice编辑器">切片<code>append</code></h3>
<p data-tool="mdnice编辑器">因为<code>string</code>类型底层也是<code>byte</code>类型数组,所以我们可以重新声明一个切片,使用<code>append</code>进行字符串拼接,使用方式如下:</p>
<pre data-tool="mdnice编辑器"><code>buf&nbsp;:=&nbsp;make([]byte,&nbsp;0)<br>base&nbsp;=&nbsp;"asong"<br>buf&nbsp;=&nbsp;append(buf,&nbsp;base...)<br>string(base)<br></code></pre>
<p data-tool="mdnice编辑器">如果想减少内存分配,在将<code>[]byte</code>转换为<code>string</code>类型时可以考虑使用强制转换。</p>
<h2 data-tool="mdnice编辑器">Benchmark对比</h2>
<p data-tool="mdnice编辑器">上面我们总共提供了6种方法,原理我们基本知道了,那么我们就使用<code>Go</code>语言中的<code>Benchmark</code>来分析一下到底哪种字符串拼接方式更高效。我们主要分两种情况进行分析:</p>
<ul class="list-paddingleft-2" data-tool="mdnice编辑器">
<li>少量字符串拼接</li>
<li>大量字符串拼接</li>
</ul>
<p data-tool="mdnice编辑器">因为代码量有点多,下面只贴出分析结果,详细代码已经上传<code>github</code>:https://github.com/asong2020/Golang_Dream/tree/master/code_demo/string_join</p>
<p data-tool="mdnice编辑器">我们先定义一个基础字符串:</p>
<pre data-tool="mdnice编辑器"><code>var&nbsp;base&nbsp;&nbsp;=&nbsp;"123456789qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASFGHJKLZXCVBNM"<br></code></pre>
<p data-tool="mdnice编辑器">少量字符串拼接的测试我们就采用拼接一次的方式验证,base拼接base,因此得出benckmark结果:</p>
<pre data-tool="mdnice编辑器"><code>goos:&nbsp;darwin<br>goarch:&nbsp;amd64<br>pkg:&nbsp;asong.cloud/Golang_Dream/code_demo/string_join/once<br>cpu:&nbsp;Intel(R)&nbsp;Core(TM)&nbsp;i9-9880H&nbsp;CPU&nbsp;@&nbsp;2.30GHz<br>BenchmarkSumString-16&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;21338802&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;49.19&nbsp;ns/op&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;128&nbsp;B/op&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;1&nbsp;allocs/op<br>BenchmarkSprintfString-16&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;7887808&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;140.5&nbsp;ns/op&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;160&nbsp;B/op&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;3&nbsp;allocs/op<br>BenchmarkBuilderString-16&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;27084855&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;41.39&nbsp;ns/op&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;128&nbsp;B/op&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;1&nbsp;allocs/op<br>BenchmarkBytesBuffString-16&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;9546277&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;126.0&nbsp;ns/op&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;384&nbsp;B/op&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;3&nbsp;allocs/op<br>BenchmarkJoinstring-16&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;24617538&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;48.21&nbsp;ns/op&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;128&nbsp;B/op&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;1&nbsp;allocs/op<br>BenchmarkByteSliceString-16&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;10347416&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;112.7&nbsp;ns/op&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;320&nbsp;B/op&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;3&nbsp;allocs/op<br>PASS<br>ok&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;asong.cloud/Golang_Dream/code_demo/string_join/once&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;8.412s<br></code></pre>
<p data-tool="mdnice编辑器">大量字符串拼接的测试我们先构建一个长度为200的字符串切片:</p>
<pre data-tool="mdnice编辑器"><code>var&nbsp;baseSlice&nbsp;[]string<br>for&nbsp;i&nbsp;:=&nbsp;0;&nbsp;i&nbsp;&lt;&nbsp;200;&nbsp;i++&nbsp;{<br>&nbsp;&nbsp;baseSlice&nbsp;=&nbsp;append(baseSlice,&nbsp;base)<br>}<br></code></pre>
<p data-tool="mdnice编辑器">然后遍历这个切片不断的进行拼接,因为可以得出<code>benchmark</code>:</p>
<pre data-tool="mdnice编辑器"><code>goos:&nbsp;darwin<br>goarch:&nbsp;amd64<br>pkg:&nbsp;asong.cloud/Golang_Dream/code_demo/string_join/muliti<br>cpu:&nbsp;Intel(R)&nbsp;Core(TM)&nbsp;i9-9880H&nbsp;CPU&nbsp;@&nbsp;2.30GHz<br>BenchmarkSumString-16&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;7396&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;163612&nbsp;ns/op&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;1277713&nbsp;B/op&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;199&nbsp;allocs/op<br>BenchmarkSprintfString-16&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;5946&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;202230&nbsp;ns/op&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;1288552&nbsp;B/op&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;600&nbsp;allocs/op<br>BenchmarkBuilderString-16&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;262525&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;4638&nbsp;ns/op&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;40960&nbsp;B/op&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;1&nbsp;allocs/op<br>BenchmarkBytesBufferString-16&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;183492&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;6568&nbsp;ns/op&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;44736&nbsp;B/op&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;9&nbsp;allocs/op<br>BenchmarkJoinstring-16&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;398923&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;3035&nbsp;ns/op&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;12288&nbsp;B/op&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;1&nbsp;allocs/op<br>BenchmarkByteSliceString-16&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;144554&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;8205&nbsp;ns/op&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;60736&nbsp;B/op&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;15&nbsp;allocs/op<br>PASS<br>ok&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;asong.cloud/Golang_Dream/code_demo/string_join/muliti&nbsp;&nbsp;&nbsp;10.699s<br></code></pre>
<h3 data-tool="mdnice编辑器">结论</h3>
<p data-tool="mdnice编辑器">通过两次<code>benchmark</code>对比,我们可以看到</p>
<ul class="list-paddingleft-2" data-tool="mdnice编辑器">
<li>当进行少量字符串拼接时,直接使用<code>+</code>操作符进行拼接字符串,效率还是挺高的,但是当要拼接的字符串数量上来时,<code>+</code>操作符的性能就比较低了;</li>
<li>函数<code>fmt.Sprintf</code>还是不适合进行字符串拼接,无论拼接字符串数量多少,性能损耗都很大,还是老老实实做他的字符串格式化就好了;</li>
<li><code>strings.Builder</code>无论是少量字符串的拼接还是大量的字符串拼接,性能一直都能稳定,这也是为什么<code>Go</code>语言官方推荐使用<code>strings.builder</code>进行字符串拼接的原因,在使用<code>strings.builder</code>时最好使用<code>Grow</code>方法进行初步的容量分配,观察<code>strings.join</code>方法的benchmark就可以发现,因为使用了<code>grow</code>方法,提前分配好内存,在字符串拼接的过程中,不需要进行字符串的拷贝,也不需要分配新的内存,这样使用<code>strings.builder</code>性能最好,且内存消耗最小。</li>
<li><code>bytes.Buffer</code>方法性能是低于<code>strings.builder</code>的,<code>bytes.Buffer</code>&nbsp;转化为字符串时重新申请了一块空间,存放生成的字符串变量,不像<code>strings.buidler</code>这样直接将底层的&nbsp;<code>[]byte</code>&nbsp;转换成了字符串类型返回,这就占用了更多的空间。</li>
</ul>
<p data-tool="mdnice编辑器">同步最后分析的结论:</p>
<p data-tool="mdnice编辑器">无论什么情况下使用<code>strings.builder</code>进行字符串拼接都是最高效的,不过要主要使用方法,记得调用<code>grow</code>进行容量分配,才会高效。<code>strings.join</code>的性能约等于<code>strings.builder</code>,在已经字符串slice的时候可以使用,未知时不建议使用,构造切片也是有性能损耗的;如果进行少量的字符串拼接时,直接使用<code>+</code>操作符是最方便也是性能最高的,可以放弃<code>strings.builder</code>的使用。</p>
<p data-tool="mdnice编辑器">综合对比性能排序:</p>
<pre data-tool="mdnice编辑器"><code>strings.join`&nbsp;≈&nbsp;`strings.builder`&nbsp;&gt;&nbsp;`bytes.buffer`&nbsp;&gt;&nbsp;`[]byte`转换`string`&nbsp;&gt;&nbsp;"+"&nbsp;&gt;&nbsp;`fmt.sprintf<br></code></pre>
<h2 data-tool="mdnice编辑器">总结</h2>
<p data-tool="mdnice编辑器">本文我们针对<code>6</code>种字符串的拼接方式进行介绍,并通过<code>benckmark</code>对比了效率,无论什么时候使用<code>strings.builder</code>都不会错,但是在少量字符串拼接时,直接<code>+</code>也就是更优的方式,具体业务场景具体分析,不要一概而论</p><br><br>
来源:https://www.cnblogs.com/cheyunhua/p/15769717.html
頁: [1]
查看完整版本: Go 字符串拼接6种,最快的方式 -- strings.builder