bytes & strings — 字节与字符串操作
bytes 和 strings 是 API 几乎完全对称的两个包。strings 操作 UTF-8 字符串,bytes 操作字节切片。选择标准:如果你处理的是文本,用 strings;如果处理的是二进制数据,用 bytes。
1. 字符串拼接性能对比
Go 有四种主要拼接方式,性能差异很大:
// 方式 1:+ 操作符(少量拼接)
s := "hello" + " " + "world"
// 方式 2:strings.Builder(大量拼接,最快)
var builder strings.Builder
builder.Grow(1024) // 预分配,避免重复扩容
for i := 0; i < 1000; i++ {
builder.WriteString("hello")
}
result := builder.String()
// 方式 3:bytes.Buffer(类似 Builder,但更通用)
var buf bytes.Buffer
for i := 0; i < 1000; i++ {
buf.WriteString("hello")
}
result := buf.String()
// 方式 4:strings.Join(拼接切片)
parts := []string{"a", "b", "c"}
result := strings.Join(parts, ", ")
// 方式 5:fmt.Sprintf(最慢,但有格式化)
result := fmt.Sprintf("%s-%d", "user", 42)| 方式 | 速度 | 内存分配 | 场景 |
|---|---|---|---|
+(少量) |
快 | 低 | < 5 次拼接 |
strings.Builder |
最快 | 最低 | 循环中大量拼接 |
bytes.Buffer |
快 | 中 | 需要同时操作 string 和 []byte |
strings.Join |
快 | 中 | 拼接已有切片 |
fmt.Sprintf |
慢 | 高 | 需要格式化时 |
⚡ 性能提示:
strings.Builder.Grow()预分配是性能关键。如果不预分配,Builder 内部需要多次扩容复制。
2. 🔬 深入原理:为什么 strings.Builder 比 + 快?
// 操作符 + 每次都会创建新字符串
s := "a" + "b" + "c" + "d"
// 实际执行:("a"+"b") → temp1; (temp1+"c") → temp2; (temp2+"d") → s
// 每次 + 都分配新内存并复制 → O(n²) 内存分配
// strings.Builder 使用内部 buffer,追加 +3. 常用的查找与操作
3.1 包含判断
strings.Contains(s, "substr") // 是否包含
strings.ContainsAny(s, "abc") // 是否包含任意字符
strings.ContainsRune(s, '你') // 是否包含指定 rune
strings.HasPrefix(s, "http://") // 前缀
strings.HasSuffix(s, ".go") // 后缀3.2 索引查找
strings.Index(s, "sub") // 第一个匹配的位置
strings.LastIndex(s, "sub") // 最后一个匹配的位置
strings.IndexByte(s, 'a') // 查找单字节(比 Index 快)
strings.IndexRune(s, '你') // 查找 rune
strings.IndexAny(s, "abc") // 查找任意字符3.3 替换与分割
strings.Replace(s, "old", "new", 1) // 仅替换第 1 次
strings.ReplaceAll(s, "old", "new") // 替换所有
strings.Split(s, ",") // 按分隔符分割
strings.SplitN(s, ",", 2) // 分割为 N 部分
strings.SplitAfter(s, ",") // 保留分隔符
strings.Fields(s) // 按空白字符分割
strings.Trim(s, " \t\n") // 修剪两端
strings.TrimSpace(s) // 修剪空白(等同于 Trim + 空白集)
strings.TrimPrefix(s, "http://") // 修剪前缀3.4 大小写转换
strings.ToUpper(s) // 全大写
strings.ToLower(s) // 全小写
strings.ToTitle(s) // 标题格式(不推荐,对 Unicode 处理有问题)
// Go 1.18+:Unicode 大小写(更准确但更慢)
cases.Title(language.English).String(s)4. strings.Replacer — 高效多替换
// 批量替换 HTML 实体
replacer := strings.NewReplacer(
"&", "&",
"<", "<",
">", ">",
`"`, """,
)
escaped := replacer.Replace(`<div class="test">Hello</div>`)
// → <div class="test">Hello</div>⚡
strings.Replacer内部使用 Trie 树,比多次strings.Replace调用快得多。
5. strings.Builder 进阶用法
// 复用 Builder
var builder strings.Builder
for _, item := range items {
builder.Reset() // 重置内容(保留底层 buffer)
builder.WriteString(item.Prefix)
builder.WriteString(item.Name)
result := builder.String()
// …
}
// Grow 预分配
builder.Grow(estimateSize)
// 直接写入 byte/rune
builder.WriteByte('a')
builder.WriteRune('你')
// 查看当前内容(不消费)
fmt.Println(builder.String())sync.Pool + strings.Builder
var builderPool = sync.Pool{
New: func() interface{} {
return &strings.Builder{}
},
}
func buildString(items []string) string {
b := builderPool.Get().(*strings.Builder)
defer builderPool.Put(b)
b.Reset()
for _, item := range items {
b.WriteString(item)
}
return b.String()
}6. bytes.Buffer 特有功能
var buf bytes.Buffer
buf.WriteString("hello")
buf.WriteByte('!')
buf.Write([]byte(" world"))
// 读取
data := make([]byte, buf.Len())
buf.Read(data)
// Next — 读取前 N 字节并移动指针
chunk := buf.Next(5)
// Truncate — 保留前 N 字节
buf.Truncate(10)
// ReadFrom / WriteTo — 高效的 IO(可能使用零拷贝)
buf.ReadFrom(file) // 从 Reader 读取所有内容
buf.WriteTo(file) // 写入到 Writer7. 字符串与 []byte 转换的成本
// string → []byte:分配新内存 + 复制
b := []byte("hello") // 有成本
// []byte → string:分配新内存 + 复制
s := string(b) // 有成本
// 热路径避免频繁转换
// 如果不需要修改,直接用 string
// 如果需要修改,一直用 []byte,最后转 string🔬 深入原理:Go 编译器在某些场景下会优化:
string([]byte)在mapkey 查找和string比较中会做优化,不分配。但不要依赖这种优化——语义上每次转换都是复制的。
8. 🚨 常见陷阱
| 陷阱 | 说明 |
|---|---|
string[i] 是 byte 不是 rune |
中文等 Unicode 字符占用多个字节 |
len(s) 是字节数不是字符数 |
用 utf8.RuneCountInString(s) 获取字符数 |
range s 按 rune 迭代 |
索引 i 是字节位置不是字符位置 |
Split("", ",") 返回 [""] |
空字符串分割得到一个包含空字符串的切片 |
| Builder 写入后忘记 Reset | 下一次使用会追加到旧内容后面 |