Go1.24で導入された型エイリアスの新機能と静的解析を用いたPhantom Typeの実現のトップ画像

Go1.24で導入された型エイリアスの新機能と静的解析を用いたPhantom Typeの実現

投稿日時:2025/05/08 23:30
tenntennのアイコン

newmo株式会社 / ソフトウェアエンジニア

tenntenn

Xアカウントリンク

はじめに

2025年2月にリリースされたGo1.24では、型エイリアスの宣言で型パラメータが利用できるようになりました。本稿では、型エイリアスの復習およびGo1.24で入った新しい機能を解説します。そして、この新しい機能と静的解析を利用したPhantom Type(ファントムタイプ)の実現方法を紹介します。また、Go1.25以降で導入が見込まれる型パラメータ(ジェネリクス)に関する変更やその議論の行方についても触れます。

Goのバージョンアップ

本題に入る前にGo1.24へのアップデート方法を説明します。すでにGoを利用しているコードベースが存在し、Go1.21以降のバージョンを利用している場合は、次のコマンドでgo.modファイルに記載しているGoのバージョンをアップデートしましょう。執筆時のGoの最新バージョンはGo1.24.2です。

$ go mod edit -go 1.24.2

go.modの編集はエディタを利用しても問題ありません。ただし、go1.24.2のようにパッチバージョンまで指定しましょう。Go1.21以降では、パッチバージョンまで記載するバージョンをリリースバージョンと呼び、マイナーバージョンまで指定するバージョンを言語バージョンと呼びます。言語バージョンで指定した場合、go1.21rc1など正式版がリリースされる前にリリースされたRC(リリース候補)版も含まれます。詳しくは公式ドキュメントを確認してください。

Go1.21以降では、go.mod(またはgo.work)のgoディレクティブの変更だけで十分です。バージョンアップはgoコマンドを使用しようとした際にダウンロードも含めすべて自動で行われます。なお、環境変数のGOTOOLCHAINがlocalや特定のバージョンに固定している場合はこの限りではありませんので注意してください。詳細は公式ドキュメントを確認してください。

まだgo.modファイルが存在しない場合でも、次のようにgo mod initコマンドを実行する場合に環境変数GOTOOLCHAINにバージョンを指定すると、そのバージョンでgo.modファイルを作成します。

$ GOTOOLCHAIN=go1.24.2 go mod init example.com/repo/samplemodule
$ cat go.mod
module example.com/repo/samplemodule

go 1.24.2

ローカルのGoのバージョンを完全にアップデートしたい場合は、公式サイトからダウンロードしてインストールする方法がもっとも確実で分かりやすいでしょう。

Goがすでにインストールされている場合は、次のようにgo installコマンドをつかってgo1.24.2コマンドをインストールし、go1.24.2 downloadコマンドでツールチェインをダウンロードできます。バージョン番号が付加されたコマンドはめずらしく読みづらいかもしれませんが、go1.24.2でひとつのコマンド名です。

$ go install golang.org/dl/go1.24.2@latest
$ go1.24.2 download

go1.24.2コマンドはgo1.24.2 buildのようにgoコマンドと同様のインタフェースで扱えます。goコマンドで呼び出したい場合には、LinuxやmacOSであれば次のようにエイリアスを貼っておくと良いでしょう。

$ alias go=go1.24.2

なお、最新のGoのバージョンは次のように https://go.dev/VERSION?m=text というURLから取得できます。

$ curl -s https://go.dev/VERSION?m=text
go1.24.2
time 2025-03-26T19:09:39Z

また、筆者が作成しているツールgoversionを使えばコマンドラインまたはパッケージ関数を通して取得が可能です。

型エイリアス

型エイリアス(type alias)は、Go1.9で導入された既存の型に新しい識別子(型名)を紐づけるための機能です。Goには、型宣言(type declaration)を行う機能が2つあり、ひとつが型定義(type definition)でもうひとつが型エイリアスです。

型定義では新しい型を作ります。たとえば、次のような型定義では、Xのunderlying typeと同じ型をunderlying typeとする型を新しく作り、その型にT1という識別子(型名)を紐づけます。

