编码方式与实质
Go 语言对字符串采用 UTF-8 编码,实质上一个字符串是一个字节数组 []byte 。
看如下代码输出可以证明:
s := "abc你好"
for i := 0; i < len(s); i++ {
fmt.Println(s[i])
}
// 输出如下
97
98
99
228
189
160
229
165
189
输出为什么是这样的呢?
因为 a, b, c 的 UTF-8 编码分别就是 97, 98, 99,“你”和“好”的 UTF-8 编码分别是(228, 189, 160)和(229, 165, 189)。
强烈建议先看下面这篇文章,弄清楚 ASCII 编码,Unicode 编码和 UTF-8 编码之间的联系。
ASCII_and_Unicode_and_UTF-8
遍历
常规遍历
s := "abc你好"
for i := 0; i < len(s); i++ {
fmt.Printf("%c\n", s[i])
}
输出如下:
a
b
c
ä
½
å
¥
½
这样的遍历方式对于纯英文字符串是没有问题的,原因在推荐文章中讲的很明白,因为英文字符的 UTF-8 编码和 ASCII 编码是一样的,但是中文字符、韩文、日文等字符就不一样了,会出现乱码。
UTF-8 遍历
s := "abc你好"
for x, y := range s {
fmt.Printf("%d, %c\n", x, y)
}
输出如下:
0, a
1, b
2, c
3, 你
6, 好
可以发现出现了 3-6 的跳跃。
如果看了上面推荐的文章就会明白,中文字符占用 2-4 个字节不等,所以才会出现用 s[3]-s[5] 共 3 个字节用来表示“你”,再一次证明了 Go 语言采用 UTF-8 编码字符串。
如果不要求下标按顺序对应字符顺序,这样的遍历方式已经可以了。
Unicode 遍历
UTF-8 遍历存在的问题是“下标按顺序不对应字符顺序”,Unicode 遍历可以解决这个问题,代价是牺牲一些空间,方案如下:
因为一个字符最多占 4 个字节空间,所以我们可以先把 string 转化为 []int32 切片,然后再输出即可。
s := "abc你好"
st := []rune(s) // rune 是 int32 的别名
for i := 0; i < len(st); i++ {
fmt.Printf("%d, %c\n", i, st[i])
}
输出如下:
0, a
1, b
2, c
3, 你
4, 好
此时,满足下标按顺序对应字符顺序。
缺点是:本来只需要 1 + 1 + 1 + 3 + 3 = 9 个字节的空间存储字符串,现在需要 个字节。
因此,非必要的情况下一般采用 UTF-8 遍历。
拼接
结论
一般使用 strings.Builder 拼接字符串。
介绍
Go 语言常用的 5 种字符串拼接方式。
使用
+
func plusConcat(s1 string, s2 string) string {
return s1 + s2
}
使用
fmt.Sprintf
func sprintfConcat(s1 string, s2 string) string {
s := fmt.Sprintf("%s%s", s1, s2)
return s
}
使用
strings.Builder
func builderConcat(s1 string, s2 string) string {
var builder strings.Builder
builder.WriteString(s1)
builder.WriteString(s2)
return builder.String()
}
使用
bytes.Buffer
func bufferConcat(s1 string, s2 string) string {
var buf bytes.Buffer
buf.WriteString(s1)
buf.WriteString(s2)
return buf.String()
}
- 使用
[]byte
func byteConcat(s1 string, s2 string) string {
buf := make([]byte, 0, len(s1)+len(s2))
buf = append(buf, s1...)
buf = append(buf, s2...)
return string(buf)
}
对比
5 种方法都会出现重新申请内存空间的情况(发生内存复制),区别在于内存分配机制。
+
和fmt.Sprintf
的机制是申请所拼接字符串的总空间。假设一个大小为 10 B 的字符串拼接 1 万次,则需要 10 + 102 + 103 + …… + 10*9999 B 约为 500 MB 内存空间。strings.Builder
,bytes.Buffer
和[]byte
则有预定义的内存分配机制(不详细讲),采用上述的假设,最后需要的空间只有上述的千分之一。strings.Builder
比bytes.Buffer
略快的原因是,两者底层都是 []byte 实现,但是前者直接把 []byte 转为了子串,而后者重新申请了一次内存空间进行转换。
一开始是 2 的次方数,后来逐渐平缓。16 32 64 128 256 512 1024 2048
2688 3456 4864 6144 8192 10240 13568 18432 24576 32768 40960 57344 73728 98304 122880
以下是简单的基准测试
// main.go
package main
import (
"fmt"
"strings"
"bytes"
)
func main() {
}
func PlusConcatenate() {
t := "t"
s := ""
for i := 0; i < 100; i++ {
s += t
}
}
func SprintfConcatenate() {
t := "t"
s := ""
for i := 0; i < 100; i++ {
s = fmt.Sprintf("%s%s", s, t)
}
}
func BufferConcatenate() {
t := "t"
var buffer bytes.Buffer
for i := 0; i < 100; i++ {
buffer.WriteString(t)
}
}
func BuilderConcatenate() {
t := "t"
var builder strings.Builder
for i := 0; i < 100; i++ {
builder.WriteString(t)
}
}
func SliceConcatenate() {
t := "t"
buf := make([]byte, 0)
for i := 0; i < 100; i++ {
buf = append(buf, t...)
}
}
// main_test.go
package main
import (
"testing"
)
func BenchmarkPlusConcatenate(b *testing.B) {
for i := 0; i < b.N; i++ {
PlusConcatenate()
}
}
func BenchmarkSprintfConcatenate(b *testing.B) {
for i := 0; i < b.N; i++ {
SprintfConcatenate()
}
}
func BenchmarkBufferConcatenate(b *testing.B) {
for i := 0; i < b.N; i++ {
BufferConcatenate()
}
}
func BenchmarkBuilderConcatenate(b *testing.B) {
for i := 0; i < b.N; i++ {
BuilderConcatenate()
}
}
func BenchmarkSliceConcatenate(b *testing.B) {
for i := 0; i < b.N; i++ {
SliceConcatenate()
}
}
/*
测试结果:
goos: windows
goarch: amd64
pkg: goproject-vscode/test-string-concatenation
cpu: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
BenchmarkPlusConcatenate-12 209023 4969 ns/op
BenchmarkSprintfConcatenate-12 67346 17912 ns/op
BenchmarkBufferConcatenate-12 1592191 717.2 ns/op
BenchmarkBuilderConcatenate-12 3648463 329.4 ns/op
BenchmarkSliceConcatenate-12 5138461 235.9 ns/op
PASS
ok goproject-vscode/test-string-concatenation 7.559s
*/
可见 Sprintf 是最差的,因为它本来就不适合用于拼接字符串;+ 与 bytes.Buffer 和 strings.Builder 相差一个数量级;bytes.Buffer 和 strings.Builder 性能相近,而 strings.Builder 略优;[]byte 最优,但实际应用中一般需要再转为 string 返回。
strings 包
strings
包里提供了许多关于字符串操作的函数。