📚

Goの型同一性を理解する

2021/03/18に公開

本記事は、2021年3月18日(木)に開催の、入門Go言語仕様輪読会 第2回のために執筆したものです。

はじめに

Goには の機能が存在します。
型を利用することで、

  • 変数の型を指定し、その変数に対して代入できる値を限定する
  • 値の型を指定し、その値に対して行える演算処理を限定する
  • 同じ型を持つ値に対して共通した処理を記述する

といった事ができます。

例として、代入や足し算について考えてみましょう。

// OKな例
var i1 int = 10
var i2 int = 1
i1 = i2 // OK
fmt.Println(i1 + i2) // OK

// NGな例
var i int = 10
var f float64 = 1.0
i = f // compile error
fmt.Println(i + f) // compile error

上記の NGな例 のように、整数型 (int) の変数に対する、64ビット浮動小数点数型 (float64) の値の代入や足し算はコンパイル時点で失敗します。

このように、Goでは 型が異なる値同士では行えない操作があり (※1)、それをコンパイル時点で検出することで、安全なプログラミングを早いサイクルで行うことが出来るようになっています。

ここで、 型が異なる と言う表現を用いました。どのような場合に型が異なっているかについてを理解するためには、Goの言語仕様中の『Type identity (型同一性)』の節を読む必要があります。

(※1: 厳密には、 型が異なる ことは、代入や演算が可能であるかどうかの判定条件の一つでしかない点に注意が必要です)

型同一性について

Type identity の節の冒頭部分では、下記の内容が説明されています。

2つの型は、同一であるか、異なります。

defined type は、常に他の型とは異なります。
そうでなければ、2つの型はそれらの underlying type のリテラルが構造的に等しければ同一です。構造的に等しいとは、それらが同じリテラルの構造を持ち、対応する構成要素が同一な型を持っているということです。

上記の内容から、型の同一性の判定条件を大きく2つに分けると、下記のようになります。

  1. 2つの型のいずれかが defined type の場合、結果は常に異なる。
  2. 2つの型のいずれも defined type ではない場合、型が同一の場合がある。

それでは、この2つを細かく見てみましょう。

1) 2つの型のいずれかが defined type の場合

defined type に該当する型は、型定義 (type A Bの構文) によって定義された型および、事前に宣言された識別子で定義された型 (intなど) です。エイリアス宣言 (type A = Bの構文) では新たな型の定義は行われないので、これによって宣言される型は defined type にはなりません。
defined type の詳細な説明については Nobishiiさんの資料 を参照ください。

別々の型定義で導入された defined type は常に互いに異なるため、たとえ同じ型に対して定義された defined type であったとしても、それらの型は同一ではありません。
また、名前が同じであったとしても、別のpackageで定義された型同士は同一ではありません。

同一な型の例

// 例1) 型定義によって定義された型
type A int

var x A
var y A // xとyの型は `A` で同一

// 例2) 事前に宣言された識別子で定義された型
var x int
var y int // xとyの型は `int` で同一

異なる型の例

// 例1) 同じ型 (`int`) に対して定義された2つの型
type A int
type B int // AとBは異なる

// 例2) `int` に対して定義された型 `A` と、それに対して定義された型 `B`
type A int
type B A // AとBは異なる

// 例3) 事前に定義された型 `int` と、それに対して定義された型 `A`
int
type A int // intとAは異なる

// 例4) 型リテラルと、それに対して定義された型 `A`
struct{ X int }
type A struct{ X int } // struct{ X int }とAは異なる

// 例5) 他packageで定義された同名の型
package pkgA

type A int
---
package pkgB

type A int
---
pkgA.A
pkgB.A // pkgA.AとpkgB.Aは異なる

いくつか例を見ましたが、 defined type に対して同一になれる型はその型自身のみである、と言うことさえ覚えておけばよいです。

補足A: 型エイリアスについて

型同一性の仕様において、 defined type に対する型エイリアスは、その宣言で参照している型そのものとみなすようです。

すなわち、

type A int
type B = A

とした時、型Aと型Bの宣言を行っていますが、比較上は型Aの1つしか存在しないこととし、

defined type は、常に他の型とは異なります。

のルールに引っかからない扱いにしているようです。

