前言

Go中字符串的拼接主要有"+"fmt.Sprintf+%sstrings.Join等方式,已经有很多人从耗时的角度比较这些方式的性能,本文则从源码的角度去分析下这些方式的实现方式,再去比较性能。

拼接字符串方式

"+"

"+"是Go中支持的最直接的字符串拼接符。

str := "a"+"b"+"c"
func contact(list []string) string{r := ""for _,v :=range list{r += v}return r
}

关于"+",我们可以在runtime.go中找到相关的func。其调用的具体细节在cmd/compile/internal/gc/walk.go文件中,对应操作符OADDSTR,其处理func是addstr。在拼接的字符串个数小于等于5个时,会直接调用对应的个数的处理concatstring%n func,这些func均在/runtime/string.go中,然后会调用concatstring;大于5个时则会直接调用concatstring。有兴趣的朋友可以去看下详细的调用处理。此处主要关注concatstring,它负责字符串的具体拼接过程。

// The constant is known to the compiler.
// There is no fundamental theory behind this number.
const tmpStringBufSize = 32type tmpBuf [tmpStringBufSize]byte
// concatstrings implements a Go string concatenation x+y+z+...
// The operands are passed in the slice a.
// If buf != nil, the compiler has determined that the result does not
// escape the calling function, so the string data can be stored in buf
// if small enough.
func concatstrings(buf *tmpBuf, a []string) string {idx := 0l := 0count := 0for i, x := range a {n := len(x)if n == 0 {continue}if l+n < l {throw("string concatenation too long")}l += ncount++idx = i}if count == 0 {return ""}// If there is just one string and either it is not on the stack// or our result does not escape the calling frame (buf != nil),// then we can return that string directly.if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) {return a[idx]}s, b := rawstringtmp(buf, l)for _, x := range a {copy(b, x)b = b[len(x):]}return s
}
func rawstringtmp(buf *tmpBuf, l int) (s string, b []byte) {if buf != nil && l <= len(buf) {b = buf[:l]s = slicebytetostringtmp(b)} else {s, b = rawstring(l)}return
}
func slicebytetostringtmp(b []byte) string {...return *(*string)(unsafe.Pointer(&b))
}
func rawstring(size int) (s string, b []byte) {p := mallocgc(uintptr(size), nil, false)stringStructOf(&s).str = pstringStructOf(&s).len = size*(*slice)(unsafe.Pointer(&b)) = slice{p, size, size}return
}

根据func的注释,也可以看出concatstrings就是实现"+"的func。参数a []string是将多个+连接的字符串组装成slice传入。

看下处理过程:

  1. 计算所有字符串的总长度l,记录非空字符串的个数,记录字符串的位置,当总长溢出时报错。
  2. 若非空字符串个数为0,返回空字符""
  3. 若只有一个非空字符串,且没有存储在buf中或数组还存储在当前goroutine的栈中,则根据字符的位置直接返回对应位置的字符串。
  4. 创建字符串s及字符串指向的字节数组b,修改b则改变s的值。
  • 如果buf!=nil且总长度小于32位,则取b=buf[:l]即可存储所有数据,s指向字节数组b;
  • 否则,直接根据总长度分配内存创建字符串,并将地址指向字节数组b.
  1. 逐个将数据拷贝至b中,返回s即可。

需要注意的是:
当一个表达式中存在多个'+'时,会封装参数至slice中,再调用concatstrings处理,而不是每个'+'都调用一遍。
对于静态的字符串,如str := x+ “a”+“b”+“c”,在编译后直接合并,会处理成str:=x+“abc”
buf在结果不会逃逸出调用func时才不会为nil,且其长度为32个字节,仅能存储长度较小的字符串
concatstrings最多重新分配内存一次

fmt.Sprintf

fmt.Sprintf是fmt包中根据格式符将数据转换为string,拼接字符串时使用的格式符为%s,用以连接字符串。

具体源码如下,本文仅关注%s的部分,无关的源码部分已忽略。

