ANDPADアドベントカレンダー2025 1日目!
先頭打者はANDPAD Tech Leadの tomtwinkle が務めさせていただきます。
この記事は hatena.go #2 で発表したGo 1.26ネタLTをベースにしたものです。 流石に5分LTだと喋りきれなかった…! developer.hatenastaff.com
前書き
Go は昔から年に2回、2月と 8月 に新バージョンをリリースするという
Go Release Cycle に沿って開発が進められています。
go.dev
rc版が出るのは大体12月なので、アドベントカレンダーの時期にはもしかするとrc版がリリースされているかもしれません。
次のGo バージョンは表題通り Go 1.26 ですね。 リリースは来年の2月予定なのでもう少し先ですが、今回はどんな機能が搭載される予定なのか、先行して見ていきましょう。
最初に断りを入れておくと、 今回紹介する機能は全てがGo 1.26で搭載されるとは限りません。
そして、 私が面白いなと思ったproposalのみで全ての修正を載せるわけではありません。
気になる人は元ネタのサイトを載せておくので自分で調べてみると良いと思います。
今回の情報の元ネタはリリースノートのDraftとGitHubで管理されているGoのマイルストーンです。
Go1.26のリリースノートのDraft tip.golang.org
Go 1.26のマイルストーン github.com
リリースノートに載っていないサイレント修正などがたまにあるため 修正を全て追っていきたいならマイルストーンに載っているissueを眺めていくのが良いと思います。
今回の記事にはproposalのissue番号とリンクを紐づけておくので、気になる情報があった場合は是非オリジナルのissueを辿って見てください。
- 前書き
- Go 1.26 機能の先読み
- Expansion new(expression) #45624
- Green Tea garbage collector #73581
- encoding/json/v2 #71497
- errors.AsType[T](err) #51945
- runtime/pprof,runtime: new goroutine leak profile #74609
- testing ArtifactDir #71287
- go/ast ParseDirective #68021
- go fix modernize #71859
- database/sql/driver#RowsColumnScanner #67546
- log/slog#NewMultiHandler function #65954
- Added context aware dial functions for TCP, UDP, IP and Unix networks #49097
- runtime/secret #21865
- spec: remove cycle restriction for type parameters #75883
- 最近のGoのproposalに関する所感
Go 1.26 機能の先読み
Expansion new(expression) #45624
Go 1.26 で new 構文の拡張が入ります。 Goを利用するユーザーにとって一番大きな修正といえばこれです。
// ポインタ引数を取る関数呼出し (Go < 1.26) timeout := 5 * time.Second client.SetTimeout(&timeout) // この書き方ではコンパイルエラーになる // client.SetTimeout(&(5 * time.Second)) // ポインタ引数を取る関数呼出し (Go 1.26) client.SetTimeout(new(5 * time.Second)) // ------ // 関数のポインタも取れる (Go 1.26) p := new(someFunc) func someFunc() {}
Go の &演算子は「メモリ上にある変数のアドレス」を取得するものです。
例えば、5 * time.Second (int64) という値そのものには、変数のような固定のメモリアドレスが割り当てられていないため
&演算子を書いても指し示すアドレスが存在せずにコンパイルエラーになります。
そのため、Go 1.25までは一旦変数に詰めたり、Genericsを利用して
func ToPointer[T any](v T) *T { return &v }
のようなhelper関数をわざわざ作って変換していたと思います。
Go 1.26 からは例に挙げたようにnew構文で代用可能になります。
この機能の設計過程では、newの拡張と、&演算子で定数のポインターを許可するという主に2つの選択肢が検討されました。
var v *int // newの拡張 v = new(3) // 定数のポインターを許可 v = &3
最終的にnew構文が選択された理由は、既存のnew(T)との概念的な一貫性にあると考えられます。
newという組み込み関数は、当初から「メモリ割り当て」という責務を担ってきました。 これを自然に拡張し、式の型の特定の値のためのメモリを割り当てるものと解釈できます。
&演算子は厳密に「アドレス可能な値のアドレスを取得する」という単一の役割を維持し、&演算子のオーバーロードを避けることができます。
newの拡張によりpointerの定義を今までよりは気軽に行えるようになるかも? (とはいえnil checkの負荷が増えるからあまり変わらないか)
Green Tea garbage collector #73581
Michael氏が横浜の茶屋で抹茶しばいている時に閃いた から名付けられたらしい 「Green tea GC」
GCの命名がハイコンテクストすぎる・・・!!
Go 1.25 で GOEXPERIMENTに入っているのでもう試すことは出来ます。
利用したい場合は GOEXPERIMENT=greenteagc で利用出来ます。
そして、Go 1.26 ではこのGreen tea GCが標準で有効になる予定です。
Green tea GCが生まれた背景として、コンピュータ内ではCPUとメモリが物理的に遠くCPU内のキャッシュメモリ(L1L2L3)に比べてメモリへのアクセスが遅い(プリント基盤の配線を流れる電気信号の速さは光速の5〜6割程度、それを素材などで幾ら改善しても、光速が実質的なボトルネックになる問題。例えば 5GHz のCPUでは、1クロックは 0.2ナノ秒。この間に電気信号が進める距離は、わずか3cm程度)ので、今までのマークスイープアルゴリズムでのGCではCPUが速くなればなるほどメモリアクセスがボトルネックになって遅くなるという問題がありました。
これをCPU内のキャッシュメモリに上手くGC対象のオブジェクトをページ単位で集めることで、CPUのSIMDアクセラレーションを有効活用しようという理屈です。
Green teaのアルゴリズムは全てのパターンでマークスイープアルゴリズムより効率的なわけではありません。 Green tea GCの賢い所は全てがGreen teaのアルゴリズムで動作する訳ではなく、特に効果の大きなファンアウト数が大きいオブジェクトが多い場合はGreen tea のアルゴリズムで処理し、ファンアウト数が小さい場合は既存のマークスイープアルゴリズムを利用します。
この選択によりGreen teaだと非効率なパターンでも全体のパフォーマンスが落ちる事がなくGCが動作します。
詳細は公式ブログをみんな読みましょう。
ここでどんな説明書くよりも公式ブログが詳しくて分かりやすいと思います。
encoding/json/v2 #71497
こちらも Go 1.25 で GOEXPERIMENT に入った encoding/json/v2。
Go 1.25では GOEXPERIMENT=jsonv2 で実行できます。
私が見ているプロダクトの中にも Gin Framework を利用して RESTful API でWebクライアントと会話しているAPIもあり json の Marshall/Unmarshall の処理が改善されるととても嬉しいです。
順調にいけば Go 1.26 に入りそうですが、どうですかねー?
This is the largest major revision of a standard Go package to date
(これはGoの標準パッケージにおける過去最大規模の改訂)
と謳われている通りかなり大規模な変更となり慎重に議論されているため、もしかしたら標準で入るにはもう少し時間がかかる可能性があります。
- JSONのsyntaxのみを扱う低レイヤーパッケージ
encoding/json/jsontext - Semanticsを扱う高レイヤーパッケージ
encoding/json/v2
現在はこの2つが議論されています。
encoding/json/jsontext は Goの reflection に依存せず、純粋にJSONのToken({, }, [, ]など)や 値の読み書きを行うパッケージです。
encoding/json の Scanner や Token より細かくトークンをコントロール出来て、高速でallocation を抑えた実装がされています。
encoding/json/v2 は GoのstructとJSONのマッピング(Marshal/Unmarshal)を行います。
内部では上記の encoding/json/jsontext パッケージが利用されており、v1よりも高速でallocationを抑えた動作が期待出来ます。
encoding/json/v2 ではエラーハンドリング周りの変更がされる予定で、重複するフィールド名がある場合や、構造体のタグに誤りがある場合にデフォルトでエラーを返します。
time.Durationも明示的にタイムフォーマットの指定がない場合はエラーになります。
そして、nil のスライスやマップの扱いや、デコード時の挙動が厳格化・変更されています。
このように v2はv1の動作と大きく異なるため、リプレイス時には注意が必要です。
単純にencoding/json を encoding/json/v2 に変更すれば終わりという話ではないという事ですね。
errors.AsType[T](err) #51945
errorを返す関数を呼び出す場合、呼び出し側でカスタムエラーを判別したい時に今まで errors.As で変換する必要がありました。
func main() { err := SomeFunc() var cerr CustomError if errors.As(err, &cerr) { fmt.Printf("%+#v", cerr) return } }
errors.AsType でGenericsを利用して戻り値の型を指定出来るようになりました。
Go 1.21 で Generics 使えるようになり、標準パッケージも徐々にGenericsに対応した関数を増やしている状況です。
func AsType[E error](err error) (E, bool)
func main() { err := SomeFunc() if cerr, ok := errors.AsType[CustomError](err); ok { fmt.Printf("%+#v", cerr) return } }
Genericsが導入されて引数の破壊的操作を行う関数の利用機会が減っていっているのは個人的に嬉しいですね。
runtime/pprof,runtime: new goroutine leak profile #74609
Goのガベージコレクタの機能を活用してgoroutine leakを検出する新しいpprofプロファイルを追加しようという提案。
内容からお察しの通り(?) uber-go/goleak を作った Uber の中の人からの提案ですね。
uber-go/goleak は私のプロダクトでも利用していますが unit test でしか使えないのがネックなんですよね。
test coverageが十分でない場合はどうしてもgoroutine leakしている処理が漏れてしまいます。
runtime/pprof を利用したgoroutine leakの検知の仕組みは既にUber社内では実績があるらしいので入ったら直ぐにでも利用してみたいですね。
goroutine leak とは以下のような状態です。
func main() { // channelを作成 ch := make(chan int) // goroutineを起動(受信待ちさせる) go func() { val := <-ch // ここでブロック!誰かが送ってくるのを待つ fmt.Println(val) }() // ch に何も送らず、ch という変数も捨ててしまう // ここで ch への参照を持つ "動いているgoroutine" は居なくなる。 // -> 受信待ちしているgoroutineは永久に放置される。 time.Sleep(time.Second) }
この例ではmainが終了すれば実際にはgoroutineは止まるんですが、time.Sleepで擬似的にgoroutine leakを再現しています。 実際にはserverアプリケーションやworkerのような長時間動作するアプリケーションで上記のような例が発生することを想像してください。
一体どんな仕組みなのかproposalを見ていくと以下のように記載されています。
The approach uses memory reachability as computed by the garbage collector to single out goroutines blocked over concurrency primitives that cannot be accessed by any (transitively) runnable goroutines.
ガベージコレクターが計算するメモリ到達可能性を用いて、いかなる実行可能なgoroutineからもtransitively(推移的)に到達できない concurrency primitives(並行処理プリミティブ)を用いた場合にブロックされた goroutine を特定します
到達不能(Unreachable) なオブジェクトを見つけ出すのが ガベージコレクター(GC)の機能の1つです。 今回その機能を利用してgoroutine leakしている処理を見つけ出すということを言っていそうです。
concurrency primitives ?
要するに channel, sync.Mutex, sync.WaitGroup などの goroutine を用いた並行処理を同期的に行うため処理をブロックするツールの事です。
transitively ?
goroutine A(実行中) -> goroutine B(ブロック中) -> goroutine C(ブロック中) goroutine A (終了) -> goroutine B (Aからの連絡待ち) -> goroutine C (Bからの連絡待ち)
Aが動いていてBでlockしている変数のポインターを参照している、同じようにBがCでlockしている変数のポインターを参照していれば、Cも 「推移的に到達可能」 です。
しかし、AがB, Cに連絡をしないで終了してしまったら、BもCもgoroutine leakした状態になります。
「推移的に到達できない」 と言うのはこの状態の事を指します。
要するに芋づる式(ドミノ倒し的)にgoroutine leakを追跡して、Cもリークしていると判定します。
この修正はGo 1.26以降でいきなり適用されるわけではなく
GOEXPERIMENTに GOEXPERIMENT=goleakprofile で入る予定で議論が進められています。
testing ArtifactDir #71287
t.ArtifactDir() を利用してファイルをWriteするテストを書くことで
go test の際に artifacts option で t.ArtifactDir() に指定したディレクトリを渡せる機能です。
func TestFoo(t *testing.T) { fmt.Println(t.ArtifactDir()) // /tmp/test }
go test -artifacts "/tmp/test"
これはGolden Testを行う場合に非常な有用な機能ですね。
私のプロダクトではよくExcel出力を行うため Excelの出力結果が正しいかプログラム上でもAssertしていますが罫線などは目視でも確認する必要があります。
そのためCIでのtest結果を目視チェック出来るようにGitHub Actions, CircleCIのartifactにpushしています。
この機能はCIのartifact機能と非常に相性が良いです。
go/ast ParseDirective #68021
Go言語の標準ライブラリである go/ast package に directive comments を解析するための機能を追加する提案です。
directive comments とはGoコメント内で //toolname:directive arguments 形式で定義されたdirectivesの事です。
公式では //go:generate //go:embed といった形式で定義されているアレです。
具体的には以下の正規表現でマッチする文字列になります。
//([a-z0-9]+):([a-z0-9]\PZ*)($|\pZ+)(.*)
サードパーティー製のLinterツールを自分で作る人は今まで //hoge:fuga みたいな自分のツールのためのdirective commentsのastを解析して *ast.CommentGroup のText()関数でstringを取得してきて自前でParseするしかなかったのですが、これを自分で書かなくても良くなったのはかなりありがたいです。
中にはコンパイラディレクティブの //line のような例外もありますが、サードパーティー製のツールなどのオプションをコメントから読み取りたい場合、上記のdirectiveがデファクトスタンダートとなっています。
サードパーティー製のdirective commentsの例としてよく見かけるのは
golangci-lint の nolint directive //nolint:xxx ですね。
もちろん、サードパーティー製の中にもdirective commentsのルールが決まっていない時期に作られたものでは
Kubernetesの Kubebuilder の directive comments // +k8s: // +kubebuilder: のような例外もあります。
func ParseDirective(pos token.Pos, c string) (Directive, bool)
// Go 1.26 https://cs.opensource.google/go/go/+/master:src/go/ast/directive.go;bpv=1;bpt=0 // //namespace:directive args 形式のディレクティブコメント type Directive struct { Tool string // "go" や "mystuff" などのツール名 Name string // "generate"や"embed" などのディレクティブ名 Args string // "stringer -type Op" などの引数文字列 Pos token.Pos // ディレクティブの "//" の開始位置 ArgsPos token.Pos // 引数(Args)の開始位置 } // text は "//" を含む単一の行コメントでなければならない。 // pos はそのコメントの開始位置(token.Pos)。 // ディレクティブとして解析できた場合、Directive構造体と true を返す。 func ParseDirective(pos token.Pos, text string) (Directive, bool) // ParseArgs によって解析されたディレクティブの引数を表す。 type DirectiveArg struct { Arg string // 解釈された引数(クォートは自動的に削除) Pos token.Pos // 引数の開始位置 } // スペース区切り、" " または ` ` で囲まれた文字列に従って解析 func (d Directive) ParseArgs() ([]DirectiveArg, error)
go fix modernize #71859
gofix (go tool fix, go fix) はドラスティックに変更が入っていた Go 1 のリリースまでの古い構文を一括変換するツールとしてリリースされました。
詳しくはRuss Cox氏が書いたGo Blogを参照してください。
https://go.dev/blog/introducing-gofix
後方互換性を維持している Go 1 以降は実質的に誰も利用する事のない死に機能でした。
しかし、Go 1.18のGenericsの追加 や any型、Go1.21のslices packageの登場、Go1.22のforvarの変更 など 最近のGoではモダンな構文の追加が増え、コード内に古い構文とモダンな構文が同居する状態になっています。
それらのコードを手動で書き直すのは面倒ですよね。
gopls ではそれらを一括で変換するための modernize tool や gofix Analyzer が提供されています。
modernize tool
Before
package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup // rangeint: replace a 3-clause "for i := 0; i < n; i++" loop by "for i := range n", added in go1.22. for i := 0; i < 5; i++ { // forvar: remove x := x variable declarations made unnecessary by the new semantics of loops in go1.22. i := i // waitgroup: replace old complex usages of sync.WaitGroup by less complex WaitGroup.Go method in go1.25. wg.Add(1) go func() { fmt.Println(i) wg.Done() }() } wg.Wait() }
go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -fix -test ./…
After
package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup for i := range 5 { wg.Go(func() { fmt.Println(i) }) } wg.Wait() }
gofix inline tool
#32816 のproposalで実装された //go:fix directive を用いたコードのAuto Migrationの仕組みです。
Before
package main import ( "fmt" "play.ground/pkg" ) func main() { // Square関数はDeprecatedになったのでPow関数に書き換えたい fmt.Println(pkg.Square(2)) } -- go.mod -- module play.ground -- pkg/pkg.go -- package pkg // Deprecated: prefer Pow(x, 2). // //go:fix inline func Square(x int) int { return Pow(x, 2) } func Pow(x, y int) int { return x * y }
go run golang.org/x/tools/gopls/internal/analysis/gofix/cmd/gofix@latest -test ./...
After
package main import ( "fmt" "play.ground/pkg" ) func main() { fmt.Println(pkg.Pow(2, 2)) } -- go.mod -- module play.ground -- pkg/pkg.go -- package pkg // Deprecated: prefer Pow(x, 2). // //go:fix inline func Square(x int) int { return Pow(x, 2) } func Pow(x, y int) int { return x * y }
このproposalは go fix を作り直してそれらの機能(modernize, go fix inline)を組み込もう と言う話です。
古いcmd/fixツールを廃止 し、go fix を go vet コマンドのラッパーとして機能出来るよう go/analysis を利用するように再構築する修正が行われます。
多分、こんな感じで実行すれば modernize と gofix inline の機能が動くようになるはず。
go fix ./...
database/sql/driver#RowsColumnScanner #67546
type RowsColumnScanner interface { Rows // ScanColumn copies the column in the current row into the value pointed at by // dest. It returns [ErrSkip] to fall back to the normal [database/sql] scanning path. ScanColumn(dest any, index int) error }
pgx などのDatabaseのdriverにはよしなに型変換してくれる機能が搭載されていますが
これまで、database/sql ではDatabase driverに直接バイパスする手段が存在せずに
標準の database/sql がサポートしていない型(例えば、独自の構造体や、データベース固有の配列型など)にデータをスキャンするには、Goのdriver側(もしくはユーザー側)でその型に変換するための sql.Scanner インターフェースを実装する必要がありました。
RowsColumnScanner インターフェースの実装により何が便利になったかというと、一言で言えば 「driver側がScanを主導する事でユーザーがわざわざ sql.Scanner を実装しなくても、特殊な型や複雑なデータを直接扱えるようになる」 という点です。
driver が RowsColumnScanner を実装していれば、driver側でGoの型変換を処理してくれます。ユーザーはただ変数のポインタを渡すだけで済みます。
例: データベースのJSONカラムを Goの構造体に直接Scanする場合、以前はラッパー型を作るか []byte で受けてから json.Unmarshal する必要がありましたが、driverが対応すれば直接 rows.Scan(&myStruct) だけで完了できるようになります。
もう1つ良いこととして、パフォーマンスの向上につながる点
database/sql パッケージ標準のScan処理は、reflectionを多用して型変換を行いますが、これは大きなオーバーヘッドがあります。
RowsColumnScanner を使うと、driverは標準パッケージの変換ロジックをバイパスして、最適化された方法で直接値を書き込むことができます。これにより、大量のデータを扱う際や、特定のデータ型においてパフォーマンス向上が期待できます。
更に driver.ErrSkip によるフォールバックの仕組みによりdriver側で全てを処理する必要がないという点も重要です。
ScanColumn メソッドで driver.ErrSkip を返す事で、自動的に従来の database/sql のScan処理に戻ります(フォールバック)。
これにより、driver開発者は「json型のような特殊な型だけ自分たちで高速に処理して、あとは(元々の処理)標準ライブラリに任せる」という実装が可能になります。
log/slog#NewMultiHandler function #65954
log/slog パッケージで、複数のHandlerを同時に利用できるようにする機能(MultiHandler)が追加されます。
func NewMultiHandler(handlers ...Handler) *MultiHandler
例えば、ファイルと標準出力両方にログを出力したいと言う用途だけなら今までも io.MultiWriter を利用すれば解決できましたが
標準出力にはWarn Logのみ、ファイルにはInfo Logも出すのような出力毎のエラーレベルの設定が出来ませんでした。
この手のペインを解決するために samber/slog-multi のようなサードパーティー製ライブラリはありますが 良くあるユースケースだしメンテナンスの観点からも標準パッケージに組み込まれていてよくない?と言う話で標準パッケージに組み込まれる流れになったみたいです。
Added context aware dial functions for TCP, UDP, IP and Unix networks #49097
Dialer.DialContext を使うと、アドレスを文字列で渡す必要があるため再度のDNS解決(Resolve)が発生してしまい
DNSサーバーが不安定な環境で 「(既にパラメーターで渡しているので)IPはわかっているのにDNSタイムアウトで接続失敗する」 という問題が起きていました。
2021年から続く長いissueなので当初は *net.TCPAddr をパラメーターに渡す形で提案されていましたが
最終的な実装では Go 1.18で導入された netip.AddrPort を受け取る形にモダナイズされました。
当初は net package に直接追加する提案でしたが、最終的にはRuss Cox氏の案が採用され net.Dialer の関数として以下が追加されました。
func (d *Dialer) DialIP(ctx context.Context, network string, laddr netip.Addr, raddr netip.Addr) (*IPConn, error) func (d *Dialer) DialTCP(ctx context.Context, network string, laddr netip.AddrPort, raddr netip.AddrPort) (*TCPConn, error) func (d *Dialer) DialUDP(ctx context.Context, network string, laddr netip.AddrPort, raddr netip.AddrPort) (*UDPConn, error) func (d *Dialer) DialUnix(ctx context.Context, network string, laddr *UnixAddr, raddr *UnixAddr) (*UnixConn, error)
これにより意図していないエラーの発生や余計なDNS解決が行われずにパフォーマンスの向上にも繋がります。
runtime/secret #21865
Goのセキュリティ上の課題だった 『メモリ内(スタック/ヒープ)の機密情報の完全消去』 が、新しい公式パッケージによってサポートされます。
Go 1.26 でGOEXPERIMENTに入る予定 GOEXPERIMENT=runtimesecret。
secret.Do(func() { // 機密情報を扱うなんらかの処理 })
WireGuardのようなセキュリティ要件が高いアプリケーションでは、使用後の暗号鍵をメモリ上から完全に消去し、漏洩を防ぐ(Forward Secrecyを担保する)必要があります。例えば、C言語などでは explicit_bzero などを使ってメモリをゼロ埋めします。
Goでは サードパーティー製の memguard を利用して無理やり制御しようと試みていましたがそれでも完全ではなく、ランタイムでの解決が望まれていました。
スタック、ヒープに機密情報が残ってしまうパターンの例としては以下の通り
goroutine ではスタックが足りなくなると、Goランタイムが新しい大きなメモリ領域を確保し、データをそこにコピーします。このとき、古いスタック領域に機密情報のコピーが残ったままになってしまうリスクがありました。
機密情報がスタックではなくヒープにある場合、プログラマが変数を nil にしても、GCが実際にそのメモリを回収するまでデータは残ります。また、GCやランタイムの都合でメモリが再配置される際にもコピーが作られる可能性があります。
更に低レイヤーの話ですが、 このissueのコメント欄で会話されている
Signal Stackにより OSの割り込み(Signal)が発生すると、CPUのレジスタにある機密情報などが一時的にスタックに退避される問題があります。
特に最後の Signal Stack の問題が難しく解決に時間がかかっていましたが、解決の目処が立ったため今回実装される流れとなりました。
具体的にどう解決したのか、2025/9/24 の Daniel氏 書き込み を見ると
https://github.com/golang/go/issues/21865#issuecomment-3327040593
I've done a bit of thinking and I might have a solution to the signal stack clearing problem. Paradoxically, it involves copying the secret state into another bit of memory.
On linux amd64 and arm64 (and I suspect many other platforms but I have not tested on them yet), there is no requirement that the sigreturn system call is executed on the signal stack registered with the kernel. During secret mode, we can allocate a shadow signal stack and use that to return. Once we get a signal, we can copy the state out of the registered stack, erase it, switch to our shadow stack for the return and execute sigreturn, leaving a pristine signal stack. When exiting secret mode, we can erase the shadow stack without getting in the way of any other signal delivery.
少し考えてみたところ、signal stackを削除する問題に対する解決策が見つかったかもしれません。逆説的ですが、secret stateを別のメモリ領域にコピーする手法です。 Linuxのamd64およびarm64では、sigreturnシステムコールがカーネルに登録されたsignal stack上で実行される必要はない。シークレットモード中は、shadow signal stackを割り当てて、それを用いることができる。シグナルを受信したら、登録されたsignal stackから状態をコピーし、それを消去してshadow signal stackに切り替えて戻り値を処理し、sigreturnを実行します。これによりsignal stackは元の状態に戻ります。シークレットモードを終了する際には、他のシグナル配信を妨げることなくshadow signal stackを消去できます。
うーん、hackey。。。
OSからSignalを受け取った場合の通常の手順では
- 実行中の処理が中断される。
- その瞬間の(機密情報が含まれる可能性がある)CPUレジスタが、カーネルによって 「登録されたシグナルスタック(Registered Signal Stack)」 に退避される。
- Signal Handlerが実行される。
- 処理が終わると
sigreturnシステムコールを呼び、スタック上の退避データを使ってレジスタを復元し、元の処理に戻る。
となる所を 機密情報の処理(Secret Mode)中 の場合は
- 事前に別途 「Shadow Signal Stack」 という別のメモリ領域を確保しておく。
- Signal発生時、OSは通常通り、「登録されたシグナルスタック(Registered Signal Stack)」 に(機密情報が含まれる可能性がある)レジスタ情報を書き込む。
- Signal Handler内で以下のすり替え操作を行う。
- 「登録されたシグナルスタック(Registered Signal Stack)」 にある退避データを、「Shadow Signal Stack」 にコピー。
- コピーが終わったら、「登録されたシグナルスタック(Registered Signal Stack)」 にあるデータを 即座にゼロ埋めする。これで元のスタックは綺麗に消された状態になる。
-
sigreturnで処理を戻す際、スタックポインタを 「Shadow Signal Stack」 に向けておく。 -
sigreturnを実行。CPUは 「Shadow Signal Stack」 にあるデータを使ってレジスタを復元し、元の処理に戻る。 - 機密情報の処理(Secret Mode)が終わったら、 「Shadow Signal Stack」 自体を消去。
という手順に置き換える事で確実にSignal Stackから機密情報を削除する事が可能になります。 Stack自体をコピーして置き換えるという逆転の発想ですが、これはかなり頭良いですねー。
spec: remove cycle restriction for type parameters #75883
これは Go 1.26 で入るかで言うと微妙ですが最近見かけた面白いproposal。
Genericsにおいて 「型パラメータが自分自身を含む制約を持つこと(循環参照)」を禁止していた制限を取り除く という内容です。
これまで Go のGenerics仕様では、実装を簡素化するために、型パラメータがその制約の中で自分自身を参照するような特定の循環パターンを禁止していました。
type T[P T[P]] interface{ m() }
コンパイラの改良により、この制限を維持する必要がなくなったため、言語仕様からこの禁止ルールが削除されます。(なんかもうmergeされとる)
これにより、いわゆる CRTP (Curiously Recurring Template Pattern) のような、より柔軟で高度なGenericsのパターンが Go で記述できるようになります。
// https://go.dev/play/p/1Dv89vpnb4R?v=gotip package main import "fmt" // T は「自分自身(T)を返す Next() メソッド」を持っている必要がある type Node[T Node[T]] interface { Next() T } // Graph は、Node[N] を満たす具体的な型 N のスライスを持つ type Graph[N Node[N]] struct { Nodes []N } // Task 構造体 type Task struct { Name string NextTask *Task // 次のタスクへのポインタ } // *Task 型に対して Node[*Task] interfaceを実装する // ポイント: 戻り値が any ではなく *Task という具体的な型 func (t *Task) Next() *Task { return t.NextTask } // 任意の Node 実装に対して、連鎖をたどる関数 // 引数 start が *Task なら、戻り値も *Task と推論される func FindLast[T Node[T]](start T) T { current := start // 本来は nil check が必要だが省略 for { next := current.Next() if any(next) == (*Task)(nil) { // 簡易的なnilチェック break } current = next } return current } func main() { // Taskを定義 task3 := &Task{Name: "Task 3: Deploy", NextTask: nil} task2 := &Task{Name: "Task 2: Test", NextTask: task3} task1 := &Task{Name: "Task 1: Build", NextTask: task2} // Graph構造体の使用 // ここで N は *Task 型になります myGraph := Graph[*Task]{ Nodes: []*Task{task1, task3, task2}, } fmt.Printf("First Node: %s\n", myGraph.Nodes[0].Name) // Next() を呼ぶと、キャストなしで *Task 型として扱える // もし Node インターフェースが `Next() Node` だったら、ここで型アサーションが必要になる secondNode := myGraph.Nodes[0].Next() // 戻り値はTask構造体なのでcastしなくても .Name に直接アクセスできる! fmt.Printf("Next Node: %s\n", secondNode.Name) // 最後のNodeを探索する lastNode := FindLast(myGraph.Nodes[0]) fmt.Printf("Last Node: %s\n", lastNode.Name) }
Genericsを利用してメソッドチェーンみたいな関数を書く場合は便利そうです。
とはいえ結構認知負荷高いので自分で書く分にはまだ良いですがこれをレビューするとなるとちとつらいですね。 コメントをしっかり書いてあるならまあ良さそうです。
最近のGoのproposalに関する所感
最近のproposalの採択具合を観察しているとGenericsやiteratorの標準ライブラリへの統合 や 全般的なパフォーマンスの改善、encoding/json/jsontext などの追加による低レイヤー層の制御の民主化を行なっているフェーズに見えます。
低レイヤー層の民主化の背景には go:linkname 騒動の件などが関連ありそうですね。
そして、errorハンドリングに関するproposalの盛り上がり や、今回Go1.26で入る予定の new構文の拡張 などのproposalを見ていると、今までであればThe Go Wayに従って即座に却下されていたであろう「省略記法」や「便利機能」に対して、実用性重視で若干軟化している傾向があるようですね。
Goのコードの読みやすさというGoのメリットを潰さないようにしつつ、より柔軟なプログラミングを可能にする方向に進んでいってもらえると良いかなと思います。
アンドパッドでは Goのproposalを読むのが大好きな Gopher も募集しています!