导航菜单

优化字符串拼接

🟡 中等

题目描述

以下三种字符串拼接方式,哪种性能最好?为什么?

// 方式 1:+ 操作符
func concat1(items []string) string {
    var result string
    for _, item := range items {
        result += item
    }
    return result
}

// 方式 2:fmt.Sprintf
func concat2(items []string) string {
    var result string
    for _, item := range items {
        result = fmt.Sprintf("%s%s", result, item)
    }
    return result
}

// 方式 3:strings.Builder
func concat3(items []string) string {
    var builder strings.Builder
    for _, item := range items {
        builder.WriteString(item)
    }
    return builder.String()
}

Benchmark 结果

BenchmarkConcat1-8     1000000    1024 ns/op    512 B/op    10 allocs/op
BenchmarkConcat2-8      100000    8192 ns/op   2048 B/op    30 allocs/op
BenchmarkConcat3-8    10000000     128 ns/op     64 B/op     1 allocs/op

提示

  • Go 字符串是不可变的
  • 每次拼接都会创建新字符串
  • strings.Builder 使用底层 []byte

解法

参考答案 (3 个标签)
字符串 性能优化 Benchmark

方式 1:+ 操作符

func concat1(items []string) string {
    var result string
    for _, item := range items {
        result += item  // 每次创建新字符串
    }
    return result
}

问题

  • 字符串不可变,每次 + 都创建新字符串
  • 循环 n 次,创建 n 个新字符串
  • 时间复杂度:O(n²)
  • 空间复杂度:O(n²)

方式 2:fmt.Sprintf

func concat2(items []string) string {
    var result string
    for _, item := range items {
        result = fmt.Sprintf("%s%s", result, item)
    }
    return result
}

问题

  • fmt.Sprintf 需要解析格式字符串
  • 反射机制获取参数类型
  • 性能比 + 更差
  • 时间复杂度:O(n²)

方式 3:strings.Builder(推荐)

func concat3(items []string) string {
    var builder strings.Builder
    builder.Grow(estimateSize(items))  // 预分配
    for _, item := range items {
        builder.WriteString(item)  // 直接写入底层 []byte
    }
    return builder.String()
}

优势

  • 底层使用 []byte,可变
  • 预分配容量,避免扩容
  • 只在最后创建一次字符串
  • 时间复杂度:O(n)

strings.Builder 底层实现

type Builder struct {
    addr *Builder  // 检测非法复制
    buf []byte     // 底层字节切片
}

func (b *Builder) WriteString(s string) (int, error) {
    b.buf = append(b.buf, s...)  // 直接 append
    return len(s), nil
}

func (b *Builder) String() string {
    return *(*string)(unsafe.Pointer(&b.buf))  // 零拷贝转换
}

关键点

  1. 可变底层:使用 []byte 而非 string
  2. 零拷贝转换String() 使用 unsafe 转换
  3. 预分配Grow() 避免扩容
  4. 禁止复制:通过 addr 字段检测

性能对比

方式时间复杂度分配次数适用场景
+O(n²)O(n)简单拼接(2-3 个)
fmt.SprintfO(n²)O(n)需要格式化
strings.BuilderO(n)O(1)大量拼接
[]byte 转换O(n)O(1)底层优化

优化建议

// ✅ 最佳实践:预分配容量
func concatOptimized(items []string) string {
    // 1. 预估大小
    size := 0
    for _, item := range items {
        size += len(item)
    }
    
    // 2. 预分配
    var builder strings.Builder
    builder.Grow(size)
    
    // 3. 拼接
    for _, item := range items {
        builder.WriteString(item)
    }
    
    return builder.String()
}

// ✅ 替代方案:使用 []byte
func concatWithByte(items []string) string {
    size := 0
    for _, item := range items {
        size += len(item)
    }
    
    buf := make([]byte, 0, size)
    for _, item := range items {
        buf = append(buf, item...)
    }
    
    return string(buf)
}

扩展:其他字符串操作优化

1. 字符串分割

// ❌ 多次分配
parts := strings.Split(s, ",")

// ✅ 预分配容量
count := strings.Count(s, ",") + 1
parts := make([]string, 0, count)
// 手动分割...

2. 子串检查

// ❌ 转换为小写再比较(分配新字符串)
if strings.ToLower(s) == "hello" {
}

// ✅ 使用 EqualFold(不分配)
if strings.EqualFold(s, "hello") {
}

3. 字符串拼接(少量)

// ✅ 2-3 个字符串用 +
result := s1 + s2 + s3

// ❌ 不要用 Builder(过度优化)
var builder strings.Builder
builder.WriteString(s1)
builder.WriteString(s2)
builder.WriteString(s3)

搜索