Golang理解-字符串拼接的几种方式
Golang中的字符串
Golang 中的string类型存储的字符串是不可变的, 如果要修改string内容需要将string转换为[]byte或[]rune,并且修改后的string内容是重新分配的, 原字符串将被gc回收;
package main
import (
"fmt"
)
func main() {
s := "hi, go"
fmt.Printf("value of str: %v\n", s)
fmt.Printf("ptr of str: %p\n", &s)
// 修改, 将,修改为!
bs := []byte(str)
bs[2] = '!'
fmt.Printf("value of lstr: %v\n", string(bs))
fmt.Printf("ptr of lstr: %p\n", &bs)
}
结果:
value of str: hi, go
ptr of str: 0xc00000e1f0
value of lstr: hi! go
ptr of lstr: 0xc00000a080
可以看到bs 和 s 的地址空间不同了,可见字符串的修改是会重新分配的;
Golang中string有2种类型, 只包含ASCII码的string, 已经包含中文等其他复杂类型的string; 我们知道中文是占3个字节的;
其中:只包含ASCII码的string的string 能通过索引的方式查找对应位置的字符;而包含中文的string类型rune,要想完整的显示中文,需要使用for…range循环;
Golang字符串拼接方法
golang中要实现字符串的拼接,有很多种方法,最常见的当然是使用运算符"+"进行拼接了,还有很多其他的方法,下面依次介绍,并说明其优缺点.
直接使用运算符
// 不换行
str := "hello, " + "golang"
// 换行,换行是"+" 必须在上一行的结尾处
str1 := "The only person " +
"standing in your way " +
"is you"
上面提到golang里面的字符串都是不可变的,每次运算都会产生一个新的字符串.
所以使用运算符"+"连接字符串会产生很多临时的无用的字符串,会给 gc 带来额外的负担,所以性能比较差.
使用fmt.Sprintf
fmt包是golang中的基础包,fmt 包实现了格式化I/O函数,类似于C的 printf 和 scanf.
在fmt包中提供了一个方法func Sprintf(format string, a ...interface{}) string
,使用格式话的方式可以将多个字符串拼接到一起
str := fmt.Sprintf("%s %s %s", "format", "string", "by fmt.Sprintf")
这种方式,使用简单,虽然不会像"+"连接那样生成多余的string,但是内部实现颇为复杂,性能不是很好.
使用strings.Join
golang的 strings 包为字符串的拼接提供了一个方法func Join(a []string, sep string) string
, Join的内部实现比fmt.Sprintf要简单的多,思路就是: Join会先根据字符串数组的内容,计算出一个拼接之后的长度,然后申请对应大小的内存,一个一个字符串填入.代码如下:
// Join 将传如的字符串连接成一个字符串
func Join(a []string, sep string) string {
// 如果字符串数量少,直接使用运算符拼接
switch len(a) {
case 0:
return ""
case 1:
return a[0]
case 2:
return a[0] + sep + a[1]
case 3:
return a[0] + sep + a[1] + sep + a[2]
}
// 计算最终字符串的字符大小
// 首先计算连接符sep的大小
n := len(sep) * (len(a) -1)
// 计算被连接的字符串的字符数
for _, value := range a {
n += len(value)
}
// 知道了总的字符数量,创建对应大小的数组
b := make([]byte, n)
bp := copy(b, a[0])
for _, s := range a[1:] {
bp += copy(b[bp:], sep)
bp += copy(b[bp:], s)
}
return string(b)
}
这种方式实现字符串的拼接,简单方便,效率也是很高的,建议使用,唯一的不足就是在生成数组的时候开销比较大;
使用bytes.Buffer
bytes包中的Buffer提供了一个方法 func (b *Buffer) WriteString(s string) (n int, err error)
WriteString将s的内容追加到缓冲区,并根据需要增加缓冲区。返回值n为s的长度;err总是nil。
如果缓冲区太大,WriteString将会因为ErrTooLarge而陷入恐慌。
package main
import (
"fmt"
"bytes"
)
func main() {
// 声明一个Buffer
var buf bytes.Buffer
buf.WriteString("good ")
buf.WriteString("boy!")
fmt.Println(buf.String()) // good boy!
}
这个比较理想,可以当成可变字符使用,对内存的增长也有优化,如果能预估字符串的长度,还可以用 buffer.Grow() 接口来设置 capacity。
使用strings.Builder
strings.Builder 内部通过 slice 来保存和管理内容。slice 内部则是通过一个指针指向实际保存内容的数组。
strings.Builder 同样也提供了 Grow() 来支持预定义容量。
当我们可以预定义我们需要使用的容量时,strings.Builder 就能避免扩容而创建新的 slice 了。strings.Builder是非线程安全,性能上和 bytes.Buffer 相差无几。
package main
import (
"fmt"
"strings"
)
func main() {
// 声明一个Buffer
var buf strings.Builder
buf.WriteString("good ")
buf.WriteString("boy!")
fmt.Println(buf.String()) // good boy!
}