厳密には型Bは defined type ではない (型定義によって導入された型ではない) ため、このルールに矛盾しているように見えますが、上記のような特別扱いをしているのであれば筋が通ります。

この部分については仕様に明記されていないので、議論の余地がありそうです。

2) 2つの型のいずれも defined type でない場合

2つの型のいずれも defined type でない場合は、 それぞれの型のunderlying typeの型リテラルが構造的に等しい場合に同一となります。

これを正確に理解するためには、下記3項目を知っておく必要があります。

  1. 型リテラルについて
  2. underlying typeについて
  3. 型リテラルの構造と、それぞれの同一性の条件

2-1) 型リテラルについて

型リテラルとは何か

配列型や、スライス型といった型の変数は、下記のように宣言することが出来ます。

var a [5]int // 配列型
type B int
var b []B // スライス型

上記の変数 ab の型は、 [5]int[]B といった、他の型を利用して構成される 型リテラル(Type literal) を使って示されています。 リテラル中に他の型が使われない、func () や、 struct{}といった型リテラルも存在します。
型リテラルを使って示される型は、型定義が行われていないため defined type ではありません。

型リテラルの種類

型リテラルは、他の型を使って構成する事ができ、下記の種類に分類することが出来ます。(※2)

  1. 要素型を持つもの (配列型、スライス型、マップ型、チャネル型)
  2. キー型を持つもの (マップ型)
  3. フィールド型を持つもの (構造体型)
  4. ベースの型を持つもの (ポインタ型)
  5. 仮引数型、結果型を持つもの (関数型)

型リテラルによって示される型同士が同一かどうかを判定するには、型リテラルの構造が同一かつ、型リテラル中に現れる型が同一であることを確認する必要があります。

(※2: この分類にインタフェース型は含んでいません。インタフェース型は、任意の型を使って構成される訳ではないためです。インタフェース型はメソッドを持ち、それぞれのメソッドは関数型を持ちます。この時使われる関数型は、他の任意の型を使って構成することが出来ます。)

2-2) underlying typeについて

underlying type とは何か

全ての型は underlying type を持っています。事前に定義された型や、型リテラルによって示される型の underlying type はその型自身で、 それ以外の型の underlying type は、型宣言の対象によって決まります。

ここで言う、型宣言とは、型定義と、エイリアス宣言の2つを指します。

type A string // 型定義
type B = int  // エイリアス宣言

わかりやすさのために、いくつかの型のunderlying typeの例を示します。

事前に定義された型や、型リテラルによって示される型の underlying type
// 事前に定義された型
int      // underlying type: int
string   // underlying type: string

// 型リテラルによって示される型
[]int    // underlying type: []int
struct{} // underlying type: struct{}
型宣言 によって宣言された型の underlying type
// 型定義
type A string // underlying type: string
type B A      // underlying type: string
type C []int  // underlying type: []int

// エイリアス宣言
type A string  // defined type `A`
type B = A     // underlying type: string
type C = []int // underlying type: []int

DQNEOさんがunderlying typeについて説明した資料があるので、詳しく知りたい方はこちらを参照ください。

型の同一性とunderlying typeの関係

先ほど、

2つの型のいずれも defined type でない場合は、 それぞれの型のunderlying typeの型リテラルが構造的に等しい場合に同一となります。

と述べました。

ここで言う defined type でない型 には、型リテラルと、型エイリアスの2つしかありません。型リテラルのunderlying typeはその型リテラル自身です。そのため、型同一性においてunderlying typeの考慮が必要なのは型エイリアスのみです。

type NamedMap = map[string][]int

// 次のm1とm2の型はunderlying typeの型リテラルの構造が同一なので同一
var m1 map[string][]int // underlying type: map[string][]int
var m2 NamedMap         // underlying type: map[string][]int

ただし、補足A: 型エイリアスについてに記載した通り、エイリアス宣言で defined type を参照している場合、その型は参照している defined type そのものとみなす点に注意しましょう。

type A []int
type B = A

// 次のxとyについて、それぞれの型のunderlying typeの構造が一致しているが、
// 型Bはdefined type扱いになるため同一ではない
var x []int // type: `[]int`, underlying type: []int
var y B     // type: `A` (defined type), underlying type: []int
補足B: underlying typeについてのルールが明記されている理由についての考察

