フューチャー技術ブログ

Go 1.23リリース連載始まります&timeパッケージ

Future Tech Blog恒例のGoリリース連載が始まります。本エントリーはインデックス記事&timeパッケージを散り上げます。

Date Title Author
7/16 インデックス & time 澁川
7/17 archive/tar 真野隼記
7/18 range over funcとiterパッケージ 棚井龍之介
7/19 unique, slices, maps 武田大輝
7/22 text/template 辻大志郎
7/23 os.CopyFS & path/filepath 市川燿
7/24 keep-alive,Cookie 大江聖太郎
7/25 Go Telemetry 棚井龍之介

1.23の更新内容の概要

Go 1.23のアップデートとしては以下のようなものがあります。多いので、Win/mac/LinuxのAMD/ARM関連以外は省略しています。リリースノートはこちらです。RC1の時点で書いているため、まだギリギリ変更があるかもしれません。

  • 言語
  • ツール
    • Goチームにツール群が壊れていないかの統計情報をフィードバックするためのgo telemetryという機能が入りました。デフォルトでは送信しない、オプトインとなっています
    • go env -changed を実行するとデフォルトから変更されたものだけを表示するようになりました
    • go mod tidy -diffで、go mod tidyをドライランして実行時の影響を事前表示できるようになりました
    • go.modgo.workで、互換性がない変更を元に戻せるgodebugディレクティブが追加。もともとGODEBUG環境変数で設定していたものをプロジェクトに記述しておけるように
    • go vetが、go.modのバージョン情報を見て、まだ導入されていないはずのシンボルがあった時に警告を出せるように・・・(だが試しても動かず)
    • CGO_LDFLAGSが長すぎるとエラーが出る問題が解消
    • クラッシュ時に生成される不完全なトレースデータもトレースできるように
    • コンパイラが最適化されてプロファイルを使う最適化が2倍(100%)時間がかかっていたのが1桁%になり、効率もintel系CPUでループの最適化で1〜1.5%の効率改善
    • //go:linkname ディレクティブを使い、//go:linknameでマークされていない標準ライブラリの内部のシンボルが参照できるように
  • ライブラリ
    • time の更新
      • 後述
    • unique パッケージの追加
      • 重複を排除してメモリ効率を上げるのに役立つパッケージ
    • イテレータ関連の更新
      • iterパッケージの追加、slices/mapsに新しい関数の追加
    • C言語の構造体のメモリレイアウトを扱う structs パッケージの追加
    • archive/tar
      • Gname/Uname取得をオーバーライド可能に
    • crypto/tls
      • Client Helloの暗号化ドラフト仕様に対応
      • QUICConnにセッション再開状態を報告するイベントが追加
      • 安全ではない3DES暗号スイートがデフォルトのリストから削除(ただし戻せる手段は提供されている)
      • ポスト量子キー交換メカニズムX25519Kyber768Draft00がデフォルトで有効に
    • crypto/x509
      • CreateCertificateRequest, CreateRevocationListが、Go 1.16以降のCreateCertificateと同じく、署名者の公開鍵を使用して生成された署名を検証するように
      • 安全ではないsha1署名を復活させるGODEBUGオプションのx509sha1=1が1.24で削除される方針に決定
      • ParseOIDがドットエンコードされたASN.1オブジェクト識別子文字列を解析します
    • database/sql
      • driver.Valuerが返したエラーがDB.Query, DB.Exec, DB.QueryRowでラップして返されるようになり、よりきめ細やかなエラーハンドリングが可能に
    • math/rand/v2
      • Uint関数が実装された
      • ChaCha8.Readメソッドが追加された
    • net
      • KeepAlive周りが更新
      • タイムアウトでDNSErrorが発生した場合は、context.DeadlineExceededをラップするため、DNSがタイムアウトしたかどうか判断できるように
    • net/http
      • Cookie周りで諸々更新
      • ファイルを返す系のハンドラでエラー時の挙動が変更
    • net/netip
      • Addrが、==、Addr.Compare(), reflect.DeepEqual()の3つの比較で結果が同じになるようになった
    • os
      • WindowsのSymlinkの扱いが改善(Stat系関数で)
      • os.CopyFSでファイルシステムコピー
      • WindowsでReadlink()の挙動が変化
    • path/filepath
      • Localize()が追加
      • WindowsでEvalSymlinks()の挙動が変化
    • reflect
      • Overflow状態のTypeが追加
      • NewAt()に近いがスライス専用のSliceAt()関数追加
      • 値を反復するシーケンス作成だったり、イテレータが回るかどうかの判定メソッドなどが追加
    • runtime/debug
      • SetCrashOutput()でクラッシュ時にレポートファイルを生成できるようにパスを指定する。
    • runtime/pprof
      • alloc、mutex、block、threadcreate、goroutineのプロファイルの最大のスタックの深さが128フレームと4倍に
    • runtime/trace
      • キャッチされていないパニックでプログラムがクラッシュした場合に、トレースデータが破損しないようにフラッシュするように
    • slices
      • Repeat()が追加
    • sync
      • 組み込み関数のclear()と同じようにマップを空にする、sync.Map.Clear()メソッドが追加
    • sync/atomic
      • 入力された値のビット単位のAND/ORを適用し、古い値を返すAnd/Or演算子が追加
    • syscall
      • WindowsでWSAENOPROTOOPTとGetsockoptInt()が追加
    • testing/fstest
      • TestFSでエラー詳細を分析できるように、構造化エラーをアンラップできるように変更
    • text/template
      • else withアクションがサポートされ、一部のユースケースでシンプルに書けるようになった
    • time
      • タイムゾーンオフセットが範囲外の場合、Parse()とParseInLocation()がエラーを返すようになった
    • unicode/utf16
      • RuneLen()関数の動作が変わり、UTF-16として不正な文字があった場合に-1を返すようになった

