📝

【Go】効率的な文字列結合の方法

2025/03/18に公開

はじめに

大量の文字列結合を効率良く行うにはstrings.Builderを使うのがおすすめと聞いたことがあるのですが、本当にそうなのか実際に確認したいと思ったので、さまざまな方法を用いて比較してみました!

文字列結合の方法

1. +演算子

main.go
s1, s2 := "Hello", "World!"
result := s1 + ", " + s2
fmt.Println(result) // 出力結果:Hello, World!

特徴

  • シンプルなので可読性が高い
  • 文字列はイミュータブル(変更不可)なため、新しい文字列を都度作成するためメモリ効率が悪い

2. fmt.Sprintf()

main.go
result = fmt.Sprintf("%s, %s", s1, s2)
fmt.Println(result) // 出力結果:Hello, World!

特徴

  • フォーマットが必要な場合に便利
  • 文字列はイミュータブル(変更不可)なため、新しい文字列を都度作成するためメモリ効率が悪い

3. strings.Join()

main.go
result = strings.Join([]string{"Hello", "World!"}, ", ")
fmt.Println(result) // 出力結果:Hello, World!

特徴

  • メモリ効率が良い
  • スライスを作るためのコストはかかる

4. strings.Builder

main.go
var sb strings.Builder
sb.WriteString("Hello")
sb.WriteString(", ")
sb.WriteString("World!")
result = sb.String()
fmt.Println(result) // 出力結果:Hello, World!

特徴

  • 文字列のバッファを管理しながら結合できるため、最もメモリ効率が良い
  • シンプルな結合をするには冗長になる

ベンチマーク比較

ベンチマークを作成するコマンド

go test -bench=. -benchmem > result.txt

2つの文字列を結合する場合

main_test.go
package main

import (
    "fmt"
    "strings"
    "testing"
)

func BenchmarkStringConcatPlus(b *testing.B) {
    s1, s2 := "Hello", "World"
    for i := 0; i < b.N; i++ {
        _ = s1 + " " + s2
    }
}

func BenchmarkStringConcatSprintf(b *testing.B) {
    s1, s2 := "Hello", "World"
    for i := 0; i < b.N; i++ {
        _ = fmt.Sprintf("%s %s", s1, s2)
    }
}

func BenchmarkStringConcatJoin(b *testing.B) {
    s := []string{"Hello", "World"}
    for i := 0; i < b.N; i++ {
        _ = strings.Join(s, ", ")
    }
}

func BenchmarkStringConcatBuilder(b *testing.B) {
    for i := 0; i < b.N; i++ {
        var sb strings.Builder
        sb.WriteString("Hello")
        sb.WriteString(", ")
        sb.WriteString("World")
        _ = sb.String()
    }
}

実行結果

+演算子を用いた場合が最も効率が良く、fmt.Sprintf()が最も効率が悪い

result.txt
goos: darwin
goarch: arm64
pkg: go-strings
cpu: Apple M1
BenchmarkStringConcatPlus-8      	73287025	        16.08 ns/op	       0 B/op	       0 allocs/op
BenchmarkStringConcatSprintf-8   	15572078	        76.39 ns/op	      48 B/op	       3 allocs/op
BenchmarkStringConcatJoin-8      	44943258	        26.42 ns/op	      16 B/op	       1 allocs/op
BenchmarkStringConcatBuilder-8   	39316868	        30.69 ns/op	      24 B/op	       2 allocs/op
PASS
ok  	go-strings	5.677s

1万の文字列を結合する場合

main_test.go
package main

    import (
    "fmt"
    "strings"
    "testing"
)

// 1万個の文字列を作成する
const N = 10000

func generateStrings() []string {
    s := make([]string, N)
    for i := 0; i < N; i++ {
        s[i] = "Hello"
    }
    return s
}

func BenchmarkStringConcatPlus(b *testing.B) {
    s := generateStrings()
    for i := 0; i < b.N; i++ {
        result := ""
        for _, v := range s {
            result += v + " "
        }
        _ = result
    }
}

func BenchmarkStringConcatSprintf(b *testing.B) {
    s := generateStrings()
    for i := 0; i < b.N; i++ {
        result := ""
        for _, v := range s {
            result = fmt.Sprintf("%s %s", result, v)
        }
        _ = result
    }
}

func BenchmarkStringConcatJoin(b *testing.B) {
    s := generateStrings()
    for i := 0; i < b.N; i++ {
        _ = strings.Join(s, " ")
    }
}

