编码方式与实质
Go 语言对字符串采用 UTF-8 编码,实质上一个字符串是一个字节数组 []byte 。
看如下代码输出可以证明:
s := "abc你好"for i := 0; i < len(s); i++ {fmt.Println(s[i])}// 输出如下979899228189160229165189
输出为什么是这样的呢?
因为 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])}输出如下:abcä½å¥½
这样的遍历方式对于纯英文字符串是没有问题的,原因在推荐文章中讲的很明白,因为英文字符的 UTF-8 编码和 ASCII 编码是一样的,但是中文字符、韩文、日文等字符就不一样了,会出现乱码。
UTF-8 遍历
s := "abc你好"for x, y := range s {fmt.Printf("%d, %c\n", x, y)}输出如下:0, a1, b2, c3, 你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, a1, b2, c3, 你4, 好
此时,满足下标按顺序对应字符顺序。
缺点是:本来只需要 1 + 1 + 1 + 3 + 3 = 9 个字节的空间存储字符串,现在需要 个字节。
因此,非必要的情况下一般采用 UTF-8 遍历。
拼接
结论
一般使用 strings.Builder 拼接字符串。
介绍
Go 语言常用的 5 种字符串拼接方式。
使用
+func plusConcat(s1 string, s2 string) string {return s1 + s2}
使用
fmt.Sprintffunc sprintfConcat(s1 string, s2 string) string {s := fmt.Sprintf("%s%s", s1, s2)return s}
使用
strings.Builderfunc builderConcat(s1 string, s2 string) string {var builder strings.Builderbuilder.WriteString(s1)builder.WriteString(s2)return builder.String()}
使用
bytes.Bufferfunc bufferConcat(s1 string, s2 string) string {var buf bytes.Bufferbuf.WriteString(s1)buf.WriteString(s2)return buf.String()}
- 使用
[]bytefunc 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 20482688 3456 4864 6144 8192 10240 13568 18432 24576 32768 40960 57344 73728 98304 122880
以下是简单的基准测试
// main.gopackage mainimport ("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.Bufferfor i := 0; i < 100; i++ {buffer.WriteString(t)}}func BuilderConcatenate() {t := "t"var builder strings.Builderfor 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.gopackage mainimport ("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: windowsgoarch: amd64pkg: goproject-vscode/test-string-concatenationcpu: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHzBenchmarkPlusConcatenate-12 209023 4969 ns/opBenchmarkSprintfConcatenate-12 67346 17912 ns/opBenchmarkBufferConcatenate-12 1592191 717.2 ns/opBenchmarkBuilderConcatenate-12 3648463 329.4 ns/opBenchmarkSliceConcatenate-12 5138461 235.9 ns/opPASSok goproject-vscode/test-string-concatenation 7.559s*/
可见 Sprintf 是最差的,因为它本来就不适合用于拼接字符串;+ 与 bytes.Buffer 和 strings.Builder 相差一个数量级;bytes.Buffer 和 strings.Builder 性能相近,而 strings.Builder 略优;[]byte 最优,但实际应用中一般需要再转为 string 返回。
strings 包
strings 包里提供了许多关于字符串操作的函数。
