Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Go 1.24でジェネリックになった型エイリアスの紹介

syumai
February 26, 2025

Go 1.24でジェネリックになった型エイリアスの紹介

Go 1.24 リリースパーティーの発表資料です
https://gocon.connpass.com/event/345795/

参考
* https://go.dev/ref/spec
* https://go.dev/doc/go1.24

syumai

February 26, 2025
Tweet

More Decks by syumai

Other Decks in Programming

Transcript

  1. 自己紹介 syumai ECMAScript 仕様輪読会 / Asakusa.go 主催 株式会社ベースマキナで管理画面のSaaS を開発中 Go

    でGraphQL サーバー (gqlgen) や TypeScript でフロント エンドを書いています Software Design 2023 年12 月号から2025 年2 月号まで Cloudflare Workers の連載をしました Twitter ( 現𝕏): @__syumai Website: https://syum.ai
  2. 従来の仕様 従来、型パラメータを持つことができたのは、型定義 (type definition) と 関数宣言 (function declaration) のみ 型定義によって導入される、型パラメータを持つ型のことを、

    ジェネリック型 (generic type) と呼ぶ 型パラメータは、以下のように、 型制約 (type constraints) を伴って宣言される // 型定義の例 type Map[K comparable, V any] map[K]V // 関数宣言の例 func Min[T cmp.Ordered](a, b T) T { if a < b { return a } return b }
  3. ジェネリックエイリアス ジェネリックエイリアスの型パラメータは、複合型 (composite type) の一部や、別の ジェネリックな型への型引数として使える // 複合型の一部 type IntMap[K

    comparable] = map[K]int // 別のジェネリックな型への型引数 // iter package の `type Seq2[K, V any] func(yield func(K, V) bool)` に対するエイリアス type IndexedSeq[V any] = iter.Seq2[int, V]
  4. ジェネリック型と、ジェネリックエイリアスの違い 型同一性 (type identity) の点で異なる 型定義によって導入されたジェネリック型は、使用する際に必ずなんらかの名前付き 型 (named type) を得る

    ある名前付き型は常に他の名前付き型とは異なる型となる( 型同一性の定義の一部) type IntMap[K comparable] map[K]int m1 := map[string]int{"a": 1} // m1 はmap[string]int 型 m2 := IntMap[string]{"b": 2} // m2 はIntMap[string] 型
  5. 型エイリアス導入の背景 パッケージ p を使うコードの例 package main import "github.com/syumai/example/p" func main()

    { // OK: p.F() はp.T 型、p.C は型無しの定数 if p.F() == p.C { println("p.F() == p.C") } var v p.T = 1 // OK: p.F() はp.T 型、v もp.T 型 if p.F() == v { println("p.F() == v") } ... }
  6. 型エイリアス導入の背景 パッケージ分割を行い、関数 F を p1 に、 定数 C を p2

    に、型 T を p3 に移動 ここで、後方互換性を維持し、パッケージ p を使用しているコードが引き続き利用可 能な状態に保つ方法を考える package p1 import "github.com/syumai/example/p3" func F() p3.T { return 1 } package p2 const C = 1 package p3 type T int
  7. 型エイリアス導入の背景 型も、パッケージ p 側に type T p3.T として改めて定義すればよいように見えるが NG p.T

    と p3.T は異なる型 type T p3.T の型定義により新たな名前付き型が導入されているため package p // p.T とp3.T は別の型 type T p3.T
  8. 型エイリアス導入の背景 パッケージ p を利用するコードで p3.T を期待するコードのビルドに失敗する package main import "github.com/syumai/example/p"

    func main() { ... var v p.T = 1 if p.F() == v { // build error (F の返すp3.T とp.T は異なる型) println("p.F() == v") } ... }
  9. 型エイリアス導入の背景 Go 1.9 で導入された型エイリアスを使うと、パッケージ p で新たな名前付き型が導 入されない p.T と p3.T

    は同じ型になるため、後方互換性が保たれる package p // p.T という名前付き型は導入されない type T = p3.T → 型エイリアスは、型定義をパッケージを跨いで移動するリファクタを可能にした
  10. ジェネリック型のリファクタリング 先ほどの例で、パッケージ p の型 T がジェネリック型だったとする package p type T[P

    any] struct { Field P } 型T をパッケージ p からパッケージ p3 に移動したとき、パッケージ p 側で p3.T にエイリアス宣言する際に問題が発生する package p3 // パッケージp から移動 type T[P any] struct { Field P } package p // NG type T = p3.T
  11. ジェネリック型のリファクタリング ジェネリック型は、使用する際に型引数を指定して、インスタンス化する必要がある 下記のエイリアス宣言 ( 前ページから引用) では p3.T の型パラメータ P に渡す

    型引数が定まらない エイリアス指定先と同じ型パラメータ宣言を暗黙的に行う仕様はない package p // NG type T = p3.T // p3.T には型引数が必要! // type T[P any] = p3.T[P] ← のように暗黙的に宣言してくれない
  12. 型パラメータ追加時の後方互換性維持 別の名前の型 CacheKV に実装を移し、 Cache はキー型を string に固定した型エ イリアスとすることで解決 type

    CacheKV[K comparable, V any] struct{ m sync.Map } func (c *Cache[K, V]) Put(key K, v V) { /* */ } func (c *Cache[K, V]) Get(key K) V { /* */ } // 後方互換性を保ったまま新しい型に移行完了! type Cache[V any] = CacheKV[string, V]
  13. 複合型の型名の省略 長い型名を単に省略する → 従来のジェネリック型の定義では、新たな名前付き型が導入されてしまうの で、このユースケースではエイリアスの方が適切 type Proxy[In, Out any] =

    func(ctx context.Context, in In) (Out, error) // generic alias あり func registerProxy1[In, Out any](p Proxy[In, Out]) {} // generic alias なし func registerProxy2[In, Out any](p func(ctx context.Context, in In) (Out, error)) {}