値の表現
型Tには、その型として有効な値の集合があります。boolならtrueとfalse、int8なら-128から127までの整数だけがint8の値です。変数に入る値は必ずこの集合のどれか1つで、型は「どんな値が存在しうるか」を決めます。
基底型と型定義
各型には基底型(underlying type)があります。事前宣言されたブール・数値・文字列型や型リテラルの基底型は、その型自身です。型定義type T Uでは、Tの基底型はUの基底型と同じになります。型エイリアスtype T = UではTとUは同一型であり、基底型も同じです。
type ID int
type Count = int // エイリアス: Countとintは同一型型定義は新しい型を作る
type Celsius float64はfloat64と別の型です。基底型が同じでも、代入には明示的な変換が必要になります。型定義は意味の違う値を混同しないための仕組みです。
var c Celsius = 36.5
var f float64 = float64(c) // OK
// var f float64 = c // コンパイルエラーtype ID int(型定義)とtype Count = int(エイリアス)では代入や変換の可否が異なるので、構文を混同しないようにします。
型の同一性
名前付き型(type T Uで定義した型)は、常に他の型と別です。型エイリアスtype T = UはUと同一型です。
複合型は、配列・スライス・構造体・ポインタ・関数・インターフェース・マップ・チャネルです。typeで別名を付けていない無名の複合型は、種類と内部の型構成が一致すれば同一型とみなされます。配列では要素型に加えて長さも型の一部になり、構造体ではフィールドの名前・型・並びが一致している必要があります。
[]intと[]intは同一type A intとtype B intは別(基底型は同じでも非同一)[]intと[3]intは別(配列の長さは型の一部)struct{ x int }とstruct{ y int }は別(フィールド名が異なる)Stack[T any]や[]Tのように型パラメータTを含む型は、具体の型引数を与えてインスタンス化したあとに同一性が決まる(例:Stack[int]とStack[string]は別)
代入可能性
値vを型Tの変数に代入できるかは代入可能性で決まります。代入可能性はコンパイル時に決まり、型の関係(同一性やインターフェースのメソッド集合など)を見ます。
主な規則:
- 同一型どうしは代入可能
- 基底型が同一の型定義どうしは、明示的変換を経て代入可能
- インターフェース変数には、そのインターフェースを実装する非インターフェース型の値を代入可能
nilはポインタ、スライス、マップ、チャネル、関数、インターフェースに代入可能
表現可能性
定数や型変換の文脈で、値が型Tの取りうる値の範囲に収まるかを調べるのが表現可能性です。こちらもコンパイル時に決まり、定数の値が具体の型の範囲に収まるかを見ます。
無型定数は、文脈の型に表現できるときだけその型として使えます。
const limit = 300
var wide int = limit // OK: 300はintの範囲内
// var tiny int8 = limit // コンパイルエラー: 300はint8の範囲外数値どうしでも、小数を整数型に載せられるかは別問題です。整数として正確に書けるかが問われます。
const whole = 1.0
var n int = int(whole) // OK
const frac = 1.5
// var n int = int(frac) // コンパイルエラー: 1.5は整数として表現できない文字や文字列の定数も、代入先の型に収まる必要があります。
const letter = 'A'
var b byte = letter // OK: 'A'は65でbyteに表現できる
var one string = string(letter) // OK: runeを1文字のstringに変換
const text = "go"
var s string = text // OK
var bs []byte = []byte(text) // OK: stringをバイト列に変換
var first byte = text[0] // OK: 先頭バイト 'g' (103)
// var lone byte = byte(text) // コンパイルエラー: 文字列全体はbyte1個に表現できない型付き定数は宣言時点でその型に表現できる値だけが許されます。範囲外は定数宣言自体がエラーになります。
const max int8 = 127 // OK
// const bad int8 = 300 // コンパイルエラー: 300はint8に表現できない変数の型変換でも同じ判定が使われます。int8(300)のように、定数でない式を変換するときは実行時の値が範囲外でもコンパイルは通る場合がありますが、定数を変換するときはコンパイル時に表現可能性がチェックされます。
メソッド集合
表現可能性が定数の値の範囲を見るのに対し、メソッド集合は型ごとにコンパイラが呼び出し可能とみなすメソッドの一覧を見ます。インターフェース変数に何を入れられるかは、この一覧で決まります。
値型とポインタ型で一覧が違う
type Box struct{ size int }
func (b Box) Size() int { return b.size } // 値レシーバ
func (b *Box) Grow(n int) { b.size += n } // ポインタレシーバ
// Boxのメソッド集合: Sizeだけ
// *Boxのメソッド集合: SizeとGrow*Boxのメソッド集合はBoxのものを常に含みます。値レシーバのメソッドはポインタ型の一覧にも入るためです。
| 型 | メソッド集合に含まれるメソッド |
|---|---|
値型T | レシーバがTのメソッド |
ポインタ型*T | レシーバがTまたは*Tのメソッド |
インターフェースに入れられるか
Boxと*Boxでは一覧が違うので、要求するメソッドによって代入可否が変わります。代入する値の型(Boxか*Boxか)のメソッド集合を求め、インターフェースが要求するメソッドがすべて含まれるかを見ます。
type Measurer interface { Size() int }
type Expander interface { Grow(int) }
var m Measurer = Box{} // OK: BoxにSizeがある
// var e Expander = Box{} // コンパイルエラー: BoxにGrowがない
var e Expander = &Box{} // OK: *BoxにGrowがある呼び出しと代入では判定が違う
変数bからGrowを呼ぶとき、bのアドレスが取得できる(&bが書ける)なら、コンパイラは(&b).Grow(...)に置き換えます。リテラル、関数の戻り値、boxes["a"]のようなmapのインデックス式はアドレスが取得できないため、置き換えは使われません。
var b Box
b.Grow(1) // OK: 変数はアドレスが取得できるので(&b).Grow(1)と解釈される
// Box{}.Grow(1) // コンパイルエラー: リテラルはアドレスが取得できない
(&Box{}).Grow(1) // OK: &Box{}でリテラルから*Boxを作れる(変数に入れないので変更は捨てられる)
boxes := map[string]Box{"a": {}}
// boxes["a"].Grow(1) // コンパイルエラー: mapのインデックス式はアドレスが取得できない
b = boxes["a"]
b.Grow(1)
boxes["a"] = b // 変数に取り出して変更し、mapへ戻すインターフェースに入れるときは置き換えは行われず、入れる値の型Boxのメソッド集合だけで判定します。インターフェース型のメソッド集合は、宣言されたメソッドと埋め込まれたインターフェースのメソッドの和集合です。
// var e Expander = b // コンパイルエラー: 入れる値の型はBoxのまま