// Sprintf formats according to a format specifier and returns the resulting string.
func Sprintf(format string, a ...interface{}) string {p := newPrinter()p.doPrintf(format, a)s := string(p.buf)p.free()return s
}func (p *pp) doPrintf(format string, a []interface{}) {end := len(format)argNum := 0         // we process one argument per non-trivial formatafterIndex := false // previous item in format was an index like [3].p.reordered = false
formatLoop:for i := 0; i < end; {p.goodArgNum = truelasti := ifor i < end && format[i] != '%' {i++}if i > lasti {p.buf.writeString(format[lasti:i])//写入'%'前的字符串}if i >= end {//结束// done processing format stringbreak}// Process one verbi++// Do we have flags?p.fmt.clearflags()simpleFormat:for ; i < end; i++ {c := format[i]switch c {...default:// Fast path for common case of ascii lower case simple verbs// without precision or width or argument indices.if 'a' <= c && c <= 'z' && argNum < len(a) {if c == 'v' {// Go syntaxp.fmt.sharpV = p.fmt.sharpp.fmt.sharp = false// Struct-field syntaxp.fmt.plusV = p.fmt.plusp.fmt.plus = false}p.printArg(a[argNum], rune(c))argNum++i++continue formatLoop}// Format is more complex than simple flags and a verb or is malformed.break simpleFormat}}...
}func (p *pp) printArg(arg interface{}, verb rune) {...case string:p.fmtString(f, verb)...
}func (p *pp) fmtString(v string, verb rune) {switch verb {...case 's':p.fmt.fmtS(v)...}
}func (f *fmt) fmtS(s string) {s = f.truncateString(s)//转换精度,仅用于number,字符串可忽略f.padString(s)
}// padString appends s to f.buf, padded on left (!f.minus) or right (f.minus).
func (f *fmt) padString(s string) {if !f.widPresent || f.wid == 0 {//仅在format number时使用f.buf.writeString(s)return}width := f.wid - utf8.RuneCountInString(s)//仅用%s,f.width=0,因此width<0if !f.minus {//f.minus仅在存在负数时为true// left paddingf.writePadding(width)f.buf.writeString(s)} else {// right paddingf.buf.writeString(s)//写入f.writePadding(width)//此处无padding}
}func (b *buffer) writeString(s string) {*b = append(*b, s...)
}// writePadding generates n bytes of padding.
func (f *fmt) writePadding(n int) {if n <= 0 { // No padding bytes needed.return}...
}

对于仅拼接字符串的处理过程为:

  1. 依次查找'%'的位置,'%'前的数据append至buf中
  2. 根据其后的format,确认处理过程,拼接字符串使用的是%s,处理过程一个%s对应一个string
  3. append追加字符串至buf中(会面临频繁扩容的问题)
  4. 将buf转为string

注意:fmt.Sprintf并没有计算字符串的总长度,而是针对每个%s进行处理,每个%s的处理最终都会调用append,而使用append可能会出现扩容的问题,尤其是多个字符串时,可能会出现多次扩容的情况。

strings.Join

strings.Join是strings包中针对字符串数组拼接的func,Join支持指定字符串slice间的分隔符。

// Join concatenates the elements of a to create a single string. The separator string
// sep is placed between elements in the resulting string.
func Join(a []string, sep string) string {switch len(a) {case 0:return ""case 1:return a[0]}n := len(sep) * (len(a) - 1)for i := 0; i < len(a); i++ {n += len(a[i])}var b Builderb.Grow(n)b.WriteString(a[0])for _, s := range a[1:] {b.WriteString(sep)b.WriteString(s)}return b.String()
}
// A Builder is used to efficiently build a string using Write methods.
// It minimizes memory copying. The zero value is ready to use.
// Do not copy a non-zero Builder.
type Builder struct {addr *Builder // of receiver, to detect copies by valuebuf  []byte
}
// Grow grows b's capacity, if necessary, to guarantee space for
// another n bytes. After Grow(n), at least n bytes can be written to b
// without another allocation. If n is negative, Grow panics.
func (b *Builder) Grow(n int) {b.copyCheck()if n < 0 {panic("strings.Builder.Grow: negative count")}if cap(b.buf)-len(b.buf) < n {b.grow(n)}
}
// grow copies the buffer to a new, larger buffer so that there are at least n
// bytes of capacity beyond len(b.buf).
func (b *Builder) grow(n int) {buf := make([]byte, len(b.buf), 2*cap(b.buf)+n)copy(buf, b.buf)b.buf = buf
}
// WriteString appends the contents of s to b's buffer.
// It returns the length of s and a nil error.
func (b *Builder) WriteString(s string) (int, error) {b.copyCheck()b.buf = append(b.buf, s...)return len(s), nil
}// String returns the accumulated string.
func (b *Builder) String() string {return *(*string)(unsafe.Pointer(&b.buf))
}

Join的处理过程:

  1. 判断字符串个数,为0返回空字符串;为1返回第一个字符串。
  2. 计算分隔符的总长度,再计算拼接后字符串的总长度
  3. 如果buf的cap不足以容纳所有字符串,进行扩容(创建容量为2*cap(b.buf)+n的新slice,拷贝旧数据至其中),此时buf足以容纳所有数据,后期append无需扩容
  4. 依次将数据、分隔符append到buf中
  5. 通过指针将buf转换为string

append仅扩容一次

比较

下面比较三种拼接字符串的优缺点:

"+"拼接字符串

优点:

  1. 使用简单
  2. 对短字符串的拼接有性能优势(结果或参数不escape,总长度不大于32位时会提前分配32的buf,这时数据可以存储在buf中)
  3. 一个表达式中有多个"+"仍只处理一次(会将多个拼接的字符串组成成slice再调用concatstrings

缺点:

  1. 当数据很多时,多个"+"可能会导致代码的不简洁
  2. 对于需要多个表达式才能拼接所有字符串的数据,意味着每次都需要调用concatstrings,需要重新计算并分配内存,一旦数据很多,性能就会变差

fmt.Sprintf拼接字符串

优点:

  1. 适用范围广,可以将其他类型转换为字符串
  2. 在表示带有具体意义的数据时更直观,尤其是带有描述性前缀

缺点:

  1. 处理过程相对复杂,多类型的判断甚至调用反射,影响效率
  2. 拼接字符串中并没有提前计算总长,每次拼接字符串都是使用的append完成,调用append意味着扩容时的内存再分配及数据拷贝等处理,一旦数据较多时,明显影响性能

strings.Join拼接字符串

优点:

  1. 一次计算总长度,只需分配一次总内存,后续无需重新分配内存
  2. 对于同一分隔符时的拼接有很大的便利性

缺点:

  1. 对于零散的数据需要主动组装成slice才能处理
  2. 对于不同的分隔符不能直接处理

整体比较

从源码实现的角度,我们可以得出以下结论:

对于拼接字符串,如果一个表达式可以全部使用'+'的方式,则使用'+'strings.Join的性能接近,否则其性能不如strings.Join,而fmt.Sprintf需要经过反射及append的处理,其性能相对来说可能最差。

原因是:三者在拼接字符串过程中,尤其是多个字符串、长度较长的字符串时,strings.Join仅需分配一次内存,'+'因使用方式会分配一次或多次,fmt.Sprintf则针对每个%s会调用一次append,可能会分配多次。每次重新分配都需要进行数据的重新拷贝,都会影响其性能。

当然,对于拼接数据量很少或很短的数据,尤其是零散的数据(strings.Join需要组装数据至slice),三者的效率差异不大,可以按照需求自行决定使用。

整体来说三者的性能:strings.Join~=单次'+'>>多次'+'>fmt.Sprintf

总结

本文主要对常见的3种字符串拼接方式,从其实现的角度分析其在使用时的优缺点,进而协助我们在不同情形使用时,选择合适的字符串拼接方式。

作为建议:

  1. 对于零散的少量数据,可以使用'+'来拼接数据;
  2. 对于少量数据且数据间有解释性的前缀或后缀,可以使用fmt.Sprintf
  3. 对于多数据或者slice数据,可以使用strings.Join

公众号

鄙人刚刚开通了公众号,专注于分享Go开发相关内容,望大家感兴趣的支持一下,在此特别感谢。

Go字符串拼接方式深入比较相关推荐

  1. 将页面多个下拉框的值以字符串拼接方式存放至数据库一个字段中

     1,当页面中有多个值,传入Controller并以字符串拼接方式,以","隔开存放至数据库一个字段中,页面中多个<select name="off"&g ...

  2. 用数据说话,Go 所有字符串拼接方式里哪种才是最稳定高效的?

    前言 日常业务开发中离不开字符串的拼接操作,不同语言的字符串实现方式都不同,在Go语言中就提供了6种方式进行字符串拼接,那这几种拼接方式该如何选择呢?使用那个更高效呢?今天我们邀请到公众号「Golan ...

  3. 选择合适的 Go 字符串拼接方式

    前言 哈喽,大家好,我是asong 日常业务开发中离不开字符串的拼接操作,不同语言的字符串实现方式都不同,在Go语言中就提供了6种方式进行字符串拼接,那这几种拼接方式该如何选择呢?使用那个更高效呢?本 ...

  4. C# 字符串拼接整理_C#字符串拼接方式整理

    C# 字符串拼接整理_C#字符串拼接方式整理 一.字符串连接使用+  注意:此方式多种语言通用,js,java中都可以如此操作 string hello = "Hello"; st ...

  5. Java 5种字符串拼接方式性能比较。

    最近写一个东东,可能会考虑到字符串拼接,想了几种方法,但对性能未知,于是用Junit写了个单元测试. 代码如下: import java.util.ArrayList; import java.uti ...

  6. Golang的五种字符串拼接方式

    1.+号 func main() {s1 := "hello"s2 := "word"s3 := s1 + s2fmt.Print(s3) //s3 = &qu ...

  7. golang字符串拼接方式

    字符串拼接是字符的常见操作.在golang中,遇见了字符串拼接.作为一个长期的C程序员,我第一反应是:字符串拼接函数strcat,但发现golang并无字符串拼接函数. 我想起了最简单的方法,通过+操 ...

  8. Go | 字符串拼接方式总结和分析

    1. 拼接方式 += append(,) buf.WriteString() fmt.Sprintf(,) copy(,) 示例代码如下: package strimport ("bytes ...

  9. golang 字符串拼接方式

    最近在做性能优化,有个函数里面的耗时特别长,看里面的操作大多是一些字符串拼接的操作,而字符串拼接在 golang 里面其实有很多种实现. 实现方法 1.直接使用运算符 func BenchmarkAd ...

最新文章

  1. 【FFmpeg】ffmpeg工具源码分析(三):分配过滤器内存(宏GROW_ARRAY)详解
  2. python函数手册68_直接在python中检索68个内置函数?
  3. 【Android 应用开发】Paint 滤镜原理 之 图像结构 ( 图片文件二进制分析 | PNG文件结构 | 数据块结构 | IHDR 数据块详解 )
  4. 更改已经收货的采购订单价格
  5. [转载] 杜拉拉升职记——23 “You deserve it”的两种解释
  6. 牛客网_PAT乙级_1016程序运行时间(15)
  7. 在 Raspberry Pi 3B 上安装最新版 Node-RED
  8. java向注册表单传递数据php_PHP提交from表单的方法
  9. C语言与JAVA内存管理_C语言内存管理
  10. ajax登录成功跳转页面_ODOO 登录后跳转到指定页面【仪表盘】而不是【讨论】模块...
  11. shell 登录mysql 然后quit_MySQL 数据库简单操作
  12. 进程占用导致linux中命令无法执行
  13. 深入理解 Spring 事务原理
  14. 您电脑的网络管家 -NetSetMan
  15. Linux运维基础软件
  16. mysql fk_MySQL FK的正确命名约定是什么?
  17. 内存攻略:SDRAM应用解析
  18. Win7-VirsualBox下学习Ubuntu--Ubuntu和Win7共享文件夹
  19. 研修国学请注意选好教材
  20. Lio_sam运行测试环节遇到的问题以及实测总结

热门文章

  1. 转载:深入浅出的讲解傅里叶变换
  2. 自定义View-仿QQ运动步数进度效果(完整代码)
  3. 第一届嵌入式电子竞赛方案设计——智能门禁系统
  4. 给俺的 CSDN 博客加背景音乐 - 高大尚的《心经》背景音乐
  5. ERA5气象数据下载经验分享
  6. html5实现canvas迷宫游戏,HTML5/Canvas/JS 迷宫生成动画
  7. Ice飞冰页面配置菜单配置日志打印环境配置《六》
  8. 飞冰 前端开发的一些坑
  9. 阿里飞冰的介绍以及使用
  10. 服务器显示断开网络驱动器,断开网络驱动器 快速映射盘符