字符串
问题
Go 中字符串的底层结构是什么?string、[]byte、[]rune 有什么区别?
答案
字符串底层结构
Go 的 string 是一个不可变的字节序列,底层由两个字段组成:
// runtime/string.go
type stringHeader struct {
Data unsafe.Pointer // 指向底层字节数组
Len int // 字节长度(不是字符数)
}
不可变性意味着:
s := "hello"
// s[0] = 'H' // 编译错误!cannot assign to s[0]
// 修改字符串需要先转为 []byte 再转回来(会拷贝)
bs := []byte(s)
bs[0] = 'H'
s = string(bs) // "Hello"
string、[]byte、[]rune 的关系
| 类型 | 说明 | 用途 |
|---|---|---|
string | 不可变字节序列 | 文本表示 |
[]byte | 可变字节切片 | 二进制数据、网络传输、文件 IO |
[]rune | 可变 Unicode 码点切片 | 处理多字节字符(中文、emoji) |
s := "Hello你好"
// len(string) 返回字节数
fmt.Println(len(s)) // 11 (5 + 3×2)
// []byte:按字节拆分
fmt.Println([]byte(s)) // [72 101 108 108 111 228 189 160 229 165 189]
// []rune:按 Unicode 码点拆分
fmt.Println([]rune(s)) // [72 101 108 108 111 20320 22909]
fmt.Println(len([]rune(s))) // 7(字符数)
// for range 遍历 string 时,按 rune 遍历
for i, r := range s {
fmt.Printf("byte index=%d, char=%c, code=%d\n", i, r, r)
}
// byte index=0, char=H, code=72
// byte index=1, char=e, code=101
// ...
// byte index=5, char=你, code=20320 ← 注意索引跳了
// byte index=8, char=好, code=22909
字符串拼接方式对比
面试常问"字符串拼接的几种方式及性能对比":
// 1. + 操作符(每次创建新字符串,最慢)
s := "hello" + " " + "world"
// 2. fmt.Sprintf(灵活但有反射开销)
s := fmt.Sprintf("%s %s", "hello", "world")
// 3. strings.Join(已知所有部分时推荐)
s := strings.Join([]string{"hello", "world"}, " ")
// 4. strings.Builder(循环拼接推荐,Go 1.10+)
var builder strings.Builder
for i := 0; i < 1000; i++ {
builder.WriteString("hello")
}
s := builder.String()
// 5. bytes.Buffer(需要同时读写时使用)
var buf bytes.Buffer
buf.WriteString("hello")
buf.WriteString(" world")
s := buf.String()
性能排序(循环拼接场景):
| 方法 | 性能 | 内存分配 | 适用场景 |
|---|---|---|---|
strings.Builder | ⭐⭐⭐⭐⭐ | 最少 | 循环拼接 |
bytes.Buffer | ⭐⭐⭐⭐ | 较少 | 需要读写的场景 |
strings.Join | ⭐⭐⭐⭐ | 一次分配 | 已知所有部分 |
+ 操作符 | ⭐ | 每次都分配 | 少量拼接 |
fmt.Sprintf | ⭐⭐ | 反射开销 | 需要格式化 |
strings.Builder 预分配
如果知道大致长度,用 builder.Grow(n) 预分配内存可以进一步减少扩容:
var builder strings.Builder
builder.Grow(5000) // 预分配 5000 字节
for i := 0; i < 1000; i++ {
builder.WriteString("hello")
}
字符串与字节切片的转换开销
string ↔ []byte 的转换涉及内存拷贝(因为 string 不可变,必须拷贝一份可变的副本):
s := "hello"
b := []byte(s) // 拷贝!分配新内存
s2 := string(b) // 拷贝!分配新内存
编译器优化
Go 编译器在以下场景会优化掉拷贝:
map[string]用[]byte查找:m[string(byteSlice)]不拷贝for range []byte(str)不拷贝- 字符串比较
string(b) == "hello"不拷贝 - 字符串拼接
"prefix" + string(b)中间转换不拷贝
strings 包常用函数
import "strings"
strings.Contains("hello", "ell") // true
strings.HasPrefix("hello", "he") // true
strings.HasSuffix("hello", "lo") // true
strings.Index("hello", "ll") // 2
strings.Replace("hello", "l", "L", -1) // "heLLo"(-1 替换全部)
strings.Split("a,b,c", ",") // ["a", "b", "c"]
strings.TrimSpace(" hello ") // "hello"
strings.ToUpper("hello") // "HELLO"
strings.ToLower("HELLO") // "hello"
strings.Repeat("ha", 3) // "hahaha"
strings.Count("hello", "l") // 2
strings.EqualFold("Hello", "hello") // true(忽略大小写比较)
strconv 包——类型与字符串互转
import "strconv"
// int ↔ string
strconv.Itoa(42) // "42"
strconv.Atoi("42") // 42, nil
// float ↔ string
strconv.FormatFloat(3.14, 'f', 2, 64) // "3.14"
strconv.ParseFloat("3.14", 64) // 3.14, nil
// bool ↔ string
strconv.FormatBool(true) // "true"
strconv.ParseBool("true") // true, nil
// int ↔ string(指定进制)
strconv.FormatInt(255, 16) // "ff"
strconv.ParseInt("ff", 16, 64) // 255, nil
常见面试问题
Q1: 为什么 Go 的 string 是不可变的?
答案:
不可变性带来几个好处:
- 并发安全:多个 goroutine 可以安全共享同一个 string,无需加锁
- 哈希缓存:作为 map key 时,哈希只需算一次
- 安全性:不会被意外修改(如函数参数传递后被改变)
- 内存共享:切片操作
s[2:5]可以共享底层数组,不需要拷贝
代价是修改字符串需要创建新的 string,涉及内存分配和拷贝。
Q2: string(intValue) 的结果是什么?
答案:
string(65) 的结果是 "A"(Unicode 码点 65 对应的字符),而不是 "65"。这是 Go 初学者常见的坑。
fmt.Println(string(65)) // "A"
fmt.Println(string(20320)) // "你"
fmt.Println(strconv.Itoa(65)) // "65"(数字转字符串用 strconv)
Go 1.15 起,go vet 会对 string(int) 发出警告。
Q3: 如何高效判断字符串是否为空?
答案:
// ✅ 推荐:直接比较或检查长度
if s == "" { }
if len(s) == 0 { }
// ❌ 不推荐:多余的转换
if len([]byte(s)) == 0 { }
s == "" 和 len(s) == 0 性能相同,编译器会优化为相同的汇编代码。
Q4: 如何反转字符串?
答案:
// 处理纯 ASCII
func reverseASCII(s string) string {
b := []byte(s)
for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return string(b)
}
// 处理含中文的 Unicode 字符串
func reverseUnicode(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
reverseUnicode("Hello你好") // "好你olleH"
Q5: strings.Builder 和 bytes.Buffer 有什么区别?
答案:
| 维度 | strings.Builder | bytes.Buffer |
|---|---|---|
| 目标 | 构建字符串 | 字节读写缓冲 |
String() | 零拷贝(直接返回内部 []byte 转的 string) | 有拷贝 |
| 可以读取吗 | ❌ 只能写和获取结果 | ✅ 实现了 io.Reader |
| 可以 Reset 吗 | ✅ | ✅ |
| 禁止拷贝 | ✅(拷贝后使用会 panic) | ❌ |
| 适用场景 | 纯字符串拼接 | 需要读写的 IO 场景 |
拼接字符串首选 strings.Builder,因为 String() 方法零拷贝。