Go 1.22 リリース
(go.dev)forループ変数が反復ごとに新しく生成されるように変更され、意図しない共有バグを避ける挙動になり、整数に対するrangeも可能になった- 互換性は Go 1 の互換性の約束を維持しており、ほぼすべての Go プログラムがこれまでどおりコンパイル・実行できると見込まれる
- ワークスペースで
go work vendorによって生成した vendor ディレクトリを使用でき、ワークスペースにvendorがある場合は-mod=vendorがデフォルトになる GO111MODULE=offのレガシーGOPATHモードで、モジュール外のgo getのサポートを終了。go buildやgo testなどの他のビルドコマンドは、レガシーGOPATHプログラムでも引き続き動作する- ランタイムがヒープオブジェクトの近くに型ベースの GC メタデータを置くように変わり、Go プログラムの CPU 性能が 1〜3% 改善し、多くのプログラムでメモリオーバーヘッドが約 1% 減少する
- 一部のオブジェクトのアドレス整列が従来の 16 バイト以上から 8 バイト整列に変わる可能性があり、より高い整列を前提としたアセンブリコードは修正が必要。暫定的な回避策として
GOEXPERIMENT=noallocheadersを利用できる - PGO ビルドでより多くの呼び出しを devirtualize できるようになり、代表的な Go プログラム群では PGO を有効にすると実行時性能が 2〜14% 改善する
- 標準ライブラリに初の
v2パッケージであるmath/rand/v2を追加。Readは継承されず、グローバル生成器は常にランダムにシードされ、ChaCha8とPCGソース、Nジェネリック関数を提供する net/http.ServeMuxのルーティングパターンがメソッドとワイルドカードをサポートし、{}を含むパターンとエスケープされたパスの処理で小さな下位互換性の破壊があるが、GODEBUG=httpmuxgo121=1で以前の挙動を復元できるcrypto/tlsサーバーのデフォルト最小バージョンは、未指定時に TLS 1.2 へ変更され、pre-TLS 1.3 ハンドシェイクでは ECDHE なしの cipher suite はデフォルト提供されない- 実行トレーサーが全面的に刷新され、ほとんどのプラットフォームで OS クロックを使用し、ストリーミング処理が可能な分割 trace、syscall 全体の duration、goroutine が実行された OS thread の情報を提供する
traceツールの Web UI は新しい tracer サポート作業の一環として更新され、Go 1.22 以降でビルドされたプログラムの trace で thread-oriented view と syscall 全体の duration 表示をサポートするvetは Go 1.22 のループ変数の意味に合わせて loop closure 警告を調整し、値のないappend、defer内の遅延されないtime.Since、log/slogの key/value 不一致に対する警告を追加- ブートストラップには Go 1.20 の最終 point release 以上が必要で、Go 1.24 では Go 1.22 の最終 point release 以上が必要になる見込み
darwin/amd64では Go toolchain がデフォルトで PIE を生成し、Go 1.22 は macOS 10.15 Catalina で動作する最後のリリースとなり、Go 1.23 は macOS 11 Big Sur 以降を必要とするgo/typesにAlias型とUnalias関数が追加され、gotypesalias=1ではクライアントがAlias型を処理する必要があるslices.Delete、DeleteFunc、Compact、CompactFunc、Replaceは縮小された区間の要素をゼロ化し、Insertはiが範囲外なら常に panic を発生させる- Windows のディレクトリ読み取り関数は項目をバッチで読み込むようになり、システムコール数を減らして性能が最大 30% 改善する
1件のコメント
Hacker News の意見
この3年ほどは主に TypeScript を使っていて、最近 Go を使い始めたのですが、最初は配列関数の不足や、あまり柔軟ではない型システムが少し心配でした
ところが、4〜5人規模のチーム向けに TypeScript プロジェクトを改めてセットアップしてみると、リンティング、サーバールーティングライブラリの選定、サーバーの選定、コーディング標準、基本的なエラー処理、ネストした
try/catch地獄を避けるためのカスタムエラーやResult型の強制、テストとモック、Prisma 設定などにあまりにも時間がかかりましたTypeScript は好きですが、優れた標準ライブラリ、きちんとした型安全性、組み込みのコーディング標準がある言語を使うのは本当に楽です。Go にも癖はありますがかなり良く、ルーティングまで標準ライブラリに入ったのは、気にすべきことがまた一つ減ったという意味でありがたいです。あとはスライス用の
map/filter/find関数を待っているところですTypeScript はリリースのたびにパワーユーザー向けにますます複雑になっていく一方で、Go は整数に対する
rangeのように、欠けていたものを追加して使いやすくシンプルにしていますゲームの続編が設定やメカニクスを増やし続け、やがて新規ユーザーのためにリセットが必要になるのに似ていますが、プログラミング言語ではそれが難しいので、Go がそこを意識しているのは良いことです
go:embedとgo:generateでできることは、他のプロジェクトで見るたびに印象的ですgolang.org/xパッケージも、ほぼあらゆるインターネット RFC 関連の実装がすぐ使える形で用意されていて良いですmap/filter/findが来るまでは、lodash に着想を得た lo ライブラリを使えますGo プロジェクトで愛用している万能ツールです: https://github.com/samber/lo
何かをしようとしてドキュメントを読むと、すぐに誰かがドキュメントには載っていない使えるトリックを教えてくれる、ということが繰り返されました
公式リリースノートが少し堅いと感じるなら、インタラクティブ版を作ってあります: https://antonz.org/go-1-22
forループ変数の共有問題は最初すぐには理解できませんでしたが、実際に実行して予想外の結果を見ると助けになりましたCompactとReplaceの例が紛らわしいのですが、これで合っているのか分かりませんGo を9年以上使っていて、最近4年は Flutter のために Dart を多く使ってきましたが、この2つの言語は複雑さに対する姿勢が正反対だと思います。Dart は可能な機能をすべて追加しようとし、Go はその逆です
第一に、Dart ではクラス内で値を初期化する方法が多すぎて、かなり時間を使うことになります。
final/const/static/lateという接頭辞によって、コンストラクタ、ファクトリ、Flutter のStatefulWidgetのinitState()など初期化場所が変わり、リファクタリングすると初期化を大きく手直ししなければなりません。関数が変数のように見える getter も、新しいコードベースでは混乱を増やします第二に、Dart にはマップやストリームなど、1行に詰め込みやすい機能が多いため、理解しにくいワンライナーをすぐ作りたくなります。しかし、デバッグ時やジュニア開発者が
firstWhereOrNullのようなハックやorElse: () => nullのようなオプション引数に出会うと、非常に混乱します。難解な null safety や型エラーで作業が止まることも多く、それを単純な Go 風のforループに置き換えると本当にほっとしますinitStateを使わないのが良いです。たとえばテキストフィールドのヒントを設定する程度でなければ、避けるようにしていますほとんどの Flutter ページはやることがごく少ないか、親が
MyPageControllerオブジェクトを作って望む形で初期化し、子ページがそのコントローラに従って動作するようにします。たとえば一覧ページでユーザーが項目を編集しようとしたときに、コントローラを作って項目を入れて子ページへ渡せば、戻ってきた後に親がコントローラの項目や変数・コールバックを見て UI 変更を決められますこうすると、
InheritedWidgetのようなものに触れなくても、ウィジェット間の相互作用を細かく制御できます。もちろん状態管理ライブラリと一緒に使うのが良いですが、実際には使わないことも多いですrangeも同様で、結局タイプする文字数はforEach(f)のようなコンプリヘンションと似ていても、3行に広がっているので長く見えるだけで、実際に展開して書いてみれば大したことではありませんただし初期化に関しては、Go に構造体フィールドを特定の値で初期化する慣用的な方法があればと思います。コンストラクタがないのは構いませんが、たまに
boolがデフォルトでtrueに初期化されてほしいことがありますinitStateはStatefulWidgetのオーバーライドであり、他のフレームワークのライフサイクルイベントと変わらず、Flutter 側の概念なのでコンストラクタと混同する性質のものではありません通常のコンストラクタとファクトリのどちらを使うべきかで迷うことはあるかもしれませんが、
initStateを理由に Go が優れているとは言いにくいですio.CopyはTCPConnからUnixConnへコピーするとき、可能なら Linux のsplice(2)システムコールを使い、新しいメソッドTCPConn.WriteToを使うようになりましたインターフェースのアップグレードによって、また一つ透過的にゼロコピー I/Oが増えたわけです。
ioパッケージでこのパターンから得られる効果は本当に大きいですそれでも期待の範囲内ではあります
改善されたルーティングパターンは、Gorilla Mux の将来が不安定な今、良い改善だと思う。
ただしこの変更は、小さな形で後方互換性を壊している。
{と}を含むパターンの挙動が変わるという明確な部分もあれば、エスケープされたパスの扱いのように分かりにくい部分もある。GODEBUGのhttpmuxgo121フィールドで制御でき、httpmuxgo121=1を設定すれば以前の挙動に戻せる。とはいえ、「Go 1 仕様で書かれたプログラムは、その仕様の存続期間中、変更なしにコンパイルされ続け、正しく実行される」という Go 1 互換性の約束と衝突しない理由が気になる。
go.modのgoディレクティブの値にこのような 後方非互換な変更を紐づけること。1.22 以上なら新しいライブラリの挙動を受け取り、そうでなければ古い挙動を受け取る。実際にどれほどうまく機能するかは見守る必要がある。
{と}を使うケースはほとんどないと判断したのだと思う。新しい挙動を無効にする方法も提供しているのでコード変更を強制しているわけではないが、新しい環境変数を設定する必要はある。
今回のリリースにおける
forループの意味変更も似た例で、実質的には破壊的変更だ。それでもすべての Go プログラムは引き続きコンパイルされ実行され、一部の挙動が少し変わるだけだ。Go は実用的なアプローチを選んでおり、それが成功理由の一つだったと思う。go.modを使ってバージョンごとに壊すのであれば、少なくとももっと良いやり方で壊すべきだ。go.modを 1.22 に変えたときに微妙なランタイム問題を見つけるより、むしろコードが コンパイル失敗してくれたほうがずっとよい。Go 1.22 では、ループの各反復が新しい変数を作るようになる。
この内容は以前にもここで議論されていた: https://news.ycombinator.com/item?id=33160236 - Go: Redefining For Loop Variable Semantics (2022)
古臭い考えかもしれないが、関数に対する
rangeの追加は好きではない。言語に追加される複雑さを正当化するほど便利だとは思えないし、関数型スタイルは、Go の明示的で命令型、冗長ではあるが機能を絞ったスタイルとずれているように見える。それが Go の大きな長所の一つだったと思う。同じ理由で、整数に対する
rangeも間違った方向だと思う。Go の薄い機能セットと低い認知負荷は、常に主要な強みであり差別化要因だった。ソフトウェア開発における大きな問題の一つは、開発者が複雑さを追加したり腕前を見せつけたりするのを防ぐことだ。特にキャリア初期の開発者は、問題を単刀直入だがあまり優雅でなく冗長な方法で解いて先に進むより、解決策を過度に複雑にしがちだ。
現在の Go のジェネリクス対応は、これを支えられるほど洗練されておらず、不自然に強制された制限が生じている。
sql.Null[T]の追加は良い。新しいプロジェクトではたぶん使い始めると思う。現在の作業物では、API がかなり似ている sqlboiler の null [0] に依存している。
sql.Nullのように動作するが、値が明示的に設定されたことがあるかを知らせるIsSet() boolメソッドが追加されていて、「意図的な null」と「初期化されていないための null」を区別するのに役立つ。良さそうに見えるが、実際には一度も使ったことがない。標準ライブラリの mux を改めて見直して、chi の削除も可能か期待している。こうした機能が標準ライブラリに入るのは本当に良いことだ。
こうした機能が標準ライブラリに入れば、常に保守され、多くの Go プログラムで同じアプローチが使われることが保証される。
Go を本番環境で使っている場合、最新バージョンへすぐ移行できるのか、それとも古いリリースに縛られるのか気になる。
公開されているプロジェクトを見ると、比較的新しい機能を避けていることが多いように見える。ジェネリクスを使わないとしても、以前の
interface{}の代わりに Go 1.18 で登場したanyを使いたいし、後者のほうがより「慣用的」だという話も聞いたことがある。今回は少しややこしく、私たちがデフォルトを 1.21 に上げる前に Go チームが 1.22 を先に出した。今では待つことはほとんど慣例に近く、Go のリリースが何かを壊すことはまれだ。
たとえばジェネリクスは、実際に使うまで 6か月ほど待ち、有用性、開発者体験、ビルド・テスト速度への影響が大きくないかを確認した。
ただし Go でエンドユーザー向けアプリも提供しており、こちらは古いクライアント OS との互換性のため 1.20 に留まる必要がある。1.21 でかなり大きな足切りがあったため、数年は 1.20 に留まる可能性が高い。
latestコンテナでビルドとテストを行い、テストスイートがかなり堅牢なので、実質的に自動更新されている。何かが合わなければパイプラインが壊れ、その時点で最後に動作したバージョンに固定するか、破壊的変更を直すまで更新を遅らせるかを決める。数年に一度くらいは、新バージョンとサードパーティパッケージの間で微妙なエッジケースのバグが表面化することもあるが、そうしたバグは定義上テストスイートをすり抜けたものなので、手動アップグレードだったとしても捕まえられなかった可能性が高い。
古い Go コンパイラのサポートが心配なら、旧バージョン向けのビルド条件を置いて、不足しているものを定義できる。たとえば
// +build !go1.7の下にtype any = interface{}を置ける。