Go 语言中拼接字符串会使用 + 符号,当我们使用这个符号对字符串进行拼接时,编译器会在类型检查阶段将 OADD 节点转换成 OADDSTR 类型的节点,随后在 SSA 中间代码生成的阶段调用 addstr 函数:
func walkexpr(n *Node, init *Nodes) *Node {
switch n.Op {
// ...
case OADDSTR:
n = addstr(n, init)
}
}
addstr 函数就是帮助我们在编译期间选择合适的函数对字符串进行拼接,如果需要拼接的字符串小于或者等于 5 个,那么就会直接调用 concatstring{2,3,4,5} 等一系列函数,如果超过 5 个就会直接选择 concatstrings 传入一个数组切片。
func addstr(n *Node, init *Nodes) *Node {
c := n.List.Len()
buf := nodnil()
args := []*Node{buf}
for _, n2 := range n.List.Slice() {
args = append(args, conv(n2, types.Types[TSTRING]))
}
var fn string
if c <= 5 {
fn = fmt.Sprintf("concatstring%d", c)
} else {
fn = "concatstrings"
t := types.NewSlice(types.Types[TSTRING])
slice := nod(OCOMPLIT, nil, typenod(t))
slice.List.Set(args[1:])
args = []*Node{buf, slice}
}
cat := syslook(fn)
r := nod(OCALL, cat, nil)
r.List.Set(args)
// ...
return r
}
其实无论使用 concatstring{2,3,4,5} 中的哪一个,最终都会调用 concatstrings ,在这个函数中我们会先对传入的切片参数进行遍历,首先会过滤空字符串并获取拼接后字符串的长度。
func concatstrings(buf *tmpBuf, a []string) string {
idx := 0
l := 0
count := 0
for i, x := range a {
n := len(x)
if n == 0 {
continue
}
if l+n < l {
throw("string concatenation too long")
}
l += n
count++
idx = i
}
if count == 0 {
return ""
}
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
}
如果非空字符串的数量为 1 并且当前的字符串不在栈上或者没有逃逸出调用堆栈,那么就可以直接返回该字符串,不需要进行任何的耗时操作。

但是在正常情况下,原始的多个字符串都会被调用 copy 将所有的字符串拷贝到目标字符串所在的内存空间中,新的字符串其实就是一片新的内存空间,与原来的字符串没有任何关联。









