#21 Goで学ぶコレクション操作の応用(フィルタ・マップ的な考え方)の基本

はじめに

Pythonのfilter()やmap()、JavaScriptのArray.prototype.filterやArray.prototype.mapのような便利な関数を使ったことがある人は多いでしょう。しかしGo言語を学び始めると、「同じような関数がGoには見当たらない」と戸惑うことがよくあります。filter(フィルタ)とは、集まったデータ(コレクション)の中から条件に合うものだけを選び出す処理のことです。map(マップ)とは、コレクションの各要素を一つずつ別の形に変換する処理のことを指します。Goは言語仕様をシンプルに保つ方針を取っているため、これらは標準の書き方としては用意されていません。基本的にはスライス(可変長の配列のようなデータ構造)とforループを組み合わせて自分で書く必要があります。本記事では、初心者の方向けに、Goでフィルタ・マップ的な処理をどう書くか、そしてGo1.21以降で使えるようになった便利なパッケージについても解説します。

フィルタ的な処理を書く

まずは「条件に合う要素だけを取り出す」フィルタ処理です。例えば整数のスライスから偶数だけを取り出すコードは次のように書きます。

package main

import "fmt"

func main() {
    nums := []int{1, 2, 3, 4, 5, 6}
    var evens []int
    for _, n := range nums {
        if n%2 == 0 {
            evens = append(evens, n)
        }
    }
    fmt.Println(evens) // [2 4 6]
}

ポイントは2つあります。1つ目は、結果を格納する空のスライス(var evens []int)を先に用意しておくことです。2つ目は、rangeで元のスライスを1件ずつ確認しながら、条件を満たしたものだけappend(スライスへの要素追加)していくことです。他言語のfilter()のように1行では書けませんが、そのぶん処理の流れが目に見えるので、慣れると「今何をしているか」が追いやすいという利点もあります。

マップ的な処理を書く

次は「各要素を別の形に変換する」マップ処理です。例えば文字列のスライスをすべて大文字に変換する例を見てみましょう。

package main

import (
    "fmt"
    "strings"
)

func main() {
    names := []string{"go", "python", "rust"}
    upper := make([]string, len(names))
    for i, name := range names {
        upper[i] = strings.ToUpper(name)
    }
    fmt.Println(upper) // [GO PYTHON RUST]
}

ここではmake([]string, len(names))で、元のスライスと同じ長さの新しいスライスをあらかじめ確保しています。マップ処理は要素数が変わらないため、こうして最初に必要な長さのスライスを用意し、インデックス番号でそのまま値を書き込むのが効率的です。フィルタ処理のように毎回appendする必要がない点が違いです。

ジェネリクスと slices パッケージで再利用する

毎回forループを書くのは面倒に感じるかもしれません。Go 1.18以降で使えるようになったジェネリクス(型を限定せずに関数を汎用的に書ける仕組み)を使うと、自分でfilter・map関数を定義できます。

func Filter[T any](s []T, f func(T) bool) []T {
    var result []T
    for _, v := range s {
        if f(v) {
            result = append(result, v)
        }
    }
    return result
}

func Map[T, U any](s []T, f func(T) U) []U {
    result := make([]U, len(s))
    for i, v := range s {
        result[i] = f(v)
    }
    return result
}

これを定義しておけば、Filter(nums, func(n int) bool { return n%2 == 0 })のように呼び出せます。また、Go 1.21以降では標準ライブラリにslicesパッケージが追加され、slices.Sortやslices.Containsといった便利な関数が使えるようになりました。ただしfilter・mapそのものはまだ標準には含まれていません。上記のような自作の関数を使うか、あるいはgolang.org/x/exp/slicesなどの補助パッケージを使うのが一般的です。

つまずきやすいポイント

初心者がよくつまずくのが、nilスライスと空スライスの違いです。nilとは「値が存在しない」ことを表すGo特有の値で、var evens []intのように宣言した時点では中身がないnilスライスになります。ただしappendすれば問題なく要素が追加されるので、実用上は気にしすぎなくて大丈夫です。fmt.Printlnで表示したときに[]と出るか、JSON変換時にnullになるかといった細かな違いはあるので、頭の片隅に置いておくと後々役立ちます。また、forループ内で条件に一致しなかった場合の処理を書き忘れると、意図しない結果になりやすくなります。条件分岐の中身を書いたら、一度必ず標準出力で確認する癖をつけましょう。

まとめ

Goにはfilter・mapのような組み込み関数は用意されていませんが、スライスとforループ、そして必要に応じてジェネリクスを使った自作関数で同様の処理を実現できます。一見遠回りに見えるかもしれませんが、処理の流れが明示的になるぶんコードの動きを追いやすく、Goらしい書き方だと言えます。まずはシンプルなforループでフィルタ・マップ処理を書けるようになり、慣れてきたらジェネリクスを使った再利用可能な関数にステップアップしていくとよいでしょう。

関連記事


本ページはプロモーションを含みます。

参考書籍(PR)

Goをもっと学びたい方には「Go言語入門」がおすすめです。


PR

コードを書かずに自動化したい方へ

「学んだけど自分の作業に組み込む時間がない」という方向けに、ダブルクリックで動く Windows専用の自動化ツールをオーダーメイドで作成しています。気になる方は こちらをご覧ください。

コメント

このブログの人気の投稿

#25 Rustで学ぶAI API(ChatGPT等)の呼び出し方の基本

#8 C++で学ぶ文字列操作の基本

#16 TypeScriptで学ぶ辞書型(マップ)の基本