🐥

goのtype identityに関して

2021/04/12に公開

初めに

筆者はgo言語歴半年ちょっとくらいで、goの型定義に関して少し曖昧なところがあったため改めて調べ直しました。
goの仕様を読んでいると こちら にいきつき、その内容を自分なりにわかりやすくまとめたものになります。

Tl;DR

  • goには型に名前をつける方法として、 defined type と type alias がある
  • defined type は新しい型として導入される
  • type alias は参照もとと同じ型として扱われる
  • defined typetype alias の挙動の違いを理解しないといけない

1.曖昧だったところ

例えばだが

type A string

a := A("string")
b := "string"

if a == b { // mismatched types A and string
  // 分岐したい処理
}

ができなくてあれって思ったのが始まり。

その明確な基準を知るためにgo言語の使用について読んでいたところ詳細な説明があったので、それをまとめていく。

https://golang.org/ref/spec#Type_identity

その中の最初の一文で

A defined type is always different from any other type. Otherwise, two types are identical if their underlying type literals are structurally equivalent; that is, they have the same literal structure and corresponding components have identical types.
定義されたタイプは、他のタイプとは常に異なります。 それ以外の場合、基になる型リテラルが構造的に同等であれば、2つの型は同一です。 つまり、それらは同じリテラル構造を持ち、対応するコンポーネントは同じタイプを持ちます。

と記述されているが、何のことかよくわからないので必要そうなところをいっこずつ読んでいく。

2.基になる型(underlying type)について

まずgoで事前に定義されている型・定数・zero value・関数はこれで

Types:
	bool byte complex64 complex128 error float32 float64
	int int8 int16 int32 int64 rune string
	uint uint8 uint16 uint32 uint64 uintptr

Constants:
	true false iota

Zero value:
	nil

Functions:
	append cap close complex copy delete imag len
	make new panic print println real recover

これ以外のものは、宣言をすることにより導入される。

type A string

複合型(配列、構造体、ポインタ、関数、インターフェース、スライス、マップ、チャネル型)はリテラルを使用して構築する。そして最後に

Each type T has an underlying type: If T is one of the predeclared boolean, numeric, or string types, or a type literal, the corresponding underlying type is T itself. Otherwise, T's underlying type is the underlying type of the type to which T refers in its type declaration.
各型Tには、基になる型があります。Tが事前に宣言されたブール型、数値型、文字列型の1つ、または型リテラルの場合、対応する基になる型はT自体です。 それ以外の場合、Tの基になる型は、Tが型宣言で参照する型の基になる型です。

とのこと。つまり基になる型というのは事前に定義されたもの(bool numeric string)を使っていればその型自身で、他の型を参照して宣言すればいればその型になるよということ。(上記のtypesにあるもの。)

https://golang.org/ref/spec#Types

3.type identityについて

ここで本題に。冒頭で記述した

A defined type is always different from any other type. Otherwise, two types are identical if their underlying type literals are structurally equivalent; that is, they have the same literal structure and corresponding components have identical types.
定義されたタイプは、他のタイプとは常に異なります。 それ以外の場合、基になる型リテラルが構造的に同等であれば、2つの型は同一です。 つまり、それらは同じリテラル構造を持ち、対応するコンポーネントは同じタイプを持ちます。

について、ここまで調べた上でやっとしっくりきました。

type A string

Aという型を宣言していて、stringタイプではないです(string型を参照して宣言)。これを A という名前の defined type と呼びます。
よって冒頭であげたサンプルコードのように型が同一である場合に可能な操作ができない。(定義されたタイプは、他のタイプとは常に異なるとはこういうこと。)
一方で、別名を付けたいだけの場合は

type A = string

として type alias を宣言することで使い分けるようです。
つまりは

type A string
type B = string
// 複合型は割愛

a := A("string")
b := B("string")
c := "string"

// mismatched types A and B
// Aという型(defined type)とBという型(defined type)を比較しているのでコンパイルエラー
if a == b {
  // 何か処理を追加
}

// 基になる型リテラルが同等なので比較可能
if a == "string" {
  // 何かの処理を追加
}

// mismathed types A and string
// Aという型(defined type)とstringという型を比較しているのでコンパイルエラー
if a == c {
  // 何かの処理を記述
}

// B(type alias)はstringのエイリアスなのでstring型として扱われるので比較可能
if b == c {
  // 何かの処理を記述
}

と言ったように defined typetype alias の仕組みを理解していないのが僕の曖昧さの原因でした。

その後、詳しい違いや用途としてはこちらの記事がわかりやすかったです。
https://budougumi0617.github.io/2020/02/01/go-named-type-and-type-alias/

最後に

すっきりした。go楽しい。

Discussion