Go 字符串
Go 中字符串是不可变的 UTF-8 编码字节序列,理解底层字节与 Unicode 码点的关系是正确处理文本的关键。
#type / concept
#status / growing
#tech / dev
#resource / go
[!info] related notes
- 所属 MOC: Go 语言基础 MOC
- 前置概念: Go 基本类型
- 并列概念: Go 数组, Go 切片
Go 字符串
一句话定义
Go 的 string 是一个不可变的 UTF-8 编码字节序列,底层结构为一个指向字节数组的指针和一个长度字段。
核心机制 / 工作原理
-
底层结构:
string本质是一个reflect.StringHeader,包含Data(指向底层字节数组)和Len(字节长度)。赋值和传参仅拷贝这个 16 字节的 header,不拷贝底层数据。 -
不可变性:
string的底层字节数组一旦创建就不能修改。s[0] = 'x'会编译报错。任何对字符串的”修改”都会产生新的字符串。 -
UTF-8 编码:Go 源码和字符串默认使用 UTF-8。ASCII 字符占 1 字节,中文等多字节字符占 3-4 字节。
-
rune 类型:
rune是int32的别名,表示一个 Unicode 码点。遍历字符串时需要用range才能正确按 rune 迭代。 -
len()vsutf8.RuneCountInString():len(s)返回字节数,utf8.RuneCountInString(s)返回 rune(字符)数。对含中文的字符串两者不同。 -
string与[]byte转换:[]byte(s)和string(b)都会复制底层数据(除非编译器优化)。高频转换应尽量避免,可用strings.Builder或unsafe零拷贝。
最小例子 / 最小场景
package main
import (
"fmt"
"strings"
"unicode/utf8"
)
func main() {
s := "Hello, 世界"
// len 返回字节数, RuneCountInString 返回字符数
fmt.Println(len(s)) // 13 (7 ASCII + 两个中文各 3 字节)
fmt.Println(utf8.RuneCountInString(s)) // 9
// 按字节索引 — 拿到的是 byte
fmt.Printf("%x\n", s[7:10]) // e4b896 ('世' 的 UTF-8 编码)
// range 按 rune 迭代
for i, r := range s {
fmt.Printf("index=%d, rune=%c, codepoint=%U\n", i, r, r)
}
// strings 包常用函数
fmt.Println(strings.Contains(s, "世界")) // true
fmt.Println(strings.HasPrefix(s, "Hello")) // true
fmt.Println(strings.Split("a,b,c", ",")) // [a b c]
fmt.Println(strings.Join([]string{"a","b"}, "-")) // a-b
fmt.Println(strings.TrimSpace(" hi ")) // hi
fmt.Println(strings.Replace("aaa", "a", "b", 2)) // bba
// 字符串构建 — 避免频繁拼接
var b strings.Builder
for i := 0; i < 1000; i++ {
b.WriteString("go")
}
result := b.String() // 高效, 内部只扩容一次或几次
// string ↔ []byte
bytes := []byte("hello")
str := string(bytes)
fmt.Println(bytes, str)
}
为什么重要
- 文本处理的基础:几乎所有程序都涉及字符串操作,理解 UTF-8 和字节 vs 字符的区别能避免截断乱码等常见 bug。
- 性能关键:在日志、序列化、模板渲染等热路径中,字符串拼接方式(
+循环 vsBuildervsfmt.Sprintf)性能差异可达数十倍。 - 国际化就绪:Go 原生 UTF-8 支持使得处理中日韩等多字节语言自然且正确,但前提是开发者理解 rune 与 byte 的区别。
- 标准库丰富:
strings、strconv、unicode、unicode/utf8、bytes、regexp等包覆盖了绝大多数文本处理需求。
边界与易混淆点
s[i]拿到的是 byte 不是字符:对"世界"取s[0]得到0xe4(“世” 的第一个字节),不是 “世”。要按字符访问需用[]rune(s)或range。string和[]byte转换有拷贝开销:虽然 Go 编译器对某些场景做了优化(如直接将[]byte传给string类型的 map key),但一般情况下会复制内存。高频场景用unsafe.String/unsafe.Slice做零拷贝需谨慎。strconvvsfmt.Sprintf:strconv.Itoa(42)比fmt.Sprintf("%d", 42)快约 5-10 倍,类型转换优先用strconv。- 字符串比较是字节级比较:
"abc" < "abd"按 UTF-8 字节逐字节比较,不是按 Unicode 排序规则。需要本地化排序应使用golang.org/x/text/collate。 strings.Builder只能用一次:Reset()后可复用,但不能并发写入。并发场景考虑bytes.Buffer+ 锁,或按分区拼接后合并。