あと、このリリースノートにはないですが、strings.Compare()が高速になったと書いている人がいました。

なお、僕が個人的に注目しているHTTP/3とQUIC対応ですが、1.22のときにはまだついていたinternalがはずれて、リリース予定の場所のgolang.org/x/net/quicができました。まだコミットはゆっくりです。まだ時間がかかりそうです。

https://github.com/golang/net/tree/master/quic

とはいえ、Goのアプリケーションサーバーが直接HTTP/3を喋れないといけないかというとそういうことはあまりないです。基本的にはロードバランサーやCDN、WAFを置いて、その後ろでアプリケーションを動かす構成がほとんどかと思います。その終端のサービスさえHTTP/3に対応していれば十分にメリットが享受できます。むしろ、それ以外はメリットが少ないというか、ネットワーク品質の高いクラウド内や構内のネットワークではパケロスはほとんど起きないでしょうし、そうなるとHTTP/3のメリットのちょっと荒れたネットワークでもパフォーマンスが落ちにくいというところは得られません。でもPure Goでサポートされるのはロマンがあるので、注目はし続けます。

timeパッケージ

timeパッケージでは以下の2つの修正がありました。

  • Timer、TickerはStop()を呼ばなくてもGCの対象になるようになった
  • Timer、Tickerのチャネルがバッファなしになった

Tickerは大量に作ることはないのであまり問題はないと思いますし、普通にリクエストを受けて処理をするようなコードだったら問題になることはほとんどないでしょうが、GoではTimerが作られたら、それが終了するまでGCされないという問題がありました。

問題になる例として出てくるのはループの中で大量のtime.NewTimer()を使うと、GCするとメモリ消費が高止まりするみたいな事例が出てきます。対処法としては、同時に使わないのであれば1度作ったTimerReset()を呼んで繰り返し使い回すようにする方法も出てきます。ただし、今までは1つバッファを持ったチャネルを内部で作っていたため、Reset()を正しく行うのが難しいという問題がありました。公式ドキュメントにもありますが、タイマーを作ったがその値を消費していない消費者がいた場合にチャネルにゴミが残ってしまい、再利用前にそのチャネルにたまった値をドレインしてからリセットしなければなりません。公式ドキュメントにもサンプルと説明があります。

if !t.Stop() {
<-t.C // ドレイン
}
t.Reset(d)

調べようとしたらすでに詳しく書いてあるブログがありました。Go 1.22と1.23での効率の良い書き方の比較のコードを引用します。

1.22以前
// resetTimer はタイマーを停止し、ドレインを行なってからリセットする
func resetTimer(t *time.Timer, d time.Duration) {
if !t.Stop() {
select {
case <-t.C:
default:
}
}
t.Reset(d)
}

func consumer(ctx context.Context, in <-chan token) {
const timeout = time.Hour
timer := time.NewTimer(timeout)
for {
resetTimer(timer, timeout)
select {
case <-in:
// do stuff
case <-timer.C:
// log warning
case <-ctx.Done():
return
}
}
}
1.23以降
func consumer(ctx context.Context, in <-chan token) {
const timeout = time.Hour
timer := time.NewTimer(timeout)
for {
timer.Reset(timeout)
select {
case <-in:
// do stuff
case <-timer.C:
// log warning
case <-ctx.Done():
return
}
}
}

チャネルがバッファなしになったことで、このドレイン処理が不要となり、いきなりReset()が呼べるようになります。便利ですね。このブログではReset()を呼ばずにtimer.After()を使うのもクリーンでいいよと書かれています(ただし、タイマーインスタンスは大量に作るのでGCはされるものの、メモリは使う)。

「そんな大量のNewTimer()を呼ぶようなコード書いてないし、TimerReset()なんか呼ぶような場面ないよ」という方もいると思います。僕もそうでした。もしやと思ってcontextパッケージのコードを読むと、context.WithDeadlineCause()context.WithTimeout()とかもみんな最終的にこれを呼ぶ)の中で、time.AfterFunc()を呼んでおり、time.AfterFunc()time.NewTimer()と同じような内部実装になっていました。

ようするに、データベースや外部API呼び出しなど、I/Oが発生するようなコードで毎回丁寧にcontext.WithTimeout()を使ってきちんとタイムアウトでエラーになるような丁寧な暮らしをしているGo開発者は全員ひっかかっていた、ということになります。なお、タイマーが作成される箇所はcontextの内部で、直接触れないため、Reset()による回避もできなかったと。それが今回の修正でGCされるようになったということで、丁寧な人には恩恵があるアップデートのようです。

明日は真野さんのでarchive/tarです。