giftee engineer blog

Go Conference 2024 セッションレポート『イテレータによってGoはどう変わるのか』

2024-06-10

Go Conference 2024 のセッションレポートです。

logo

こんにちは、ギフティのエンジニアの nakatsu です。ギフトの発行基盤のプロダクト開発をしています。普段は Go を使って開発していて、趣味で Rust を触ったりしています。

6/8 (土) に東京の渋谷で開催された Go Conference 2024 に参加してきたので、その中で印象に残ったセッションについて紹介しようと思います。

Go Conference 2024 について

今回はギフティとして初めて Go Conference のスポンサーとして協賛しました。

https://gocon.jp/2024/

Go Conference 2024 は数年ぶりのオフライン開催とのことで、いつもよりも盛り上がりを感じました。会場には多くのエンジニアが集まり、様々なブースが設置されていました。セッションについても、初級者から上級者向けまで幅広い内容が発表されていました。

opening_slide

印象に残ったセッション

個人的には、tenntenn さんの「イテレータによってGoはどう変わるのか」というセッションが特に印象に残りました。

イテレータがリリースされることで、どのようなコードになるのか、将来どんなことが実現され得るのか、など興味深い内容が盛りだくさんでした。

詳しい内容については tenntenn さんのスライドを参照いただければと思います。

イレテータを試してみる

普段の業務では Go でイテレータを使ったコードを書くことがないため、せっかくなのでイテレータによる実装を試してみました。

まずは、言語の標準仕様としてイテレータが提供されている Rust で簡単な処理を書いてみます。

ここでいう「イテレータ」とは、配列のようなデータ構造の各要素に対して操作をする際に、毎回全要素をループで回して処理をするのではなく、1 回のループで各要素に対する処理をすることを指しています。

fn main() {
    let v: Vec<i32> = vec![1, 2, 3];

    let output: i32 = v.iter()  // イテレータを返す
        .map(|v| v + 1)         // 各要素に 1 を足す
        .filter(|v| v % 2 == 0) // 偶数の要素のみに絞る
        .sum();                 // 全ての要素の和を返す

    println!("{:?}", output);   // 6 が出力される
}

同じ処理を Go で書いてみます。以降は説明を単純化するために、あえてジェネリクスを使っていませんが、ご了承ください。

イテレータを使わずに書いた場合は、以下のようになります。

package main

func main() {
	s := []int{1, 2, 3}

	var output int
	for _, v := range s {
		v = v + 1
		if v%2 == 0 {
			output += v
		}
	}

	println(output)
}

ただ、上記のコードは可読性が低く、あらゆる変換処理を一緒にしてしまっていて拡張性が低いかと思います。

そこで、以下のように変換処理の関数を呼び出す形で、コードをスッキリさせてみます。

package main

import "slices"

func main() {
	s := []int{1, 2, 3}

	s = mapSlice(s, func(v int) int { // map が予約語なので、関数名を mapSlice にした
		return v + 1
	})
	s = slices.DeleteFunc(s, func(v int) bool {
		return v%2 != 0
	})
	output := sum(s)

	println(output)
}

// スライスを受け取り、引数の関数を適用した新しいスライスを返す
func mapSlice(s []int, mapper func(int) int) []int {
	output := make([]int, len(s))
	for i, v := range s {
		output[i] = mapper(v)
	}
	return output
}

// スライスを受け取り、要素の合計を返す
func sum(s []int) int {
	var output int
	for _, v := range s {
		output += v
	}
	return output
}

Rust のイディオムに少し近づきましたが、スライスの操作をするたびに毎回新しいスライスを生成しているため、無駄な処理が発生してしまっています。

そこで、Go 1.23 でリリース予定の range over func を使って書き直してみます。

package main

func main() {
	s := []int{1, 2, 3}

	i := iter(s)
	i = mapIter(i, func(v int) int { // map が予約語なので、関数名を mapIter にした
		return v + 1
	})
	i = filter(i, func(v int) bool {
		return v%2 == 0
	})
	output := sum(i)

	println(output)
}

// スライスをイテレータに変換する
func iter(s []int) func(yield func(int) bool) {
	return func(yield func(int) bool) {
		for _, v := range s {
			if !yield(v) {
				break
			}
		}
	}
}

// イテレータを受け取り、引数の関数を適用した新しいイテレータを返す
func mapIter(
	i func(yield func(int) bool),
	mapper func(int) int,
) func(yield func(int) bool) {
	return func(yield func(int) bool) {
		i(func(v int) bool {
			return yield(mapper(v))
		})
	}
}

// イテレータを受け取り、条件に合致する要素のみの新しいイテレータを返す
func filter(
	i func(yield func(int) bool),
	filter func(int) bool,
) func(yield func(int) bool) {
	return func(yield func(int) bool) {
		i(func(v int) bool {
			if filter(v) {
				return yield(v)
			}
			return true
		})
	}
}

// イテレータを受け取り、要素の合計を返す
func sum(i func(yield func(int) bool)) int {
	var s int
	for v := range i {
		s += v
	}
	return s
}

無駄がなくなって、だいぶいい感じです..!! (理想的には . でチェーンしたいですが)

GOEXPERIMENT=rangefunc を使って、以下のコマンドで実行もできました!

$ go version
go version go1.22.0 darwin/arm64

$ GOEXPERIMENT=rangefunc go run main.go
6

今回 iter , mapIter, filter, sum といった関数を自前で書きましたが、これらが標準パッケージに含まれれば、より簡単に書けるようになります。

個人的には reduce , collect といったイディオムも将来 Go の標準パッケージで利用できるようになったら嬉しいなと思っています。

おわりに

今回は Go Conference 2024 のセッションレポートを書いてみました。イテレータによって Go がどのように変わるのか、今後の Go の発展が楽しみです。

Go Conference 2024 のセッション資料の一覧については、こちらにまとまっているようなので、ぜひご確認いただければと思います。

今後も Go のコミュニティをもっと盛り上げていきたいと思います。

開催していただいた関係者の方々にも感謝したいです。楽しいイベントをありがとうございました!