型同一性についてunderlying typeについてのルールを削ると、

2つの型のいずれも defined type でない場合は、 それぞれの型の型リテラルが構造的に等しい場合に同一となります。

と言う内容になり、

type A = []int
type B = []int

とした時の型A、型Bの比較といった、defined type ではなく、型リテラルでもない パターンについて説明ができなくなってしまいます。(A, Bは型名であって、型リテラルではない)
これを避けるために、underlying typeのリテラル、と明記しているものと思われます。

2-3) 型リテラルの構造と、それぞれの同一性の条件

Spec上では各種型リテラルについて、同一性の条件が細かく書かれています。

  • 配列型
  • スライス型
  • 構造体型
  • ポインタ型
  • 関数型
  • インタフェース型
  • マップ型
  • チャネル型

また、型リテラルが他の型を使って構築される場合、同一性のルールが再帰的に適用されます。

type T = []int // []int に対して宣言された型エイリアス
type V string  // stringに対して定義された型
var a struct {
  X map[V][]T 
  Y func() T
}
// aとbの型は同一
var b struct {
  X map[V][][]int // struct -> map -> slice of []int -> slice of int で同一
  Y func() []int  // struct -> func -> slice of int で同一
}

配列型

2つの配列型は、同一の要素型を持ち、配列の長さが同じ場合同一です。

var a [5]int
var b [5]int // aとbの型は同一
var c [3]int // aとc、bとcの型は同一ではない

スライス型

2つのスライス型は、同一の要素型を持つ場合同一です。

var a []string
var b []string // aとbの型は同一

構造体型

2つの構造体型は、

  • 同一のフィールドの並びを持ち
  • 対応するフィールドが同じ名前、同一の型、そして同一のタグを持つ

場合に同一です。

また、異なるpackageからのexportされていないフィールド名は常に異なります。

同一な構造体型の例
// aとbの型は同一
var a struct{ ID int `json:"id"` }
var b struct{ ID int `json:"id"` }
// packageを跨いでも同一
package pkgA

func F() struct{ ID int } {
	// ...
	return v
}
---
package pkgB

import "pkgA"

func main() {
	// aとbの型は同一
	a := pkgA.F()
	var b struct{ ID int }
}
同一でない構造体型の例
// 下記の全ての変数の型が `a` と異なる
var a struct{
	ID int `json:"id"`
	Name string `json:"name"`
}
// フィールドの並びが違う
var b struct{
	Name string `json:"name"`
	ID int `json:"id"`
}
// フィールドの名前が違う
var c struct{
	MyID int `json:"id"`
	MyName string `json:"name"`
}
// フィールドの型が違う
var d struct{
	ID string `json:"id"`
	Name []byte `json:"name"`
}
// フィールドのタグが違う
var e struct{
	ID int `json:"my_id"`
	Name string `json:"my_name"`
}
// 異なるpackageからのexportされていないフィールド名は常に異なる
package pkgA

func F() struct{ id int } {
	// ...
	return v
}
---
package pkgB

import "pkgA"

func main() {
	// aとbの型は異なる
	a := pkgA.F()
	var b struct{ id int }
}

ポインタ型

2つのポインタ型は、同一のベースの型を持つ場合同一です。

// aとbの型は同一
var a *int
var b *int
// aとcの型は異なる
var c *int32

関数型

2つの関数型は、

  • 同じ数の仮引数と結果の値を持ち
  • 対応する仮引数と結果の値の型が同一で
  • どちらの関数もvariadicである (可変長引数を持つ) か、どちらもそうではない

場合に同一です。

同一な関数型の例

どちらの関数もvariadicでない (可変長引数を持たない) 例

var a func(a int, b string) (c int, err error)
// aとbの型は、仮引数、結果の値の名前は関係無く同一
var b func(x int, y string) (z int, zz error)
// aとcの型は、仮引数、結果の値の名前が与えられなくても同一
varfunc(int, string) (int, error)

どちらの関数もvariadicである (可変長引数を持つ) 例

