この記事は CyberAgent Developers Advent Calendar 2024 18日目の記事です。
前日の担当は星博之さんのAmeba広告、効果的な施策を進めるためにでした。
こんにちは、サイバーエージェント CIU であり Go の Next Experts である渋谷(@sivchari)です。
Go は2月と6月の年2回、新しいリリースがあります。2024年には Go1.22 と Go1.23 のリリースがありました。そして来年の2月には Go1.24 がリリースされます。
本記事では2024年の間にリリースされた Go の大きな変更や出来事を見ていきながら来年リリース予定の Go1.24 の中からいくつかの注目機能についていち早く触れてみたいと思います。
Go1.22
Go1.22 の大きな言語変更として、forの文法が変更されました。
Go1.22 以前のforに関して、大きくわけて2つの課題がありました。
1つ目はforのスコープ内で参照されるアドレスが同一であることにより並行処理と組み合わせたり、クロージャーをあとから実行した時に期待した結果が得られないことです。
package main
import "sync"
func main() {
var wg sync.WaitGroup
for _, i := range []int{1, 2, 3} {
wg.Add(1)
go func() {
defer wg.Done()
println(i)
}()
}
wg.Wait()
}
たとえば、Go1.21では変数 i のアドレスが全て同じであったため、このコードの出力結果は全て3となってしまいます。
この問題の回避方法としては for の中で i を再度宣言してアドレスを別にするという手法が取られていました。
しかしLet’s Encrypt を代表に様々なコードで再度宣言することを忘れていることに起因するバグが発生していることや実行されるべきサブテストが実際には実行されていなかったなどから自動的に別のアドレスとして割り当てられるように変化しました。
またこの変更によって管理するコードに影響が生じるかどうかについても go build -gcflags=all=-d=loopvar=2 ./...
と bisect
というツールを利用することで安全に移行することが可能です。
2つ目は range over int
の導入によりint
をループさせるコードがよりシンプルに記述できるようになりました。
Go1.21 まで for ... range
の文法としては下記のみがサポートされていました。
Range expression 1st value 2nd value array or slice a [n]E, *[n]E, or []E index i int a[i] E string s string type index i int see below rune map m map[K]V key k K m[k] V channel c chan E, <-chan E element e E
この文法に新たに
integer value n integer type, or untyped int value i see below
上記の文法が追加されました。一見この変更だけでは小さく見えるかもしれませんが、後述する range over func
という機能と合わせて for ... range
のサポートされる型が増えたことは大きな変更と言えるでしょう。
Go1.23
Go1.23 ではついに range over func
が追加され、for ... range
の構文に関数を指定することができるようになりました。おそらく今年の一番大きな変更を考えた時にこの機能が浮かぶ方が多いのではないでしょうか。
range over func
は2022年から Discussion されていた内容であり、私が外部登壇した 2023年の Go1.21 のイベントですでに Go1.22 でくるのではないかと話していた内容でした。
この変更により下記の3つの文法が追加でサポートされました。
func(func() bool)
func(func(K) bool)
func(func(K, V) bool)
これにより Go1.23までで for ... range
では以下の文法がサポートされています。
Range expression 1st value 2nd value array or slice a [n]E, *[n]E, or []E index i int a[i] E string s string type index i int see below rune map m map[K]V key k K m[k] V channel c chan E, <-chan E element e E integer value n integer type, or untyped int value i see below function, 0 values f func(func() bool) function, 1 value f func(func(V) bool) value v V function, 2 values f func(func(K, V) bool) key k K v V
また、 iter パッケージという range over func
をサポートする構造体、関数が追加されました。
// Seq is an iterator over sequences of individual values.
// When called as seq(yield), seq calls yield(v) for each value v in the sequence,
// stopping early if yield returns false.
// See the [iter] package documentation for more details.
type Seq[V any] func(yield func(V) bool)
// Seq2 is an iterator over sequences of pairs of values, most commonly key-value pairs.
// When called as seq(yield), seq calls yield(k, v) for each pair (k, v) in the sequence,
// stopping early if yield returns false.
// See the [iter] package documentation for more details.
type Seq2[K, V any] func(yield func(K, V) bool)
// Pull converts the “push-style” iterator sequence seq
// into a “pull-style” iterator accessed by the two functions
// next and stop.
//
// Next returns the next value in the sequence
// and a boolean indicating whether the value is valid.
// When the sequence is over, next returns the zero V and false.
// It is valid to call next after reaching the end of the sequence
// or after calling stop. These calls will continue
// to return the zero V and false.
//
// Stop ends the iteration. It must be called when the caller is
// no longer interested in next values and next has not yet
// signaled that the sequence is over (with a false boolean return).
// It is valid to call stop multiple times and when next has
// already returned false. Typically, callers should “defer stop()”.
//
// It is an error to call next or stop from multiple goroutines
// simultaneously.
//
// If the iterator panics during a call to next (or stop),
// then next (or stop) itself panics with the same value.
func PUll[V any](seq Seq[V]) (next func() (V, bool), stop func()) {}
// Pull2 converts the “push-style” iterator sequence seq
// into a “pull-style” iterator accessed by the two functions
// next and stop.
//
// Next returns the next pair in the sequence
// and a boolean indicating whether the pair is valid.
// When the sequence is over, next returns a pair of zero values and false.
// It is valid to call next after reaching the end of the sequence
// or after calling stop. These calls will continue
// to return a pair of zero values and false.
//
// Stop ends the iteration. It must be called when the caller is
// no longer interested in next values and next has not yet
// signaled that the sequence is over (with a false boolean return).
// It is valid to call stop multiple times and when next has
// already returned false. Typically, callers should “defer stop()”.
//
// It is an error to call next or stop from multiple goroutines
// simultaneously.
//
// If the iterator panics during a call to next (or stop),
// then next (or stop) itself panics with the same value.
func Pull2[K, V any](seq Seq2[K, V]) (next func() (K, V, bool), stop func()) {}
なぜ range over func
が Go にとって
それは Go1.22 までは他言語の iterator に相当する共通の API が文法として存在しなかったため、終わりをあらかじめ指定することができないようなデータ構造に対するアプローチが標準パッケージ内でも分かれていました。。
例として標準入力をあげます。標準入力から受け取った文字列を1行ずつ処理するプログラムを Go で書くことを考えた場合、入力依存になるため終わり(= 標準入力の終了タイミング)は実行側に依存するため指定することができません。そのため range
を使用できず bufio
というパッケージでは以下のように記述していました。
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
}
他にもおもいつくケースとしては database/sql
の (*Rows).Next()
や filepath.Walk
のようなコールバックを渡して処理するような API も当てはまるでしょう。
range over func
および、iter
パッケージはこれらを統一した文法で扱うことで連続する任意のデータ構造を抽象的に扱うための仕組みとなります。
range over func
が登場したことで個人的にとても嬉しい関数が maps.Keys
と slices.Sorted
を組み合わせてソートされたキーを全て取り出す書き方です。
package main
import (
"fmt"
"maps"
"slices"
)
func main() {
m := map[string]int{
"a": 1,
"b": 2,
"c": 3,
"d": 4,
"e": 5,
}
for _, v := range slices.Sorted(maps.Keys(m)) {
fmt.Println(v)
}
}
map 型のキーを取り出す場合、今まで一度 slice に append したのち、順不同であるキーを並び替えるために sort.Slice
または sort.SliceStable
を使用する必要がありました。
取り出したい要素に対して複数行書かなければいけなかったものがデータ構造を抽象化した恩恵により1行で取り出せるようになったのは嬉しいですよね。
このように slices
や maps
をはじめとし、 iter.Seq
や iter.Seq2
に統一された APIは今後も順次登場するでしょう。
余談として range over func
は最終的に range over func
を使用しないコードへと変換され実行されます。
例えば
for x := range f {
...
}
といったコードがあった場合
f(func(x T) bool {
...
})
と変換されてからコンパイルされるということです。 この変換は内部の range func パッケージが行っているため、気になる方はぜひ実装を読んでみてください。
他にも Go1.23 では time.Timer
の内部的な実装の変更や、unique
パッケージの追加、Go のコマンドに telemetry が追加されるなど重要な変更が多くあったリリースであったと思います。
Go 1.24
ここまでで今年リリースされたものの中からいくつかピックアップしてご紹介しました。
ここからは Go1.24 を先取りして私が現時点で注目するいくつかの変更を簡単に紹介します。
tool ディレクティブの追加
Go1.23 まで実際のプログラムとしては依存していないがプロジェクトで使用するような CLI のバージョン固定方法として、 tools.go
というファイルを作成して依存関係をまとめておくといった方法が Wiki では紹介されていました。おそらく多くのプロジェクトではこのファイルを参照して Makefile やスクリプトを使用してツールをインストールする仕組みを作成していると思います。
このような管理に対して Go1.24 以降では go.mod
に tool
ディレクティブを追加して記載することで go install
と同等の使用感で特定の CLI を使用することができます。注目される点として、go tool
により実行されたバイナリは $GOCACHE
配下にモジュールごとにキャッシュをすることです。これによりプロジェクトごとに同一の CLI を別のバージョンで使用しているといったケースでも期待するバージョンで動かせるようになるため非常に嬉しいですね。
testing.TB.Contextの追加
testing
パッケージに Context
という関数が追加されました。これにより今まで 自前で終了する必要のあった context.Context
が各テストスコープの終了時にキャンセルしてもらえるようになりました。これにより context のリークが起きていた箇所やテストの事前準備のために context の依存関係がシビアになっていたようなテストがすっきりして書けそうですね。都度 context.Background
を書く煩わしさからも解放されてこれを使用していないテストコードは全て linter で失敗させたいなと思うくらいです。
実際にこのモチベーションを叶えてくれる linter が次回の golangci-lint に追加されるそうで今からとても楽しみです。
synctest パッケージの実験的導入
GOEXPERIMENT=synctest
を有効にすることで synctest というパッケージを使用できるようになりました。 GOEXPERIMENT 自体はプレビュー機能に対して設定され将来的に消される可能性もあるため、注意して使用してください。
概要としては今まで数秒後に期待する状態になっているかといった非同期で検証されるテストを通すために time.Sleep
で妥当な待機する等、問題として認識しながらも修正できていなかった flaky になりかけのテストを目にした機会があったのではないでしょうか。このようなテストに対して偽のクロックを用意して time
を扱えるようにするような API となっています。
詳しい実装についてはこちらにまとめられています。
まとめ
本記事では2024年初のリリースである Go1.22 から 来年リリース予定の Go1.24 までの機能を見ていきました。リリースノートが随時更新されはじめていたり、 Go1.24 のRC版がリリースされたりとわくわくが高まっていますね。
今年は Russ Cox 氏がテックリードをやめたりと Go Team としても大きな決断が多い年だと感じました。今後も動向に注力して機会があれば私も新しい API の提案をしたいなと思いました。