基本型
数値型
整数型には符号付きのint8、int16、int32、int64、int、符号なしのuint8、uint16、uint32、uint64、uint、uintptrがあります。
浮動小数点はfloat32とfloat64です。複素数はcomplex64とcomplex128です。
整数リテラルは10進、2進(0b/0B)、8進(0/0o/0O)、16進(0x/0X)を書けます。可読性のため桁の間に_を入れられます。
decimal := 42
binary := 0b101010
octal := 0o52
hex := 0x2A
withSep := 1_000_000浮動小数点リテラルは小数点表記や指数表記を取れます。虚数リテラルは末尾にiを付けます。1のような整数リテラルは、代入先がfloat64やfloat32ならその浮動小数点型として解釈されます。
f := 3.14159
e := 1.5e-10
imag := 2i
var x float64 = 1 // 1はfloat64になる
var y float32 = 2 // 2はfloat32になる論理型
boolはtrueとfalseのみを取ります。
文字と文字列
byteはuint8のエイリアス、runeはint32のエイリアスです。エイリアスなのでそれぞれuint8、int32と同一型です。runeはUnicodeコードポイントを表すときに使います。
ルーンリテラルはシングルクォートで囲み、1つのUnicodeコードポイントを表します。
r := 'あ'
newline := '\n'stringはイミュータブルなバイト列です。
文字列リテラルはダブルクォートまたはバッククォートで囲みます。バッククォート文字列は改行を含む生の文字列です。
s := "hello"
raw := `line1
line2`文字列はrangeでルーン単位に走査できます。
for i, r := range "Go言語" {
fmt.Printf("%d: %c\n", i, r)
}
// 0: G
// 1: o
// 2: 言
// 5: 語 // 日本語はUTF-8で1文字が複数バイトのためインデックスが飛ぶエイリアスなので、%Tはuint8とint32を表示します。
s := "Go言語"
b := []byte(s)
fmt.Println(b) // [71 111 232 168 128 232 170 158]
fmt.Println(string(b)) // Go言語
var ch byte = 'A'
var r rune = '語'
fmt.Printf("%T %c\n", ch, ch) // uint8 A
fmt.Printf("%T %c\n", r, r) // int32 語[]byte(s)とstring(b)でバイト列と文字列を相互変換できます。文字列はイミュータブルなので要素を書き換えられません。[]byteは要素を書き換えられます。変換で得たスライスを書き換えても、変換元の文字列は変わりません。[]byteから得た文字列も、その後スライスを書き換えても変わりません。
s := "hello"
b := []byte(s)
b[0] = 'J'
fmt.Println(s) // hello
fmt.Println(string(b)) // Jello
b2 := []byte{'h', 'i'}
t := string(b2)
b2[0] = 'H'
fmt.Println(t) // hi
fmt.Println(string(b2)) // Hi型定義と型エイリアス
typeで新しい名前を付けられます。
type Celsius float64 // 型定義: Celsiusはfloat64とは別の型
type ID = int // 型エイリアス: IDとintは同一型型定義は新しい型を導入し、基底型が同じでも代入には変換が必要な場合があります。型エイリアスは既存の型に別名を付けるだけです。
型変換
型付きの値どうしに暗黙変換はありません。異なる型の値を使うときはfloat64(n)のように明示的に変換します。非型付き定数は、代入先の型で表現できるなら変換なしで使えます。
var i int = 42
f := float64(i)
type Celsius float64
var c Celsius = 100 // OK: 非型付き定数100はCelsiusとして使える
var t float64 = float64(c)
// var x float64 = i // NG: 型付きのintはfloat64へ代入できないポインタ型
ポインタ型*Tは、型Tの値が格納されているメモリ上の位置であるアドレスを保持する型です。例えばint型のポインタ型は*int、配列[3]intなら*[3]intと表記します。元の型Tとポインタ型*Tは別の型であり、int型の変数に*intの値を直接代入することはできません。
未初期化のポインタのゼロ値はnilです。nilは有効なアドレスを指していない状態を表し、この状態で間接参照を行うと実行時パニックが発生します。また、Go言語にはポインタに整数を加減算するようなポインタ演算(p + 1など)はありません。
var p *int // ゼロ値はnil
fmt.Println(*p) // panic: nilポインタの間接参照アドレス取得と間接参照
&xは変数などのアドレスを取得し、型*Tのポインタを生成します。*pは間接参照(デリファレンス)であり、ポインタpが指すアドレスの実体を表します。*pを評価すると参照先の値を取得でき、*pへ代入するとその参照先の値を直接書き換えられます。
x := 42
p := &x
fmt.Println(*p) // 42
*p = 100
fmt.Println(x) // 100ポインタ変数を代入するとアドレス値がコピーされ、双方が保持する参照先が同じになります。
x := 42
p := &x
q := p
*q = 7
fmt.Println(x, *p, *q) // 7 7 7ポインタ同士の比較(==)は、同じ変数を指しているか、または両方がnilの場合にtrueになります。参照先の値が等しくても、別の変数を指していればfalseです。
a, b := 1, 1
p, q := &a, &a
r := &b
fmt.Println(p == q) // true(同じ変数a)
fmt.Println(p == r) // false(値は同じでも別の変数)サイズが0の型(空の構造体struct{}や長さ0の配列など)では、別々の変数でも同じアドレスになることがあり、その場合の==の結果は仕様上どちらでもよいとされています。
var emptyA, emptyB struct{}
pa, pb := &emptyA, &emptyB
fmt.Println(pa == pb) // 結果は仕様で決まっていないアドレス可能な式
&xでアドレスを取得できるのは、xがアドレス可能な式(addressable)の場合に限られます。変数、間接参照(*p)、スライスの要素、アドレス可能な構造体のフィールド、アドレス可能な配列の要素が該当します。配列・スライス・マップ・構造体の複合リテラルも例外としてアドレスを取得できます。
一方、数値や文字列などの基本リテラル、定数(const)、マップの要素、関数の戻り値などはアドレスを取得できません。
n := 10
p := &n // OK: 変数
q := &(*p) // OK: 間接参照(pは非nil。同じ変数nを指す)
nums := []int{1, 2}
r := &nums[0] // OK: スライスの要素
arrLit := &[3]int{1, 2, 3} // OK: 配列の複合リテラル
sliceLit := &[]int{1, 2} // OK: スライスの複合リテラル
mapLit := &map[string]int{"a": 1} // OK: マップの複合リテラル
// &10 // NG: 基本リテラルはアドレスを取れない
const c = 10
// &c // NG: 定数はアドレスを取れない
m := map[string]int{"a": 1}
// &m["a"] // NG: マップの要素はアドレスを取れない
// &time.Now() // NG: 関数の戻り値はアドレスを取れない
// var np *int
// _ = &(*np) // NG: nilの間接参照はパニック値渡しとポインタ
Go言語の関数呼び出しは常に値渡しです。引数に変数を渡すとその値のコピーが渡るため、関数内で引数を書き換えても呼び出し元の変数は変化しません。
呼び出し元の変数を直接書き換えたい場合は、ポインタを渡します。ポインタを渡す場合もアドレス値自体はコピーされますが、参照先の実体が共有されるため、間接参照(*n)を経由して呼び出し元の値を更新できます。
func setToZero(n int) { n = 0 } // コピーされた引数nのみが0になる
func setToZeroPtr(n *int) { *n = 0 } // *n経由で呼び出し元の変数を0にする
count := 5
setToZero(count)
fmt.Println(count) // 5
setToZeroPtr(&count)
fmt.Println(count) // 0newによる割り当て
組み込み関数newは引数として型または式を1つ受け取り、初期化した変数へのポインタを返します。new(x)を使うと、中間変数を置かずに値xへのポインタを1つの式で得られます。
型Tを渡したnew(T)は、型Tをゼロ値で初期化した変数を確保し、そのポインタ*Tを返します。式xを渡したnew(x)は、式xの型の変数をxの値で初期化して確保し、そのポインタを返します。非型付き定数を渡した場合はそのデフォルト型(new(42)なら*int)になります。事前宣言された識別子nilはnewの引数にできません。
// 型を渡すとゼロ値へのポインタを返す
pZero := new(int)
fmt.Println(*pZero) // 0
// 式を渡すとその値で初期化されたポインタを返す
pVal := new(42)
fmt.Println(*pVal) // 42
// new(nil) // NG: nilはnewの引数にできないスライス、マップ、チャネルに対してnewを使うと、ゼロ値(nil)を指すポインタが返ります。スライスのゼロ値はappendできますが、戻り値を代入し直す必要があります。マップのゼロ値への要素代入はパニックになります。チャネルのゼロ値への送受信は、そのゴルーチンでは完了せず永久に待ち続けます。nilチャネルをcloseするとパニックになります。
送受信できるチャネルと、指定した長さや容量を持つスライスを作るには、内部データ構造を初期化した値そのものを返すmakeを使います。makeの戻り値はポインタではありません。
pMap := new(map[string]int) // *map[string]int(*pMapはnil)
// (*pMap)["key"] = 1 // NG: nilマップへの代入でパニック
m := map[string]int{} // リテラルで初期化したら書き込み可能
m["key"] = 1
pSlice := new([]int) // *[]int(*pSliceはnil)
*pSlice = append(*pSlice, 1) // OK: 戻り値を代入すればappendできる
s := make([]int, 0, 5) // []int(指定したlenとcap)
ch := make(chan int) // chan int(送受信できる)構造体型
構造体は、名前と型を持つフィールドを並べた型です。フィールドは宣言した順に並び、名前は重複できません。同じ型のフィールドはX, Y float64のように1行にまとめられます。フィールド名も識別子なので、先頭が大文字なら他パッケージから参照できます。フィールドにアクセスするときは、値.フィールド名の形式で書きます。
構造体型の変数を定義して何も代入しない場合、各フィールドはゼロ値で埋まります。
type Point struct {
X, Y float64
}
var p Point
p.X = 1
fmt.Println(p.X) // 1_はブランク識別子で、フィールド名にも使えますが、名前として参照できないためp._とは書けません。同じ構造体に何度でも書くことができ、領域だけ確保するパディングの用途で使用できます。
type Header struct {
Flag uint8
_ [3]byte
Size uint32
}構造体リテラル
フィールドをまとめて初期化するときは構造体リテラルを使います。
p1 := Point{1, 2} // Pointの値
p2 := Point{X: 1, Y: 2} // Pointの値
p3 := &Point{X: 1, Y: 2} // *Point
// p3と同じ意味
p := Point{X: 1, Y: 2}
p4 := &p // *Pointp1とp2は構造体型の値です。p3は構造体リテラルのアドレスをその場で取った*Pointです。pを用意してp4 := &pとする書き方も同じ意味で、p3とp4は同じ型・同じ内容のポインタになります。
*Pointを引数に取る関数へ渡すときや、構造体のフィールドがポインタ型のときに&T{...}がよく使われます。ゼロ値へのポインタだけ欲しいときはnew(Point)も使えますが、フィールドを初期値で埋めたいときは&Point{X: 1, Y: 2}の方が意図が読み取りやすいです。
埋め込みによる合成
フィールドに別の型名を書くと埋め込みになります。埋め込みは継承ではなく合成です。埋め込まれたフィールドやメソッドは外側に昇格し、c.Powerのように直接アクセスできます。
type Engine struct{ Power int }
type Car struct {
Engine
Model string
}
c := Car{Engine: Engine{Power: 150}, Model: "compact"}
fmt.Println(c.Power) // 昇格比較
すべてのフィールドの型が比較可能なときだけ==と!=が使えます。スライス、マップ、関数を含む構造体は比較できません。等価なのは、型が同じで、対応する_以外のフィールドがすべて等しいときです。
a := Point{X: 1, Y: 2}
b := Point{X: 1, Y: 2}
fmt.Println(a == b) // true
c := Point{X: 1, Y: 3}
fmt.Println(a == c) // false
type Vertex struct {
X, Y float64
}
// fmt.Println(a == Vertex{X: 1, Y: 2}) // コンパイルエラー: 型が違う
fmt.Println(a == Point(Vertex{X: 1, Y: 2})) // true
type Record struct {
Tags []string
}
var r1, r2 Record
// fmt.Println(r1 == r2) // コンパイルエラー: 比較できないフィールドがある配列
配列[n]Tは、型Tの要素を固定数n個並べたデータ構造です。要素数nは型情報の一部であり、[3]intと[4]intは異なる型として扱われます。型が異なるため、相互に代入することはできません。
var a [3]int
// var b [4]int = a // コンパイルエラー: 型が異なるGo言語の配列は値型です。配列を変数に代入したり関数へ引数として渡したりすると、すべての要素が丸ごとコピーされます。
arr := [3]int{1, 2, 3}
dup := arr // 配列全体のコピー
dup[0] = 0
fmt.Println(arr[0], dup[0]) // 1, 0(元の配列arrは変わらない)配列の要素数が多い場合、丸ごとのコピーはオーバーヘッドになります。また、長さが型に固定されているため、要素数が可変のデータを扱う用途には適していません。そのため、Go言語では可変長データを扱うコレクションとして主にスライス[]Tを用います。
スライス
スライス([]T)は、配列の連続した領域を参照する可変長のデータ構造です。
スライス値の実体は、元配列へのポインタ、参照している要素数(len)、参照可能な最大容量(cap)の3つのフィールドからなるヘッダです。capは再割り当てなしに要素を追加できる上限を表します。
スライスを変数へ代入したり関数へ渡したりした際にコピーされるのは、この小さなヘッダのみです。元配列の実体は共有されるため、一方のスライスを経由して要素を書き換えると、同じ元配列を参照している他のスライスにも変更が反映されます。
スライスはリテラルや組み込み関数makeで作成できます。
nums := []int{1, 2, 3}
buf := make([]byte, 0, 64) // len=0, cap=64s[low:high]というスライス式は、元配列の一部を参照する新しいスライスを生成します。スライス自体は別個に作られますが、要素が格納されている元配列は共有されます。例えばall[low:high]と書くと、インデックスlow以上high未満の要素を切り出します。
all := []int{0, 1, 2, 3, 4}
mid := all[1:4] // midは[1, 2, 3]、len=3
mid[0] = 99
fmt.Println(all) // [0 99 2 3 4](元配列が書き換わるためall側にも反映される)s[low:high]で生成されたスライスの容量はcap(元) - lowです。元のallのcapが5、lowが1の場合、切り出されたmidのcapは4になります。
fmt.Println(len(mid), cap(mid)) // 3, 4s[low:high:max]というスライス式を使うと、スライスの容量をmax - lowに明示的に制限できます。部分スライスが元配列の空き容量を意図せず上書きするのを防ぎたい場合に有効です。
fullCap := all[1:4:4] // len=3, cap=3(max - low = 4 - 1)
fmt.Println(len(fullCap), cap(fullCap)) // 3, 3スライスに対してappendを実行した際の挙動は、そのスライスのcapに空きがあるかによって異なります。capに空きがある場合、新しい配列は確保されず、共有している元配列へ直接値が書き込まれます。元配列の要素に既に値がある場合は、その要素の値が上書きされます。一方、capに空きがない場合、別のメモリ領域に新しい配列を丸ごと確保し、既存の要素をコピーした上で末尾に追記します。部分スライスの参照先はこの新しい配列を指すように切り替わるため、元のスライスや元配列の内容は書き換えられず、参照関係が切り離されます。
all := []int{0, 1, 2, 3, 4}
wide := all[1:4] // len=3, cap=4(元配列のインデックス4に既に値がある)
wide = append(wide, 9)
fmt.Println(all) // [0 1 2 3 9](元配列のall[4]が9で上書きされる)
all = []int{0, 1, 2, 3, 4}
tight := all[1:4:4] // len=3, cap=3(空き容量なし)
tight = append(tight, 9) // 新しい配列が別個に確保されてコピーされ、tightの参照先が切り替わる
fmt.Println(all) // [0 1 2 3 4](元のallや元配列は影響を受けない)スライス間の要素共有を切り離したい場合は、makeで独立した元配列を確保し、組み込み関数copyで要素を複製します。
all := []int{0, 1, 2, 3, 4}
mid := all[1:4] // [1, 2, 3]
dst := make([]int, len(mid)) // 独立した新しい元配列を確保
copy(dst, mid) // 要素をコピー
dst[0] = 0
fmt.Println(dst[0], mid[0]) // 0, 1(dstの変更はmidやallに影響しない)未初期化のnilスライスであっても、lenやcapの取得、appendによる要素追加は正常に動作します。
var s []int
fmt.Println(s == nil, len(s), cap(s)) // true 0 0
s = append(s, 10, 20)
fmt.Println(s, s == nil) // [10 20] falseappendは要素を追加した新しいスライスを返すため、戻り値を変数へ代入し直さないと長さ(len)の変更が反映されません。
nums := []int{1, 2, 3}
nums = append(nums, 4, 5) // [1, 2, 3, 4, 5]
s := []int{1}
append(s, 2) // 戻り値を捨てるとsのlenは1のまま
s = append(s, 2) // 再代入することでlen=2になる
v := []int{1}
t := v
t = append(t, 99) // 変数tのみlen=2に更新
fmt.Println(len(v), len(t)) // 1, 2(vは変化しない)関数へ引数としてスライスを渡す場合も、値渡しによりスライスの情報がコピーされます。関数内でappendを実行しても、呼び出し元のスライス変数のlenは変化しません。ただし、capに空きがあって元配列の再割り当てが発生しなかった場合は、元配列の内容自体が関数内から書き換えられることがあります。
func grow(s []int) {
s = append(s, 99) // 関数内の変数sのみlen=2になる
}
data := make([]int, 1, 2) // len=1, cap=2
grow(data)
fmt.Println(len(data)) // 1(呼び出し元のスライスのlenは不変)
fmt.Println(data[1]) // 99(capに空きがあったため元配列には書き込まれている)
narrow := []int{1} // len=1, cap=1
grow(narrow)
fmt.Println(len(narrow), narrow) // 1, [1](cap不足で別配列に再割り当てされたため影響なし)スライスを入れ子にすることで、多次元データ構造を表現できます。
rows, cols := 2, 3
matrix := make([][]int, rows)
for i := range matrix {
matrix[i] = make([]int, cols)
}スライスをfor-rangeで走査する際、ループ内で要素を追加してスライス自体の長さを変更すると、意図しない無限ループや要素の読み飛ばしにつながる恐れがあります。走査対象のスライス自体をループ内で変更することは避けてください。
// 避けるべき例: 走査中にスライスを変更する
s := []int{1, 2, 3}
for i, v := range s {
s = append(s, v) // 反復対象や終了条件が不定になる
}マップ
マップ(map[K]V)は、キーと値を結びつけるハッシュテーブルです。
キー型Kには、比較演算子==および!=が定義されている型(comparableな型)のみ指定できます。スライス、マップ、関数型は比較できないためキーに指定できません。構造体は、すべてのフィールドの型が比較可能である場合に限りキーとして使用できます。
makeまたはリテラルを用いて初期化し、キーを指定して要素の読み書きを行います。
scores := make(map[string]int) // map[string]int{"go": 90} でも初期化可能
scores["go"] = 90
scores["rust"] = 85キーの存在確認には2値受け取りを用います。指定したキーが存在しない場合、2番目の戻り値okがfalseとなり、1番目の戻り値には値型のゼロ値が返ります。
fmt.Println(scores["go"]) // 90
v, ok := scores["java"]
fmt.Println(v, ok) // 0, false(キーが存在しない)不要になったキーは組み込み関数deleteで削除します。
delete(scores, "go")
fmt.Println(scores["go"], scores["rust"]) // 0, 85未初期化のマップ変数はnilです。nilマップからの読み取りやdeleteの実行はパニックになりませんが、要素を書き込もうとすると実行時パニックが発生します。
var m map[string]int
// m["k"] = 1 // panic: nilマップへの書き込み
v, ok := m["k"] // v=0, ok=false(読み取りは安全)
delete(m, "k") // キーが存在しなくても安全マップをfor-rangeで走査する際の順序は、言語仕様として保証されていません。実装上も走査ごとに順序がランダム化され、プログラムが特定の反復順序に依存しないよう設計されています。
scores := map[string]int{"go": 90, "rust": 85, "java": 80}
for lang, score := range scores {
fmt.Println(lang, score)
}
// 走査を実行するたびに出力順が変わることがあるキーの昇順など一定の順序で処理したい場合は、一度キーをスライスに集めてソートしてから参照します。
keys := make([]string, 0, len(scores))
for lang := range scores {
keys = append(keys, lang)
}
sort.Strings(keys)
for _, lang := range keys {
fmt.Println(lang, scores[lang]) // go, java, rustの昇順で出力
}型定義
type T Uと宣言することで、型Uと同じ性質を持つ別の型Tを定義することができます。例えば、type Celsius float64と定義したとき、Celsiusとfloat64は別の型ですが、明示的な変換をすることで同じ値を代入することができます。
var c Celsius = 36.5
var f float64 = float64(c) // OK
// var f float64 = c // コンパイルエラー型エイリアス
type T = Uで定義できる型エイリアスでは、型Tと型Uは同一型として扱うことができます。
type Count = int
var n int = 0
var c Count = n // 同一型なので明示的な型変換が不要基底型
各型には基底型があります。基底型とは、type T Uによる定義をたどったとき終端にある型のことです。
type ID int
type UserID ID // UserIDの基底型はint複合型の同一性
配列・スライス・構造体・ポインタ・関数・インターフェース・マップ・チャネルといった複合型では、種類と内部の型構成が一致しなければ同一型とはみなされません。配列では要素型に加えて長さも型の一部になり、構造体ではフィールドの名前・型・並びが全て一致している必要があります。
[]intと[]intは同一型[]intと[3]intは別の型type A intとtype B intは別の型struct{ x int }とstruct{ y int }は別の型Stack[T any]や[]Tのように型パラメータTを含む型は、具体的な型引数を与えてインスタンス化したあとに同一型かどうか決まる。例えば、型引数を与えた結果Stack[int]とStack[string]となれば別の型になります。
代入可能性
値vを型Tの変数に代入できるかは、型の関係をもとにコンパイル時に判定されます。
同一型どうしはそのまま代入可能です。
var a int = 1
var b int = a基底型が同一の型定義どうしは、明示的に変換することで代入可能です。
var c Celsius = 36.5
var f float64 = float64(c)nilはポインタ、スライス、マップ、チャネル、関数、インターフェースに代入可能です。
var ptr *int = nil
var slice []int = nil
var m map[string]int = nil
var ch chan int = nil
var fn func() = nil
var i any = nilインターフェース変数には、そのインターフェースを実装する非インターフェース型の値を代入可能です。
import (
"errors"
)
// 組み込みのerrorインターフェースを実装した非インターフェース型が返る
var err error = errors.New("something went wrong")表現可能性
定数の型変換と代入において、値が型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) // NG: 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) // NG: 文字列全体はbyte1個に表現できない型付き定数は宣言時点でその型に表現できる値だけが許されます。
const max int8 = 127 // OK
// const bad int8 = 300 // NG: 300はint8に表現できない変数の型変換でコンパイルが通るかは、型の互換性によって決まります。値が範囲がだったとしても、float64からintのようにどちらも数値型の場合はコンパイルエラーは発生しません。実行時に切り捨てやオーバーフローが発生します。stringからintのような変換は互換性がないためコンパイルエラーになります。
// 定数の場合はコンパイルエラー
// const x float64 = 1.5
// var y int = int(x)
// 変数の場合はコンパイル可能
var n float64 = 1.5
var m int = int(n) // 切り捨てられて1になるインターフェース型
インターフェース型は、具体的な型を指定せず呼び出せるメソッドの条件だけを書いた型です。条件を満たす値なら、中身の型が違っても同じ変数に入れられます。
type Shape interface {
Area() float64
}メソッドを1つも宣言しないインターフェースは空インターフェースと呼ばれ、任意の型(インターフェース型を含む)を格納できます。anyは空インターフェースの別名です。
インターフェース型の変数の中身
インターフェース型の変数には、コンパイル時に決まる静的型と、実行時に決まる動的型・動的値が入ります。
静的型(static type)
コンパイル時に確定する型のことです。var a Shapeと定義されていればShapeが静的型です。実行中にaの中身が*Rectでも別の型でも、静的型はShapeのままです。
動的型(dynamic type)
実行時にそのインターフェースの中に実際に格納されている具体的な型のことです。aに*Rectを入れれば動的型は*Rectとなり、別の値を入れ直せば動的型も変わります。何も代入されていない初期状態では動的型も nil です。
動的値(dynamic value)
実行時にそのインターフェースの中に実際に格納されているデータのことです。*Rectを入れていれば、データが格納されているアドレスへのポインタ(またはnil)が動的値です。
暗黙の実装
オブジェクト指向な言語とは異なり、implementsのような宣言はありません。型がインターフェースが要求するメソッドをすべて備えていれば、宣言なくそのインターフェースを実装したとみなされます。
type Shape interface {
Area() float64
}
type Rect struct{ W, H float64 }
func (r Rect) Area() float64 {
return r.W * r.H
}
var a Shape = Rect{W: 3, H: 4} // RectがShapeのメソッドを持つので代入可能値型Tのメソッド集合にはレシーバがTのメソッドが入ります。ポインタ型*Tのメソッド集合には、レシーバがTまたは*Tのメソッドが入ります。次の例では、値型Rectのメソッド集合にはAreaだけが含まれ、ポインタ型*Rectのメソッド集合にはAreaとScaleが含まれます。
type Rect struct{ W, H float64 }
func (r Rect) Area() float64 { return r.W * r.H }
func (r *Rect) Scale(f float64) { r.W *= f; r.H *= f }メソッド集合が異なるため、要求するメソッドによって代入可否が変わります。
type Shape interface { Area() float64 }
type Scaler interface { Scale(float64) }
var a Shape = Rect{} // OK: RectにAreaがある
// var s Scaler = Rect{} // NG: RectにScaleがない
var s Scaler = &Rect{} // OK: *RectにScaleがある埋め込みによって埋め込まれた型のメソッドは外側の型のメソッド集合に入ります。
type Shape interface { Area() float64 }
type Rect struct{ W, H float64 }
func (r Rect) Area() float64 { return r.W * r.H }
type Frame struct {
Rect
}
var a Shape = Frame{} // OK: AreaがFrameのメソッド集合に入るnilの扱い
インターフェース型の変数では、動的型と動的値の両方がnilのときだけnilと等価になります。
インターフェース型の変数のゼロ値は、動的型も動的値もnilです。
var a Shape
fmt.Println(a == nil) // truenilポインタをインターフェースに入れると、ポインタ自体はnilでもインターフェースとしては空ではないためnilと等価になりません。
var p *Rect
fmt.Println(p == nil) // true: ポインタ変数としてはnil
var a2 Shape = p
fmt.Println(a2 == nil) // false: 動的型*Rectがあるインターフェースの埋め込み
インターフェース定義に別のインターフェース型名を並べると埋め込みになります。外側のインターフェースは、内側のインターフェースが要求するメソッドをすべて引き継ぎます。
type Shape interface {
Area() float64
}
type Perimer interface {
Perimeter() float64
}
type Measurer interface {
Shape
Perimer
}
type Rect struct{ W, H float64 }
func (r Rect) Area() float64 { return r.W * r.H }
func (r Rect) Perimeter() float64 { return 2 * (r.W + r.H) }
type Circle struct{ R float64 }
func (c Circle) Area() float64 { return 3.14159 * c.R * c.R }
var m Measurer = Rect{} // OK
var a Shape = Rect{} // OK
var p Perimer = Rect{} // OK
// var c Measurer = Circle{} // NG: Perimeterがない型アサーション
インターフェース値から具体的な値を取り出すには型アサーションを使います。s, ok := v.(T)の2値形式で取り出し、動的型がTでなければokがfalseになります。
var v any = Rect{W: 3, H: 4}
r, ok := v.(Rect)
if !ok {
// 動的型がRectでない
return
}
// ここではrをRectとして使えるokを受け取らないs := v.(T)は、動的型がTでないときにパニックになります。ユーザー入力や外部データから復元するときは、必ず2値形式で分岐します。
複数の動的型を分岐するときはswitch v := x.(type)の型スイッチが使えます。動的型に応じてcaseが選ばれ、どのcaseにも当てはまらなくてもパニックになりません。
func show(x any) {
switch v := x.(type) {
case Rect:
fmt.Println("rect:", v)
case Circle:
fmt.Println("circle:", v)
default:
fmt.Println("other type")
}
}インターフェース実装の確認
コンパイラは代入が起きたときにメソッド集合を見てインターフェースを満たすか判断します。型定義の近くにインターフェース型の変数への代入を書いておくと、メソッドの追加漏れやレシーバ型の取り違えをビルド時に検知できます。
type Shape interface { Area() float64 }
type Scaler interface { Scale(float64) }
type Rect struct{ W, H float64 }
func (r Rect) Area() float64 {
return r.W * r.H
}
func (r *Rect) Scale(f float64) {
r.W *= f
r.H *= f
}
var _ Shape = Rect{} // OK
// var _ Scaler = Rect{} // NG: RectにはScaleがない
var _ Scaler = (*Rect)(nil) // OK左辺の_は値を使わない時に用いる識別子です。(*Rect)(nil)は実体を作らず、右辺の型が*Rectであることだけをコンパイラに伝えます。
型パラメータと制約
関数や型の名前の直後に角括弧で型パラメータと制約を宣言することで、具体的な型に依存しない汎用的なコードを記述できます。たとえば[T any]と書くと、Tが型パラメータ、anyが制約を表します。型パラメータは呼び出し時やインスタンス化時にintなどの具体的な型に置き換わります。
制約は、型引数として受け入れ可能な型の集合を定めます。型パラメータに対して実行できる演算や呼び出せるメソッドも制約によって決まります。
func Min[T any](a, b T) T {
if a < b { // コンパイルエラー: Tがanyでは<が使えない
return a
}
return b
}順序比較演算子<や>を使うには、順序付けが可能な型を表す制約が必要です。標準ライブラリのcmpパッケージが提供するcmp.Orderedを指定します。
import "cmp"
func Min[T cmp.Ordered](a, b T) T {
if a < b {
return a
}
return b
}ジェネリックな関数を使用する際は、型パラメータに具体的な型引数を渡してインスタンス化します。関数の呼び出しでは引数の型から型引数が推論されるため、通常は型引数の指定を省略できます。
n := Min(3, 5) // 引数からTがintと推論される
m := Min[int](3, 5) // 角括弧で型引数を明示的に指定型引数はコンパイル時に確定します。インターフェースのように実行時に動的型が変化するのではなく、静的な型として扱われます。
制約の定義
型制約はインターフェースとして定義します。
type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](xs []T) T {
var total T
for _, x := range xs {
total += x
}
return total
}|は型集合の和集合を表します。チルダ記号~は基底型を指定し、~intと書くことで基底型がintであるすべての型を集合に含めます。int自身に加え、type MyInt intのように新しく定義した型もNumberを満たすため、Sum([]MyInt{1, 2, 3})のように呼び出せます。
any
組み込みのanyは、すべての型を許容する制約です。
comparable
組み込みのcomparableは、等値比較演算子==および!=が使用できる型に限定する制約です。マップのキーとして使う型パラメータなどに指定します。制約としてのみ使用でき、変数やフィールドの型としては宣言できません。
ジェネリック型
構造体などの型定義で型パラメータを宣言できます。定義した型パラメータは、フィールドやメソッドレシーバで使用します。
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}
var s Stack[int]
s.Push(10)
// s.Push("text") // コンパイルエラー: stringはintに代入できないStack[int]のように型引数を指定すると、フィールドの[]Tやメソッド引数のTがすべてintに置き換わります。
ジェネリックメソッド
Go言語のバージョン1.27以降では、メソッド名の直後にも独自の型パラメータを宣言できます。レシーバの型パラメータとは独立した型パラメータを追加できるため、別の型へ変換する処理などをパッケージレベルの関数に切り出すことなくその型のメソッドとして定義できます。
func (s Stack[T]) Map[U any](f func(T) U) Stack[U] {
items := make([]U, len(s.items))
for i, v := range s.items {
items[i] = f(v)
}
return Stack[U]{items: items}
}渡す関数の引数と戻り値から型引数Uが推論されます。
var numbers Stack[int]
numbers.Push(1)
numbers.Push(2)
// intのStackからboolのStackへ変換
flags := numbers.Map(func(v int) bool {
return v > 0
})