🧠

【Go】コードの読み方を学ぶ - strings.Joinを題材に

に公開

はじめに

Go の標準ライブラリには、効率や安全性に配慮された設計が随所に見られます。
今回は、その中でもよく使われる strings.Join を題材に、「コードをどう読み解けばよいか」を順に見ていきます。

背景・目的

例えば、このようなコードがあります。

// Join concatenates the elements of its first argument to create a single string.
// The separator string sep is placed between elements in the resulting string.
func Join(elems []string, sep string) string {
    switch len(elems) {
    case 0:
        return ""
    case 1:
        return elems[0]
    }

    var n int
    if len(sep) > 0 {
        if len(sep) >= maxInt/(len(elems)-1) {
            panic("strings: Join output length overflow")
        }
        n += len(sep) * (len(elems) - 1)
    }
    for _, elem := range elems {
        if len(elem) > maxInt-n {
            panic("strings: Join output length overflow")
        }
        n += len(elem)
    }

    var b Builder
    b.Grow(n)
    b.WriteString(elems[0])
    for _, s := range elems[1:] {
        b.WriteString(sep)
        b.WriteString(s)
    }
    return b.String()
}

まずは、この関数が 何をしたいのかを言葉で理解する
Go の標準ライブラリでは、「コードそのものを読む前に、『何をする関数か』を把握する」ことが大切です。

1. ドキュメントコメントを見る

// Join concatenates the elements...
  • 関数の説明に目を通し、「文字列のスライスを結合し、セパレータを挟む」という振る舞いを理解しておきます。
  • 実装に入る前に目的を言葉で説明できる状態にすることで、読む視点が明確になります。

2. シグネチャを解析する

func Join(elems []string, sep string) string
  • 引数と戻り値を確認し、「どんな入力を受け取り、何を返す関数なのか」を整理します。
  • この段階で「文字列リストを受け取り、連結された文字列を返す関数だ」と予測できます。

3. 早期リターンのパターンを見つける

switch len(elems) {
case 0:
    return ""
case 1:
    return elems[0]
}
  • 要素が 0 個なら ""、1 個ならそのまま返す。
  • 特殊ケースを最初に処理する「ガード節」は、ライブラリコードによくある構造です。
  • これにより、残りのコードがシンプルに読めるようになります。

4. 出力サイズを前もって計算する

var n int
if len(sep) > 0 {
    if len(sep) >= maxInt/(len(elems)-1) {
        panic("strings: Join output length overflow")
    }
    n += len(sep) * (len(elems) - 1)
}
  • セパレータの分だけ先に長さを加え、オーバーフローの可能性を検査。
  • 「安全性」と「効率性」の両立がここから読み取れます。

5. 各要素の長さを合計する

for _, elem := range elems {
    if len(elem) > maxInt-n {
        panic("strings: Join output length overflow")
    }
    n += len(elem)
}
  • 各要素の長さを足して全体のサイズ n を確定。
  • ここでもオーバーフローへの配慮が施されている点に注目します。

6. Builder でバッファを確保する

var b Builder
b.Grow(n)
  • strings.Builder を使い、初期にバッファを確保することで再割り当てを防ぎます。
  • 「余分なメモリ割り当てが起こらないように先回りして設計されている」ことがわかります。

7. 文字列の組み立て

b.WriteString(elems[0])
for _, s := range elems[1:] {
    b.WriteString(sep)
    b.WriteString(s)
}
  • 最初の要素を書き込み、続けて「セパレータ → 要素」の手順を繰り返す。
  • 読みやすく、効率的な構造です。

8. 完成した文字列を返す

return b.String()
  • Builder が保持していた内容を文字列化し、余計なコピーなしに返します。

読み方の型(汎用)

ここまでの流れは、他の Go 標準ライブラリを読む際にも応用できます。

  1. ドキュメントコメントで目的を把握する
  2. シグネチャから入力と出力を整理する
  3. 早期リターン(ガード節)に注目する
  4. 変数の役割を明確にする
  5. ループや分岐を日本語に置き換えて理解する
  6. 最適化の意図を読み取る
  7. 全体を「どんな流れで何をする処理か」の物語にする

まとめ

strings.Join を読み解く流れから、以下の設計ポイントが見えてきます。

  • 特殊ケースを早めに処理してシンプルにする
  • 出力サイズを事前に計算してオーバーフローを防ぐ
  • 最適化されたメモリ確保(Builder + Grow)
  • シンプルに書き進められるループ構造
  • コピーを避けた効率的な返却方法

次に標準ライブラリを読むときは、この「読む型」を意識して進めてみると理解が深まるはずです。

Discussion