func BenchmarkStringConcatBuilder(b *testing.B) {
    s := generateStrings()
    for i := 0; i < b.N; i++ {
        var sb strings.Builder
        for _, v := range s {
            sb.WriteString(v)
            sb.WriteString(" ")
        }
        _ = sb.String()
    }
}

実行結果

strings.Join()やstrings.Builderが最も効率が良く、fmt.Sprintf()が最も効率が悪い

result.txt
goos: darwin
goarch: arm64
pkg: go-strings
cpu: Apple M1
BenchmarkStringConcatPlus-8      	      45	  25633419 ns/op	324621826 B/op	   10015 allocs/op
BenchmarkStringConcatSprintf-8   	      39	  30674366 ns/op	326258795 B/op	   30289 allocs/op
BenchmarkStringConcatJoin-8      	   16693	     71537 ns/op	   65545 B/op	       1 allocs/op
BenchmarkStringConcatBuilder-8   	   18127	     66611 ns/op	  285442 B/op	      22 allocs/op
PASS
ok  	go-strings	7.253s

10万の文字列を結合する場合

main_test.go
package main

    import (
    "fmt"
    "strings"
    "testing"
)

// 10万個の文字列を作成する
const N = 100000

func generateStrings() []string {
    s := make([]string, N)
    for i := 0; i < N; i++ {
        s[i] = "Hello"
    }
    return s
}

func BenchmarkStringConcatPlus(b *testing.B) {
    s := generateStrings()
    for i := 0; i < b.N; i++ {
        result := ""
        for _, v := range s {
            result += v + " "
        }
        _ = result
    }
}

func BenchmarkStringConcatSprintf(b *testing.B) {
    s := generateStrings()
    for i := 0; i < b.N; i++ {
        result := ""
        for _, v := range s {
            result = fmt.Sprintf("%s %s", result, v)
        }
        _ = result
    }
}

func BenchmarkStringConcatJoin(b *testing.B) {
    s := generateStrings()
    for i := 0; i < b.N; i++ {
        _ = strings.Join(s, " ")
    }
}

func BenchmarkStringConcatBuilder(b *testing.B) {
    s := generateStrings()
    for i := 0; i < b.N; i++ {
        var sb strings.Builder
        for _, v := range s {
            sb.WriteString(v)
            sb.WriteString(" ")
        }
        _ = sb.String()
    }
}

実行結果

strings.Join()やstrings.Builderが効率良く、+演算子やfmt.Sprintf()が効率悪い

result.txt
goos: darwin
goarch: arm64
pkg: go-strings
cpu: Apple M1
BenchmarkStringConcatPlus-8      	       1	4837808791 ns/op	30394873776 B/op	  100121 allocs/op
BenchmarkStringConcatSprintf-8   	       1	4919153375 ns/op	60438743120 B/op	  397674 allocs/op
BenchmarkStringConcatJoin-8      	    1600	    748239 ns/op	  607211 B/op	       1 allocs/op
BenchmarkStringConcatBuilder-8   	    1360	    858256 ns/op	 3243932 B/op	      31 allocs/op
PASS
ok  	go-strings	13.592s

それぞれの方法について

  1. +演算子
  • 毎回新しい文字列を作成するため、メモリ使用量が膨大になる
  • 不要になった中間の文字列が大量にGC(ガベージコレクション)の対象となる¥
  1. fmt.Sprintf()
  • +演算子同様、毎回新しい文字列を作成するため、メモリ使用量が膨大になる
  • 内部でバッファを利用するものの、新しいメモリ割り当てが頻繁に発生するため、GCコストが高い
  1. strings.Join()
  • 1回だけのメモリ確保で済むため、GC(ガベージコレクション)の負担が少ない
  • メモリの割り当て回数(allocs/op)が最小限(通常1回)に抑えられる
  • 不要な中間オブジェクトを作らないため、メモリ効率が良い
  1. strings.Builder
  • 新しい文字列を毎回作らず、バッファに直接書き込むため、メモリの確保回数が少ない
  • 必要に応じてバッファを拡張しながらメモリを再利用 するため、GC負担が小さい
  • 動的に文字列を追加するのに長けている

まとめ

  • シンプルな文字結合→+演算子
  • フォーマットが絡む結合→fmt.Sprintf()
  • 大量の文字列結合→strings.Join()strings.Builder

参考

Discussion