type T1 X

一方、型エイリアスは新しい型を作りません。たとえば、次のような型エイリアスの宣言では、T2はYのエイリアス(別名)として宣言されるだけで、T2とYは同じ型です。型に紐づけられる識別子が増えるだけで、型が増えてはいません。組込み型のbyte型とuint8型、rune型とint32、any型とinterface{}型と同じような関係だと考えてよいでしょう。

type T2 = Y

型エイリアスは、大規模なリファクタリングを行うために導入された機能です。型エイリアスが提案された背景を知るには当時のプロポーザルを確認すると良いでしょう。golang.design/historyにまとまっているリンクから詳細を調べることもできます。

あるパッケージのパッケージ名を変えたいと考えた場合に定数、変数、関数であれば、簡単に移行ができます。

たとえば、次のような定数C、変数V、関数Fが宣言されたパッケージaがあった場合を考えます。

// 移行元
package a

const C = 100
var V = 200
func F(x int) int {
    return x
}

パッケージaを残しつつ、パッケージbに移行する場合は次のように宣言すれば簡単に移行できます。

// 移行先
package b

import "example.com/a"

const C = a.C
var V = a.V
func F(x int) int {
    return a.F(x)
}

一方、型は簡単には移行できませんでした。Go1.7で導入されたcontextパッケージを例に解説します。contextパッケージが標準ライブラリに導入されるまでは、golang.org/x/net/contextパッケージとして公開していました。

標準ライブラリに導入される前も後もContext型は次のようにインタフェースとして宣言しています。

type Context interface {
        Done() <-chan struct{}
        Err() error
        Value(key any) any
}

インタフェースのため、宣言(型定義)さえ同じであれば、ほとんどのケースでは標準ライブラリのcontext.Context型であってもgolang.org/x/net/context.Context型であってもコンパイルが通りました。

しかし、次のようなケースではcontextパッケージの既存の利用者はインポートパスを変えるだけではコンパイルが通りませんでした。

package main

import stdctx "context"
import netctx "golang.org/x/net/context"

func X(ctx stdctx.Context) {
}

func Y(f func(ctx netctx.Context)) {
}

func main() {
    // stdctx.Contextとnetctx.Contextは別の型なので代入できない
    Y(X)
}

これは標準ライブラリのcontextパッケージとgolang.org/x/net/contextパッケージで宣言している型が型定義で行われており、underlying typeは同じであっても別の型だったからです。

そのため、Go1.9から型エイリアスが導入されるにあたって、golang.org/x/net/contextパッケージでは、Context型は次のように型エイリアスとして宣言されています。なお、Go1.15まではbuild constraintでGoのバージョンごとにファイルが分けてありましたが、Go1.16からはGo1.8以下の対応はなくなっているようです。

import "context"

type Context = context.Context

このように型エイリアスのおかげでContext型を利用している場合は、インポートパスをgolang.org/x/net/contextからcontextに変更するだけで対応できるようになりました。また、インポートパスの変更もgo fixコマンドで簡単に行えます。

go fixコマンドはGoがバージョン1.0としてリリースされる以前に活躍したコマンドです。Go1.0リリース前は毎週のように破壊的な変更が入っていました。そのため、新しいバージョンのツールチェインを用いる場合は、go fixコマンドを用いて新しいAPIに簡単に移行していました。Go1.0リリース後はContext型の移行のようなリファクタリングに用いられるようになりました。

また、issue #32816の提案が承認されたため、Go1.25以降では//go:fix inlineコメントディレクティブが導入される見込みです。標準ライブラリに限らず、サードパーティライブラリにおいてもContext型の移行のような場合にgo fixコマンドを用いたリファクタリングが行えるようになります。

型パラメータを持つ型エイリアス

次のように型引数を指定してインスタンス化した型のエイリアス宣言は、型パラメータ(ジェネリクス)が導入された当初(Go1.18)からできていました。

type Vector2[T any] [2]T
type IntVector2 = Vector2[int]

