iface
と はeface
どちらも Go のインターフェイスを記述する基礎となる構造ですが、iface
で記述されるインターフェイスにはメソッドが含まれているのに対し、eface
はメソッドを含まない空のインターフェイスであるという違いがありますinterface{}
。
ソースコードレベルを見てみましょう。
type iface struct {
tab *itab
data unsafe.Pointer
}
type itab struct {
inter *interfacetype
_type *_type
link *itab
hash uint32 // copy of _type.hash. Used for type switches.
bad bool // type does not implement interface
inhash bool // has this itab been added to hash?
unused [2]byte
fun [1]uintptr // variable sized
}
iface
2 つのポインタが内部的に維持され、インターフェイスのタイプとこのインターフェイスに割り当てられたエンティティ タイプを表すエンティティtab
を指しますitab
。data
これは、インターフェイスの特定の値、通常はヒープ メモリへのポインタを指します。
itab
構造を詳しく見てみましょう:_type
フィールドはメモリの配置、サイズなどを含むエンティティのタイプを記述し、inter
フィールドはインターフェイスのタイプを記述します。fun
フィールドの配置と、インターフェイス メソッドに対応する特定のデータ型のメソッド アドレスは、インターフェイス呼び出しメソッドの動的ディスパッチを実装します。通常、このテーブルは、インターフェイスまたはキャッシュされた itab に値を割り当てるときに変換が発生するたびに更新されます。直接入手することになります。
ここにはエンティティ タイプとインターフェイスに関連するメソッドのみがリストされており、エンティティ タイプの他のメソッドはここには表示されません。C++ を学習している場合は、ここで仮想関数の概念を類推できます。
さらに、なぜfun
配列のサイズが 1 なのか疑問に思うかもしれません。インターフェイスで複数のメソッドが定義されている場合はどうなるでしょうか。実際、ここに格納されているのは最初のメソッドの関数ポインタであり、メソッドがさらに存在する場合は、それ以降もメモリ空間に格納され続けます。アセンブリの観点から見ると、これらの関数ポインタはアドレスを追加することで取得でき、影響はありません。なお、これらのメソッドは関数名ごとに辞書順に並べてあります。
interfacetype
インターフェースのタイプを表す typeをもう一度見てみましょう。
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}
ご覧のとおり、型をラップしています。_type
型は、_type
実際には Go 言語でさまざまなデータ型を記述する構造体です。mhdr
ここには、インターフェースによって定義された関数のリストを表し、pkgpath
インターフェースを定義するパッケージの名前を記録するフィールドもあることに気付きました。
iface
全体的な構造を示す図は次のとおりです。
eface
ソースコードを見てみましょう:
type eface struct {
_type *_type
data unsafe.Pointer
}
それに比べてiface
、eface
それは比較的単純です。維持されるフィールドは 1 つだけで_type
、空のインターフェイスによって伝送される特定のエンティティ タイプを示します。data
具体的な値について説明します。
例を見てみましょう:
package main
import "fmt"
func main() {
x := 200
var any interface{} = x
fmt.Println(any)
g := Gopher{"Go"}
var c coder = g
fmt.Println(c)
}
type coder interface {
code()
debug()
}
type Gopher struct {
language string
}
func (p Gopher) code() {
fmt.Printf("I am coding %s language\n", p.language)
}
func (p Gopher) debug() {
fmt.Printf("I am debuging %s language\n", p.language)
}
コマンドを実行し、アセンブリ言語を出力します。
go tool compile -S ./src/main.go
ご覧のとおり、main 関数で 2 つの関数が呼び出されます。
func convT2E64(t *_type, elem unsafe.Pointer) (e eface)
func convT2I(tab *itab, elem unsafe.Pointer) (i iface)
上記の 2 つの関数のパラメータとiface
構造体のフィールドeface
はリンクできます。両方の関数はパラメータを結合して组装
最終的なインターフェイスを形成します。
補足として、最後に_type
構造を見てみましょう。
type _type struct {
// 类型大小
size uintptr
ptrdata uintptr
// 类型的 hash 值
hash uint32
// 类型的 flag,和反射相关
tflag tflag
// 内存对齐相关
align uint8
fieldalign uint8
// 类型的编号,有bool, slice, struct 等等等等
kind uint8
alg *typeAlg
// gc 相关
gcdata *byte
str nameOff
ptrToThis typeOff
}
Go 言語のさまざまなデータ型は、_type
フィールドに基づいていくつかの追加フィールドを追加することで管理されます。
type arraytype struct {
typ _type
elem *_type
slice *_type
len uintptr
}
type chantype struct {
typ _type
elem *_type
dir uintptr
}
type slicetype struct {
typ _type
elem *_type
}
type structtype struct {
typ _type
pkgPath name
fields []structfield
}
これらのデータ型の構造定義は、リフレクション実装の基礎となります。
Go言語とダックタイピングの関係
Wikipedia の定義を直接見てみましょう。
アヒルのように見え、アヒルのように泳ぎ、アヒルのように鳴く場合、それはおそらくアヒルです。
翻訳: アヒルのように見え、アヒルのように泳ぎ、アヒルのように鳴く場合、それはアヒルとみなされます。
Duck Typing
ダック タイピングは動的プログラミング言語のオブジェクト推論戦略であり、オブジェクト自体の型ではなく、オブジェクトがどのように使用されるかに重点を置きます。Go 言語は静的言語として、インターフェースを介したダックタイピングを完全にサポートしています。
たとえば、動的言語 Python では、次のような関数を定義します。
def hello_world(coder):
coder.say_hello()
say_hello()
この関数を呼び出すときは、関数を実装している限り、任意の型を渡すことができます。未実装の場合、動作時にエラーが発生します。
Java や C++ などの静的言語では、このインターフェイスが必要な場所で使用する前に、インターフェイスを実装することを明示的に宣言する必要があります。プログラム内で関数を呼び出し、まったく実装されていない型をhello_world
渡しても、コンパイル段階では渡されません。say_hello()
これが、静的言語が動的言語より安全である理由です。
動的言語と静的言語の違いがここに反映されています。静的言語は、エラーが報告される前にそのコード行まで実行する必要がある動的言語とは異なり、コンパイル中に型の不一致エラーを検出できます。ちなみに、これもpython
私が使いたくない理由の一つです。もちろん、静的言語ではプログラマがコーディング段階で規定に従ってプログラムを書いたり、変数ごとにデータ型を指定したりする必要があり、ある程度の作業負荷とコード量が増加します。動的言語にはこれらの要件がないため、人々はビジネスに集中することができ、コードは短く、より速く書くことができます。Python を書く学生はこのことをよく知っています。
Go 言語は、最新の静的言語としては後発であるという利点があります。動的言語の利便性を導入し、同時に静的言語の型チェックも行うので、書いていてとても楽しいです。Go は妥協的なアプローチを採用しており、型がインターフェイスを実装していることを明示的に宣言する必要はなく、関連するメソッドが実装されている限り、コンパイラはそれを検出できます。
例を見てみましょう:
まず、インターフェイスと、このインターフェイスをパラメータとして使用する関数を定義します。
type IGreeting interface {
sayHello()
}
func sayHello(i IGreeting) {
i.sayHello()
}
2 つの構造を定義しましょう。
type Go struct {}
func (g Go) sayHello() {
fmt.Println("Hi, I am GO!")
}
type PHP struct {}
func (p PHP) sayHello() {
fmt.Println("Hi, I am PHP!")
}
最後に、main 関数で SayHello() 関数を呼び出します。
func main() {
golang := Go{}
php := PHP{}
sayHello(golang)
sayHello(php)
}
プログラム出力:
Hi, I am GO!
Hi, I am PHP!
main 関数では、sayHello() 関数を呼び出すときにgolang, php
オブジェクトが渡されます。これらのオブジェクトは、IGreeting 型の実装を明示的に宣言せず、インターフェイスで指定された SayHello() 関数のみを実装します。実際、コンパイラが SayHello() 関数を呼び出すと、golang, php
オブジェクトは暗黙的に IGreeting 型に変換されます。これは、静的言語の型チェック関数でもあります。
ところで、動的言語の特徴について触れておきます。
変数バインディングのタイプは未定義であり、実行時にのみ決定できます。
関数とメソッドは任意のタイプのパラメータを受け取ることができ、呼び出し時にパラメータのタイプはチェックされません。
インターフェイスを実装する必要はありません。
要約すると、ダックタイピングは、オブジェクトの有効なセマンティクスが特定のクラスからの継承や特定のインターフェイスの実装によって決定されるのではなく、その「現在のメソッドとプロパティのセット」によって決定される動的言語のスタイルです。Go は静的言語として、 インターフェイス を通じて実装されており鸭子类型
、実際、Go のコンパイラはその中で隠れた変換作業を実行します。