var a func(a int, ids ...string) error
// aとbの型は同一
var b func(int, ...string) error
同一でない関数型の例
// 下記の全ての変数の型が `a` と異なる
var a func(int, string) (int, error)
// 仮引数の数が違う
var b func(int) (int, error)
// 結果の値の数が違う
var c func(int, string) error
// 仮引数の型が違う
var d func(int32, []byte) (int, error)
// 結果の値の型が違う
var e func(int, string) (int32, bool)
// 片方だけvariadicになっている
var f func(int, string, ...int) (int, error)

インタフェース型

2つのインタフェース型は、同じ名前と同じ関数型を持つ同じセットのメソッドを持つ場合同一です。

この時、メソッドの順序は問いません。

同一なインタフェース型の例
var a interface{
	A(int) error
	B()
}
// aとbは同じ名前と同じ関数型を持つ同じセットのメソッドを持っているので同一
var b interface{
	B()
	A(int) error
}
同一でないインタフェース型の例
// 下記の全ての変数の型が `a` と異なる
var a interface{
	A(int) error
	B()
}
// メソッドの名前が違う
var b interface{
	X(int) error
	Y()
}
// メソッドの関数型が違う
var c interface{
	A(int32) bool
	B()
}
// メソッドのセットが違う
var d interface{
	A(int) error
}

マップ型

2つのマップ型は、同一のキーと要素の型を持つ場合同一です。

var a map[int]bool
// aとbの型は同一
var b map[int]bool
// キーの型が異なるため、aとcの型は異なる
var c map[int32]bool
// 要素の型が異なるため、aとdの型は異なる
var d map[int]string

チャネル型

2つのチャネル型は、同一の要素型と同じ方向 (direction) を持つ場合同一です。

// aとbの型は同一
var a chan int
var b chan int

// cとdの型は同一
var c <-chan string
var d <-chan string

// eとfの型は同一
var e chan<- bool
var f chan<- bool
// 下記の全ての変数の型が `a` と異なる
var a chan int
var b chan string // 要素型が異なる
var c <-chan int  // 方向が異なる
var d chan<- int  // 方向が異なる

まとめ

型の同一性についてのルールは、大きく分けると2つ。

  1. 2つの型のいずれかが defined type の場合、結果は常に異なる。
  2. 2つの型のいずれも defined type ではない場合、型が同一の場合がある。

いずれも defined type ではない場合、

  • いずれの型も型リテラルであれば、それらが構造的に等しければ同一となる。
  • いずれか (またはいずれも) が型エイリアスであれば、それぞれのunderlying typeが構造的に等しければ同一となる。

型リテラルの構造的な等しさについては、それぞれの型の種類によって個別のルールが定義されている。

(ただし、defined typeに対する型エイリアスはdefined typeとして扱う)

演習

最後に、演習として言語仕様中で紹介されている例を引用します。
書かれている内容が自身の理解と合っているか、確認してみてください。

下記の宣言について、

type (
	A0 = []string
	A1 = A0
	A2 = struct{ a, b int }
	A3 = int
	A4 = func(A3, float64) *A0
	A5 = func(x int, _ float64) *[]string
)

type (
	B0 A0
	B1 []string
	B2 struct{ a, b int }
	B3 struct{ a, c int }
	B4 func(int, float64) *B0
	B5 func(x int, y float64) *A1
)

type	C0 = B0

これらの型は同一です:

A0, A1, and []string
A2 and struct{ a, b int }
A3 and int
A4, func(int, float64) *[]string, and A5

B0 and C0
[]int and []int
struct{ a, b *T5 } and struct{ a, b *T5 }
func(x int, y float64) *[]string, func(int, float64) (result *[]string), and A5

B0とB1は、個別の型宣言で作られた新しい型なので異なります。func(int, float64) *B0とfunc(x int, y float64) *[]stringは、B0と[]stringが異なるため異なります。

最後に

以上、Goの型同一性についてのルールを説明してきました。
ルールが多く、全体を把握するのは難しいですが、型同一性を理解することでGoの他の言語仕様の理解に繋がるので、ぜひここまでの内容を読み返して、仕様と照らし合わせつつ読み込んでみてください。

Gophers Slackにて本記事の記載内容に意見をくださった、tenntennさん、DQNEOさん、Mikiさん、Nobishiiさん、ありがとうございました!

GitHubで編集を提案

Discussion