しかし、次のように型エイリアスの宣言で型パラメータを新たに設定する機能は提供されていませんでした。

type OrderedVector2[T cmp.Ordered] = Vector2[T]

そのため、型パラメータを持つ型を別のパッケージに移行したい場合など、contextパッケージのように型エイリアスを利用したリファクタリングが行えませんでした。

Go1.24から型パラメータを持つ型エイリアスを宣言できるようになり、型パラメータを持つ型であっても安全にリファクタリングができるようになりました。

GoにおけるPhantom Typeの実現

Phantom Type(ファントムタイプ)は型チェックのためだけに存在する型パラメータです。実行時には使用されませんが、コンパイル時に型チェックされることにより、型の誤用などを防ぎます。

一般的にGoでファントムを実現するためには、次のように型を定義します。

type ID[T any] uuid.UUID

型IDの型パラメータTは、型定義で使用されておらず、実質的になくても問題ありません。しかし、意味の異なる複数のIDがあるような場合、異なる型引数を指定して別の型としてインスタンス化することで誤用を防げます。たとえば、次に示すようなID[User]型とID[Group]型が別の型であるときは、UserByGroupID関数に誤ってID[User]型の値を引数として指定することはありません。

type User struct {
	UserID  ID[User]
	GroupID ID[Group]
	// ...略...
}

type Group struct {
	GroupID ID[Group]
	// ...略...
}

func UserByGroupID(ctx context.Context, id ID[Group], limit int) ([]*User, error) {
	// ...略...
}

GoにおけるPhantom Typeの実現は、アメリカで開催されたGopherCon 2024のAxel Wagner氏のAdvanced Generics Patternsというセッション(動画スライド)でも紹介があったので、興味がある方はセッションを見てみると良いでしょう。

ここまでに示したようにPhantom TypeはGoにおいても実現できており、活用方法を選べば便利でしょう。しかし、type ID[T any] uuid.UUIDという型定義では、underlying typeがuuid.UUID型のものと同じ新しい型を作成しています。そのため、uuid.UUID型が持つメソッドはID[T]型には引き継がれず、次のように明示的に型変換を行う必要があります。

func printUserID(userID ID[User]) {
fmt.Println(uuid.UUID(userID).String())
}

また、ID[T]型とuuid.UUID型は別の型であるため、既存のコードベースのuuid.UUID型からID[T]型への置き換えは単なる置換では実現ができず手動でリファクタリングを行うには骨が折れるでしょう。

型エイリアスと静的解析を用いたPhantom Typeの実現

Phantom Typeの実現にあたって型定義の代わりに型エイリアス宣言を用いると既存の型との互換性が保てます。たとえば、ID型はGo1.24から次のように型エイリアスとして宣言できます。=があるだけで型定義の場合とあまり変わらないように見えますが、ID[T]型とuuid.UUID型は同じ型になっている点で大きく違います。

type ID[T any] = uuid.UUID

同じ型なので型変換は不要になり、次のように直接uuid.UUID型のメソッドを呼び出せます。

func printUserID(userID ID[User]) {
fmt.Println(userID.String())
}

uuid.UUID型とID[T]型をインスタンス化したID[UserID]型、ID[Group]型はそれぞれ同じ型です。そのため、コンパイルでは型エイリアスで宣言した型(ID[T]型)とエイリアスされる型(uuid.UUID型)の区別はしません。つまり、Phantom Typeの利点であるコンパイル時の型チェックにおける誤用の発見はできません。

そこで、ここではコンパイル時に誤用を発見するのではなく、静的解析によって見つける方法を紹介します。型エイリアスで宣言された型は、go/typesパッケージでは、Go1.22で導入されたtypes.Alias型として表現されます。また、Go1.23で導入されたTypeParamsメソッドTypeArgsメソッドを用いると型エイリアス宣言で宣言された型パラメータやインスタンス化された型エイリアスの型引数を取得できます。

