for文を書きたくないGoプログラマー達へ

Go言語は書き振りが冗長であると言われることが多々ある。

例えば、int型のスライスから値が10の値だけを抽出したい場合には下記のように書く。

package main

import "fmt"

func main() {
    intSlice := []int{1, 10, 100}
    result := []int{}
    for _, v := range intSlice {
        if v == 10 {
            result = append(result, v)
        }
    }
    fmt.Println(result) // [10]
}

抽出元のスライスの要素をfor文で一件ずつ確認

抽出条件に一致した要素を抽出先のスライスに格納

といった流れである。

比較の為に同じことをJavascriptRubyHaskellで書いてみよう。

const intArray = [1, 10, 100];
const result = intArray.filter((value) => value === 10);
console.log(result);
intArray = [1, 10, 100]
result = intArray.filter {|i| i == 10 }
puts result
main = do
  let intArray = [1, 10, 100]
  let result = filter (==10) intArray
  print result

これらの言語ではfilter関数を使うことで、for文を使わずに実現できている。

しかし、Go言語にはfiltermapなどのコレクション操作関数が組み込み関数にも標準関数にも実装されていない。

コレクション操作を手続き的に書かないといけない点がGo言語が冗長だと言われる理由の一つであると思う。

本記事では、コレクション操作の度にforで回して手続き的に書くことが耐えられないGoプログラマー達に向けて samber/lo を紹介する。

samber/lo とは

github.com

  • Javascript の Lodash 風のライブラリである。
  • ジェネリクス対応していて、型に守られながらも汎用的に使用できる。
  • コレクション操作をfor文を用いずに書ける他にも、三項演算子っぽく書けるような関数なども提供されている。

個人的にオススメの関数をいくつか紹介していく。

Map

オススメ度 : ★★★★★

スライスから別の型のスライスを生成する。 (個人的な話だが)構造体のスライスから特定のフィールドの値だけを抽出したスライスを生成したいことがよくある。その際に使用できるMapが最もオススメ。

package main

import (
    "fmt"

    "github.com/samber/lo"
)

func main() {
    type s struct {
        i int
    }
    sSlice := []s{
        {i: 1},
        {i: 10},
        {i: 100},
    }

    result := lo.Map(sSlice, func(v s, _ int) int {
        return v.i
    })
    fmt.Println(result) // [1 10 100]
}

FilterMap

オススメ度 : ★★★★☆

Mapと同じくスライスから別の型のスライスを生成するが、抽出条件を付けられる。 引数に渡す関数で値と共に真偽値を返すことで、その値を抽出するかどうかを判定する。

package main

import (
    "fmt"

    "github.com/samber/lo"
)

func main() {
    type s struct {
        i int
    }
    sSlice := []s{
        {i: 1},
        {i: 10},
        {i: 100},
    }

    result := lo.FilterMap(sSlice, func(v s, _ int) (int, bool) {
        if v.i > 50 {
            return 0, false
        }
        return v.i, true
    })
    fmt.Println(result) // [1, 10]
}

Filter

オススメ度 : ★★★★☆

スライスから特定の条件で抽出した値で構成されるスライスを生成する。

package main

import (
    "fmt"

    "github.com/samber/lo"
)

func main() {
    type s struct {
        i int
    }
    sSlice := []s{
        {i: 1},
        {i: 10},
        {i: 100},
    }

    result := lo.Filter(sSlice, func(v s, _ int) bool {
        return v.i > 1
    })
    fmt.Println(result) // [{10} {100}]
}

Find

オススメ度 : ★★★★☆

スライスから条件一致した要素を抽出する。

インデックスの先頭から順に操作して見つかった時点で値を返す。

複数件一致する場合は、インデックスが小さいものが優先される。

package main

import (
    "fmt"

    "github.com/samber/lo"
)

func main() {
    type s struct {
        i int
    }
    sSlice := []s{
        {i: 1},
        {i: 10},
        {i: 100},
    }

    result, ok := lo.Find(sSlice, func(v s) bool {
        return v.i > 1
    })
    fmt.Println(result, ok) // {10} true
}

contains

オススメ度 : ★★★★☆

スライスに条件一致する要素が含まれているかを判定する。

package main

import (
    "fmt"

    "github.com/samber/lo"
)

func main() {
    intSlice := []int{1, 10, 100}
    ok := lo.Contains(intSlice, 1)
    fmt.Println(ok) // true
}

RangeFrom

オススメ度 : ★★★☆☆

指定の開始位置から終了位置までのスライスを生成する。

package main

import (
    "fmt"

    "github.com/samber/lo"
)

func main() {
    intSlice := lo.RangeFrom(1, 3)
    fmt.Println(intSlice) // [1 2 3]
}

Ternary

オススメ度 : ★☆☆☆☆

三項演算子風に関数に引数を渡すことで、真偽値によって返却値を分岐できる。 なんの相談も無しにいきなり使用すると多分レビューで突っ込まれる。 どうしても文を書きたくない場合に使う。(私は思ったことないので予想です)

package main

import (
    "fmt"

    "github.com/samber/lo"
)

func main() {
    n := 1
    str := lo.Ternary(n > 0, "OK", "NG")
    fmt.Println(str) //OK
}

Transaction

オススメ度 : ★★☆☆☆

Sagaパターンを実装している。

saga 設計パターンは、分散トランザクション シナリオでマイクロサービス間のデータの一貫性を管理する方法です。 saga はトランザクションのシーケンスです。この saga によって各サービスが更新され、次のトランザクション ステップをトリガーするメッセージまたはイベントが発行されます。 また、ステップが失敗すると、その前のトランザクションを無効にする補正トランザクションを実行されます。

learn.microsoft.com

トランザクションの一連の処理の中で、一つの処理が終われば値を引き継いで次の処理に移る。

途中で処理が失敗した場合は、ロールバック処理が逆の順番で実行される。

package main

import (
    "fmt"

    "github.com/samber/lo"
)

func main() {
    tx := lo.NewTransaction[int]().
        Then(
            func(s int) (int, error) { return s + 10, nil },
            func(s int) int { return s - 10 },
        ).
        Then(
            func(s int) (int, error) { return s + 20, nil },
            func(s int) int { return s - 20 },
        )
    result, err := tx.Process(0)
    fmt.Println(result, err) // 30 <nil>
}

最後に

samber/lo を上手く使えば、for文が無くなりネストの量やコードの量が減らすことが可能です。

個人的には、似たような手続き処理をコピペして使うくらいであれば、ライブラリを導入するべきだと考えています。コピペミスでのバグは本当に気が付きにくい上に結構な頻度で発生しますからね。

ただし、今回紹介したコレクション操作関数を使って見た目上からはfor文が無くなっても、当然関数内ではfor文が実行されています。ブラックボックス化した分、ループ回数への意識が若干薄れてしまったりレビューで見落としてしまったりといったことも考えられます。

ライブラリを使用している箇所こそ、そういった点にも十分考慮した実装レビューが必要であるかと思います。