[Go] strings.Join関数の中身を、初学者向けに解説してみた

はじめに

今日は、標準パッケージでもよく使われる関数、strings.Join関数について中身を見てみたいと思います。

公式のコード

まずは、コードを貼っておきます。

// Join concatenates the elements of a to create a single string. The separator string
// sep is placed between elements in the resulting string.

func Join(a []string, sep string) string { // => A
    switch len(a) {                        // => B
    case 0:
        return ""
    case 1:
        return a[0]
    }
    n := len(sep) * (len(a) - 1)          // => C
    for i := 0; i < len(a); i++ {
        n += len(a[i])
    }

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

解説

部分ごとに解説します。

A.引数はスライスと文字列、返り値は文字列

// Join concatenates the elements of a to create a single string. The separator string
// sep is placed between elements in the resulting string.

func Join(a []string, sep string) string { // => A

コメントにも書いてあるので、そのままGoogle翻訳に突っ込んで和訳すると…

Joinはaの要素を連結して単一の文字列を作成します。 区切り文字列 sepは結果の文字列の要素の間に配置されます。

まとめると、この関数の引数は

  a : []string型のスライス。結合したい対象。
sep : string型の文字列。結合する際、間に配置するもの。この記事内ではセパレーターと呼ぶことにします。

という感じのようです。

では、関数の中身を上から順に見て行きましょう。

B.スライスの要素数が0か1の場合は、フィルタリング

    switch len(a) { // => B
    case 0:
        return ""
    case 1:
        return a[0]
    }

与えたスライスの要素数が、0または1なら…というフィルタリングですね。
要素数0なら、空文字を返す。
要素数1なら、その1つだけの要素を返す。
ここはシンプルです。

C.返す文字列のバイト数計算

    n := len(sep) * (len(a) - 1) // => C
    for i := 0; i < len(a); i++ {
        n += len(a[i])
    }

出ました。定番の len 関数。文字列だと文字数(正式にはバイト数)を数えてくれます。
よく使う、 , とか : だと1を返してくれます。

この変数 n には、

n = セパレーターのバイト数 × 対象のスライスの要素数から1を引いたもの

が入ります。

…。

どうしてこんなことをするのか。

ここで少しハマったのですが、通りがかった先輩からご助言を頂けました。

このnはバイト数を数えている

だそうです。

つまり、「セパレーターのバイト数×セパレーターが必要な数」を計算することで、先に全セパレーターのバイト数を計算し、その後のfor文で要素1つ1つのバイト数を加算していき、 最終的にJoinして出力する文字列のバイト数の合計がnに入る という処理なようです。

そして、このバイト数の合計はこの後の処理に使われる様子。
なるほど。

D.文字列の書き出し

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

ここでは、見慣れない関数がいくつかありますが、1つ1つ追うとキリがなさそうなので、GoDocの説明をGoogle翻訳に丸投げしつつ、簡単に紹介します。

関連する関数と構造体の紹介

構造体 Builder

Builderは、Writeメソッドを使用して文字列を効率的に構築するために使用されます。 メモリのコピーを最小限に抑えます。 ゼロ値はすぐに使用できます。 ゼロ以外のビルダーをコピーしないでください。

  • Writeメソッドを使うための、空の構造体みたいです。
関数 Grow

Growは、必要に応じてbの容量を増やし、さらにnバイト。 Grow(n)の後、少なくともnバイトをbに書き込むことができます。別の割り当てなし。 nが負の場合、成長がパニックします。

  • bは上の構造体Builderを宣言したときの変数名。
  • nは引数に入れた数値で、バイト数を想定している様子。
  • 引数に正の数を入れると、そのバイト数だけ書き込めるように、変数b(型は構造体Builder)の容量を増やしてくれます。
関数 WriterString

WriteStringは、sの内容をbのバッファーに追加します。 sの長さとnilエラーを返します。

  • sは、引数に入れた文字列。
  • このsの内容をbのバッファーに追加するそうな。
関数 String

Stringは蓄積された文字列を返します。

  • ここは、シンプルに蓄積された文字列を返している様子です。

処理について

それではもう一度、Dの部分のコード全体を確認してみましょう。

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

上記の、さらっと説明した関数などを考慮すると、

  • 変数名 b で構造体 Builder を宣言
  • Cで用意したバイト数の入った変数 n を、Grow 関数に入れてバイト数を確保
  • b.WriteString(a[0])で、aの1つ目の要素をそのまま、bのバッファーに追加。
  • for文で、aの2つ目移行の要素をループ。
  • for文内では、b.WriteString(sep)b.WriteString(s)で、セパレーターと要素のバイト数をbのバッファーにどんどん追加。
  • 最後に蓄積した文字列を一気に返して終了。

という処理になります。

さいごに

最後まで読んで頂き、ありがとうございます。
初学者の解説で、拙いところも多いかと思うので、またコメント頂けると嬉しいです。
ありがとうございました。

参考サイト

https://golang.org/src/strings/strings.go#L425
https://godoc.org/strings#example-Join

おまけ

string.Join関数を使った過去の記事

PON3

PON3

5年くらい前に、30代完全未経験から独学でWeb系のソフトウェアエンジニアになった人。 現在はフリーランスエンジニアとして、大阪の自宅からフルリモートで東京の自社開発企業のお仕事をしている。 主戦場はバックエンドで、Go言語でのAPI開発や、Pythonでのデータ分析が武器。 とは言いつつ、フロントエンドをSPAを実装したり、IaCでクラウドインフラの設計構築したり、スクラム開発でプロジェクト運営したりするなんでも屋。 いつも、ググってきては誰かが書いてくれた記事を見て開発していたが、もらってばかりでなく世の中に返すこともしたいと思い、技術ブログをはじめる。 妻と2歳になる娘の3人暮らし。 何かご用件がある方は、TwitterのDMからどうぞ。