静的解析によってある型がPhantom Typeを持つかどうかは次の方法で確認できます。

  1. 対象の型がtypes.Aliasで表現されているか調べる
    1.1表現されていない場合はPhantom Typeを持たない
  2. types.Alias型であればTypeArgsメソッドで型パラメータを取得する
    2.1. 型パラメータを取得できなければPhantom Typeを持たない
  3. Originメソッドでエイリアス元の型を取得する
  4. 取得した型がtypes.Named型で表現されているか調べる
    4.1. types.Named型で表現されていない場合はPhantom Typeを持つ
  5. TypeArgsメソッドで型引数を取得し、対象の型から取得した型パラメータが使用されているか調べる
    5.1. 使用されていればPhantom Typeを持たない
  6. 対象の型はPhantom Typeを持つ

また、Phantom Typeを用いて変数の代入や関数呼び出しの際の誤用を発見するには、次の方法で確認できます。

  1. 代入文、関数呼び出しなどを見つける
  2. 代入先の変数の型や関数の引数の型を調べる
  3. 代入先の型がPhantom Typeを持つか調べる(このPhantom Typeを型Xとする)
    3.1. Phantom Typeを持たない場合はエラーにしない
  4. 代入する値や式の型がPhantom Typeを持つか調べる(このPhantom Typeを型Vとする)
    4.1. Phantom Typeを持たない場合はエラーにしない
  5. 型X型に対して型Vが代入可能か調べる
  6. 代入できない場合はエラーとして報告する

この静的解析によって次のようなコードをエラーとして報告できます。

var v ID[Group] = uuid.New() // 右辺がPhantom Typeを持たないのでNGにならない
var x ID[User] = v // エラー
func(id ID[User]) { /* ...略... */ }(v) // エラー

なお、この静的解析のソースコードは筆者のGitHubリポジトリで確認できます。

Go1.25以降のジェネリクス(型パラメータ)

Goの公式ブログのGoodbye core types - Hello Go as we know and love it!にあるとおり、2025年8月にリリース予定であるGo1.25ではCore Typeという概念がなくなります。Core typeはGo1.18でジェネリクス(型パラメータ)と同時に導入された機能で、ジェネリックな型の値に対する操作を簡易的なルールで実現(実装)しています。

しかし、既存の言語仕様の多くの部分でCore typeの概念が必要となり、ジェネリクスの理解なしに言語仕様を正しく把握するのが難しくなりました。Go1.25でCore typeの概念がなくなることで、言語仕様の多くの部分がGo1.18より前の記述に戻りました。

この変更によって後方互換性が崩れたり、何か新しいことができるようになるわけではありません。しかし、ジェネリクス周りの仕様が整理されることにより、型推論が強化されたり、issue #48522で提案されているように制限が撤廃される可能性は高くなるでしょう。

おわりに

本稿ではGo1.24で導入された型エイリアスに対する型パラメータについて紹介しました。また、静的解析を活用し、Phantom Typeを実現する方法について解説しました。

Go1.24ではここで紹介した機能以外にもイテレータの改善や新しいweakパッケージの導入など、さまざまな新しい機能を提供しています。気になる方は、リリースノートを読み、あわせてGo1.24リリースパーティーのアーカイブ動画フューチャー技術ブログのGo1.24 リリース連載などを確認するとよいでしょう。

また、2025年8月にリリース予定のGo1.25においても大きめの機能が導入されることが予想されます。たとえば、issue #71497で追加提案しているencoding/json/v2パッケージについて、GOEXPERIMENT=jsonv2フラグをONにすることで先んじて試せる提案がissue #71845で行われ、承認されています。 Go1.24で先にomitzeroオプションが導入されたものの、長らく議論されていたencoding/json/v2がいよいよ標準ライブラリに入る兆しがあります。

8月にはアメリカでGopherCon、イギリスでGopherCon UK、そして日本では9月にGo Conference、12月にGo Workshop Conferenceが開催予定です。Go1.25のリリース前後にはリリースパーティーが開催される予定です。新しい機能に興味のある方はぜひこれらのイベントにもご参加ください。

■査読:osamingo(@osamingo) / mhidaka(@mhidaka) / Sixeight(@tomohi_ro