• 设为首页
  • 收藏本站
  • 积分充值
  • VIP赞助
  • 手机版
  • 微博
  • 微信
    微信公众号 添加方式:
    1:搜索微信号(888888
    2:扫描左侧二维码
  • 快捷导航
    福建二哥 门户 查看主题

    Golang中拼接字符串的6种方式性能对比

    发布者: 涵韵3588 | 发布时间: 2025-8-14 10:15| 查看数: 15| 评论数: 0|帖子模式

    golang的string类型是不可修改的,对于拼接字符串来说,本质上还是创建一个新的对象将数据放进去。主要有以下几种拼接方式

    拼接方式介绍

    1.使用string自带的运算符+
    1. ans = ans + s
    复制代码
    2. 使用格式化输出fmt.Sprintf
    1. ans = fmt.Sprintf("%s%s", ans, s)
    复制代码
    3. 使用strings的join函数
    一般适用于将字符串数组转化为特定间隔符的字符串的情况
    1. ans=strings.join(strs,",")
    复制代码
    4. 使用strings.Builder
    1. builder := strings.Builder{}
    2. builder.WriteString(s)
    3. return builder.String()
    复制代码
    5. 使用bytes.Buffer
    1. buffer := new(bytes.Buffer)
    2. buffer.WriteString(s)
    3. return buffer.String()
    复制代码
    6. 使用[]byte,并且提前设置容量
    1. ans := make([]byte, 0, len(s)*n)
    2. ans = append(ans, s...)
    复制代码
    性能对比

    先写一个随机生成长度为n的字符串的函数
    1. func getRandomString(n int) string {
    2.     var tmp = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    3.     ans := make([]uint8, 0, n)
    4.     for i := 0; i < n; i++ {
    5.         ans = append(ans, tmp[rand.Intn(len(tmp))])
    6.     }
    7.     return string(ans)
    8. }
    复制代码
    接下来分别写出上述拼接方式的实现,假设每次都拼接n次字符串s后返回。
    1.使用string自带的运算符+
    循环n次,每次都令答案字符串ans+源字符串s
    1. func plusOperatorJoin(n int, s string) string {
    2.     var ans string
    3.     for i := 0; i < n; i++ {
    4.         ans = ans + s
    5.     }
    6.     return ans
    7. }
    复制代码
    2. 使用格式化输出fmt.Sprintf
    循环n次,使用fmt.Sprintf达到拼接的目的
    1. func sprintfJoin(n int, s string) string {
    2.     var ans string
    3.     for i := 0; i < n; i++ {
    4.         ans = fmt.Sprintf("%s%s", ans, s)
    5.     }
    6.     return ans
    7. }
    复制代码
    3. 使用strings的join函数
    拼接同一个字符串的话不适合用join函数,所以跳过这种方式
    4. 使用strings.Builder
    初始化strings.Builder,循环n次,每次调用WriteString方法
    1. func stringBuilderJoin(n int, s string) string {
    2.     builder := strings.Builder{}
    3.     for i := 0; i < n; i++ {
    4.         builder.WriteString(s)
    5.     }
    6.     return builder.String()
    7. }
    复制代码
    5. 使用bytes.Buffer
    初始化bytes.Buffer,循环n次,每次调用WriteString方法
    1. func bytesBufferJoin(n int, s string) string {
    2.     buffer := new(bytes.Buffer)
    3.     for i := 0; i < n; i++ {
    4.         buffer.WriteString(s)
    5.     }
    6.     return buffer.String()
    7. }
    复制代码
    6. 使用[]byte,并且提前设置容量
    定义ans为byte数组,并提前设置容量为len(s)∗n
    1. func bytesJoin(n int, s string) string {
    2.     ans := make([]byte, 0, len(s)*n)
    3.     for i := 0; i < n; i++ {
    4.         ans = append(ans, s...)
    5.     }
    6.     return string(ans)
    7. }
    复制代码
    测试代码

    先随机生成一个长度为10的字符串,然后拼接10000次。
    1. package high_strings

    2. import "testing"

    3. func benchmark(b *testing.B, f func(int, string) string) {
    4.         var str = getRandomString(10)
    5.         for i := 0; i < b.N; i++ {
    6.                 f(10000, str)
    7.         }
    8. }

    9. func BenchmarkPlusOperatorJoin(b *testing.B) {
    10.         benchmark(b, plusOperatorJoin)
    11. }
    12. func BenchmarkSprintfJoin(b *testing.B) {
    13.         benchmark(b, sprintfJoin)
    14. }
    15. func BenchmarkStringBuilderJoin(b *testing.B) {
    16.         benchmark(b, stringBuilderJoin)
    17. }
    18. func BenchmarkBytesBufferJoin(b *testing.B) {
    19.         benchmark(b, bytesBufferJoin)
    20. }
    21. func BenchmarkBytesJoin(b *testing.B) {
    22.         benchmark(b, bytesJoin)
    23. }
    复制代码
    测试结果


    使用[]byte>strings.Builder≥bytes.Buffer>ffmt.Sprintf > +运算符

    源码分析

    1.使用string自带的运算符+
    代码在runtime\string.go里
    1. // concatstrings implements a Go string concatenation x+y+z+...
    2. // The operands are passed in the slice a.
    3. // If buf != nil, the compiler has determined that the result does not
    4. // escape the calling function, so the string data can be stored in buf
    5. // if small enough.
    6. func concatstrings(buf *tmpBuf, a []string) string {
    7.     idx := 0
    8.     l := 0
    9.     count := 0
    10.     for i, x := range a {
    11.         n := len(x)
    12.         if n == 0 {
    13.             continue
    14.         }
    15.         if l+n < l {
    16.             throw("string concatenation too long")
    17.         }
    18.         l += n
    19.         count++
    20.         idx = i
    21.     }
    22.     if count == 0 {
    23.         return ""
    24.     }

    25. ​​​​​​​    // If there is just one string and either it is not on the stack
    26.     // or our result does not escape the calling frame (buf != nil),
    27.     // then we can return that string directly.
    28.     if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) {
    29.         return a[idx]
    30.     }
    31.     s, b := rawstringtmp(buf, l)
    32.     for _, x := range a {
    33.         copy(b, x)
    34.         b = b[len(x):]
    35.     }
    36.     return s
    37. }
    复制代码
    首先计算拼接后的字符串长度
    如果只有一个字符串并且不在栈上就直接返回
    如果buf不为空并且buf可以放下这些字符串,就把拼接后的字符串放在buf里,否则在堆上重新申请一块内存
    1. func rawstringtmp(buf *tmpBuf, l int) (s string, b []byte) {
    2.     if buf != nil && l <= len(buf) {
    3.         b = buf[:l]
    4.         s = slicebytetostringtmp(&b[0], len(b))
    5.     } else {
    6.         s, b = rawstring(l)
    7.     }
    8.     return
    9. }
    10. // rawstring allocates storage for a new string. The returned
    11. // string and byte slice both refer to the same storage.
    12. // The storage is not zeroed. Callers should use
    13. // b to set the string contents and then drop b.
    14. func rawstring(size int) (s string, b []byte) {
    15.     p := mallocgc(uintptr(size), nil, false)
    16.     return unsafe.String((*byte)(p), size), unsafe.Slice((*byte)(p), size)
    17. }
    复制代码
    然后遍历数组,将字符串copy过去
    2. 使用strings.Builder
    介绍:strings.Builder用于使用Write方法高效地生成字符串,它最大限度地减少了内存复制
    拼接过程:builder里有一个byte类型的切片,每次调用WriteString的时候,是直接往该切片里追加字符串。因为切片底层的扩容机制是以倍数申请的,所以对比1而言,2的内存消耗要更少。
    **结果返回:**在返回字符串的String方法里,是将buf数组转化为字符串直接返回的。
    扩容机制: 想要缓冲区容量增加n个字节,扩容后容量变为2∗len+n
    1. // A Builder is used to efficiently build a string using Write methods.
    2. // It minimizes memory copying. The zero value is ready to use.
    3. // Do not copy a non-zero Builder.
    4. type Builder struct {
    5.         addr *Builder // of receiver, to detect copies by value
    6.         buf  []byte
    7. }

    8. // String returns the accumulated string.
    9. func (b *Builder) String() string {
    10.         return unsafe.String(unsafe.SliceData(b.buf), len(b.buf))
    11. }

    12. // grow copies the buffer to a new, larger buffer so that there are at least n
    13. // bytes of capacity beyond len(b.buf).
    14. func (b *Builder) grow(n int) {
    15.         buf := make([]byte, len(b.buf), 2*cap(b.buf)+n)
    16.         copy(buf, b.buf)
    17.         b.buf = buf
    18. }
    19. // WriteString appends the contents of s to b's buffer.
    20. // It returns the length of s and a nil error.
    21. func (b *Builder) WriteString(s string) (int, error) {
    22.         b.copyCheck()
    23.         b.buf = append(b.buf, s...)
    24.         return len(s), nil
    25. }
    复制代码
    3. 使用bytes.Buffer
    介绍bytes.Buffer跟strings.Builder的底层都是byte数组,区别在于扩容机制和返回字符串的String方法。
    结果返回: 因为bytes.Buffer实际上是一个流式的字节缓冲区,可以向尾部写入数据,也可以读取头部的数据。所以在返回字符串的String方法里,只返回了缓冲区里未读的部分,所以需要重新申请内存来存放返回的结果。内存会比strings.Builder稍慢一些。
    扩容机制: 想要缓冲区容量至少增加n个字节,m是未读的长度,c是当前的容量。
    优化点在于如果n<=c/2−m,也就是当前容量的一半都大于等于现有的内容(未读的字节数)加上所需要增加的字节数,就复用当前的数组,把未读的内容拷贝到头部去。
    1. We can slide things down instead of allocating a new slice. We only need m+n <= c to slide, but we instead let capacity get twice as large so we don’t spend all our time copying.
    复制代码
    我们可以向下滑动,而不是分配一个新的切片。我们只需要m+n<=c来滑动,但我们让容量增加了一倍,这样我们就不会把所有的时间都花在复制上。
    否则的话也是2∗len+n的扩张
    1. // A Buffer is a variable-sized buffer of bytes with Read and Write methods.
    2. // The zero value for Buffer is an empty buffer ready to use.
    3. type Buffer struct {
    4.     buf      []byte // contents are the bytes buf[off : len(buf)]
    5.     off      int    // read at &buf[off], write at &buf[len(buf)]
    6.     lastRead readOp // last read operation, so that Unread* can work correctly.
    7. }
    8. // String returns the contents of the unread portion of the buffer
    9. // as a string. If the Buffer is a nil pointer, it returns "<nil>".
    10. //
    11. // To build strings more efficiently, see the strings.Builder type.
    12. func (b *Buffer) String() string {
    13.     if b == nil {
    14.         // Special case, useful in debugging.
    15.         return "<nil>"
    16.     }
    17.     return string(b.buf[b.off:])
    18. }
    19. // WriteString appends the contents of s to the buffer, growing the buffer as
    20. // needed. The return value n is the length of s; err is always nil. If the
    21. // buffer becomes too large, WriteString will panic with ErrTooLarge.
    22. func (b *Buffer) WriteString(s string) (n int, err error) {
    23.     b.lastRead = opInvalid
    24.     m, ok := b.tryGrowByReslice(len(s))
    25.     if !ok {
    26.         m = b.grow(len(s))
    27.     }
    28.     return copy(b.buf[m:], s), nil
    29. }

    30. ​​​​​​​// grow grows the buffer to guarantee space for n more bytes.
    31. // It returns the index where bytes should be written.
    32. // If the buffer can't grow it will panic with ErrTooLarge.
    33. func (b *Buffer) grow(n int) int {
    34.     m := b.Len()
    35.     // If buffer is empty, reset to recover space.
    36.     if m == 0 && b.off != 0 {
    37.         b.Reset()
    38.     }
    39.     // Try to grow by means of a reslice.
    40.     if i, ok := b.tryGrowByReslice(n); ok {
    41.         return i
    42.     }
    43.     if b.buf == nil && n <= smallBufferSize {
    44.         b.buf = make([]byte, n, smallBufferSize)
    45.         return 0
    46.     }
    47.     c := cap(b.buf)
    48.     if n <= c/2-m {
    49.         // We can slide things down instead of allocating a new
    50.         // slice. We only need m+n <= c to slide, but
    51.         // we instead let capacity get twice as large so we
    52.         // don't spend all our time copying.
    53.         copy(b.buf, b.buf[b.off:])
    54.     } else if c > maxInt-c-n {
    55.         panic(ErrTooLarge)
    56.     } else {
    57.         // Add b.off to account for b.buf[:b.off] being sliced off the front.
    58.         b.buf = growSlice(b.buf[b.off:], b.off+n)
    59.     }
    60.     // Restore b.off and len(b.buf).
    61.     b.off = 0
    62.     b.buf = b.buf[:m+n]
    63.     return m
    64. }
    复制代码
    参考:GoLang bytes.Buffer基础使用方法详解
    到此这篇关于Golang中拼接字符串的6种方式性能对比的文章就介绍到这了,更多相关Go拼接字符串内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

    来源:互联网
    免责声明:如果侵犯了您的权益,请联系站长(1277306191@qq.com),我们会及时删除侵权内容,谢谢合作!

    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有账号?立即注册

    ×

    最新评论

    QQ Archiver 手机版 小黑屋 福建二哥 ( 闽ICP备2022004717号|闽公网安备35052402000345号 )

    Powered by Discuz! X3.5 © 2001-2023

    快速回复 返回顶部 返回列表