跳到主要内容

字符串

问题

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 是不可变的?

答案

不可变性带来几个好处:

  1. 并发安全:多个 goroutine 可以安全共享同一个 string,无需加锁
  2. 哈希缓存:作为 map key 时,哈希只需算一次
  3. 安全性:不会被意外修改(如函数参数传递后被改变)
  4. 内存共享:切片操作 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.Builderbytes.Buffer
目标构建字符串字节读写缓冲
String()零拷贝(直接返回内部 []byte 转的 string)有拷贝
可以读取吗❌ 只能写和获取结果✅ 实现了 io.Reader
可以 Reset 吗
禁止拷贝✅(拷贝后使用会 panic)
适用场景纯字符串拼接需要读写的 IO 场景

拼接字符串首选 strings.Builder,因为 String() 方法零拷贝。

相关链接