Go1.21 New Features
Go1.21 が 2023年8月9日(JST) にリリースされ、そのリリースノートやブログが公開されています。この記事では前回の Go1.20 New Features に引き続き、Go1.21 の新機能の中から気になった機能を解説していきます。
spec
Go1.20 までは、メジャーリリースの一番最初のバージョンの末尾にパッチバージョンが付いておらず、次のバージョンからはパッチバージョンが付くようになっていました。
例えば Go1.20 の最初のバージョンは go1.20 で、次のバージョンからは go1.20.1, go1.20.2, ... のようにパッチバージョンがついていました。
Go1.21 からは、一番最初のバージョンが go1.21.0 のようにパッチバージョンの .0 が付くようになります。
import している package を初期化する順序が Spec に明記されました。実行時に、import している全ての package を package パスの昇順でソートし最初のほうから初期化していきます。ある package が別の package を import している場合は、後者の package が先に初期化されます。ただし、一度初期化された package は再度初期化されることはありません。
for ループ内でクロージャを呼ぶ際にループ変数をクロージャ内でそのまま使用すると、ループを抜けた時のループ変数がループ全体で使われている、という Go では有名な挙動があります。この挙動を知らずに意図せずバグを生んでしまうことがしばしばありました。これはループ全体でループ変数を使いまわしているゆえの挙動で、それを回避するために別の変数に一旦代入しなおす必要がありました。
Go1.21 からは実験的にですが、そのような対処をせずとも、ループをするたびにループ変数を初期化するようになり、上述の挙動を知らずとも素直にコードを書けるようになりました。「実験的」とあるように、ビルドする際は GOEXPERIMENT=loopvar をつけて実行する必要があります。まだ検討段階ですが、Go1.22からは正式導入されるかもしれないとのことです。
この変更により影響を受ける既存実装は、Google 内のコードベースやいくつかの Go 製 OSS を試した結果、上記の変数に代入しなおす回避をきちんと行っていないところ以外はほぼないとのことです。影響を受けるループの洗い出し用に bisect と呼ばれるツールや、go vet の loopclosure、go build 時のフラグ -gcflags を使う方法など様々なやり方があるので、詳しくは以下の Wiki をご覧ください。
ビルトイン関数
2つのビルトイン関数 max, min が追加されました。与えた引数の中でそれぞれ最大・最小の値を返却します。引数の数は1個以上で、引数の型は ordered types である必要があります。ビルトイン関数の max, min にはスライスを渡せないですが、後ほど紹介する slices package の関数が代わりに使用できます。
max, min の他にも、もう1つビルトイン関数 clear が追加されました。与えた引数の中身をクリアします。引数の型はマップかスライスである必要があり、マップの場合は中身を空(要素数を0)にし、スライスの場合は要素を全てゼロ値に置き換えます。
型パラメータの型推論の強化
部分的に型引数が適用された関数を関数変数に代入する際に型推論を行うようになりました。
例えば次のコードでは、int と bool を引数に取る関数変数 f に、型パラメータ P, Q を持つ関数 g を代入しようとしています。g の1つ目の型引数は int と明示しているので推論は行われずに P が int であることは分かるのですが、2つ目の型引数は明示していなくても直感的に Q が bool と型推論して欲しいと思います。ただ、Go1.20 まではビルドする際に Q を明示する旨のエラーが出てビルドに通りません。一方で、Go1.21 からは型引数の最初のほうだけを指定していても型推論が成功しビルドが通るようになります。
func g[P, Q any](p P, q Q) {}
var f func(int, bool) = g[int]
上記は型引数を中途半端にしていしていたから Go1.20 では動作しなかったと思うかもしれません。それでは型引数を指定しない場合はどうでしょうか。この場合こそ直感的に P は int、Q は bool と型推論してくれるのではないかと期待してしまいます。しかし、直感に反してこれもビルドは通りません。一方で、Go1.21 からはきちんとビルドが通るようになりました。
var f func(int, bool) = g
func less[P ordered](x, y P) bool {
return x < y
}
func sort[P any](list []P, less func(x, y P) bool) {}
func main() {
a := []int{1, 3, 2}
sort(a, less)
}
インターフェース I1 の型引数に int を取った変数 V1 を関数 g に代入する際に、V1 が g の引数であるインターフェース I1[T] を満たしてかつ、g の型パラメータ T が int であることが型推論できるため、ビルドが通っていました。同様に変数化した V2 も I1[T] を満たすため g に代入できると期待するのですが、Go1.20 ではメソッドのセットが異なると T の型推論ができずにビルドが通りませんでした。Go1.21 からは期待通りビルドが通るようになってます。
type I1[T any] interface {
m1(T)
}
type I2[T any] interface {
I1[T]
m2(T)
}
var V1 I1[int]
var V2 I2[int]
func g[T any](I1[T]) {}
func main() {
g(V1)
g(V2)
}
構造体 S を関数 f に渡すと、S のメソッド M の戻り値の型が byte なので、インターフェース I の型パラメータ T は I のメソッド M の戻り値の型が byte であることから同じく byte と分かります。これにより、f の型パラメータ T も byte となるはずです。しかし、Go1.20 では T の型推論ができずにビルドが通りませんでした。Go1.21 からはビルドが通るようになっています。
type S struct{}
func (S) M() byte {
return 0
}
type I[T any] interface {
M() T
}
func f[T any](x I[T]) {}
func main() {
f(S{})
}
型パラメータ T と、メソッドの引数か戻り値の型が T である型パラメータ I の型推論を行う際に、T の型推論を直接行わずとも I の型推論から T の型推論を行うことができるようになりました。
func f[I interface{ m() T }, T any](T) {}
func main() {
var x interface { m() int }
f(x)
}
型推論時の同一性チェックをより厳密に行えるようになり、型推論に失敗した時のエラー文がより分かりやすくなりました。
type R[T any] interface {
m(R[T])
}
func f[T any](R[T]) {}
type R1 struct{}
func (R1) m(R[int]) {}
func main() {
f(R1{})
}
type T[P any] interface {
m()
}
func g[P any](T[P]) {}
func main() {
var x T[int]
g(x) // here we infer P == int, but in fact any type of P would be ok
g[string](x) // here we set P == string
}
type F[T any] func(func(F[T]))
func f(F[int]) {}
func g[T any](F[T]) {}
func main() {
g(f)
}
異なるデフォルト型を持つ複数の型指定していない定数を同じ型パラメータを持つ引数に渡したときに、型指定していない定数同士の演算と同じように型推論するようになりました。例示した以外に型推論した結果がどのような型になるかは、Spec の定数同士の演算をご覧ください。
func Add[T addable](x, y T) T {
return x + y
}
var y = Add(1, 2.5)
func g[P any](...P) P { var x P; return x }
func main() {
g(1, 2) // int
g(1, 'a') // rune
g(1, 'a', 2.3) // float64
g('a', 2.3) // float64
g(2.3, 'a', 1i) // complex128
}
Spec に型パラメータの型推論の方法がより詳しく記述されました。
go command
Go1.20 で導入された Profile-guide optimization(PGO) を有効にするかどうかを制御するビルドフラグ -pgo がデフォルトで有効(-pgo=auto)になりました。これにより、main package のディレクトリにプロファイル結果のファイル default.pgo が存在する場合は、PGO が有効となり、そのファイルを利用してビルドされます。
go コマンドを実行するディレクトリを指定できるフラグ -C を用いる際は、各種フラグよりも先に記述する必要があります。
go test の新たなフラグとして、-fullpath が追加されました。これにより、テスト実行時にログを出力際に、ファイル名だけでなく、フルパスを出力できるようになりました。
テストバイナリを出力する go test のフラグ -c に複数パッケージを指定できるようになりました。テストバイナリはパッケージそれぞれで別のファイルとして出力されます。
テストバイナリの出力先を指定できる go test のフラグ -o にディレクトリを指定できるようになりました。
ディレクティブ go:wasmimport が新規で導入され、WebAssembly Runtime から関数をインポートできるようになりました。
wasip1(WASI Preview 1)向けに実験的にビルドできるようになりました。
runtime
非常に深いスタックを出力する際に、Go1.21 以前ではスタックの一番上から100行を出力していましたが、Go1.21 からはスタックの1番上から50行と1番下から50行が出力されるようになりました。これにより、出力されたスタックの関数の呼び出し元が特定しやすくなりました。
ガベージコレクションをチューニングした結果、アプリケーションのテールレイテンシを最大40%削減することができました。テールレイテンシについては以下の記事が詳しいです。
panic を recover で受ける時、recover の戻り値が nil ではないことが、Go1.21から保証されました。その代わりに、panic の引数として素の nil や interface に格納された nil を与えると panic することは変わりませんが、型 *runtime.PanicNilError の panic を起こすようになりました。
Go1.21 以前の動作のままにするには、環境変数 GODEBUG=panicnil=1 を指定していしてプログラムを実行する必要があります。また、main package の go.mod で指定している go directive が go1.21 以前であれば自動的にこの環境変数が設定されます。
compiler
Go1.20 で導入された Profile-guide optimization(PGO) が一般利用可能(GA)となりました。
Go1.20 では PGO によって3~4%の実行速度向上が見込めると Release Notes に書かれていましたが、Go1.21 ではそれが2~7%となっています。
Go1.20 では PGO によって関数やメソッドのインライン化が促進される最適化が行われていましたが、Go1.21 ではそれに加えて devirtualization が可能となります。devirtualization は簡単に言うと interface のメソッド呼び出しを struct のメソッド呼び出しに変換する最適化のことで、これによりメソッドのインライン化を適用できる範囲を広げることができるようになります。
Go1.21 では PGO を有効にして go コマンドをビルドしているため、go build の実行速度が最大6%向上しています。
new packages
log/slog
構造化ログやログレベルを設定可能な logger が導入されました。概要や使い方についてまとめようと思いましたが、それだけで1つの記事になるのと、すでにまとめられている方がいるので、詳しくはそちらの記事を参照下さい。
testing/slogtest
samber/lo で有名な samber さんが slog.Handler を満たす Handler の実装をたくさんされていることからも分かる通り、これから様々な Handler が実装されていくと思います。
testing/slogtest package の関数 TestHandlerは、特に Handler のテストを補助する関数になります。
cmp
cmp package には、2つの引数を比較してその結果を返す2つの関数 Compare と Less が追加されました。Compare は比較結果によって -1, 0, 1 を返し、Less は比較結果によってbool値を返します。
また、比較可能なことを表す type constraints として Ordered も追加されています。上記2つの関数は type parameters を持つ関数で、引数の型が Ordered になっています。
slices
slices package には非常に多くの関数が追加されたので、抜粋してご紹介します。
func BinarySearch[S ~[]E, E cmp.Ordered](x S, target E) (int, bool)
スライス x の中に要素 target があるかどうか二分探索します。第一戻り値に見つかった位置、第二戻り値に見つかったどうかを bool 値で返します。x は昇順にソートされている必要があります。sort package にも二分探索を行う sort.Search がありますが、こちらの関数のほうが直感的に使用できるかなと思います。
func Clip[S ~[]E, E any](s S) S
スライス s の cap を len と等しくなるように縮退したスライスを返却します。
func Clone[S ~[]E, E any](s S) S
スライス s をシャローコピーして返却します。
func Compact[S ~[]E, E comparable](s S) S
スライス s の中で連続する同じ要素を一つにして返却します。Compact 内部では新しくスライスを確保するのではなく、引数に渡したスライスを再利用するため、Compact 実行後は元のスライスが書き換わっていることに注意が必要です。
seq := []int{0, 1, 1, 2, 3, 5, 8}
fmt.Println(seq)
// [0 1 1 2 3 5 8]
fmt.Println(slices.Compact(seq))
// [0 1 2 3 5 8]
fmt.Println(seq)
// [0 1 2 3 5 8 8]
func Compare[S ~[]E, E cmp.Ordered](s1, s2 S) int
スライス s1 と s2 のそれぞれの要素の大小比較や長さを比較し、その結果に応じて -1, 0, 1 のいずれかの値を返却します。具体的な比較方法は GoDoc か内部実装をご覧ください。
func Contains[S ~[]E, E comparable](s S, v E) bool
スライス s の中に要素 v があれば true を返却します。
func Delete[S ~[]E, E any](s S, i, j int) S
スライス s のうち i 番目から j 番目までの要素を削除したスライス s[i:j] を返却します。関数 Compact と同じく引数に渡した元のスライスを書き換えるので注意が必要です。
letters := []string{"a", "b", "c", "d", "e"}
fmt.Println(letters)
// [a b c d e]
fmt.Println(slices.Delete(letters, 1, 4))
// [a e]
fmt.Println(letters)
// [a e c d e]
func Equal[S ~[]E, E comparable](s1, s2 S) bool
スライス s1 と s2 の各要素を比較し、等しければ true を返却します。
func Grow[S ~[]E, E any](s S, n int) S
スライス s の cap を n だけ増やします。n が負の値の場合、panic を引き起こします。
func Index[S ~[]E, E comparable](s S, v E) int
スライス s の中で要素 v が存在する位置を返却します。v が見つからなかった場合は、-1 を返却します。
func Insert[S ~[]E, E any](s S, i int, v ...E) S
スライス s に対して i 番目から要素 v を追加したスライスを返却します。関数 Compact とは違って元のスライスは書き換えず、内部でシャローコピーしています。
func IsSorted[S ~[]E, E cmp.Ordered](x S) bool
スライス x が昇順にソートされていれば true を返却します。
func Max[S ~[]E, E cmp.Ordered](x S) E
func Min[S ~[]E, E cmp.Ordered](x S) E
スライス x の中でそれぞれ最大・最小の要素を返却します。
func Replace[S ~[]E, E any](s S, i, j int, v ...E) S
スライス s の i 番目から j 番目までを要素 v で置き換えたスライスを返却します。元のスライスは書き換えません。
func Reverse[S ~[]E, E any](s S)
スライス s の要素の位置を反転させます。反転したスライスを返却するのではなく、元のスライス自体を反転させます。sort package を使って反転することもできますが、だいぶ楽に書けるようになりました。
sort.Sort(sort.Reverse(sort.IntSlice(s)))
slices.Reverse(s)
func Sort[S ~[]E, E cmp.Ordered](x S)
スライス s をソートします。関数 Reverse と同様に、元のスライス自体をソートします。sort package にもソートする関数はありますが、sort.Interface を実装しなくて済んだり、type parameters のおかげで要素の型の制限が緩和されました。
maps
maps package にも様々な便利関数が追加されています。
func Clone[M ~map[K]V, K comparable, V any](m M) M
マップ m をシャローコピーしたマップを返却します。
func Copy(dst M1, src M2)
マップ src のキー・バリューのペアを dst に追加します。
func DeleteFunc[M ~map[K]V, K comparable, V any](m M, del func(K, V) bool)
マップ m の中から無名関数 del が true になるようなキー・バリューのペアを削除します。
func Equal[M1, M2 ~map[K]V, K, V comparable](m1 M1, m2 M2) bool
マップ m1 と m2 のキー・バリューのペアそれぞれを比較し、等しければ true を返却します。
minor change to packages
hash/maphash
ビルドタグ purego を指定することで、Go ベースの maphash を使用できるようになります。
math/big
構造体 math/big.Int に新しくメソッド Float64 が追加されました。math/big.Int に最も近い float64 の値と、その値が math/big.Int と同じか・より小さいか・より大きいかを表す値を返却します。
net
Linux 限定ですが、カーネルがマルチパス TCP をサポートしている場合、net package でマルチパス TCP を利用できるようになりました。Go1.21 では、実行環境が限定されいて、かつクライアント・サーバー初期化時にマルチパス TCP を使用するように設定をしないといけないですが、将来的に対応する OS の種類は増え、マルチパス TCP が使用可能な環境ではデフォルトで使用するようになる予定です。
net/http
構造体 ResponseController に新しく追加されたメソッド EnableFullDuplex を用いることで、HTTP/1 においてリクエストの読み込みとレスポンスの書き込みを同時に行えるようになります。なお、HTTP/2 はデフォルトで同時に読み書きできます。
regexp
構造体 Regexp はメソッド MarshalText, UnmarshalText をサポートするようになりました。これにより、例えば JSON のフィールドとして正規表現を透過的に扱えるようになります。
runtime/metrics
ライブヒープサイズなど GC 内部にあったいくつかのメトリクスを利用できるようになりました。これにより、GC の動きをより追いやすくなります。
sync
sync package にある構造体 Once を応用した関数 OnceFunc, OnceValue, OnceValues が追加されました。Once 自体は1度だけ実行されることを期待する処理があるときに併用する構造体です。ただ汎用的な使われ方に関しては、毎回似たような書き方をする必要があったため、今回新たに関数が用意されました。
OnceFunc は引数に渡して返却された関数の中身が1度だけ実行されることを期待し、返却された関数を1度呼んだ後は何度呼んでも中身を処理することなく、そのまま return します。
OnceValue も OnceFunc と同様に、引数に渡して返却された関数の中身が1度だけ実行されることを期待します。OnceFunc との違いは、戻り値があるところで、何度呼んでも初回実行時の戻り値が返却されるようになっています。
OnceValues は OnceValue と似ていますが、1度だけ中身を実行して欲しい関数の戻り値が2つある場合、例えば第1戻り値が返却して欲しい値で、第2戻り値が errorの場合は、こちらの OnceValues を使います。
testing
新しい関数 Testing によって、実行しているバイナリが go test によって生成されたか、されてないか(go build によって生成されたか)を判定できるようになりました。
Discussion
s/-pgo=auo/-pgo=auto/でしょうか。ですね!修正しておきました🙏