Go言語 インターフェイスとポインタの違い

Go言語ではインターフェイスとポインタの違いについて
ちゃんと理解しておく必要があります。

Go言語にはインターフェイス値がある

C/C++を勉強した人にとってはポインタは理解できていると思いますが、Go言語にはインターフェイスというものも登場します。

C++でももちろん、interfaceというのは出てきますが、
C++の印象とはちょっと違ったものになっています。

Go言語がちょっとだけ違うのは、
インターフェイス値というものがあることです。

Goのインターフェイスは、実際には型情報と値情報の2つをもった構造体になっています。
その構造体に、型情報と値情報を動的に代入して扱っているのです。

ポインタとは

ポインタは、値のメモリ空間を指し示す番地。

ポインタは、
プログラム実行時にメモリ空間を指し示す数字(番地)しか持ちません。

i1 := 123
fmt.Printf("%p\n", &i1)
// ==> 0xc00009a728

i2 := 456
fmt.Printf("%p\n", &i2)
// ==> 0xc00009a730

型情報自体はソースコード中の宣言で網羅されている為、
コンパイラは、コンパイル時にコード中の型情報をもとにポインタが適切な扱い方をされているのかチェックします。
そのため、プログラム実行時には実データの番地さえわかればいいというわけです。

ポインタの型チェック

次に示すのは、ポインタはコンパイル時に型チェックされていることを確認する例です。

// intのポインタであると宣言している
var ptr *int

// 型情報が合っているのでもちろんOK
intValue := 123
ptr = &intValue

// コンパイル時に型情報が違うことがわかるのでエラーにしてくれる
strValue := "hoge"
ptr = &strValue

cannot use &strValue (type string) as type int in assignment

C/C++ の場合は、上記のようなものでもキャストすれば全然代入できたんですが、Go言語では厳密にチェックされています。

インターフェイスとは

インターフェイスは抽象型のことである。
実装メソッドを宣言だけしている型になります。

このインターフェイスには、こういったメソッドがある!
とだけ宣言している型です。

インターフェイス値

インターフェイス値とは、動的な型情報と値情報をもった構造体。
インターフェイス宣言した抽象型の値にあたるものです。

実装クラスをインスタンス化したものを入れる事ができる構造体と考えるとイメージしやすいでしょう。

*実際にはitabという構造体がそれにあたりますが、実はもうちょっと追加で情報を保持しています。

動的に型情報・値情報が入る

次に示すのは、
インターフェイス値に型情報と値情報が動的に代入されていく例です。

// 空インターフェイスで宣言なんでも代入できる
var i interface{}

// type=int, value=123
i = 123
fmt.Printf("%T: %+v\n", i, i)
// ==> int: 123

// type=string, value="hoge"へのポインタ
i = "hoge"
fmt.Printf("%T: %+v\n", i, i)
// ==> string: hoge

// type=Person, value=Person{Name: "sam", Age: 16}をコピーした実態へのポインタ
i = Person{Name: "sam", Age: 16}
fmt.Printf("%T: %+v\n", i, i)
// ==> main.Person: {Name:sam Age:16}

// type=*Person, value=Person{Name: "jessy", Age: 18}へのポインタ
i = &Person{Name: "jessy", Age: 18}
fmt.Printf("%T: %+v\n", i, i)
// ==> *main.Person: &{Name:jessy Age:18}

上記のinterface{} というのは、いわゆる空インターフェイス型です。
メソッド実装をなにも宣言していないインターフェイスの為、どのような型でも格納することができます。

自動変換される代入

インターフェイス値への代入時には、Go言語が自動的に
iに対して型情報と値情報を動的に代入しているのです。

これは、関数の引数がインターフェイス型の引数であっても同様の理屈で処理されています。
値やポインタをそのまま利用しているわけではなく、
新しく生成されたインターフェイス値に対して、型情報と値情報が自動的に代入されているのです。

構造体をインターフェイス値に代入した場合

また、インターフェイス値には
構造体もポインタも代入することができますが、構造体をそのまま代入した際には内部でコピーが生成されてその参照であるポインタをインターフェイス値が保持している事を理解しておきましょう。

// 構造体のインスタンス化
p := Person{Name: "sam", Age: 16}

// 構造体のままインターフェイスに代入
//(構造体のコピーが生成される)
var i interface{} = p
fmt.Printf("%T: %+v\n", i, i)
// ==> main.Person: {Name:sam Age:16}

// もとの構造体を書き換える
p.Age = 36

// インターフェイス値にはコピーが渡っているので、変わらない。
fmt.Printf("%T: %+v\n", i, i)
// ==> main.Person: {Name:sam Age:16}

同じ例で今度はポインタをインターフェイス値に代入した例です。

// 構造体のインスタンス化
p := Person{Name: "sam", Age: 16}

// 構造体のポインタをインターフェイスに代入
var i interface{} = &p
fmt.Printf("%T: %+v\n", i, i)

// もとの構造体を書き換える
p.Age = 36

// 同じポインタアドレスなので、反映されている。
fmt.Printf("%T: %+v\n", i, i)
// ==> *main.Person: &{Name:sam Age:36}

同じ構造体のポインタアドレスを保持している為、インターフェイス値からも変更が反映されているのがわかります。

まとめ

インターフェイスはポインタとは異なる。

実際使うときには、ポインタと同じようにメソッドをコールしたりするだけなので、ポインタと同じような感覚になりますが、全然異なるもので、実態はインターフェイス値という構造体である。

インターフェイスは動的に型が変更できる。

インターフェイス値の実態は、型情報と値情報を保持した構造体の為、動的に型情報を変更することができる。
様々な型を受け取るような関数引数ができるのはその仕組のおかげ。

1件のコメント

コメントを残す