Golang インターフェイスの実装原理の分析

界面分析


この記事は go1.12.12 ソース コードの分析に基づいており、コードは amd64 マシンで実行およびデバッグされています。

1.ダックタイピング

1.1 ダックタイピングとは

1

(出典:百度百科事典)

写真の大きな黄色いアヒルはアヒルですか?伝統的な観点からは、写真の大きな黄色いアヒルはアヒルではありません。なぜなら、叫ぶことも走ることもできず、生きていないからです。

ウィキペディアから取得したアヒル型の定義を最初に見てください

アヒルのように歩き、アヒルのように鳴くなら、それはアヒルに違いない

何かがアヒルのように歩き、アヒルのように鳴くなら、それはアヒルに違いない

だから、Duck Typing視点見ると、写真の大きな黄色いアヒルはアヒルです

ダックタイピング、プログラミングにおける型推論のスタイルであり、物事の内部構造ではなく外部の振る舞いを記述します

1.2 Go言語でのダックタイピング

Go 言語は、インターフェースを通じて実装されますDuck Typing型の不一致は実行時にのみチェックできる他の動的言語とは異なり、実装するインターフェイスを明示的に宣言する必要があるほとんどの静的言語とは異なり、Go 言語インターフェイスは、それらが隐式实现

2. 概要

2.1 インターフェースタイプ

インターフェイスとは抽象类型、含まれるデータのレイアウトや内部構造を公開しないインターフェイスの一種であり、もちろんデータの基本的な操作はなく、一部のメソッドのみが提供されます。インターフェイス型の変数を取得すると、それが何であるかを知る方法はありませんが、何ができるか、より正確には、それが提供するメソッドだけを知ることができます

2.2 インターフェース定義

Go 言語はinterfaceキーワード、インターフェイスは実装が必要なメソッドのみを定義でき、変数を含めることはできません

type 接口类型名 interface{
    
    
    方法名1( 参数列表1 ) 返回值列表1
    方法名2( 参数列表2 ) 返回值列表2}

たとえば、io.Writerタイプは実際にはインターフェースタイプです

type Writer interface {
    
    
    Write(p []byte) (n int, err error)
}

次のように、新しいインターフェイスをインターフェイス間でネストできますio.ReadWriter

type Reader interface {
    
    
    Read(p []byte) (n int, err error)
}

type ReadWriter interface{
    
    
    Reader
    Writer
}

メソッドを含まないインターフェースは、空のインターフェース型と呼ばれます

interface{
    
    }

2.3 インターフェースの実装

インターフェイスに必要なすべてのメソッドを実装する場合、具象型はインターフェイスを実装します。式がインターフェイスを実装する場合、式はインターフェイスにのみコピーできます

次の例では、メソッドを 1 つだけ含むRunnerインターフェイスrun()Person構造体がRun()メソッドをと、Runnerインターフェイスが実装されます。

type Runner interface {
    
    
    Run()
}

type Person struct {
    
    
    Name string
}

func (p Person) Run() {
    
    
    fmt.Printf("%s is running\n", p.Name)
}

func main() {
    
    
    var r Runner
    r = Person{
    
    Name: "song_chh"}
    r.Run()
}

また、空のインターフェース型はメソッドを定義しないインターフェースであるため、すべてのタイプが空のインターフェースを実装しています。つまり、空のインターフェース型には任意の値を割り当てることができます。

2.4 インターフェースとポインタ

インターフェイスが一連のメソッドを定義する場合、実装の受信者を制限しないため、2 つの実装メソッドがあり、1 つはポインター レシーバーで、もう 1 つは値レシーバーです。

同じメソッドの 2 つの実装が同時に存在することはできません

Say()メソッドをRunner インターフェイスに追加すると、 Person 構造体型はポインター レシーバーを使用して Say() メソッドを実装します。

type Runner interface {
    
    
    Run()
    Say()
}

type Person struct {
    
    
    Name string
}

func (p Person) Run() {
    
    
    fmt.Printf("%s is running\n", p.Name)
}

func (p *Person) Say() {
    
    
    fmt.Printf("hello, %s", p.Name)
}

インターフェイス変数を初期化するときは、構造体または構造体ポインターを使用できます

var r Runner
r = &Person{
    
    Name: "sch_chh"}
r = Person{
    
    Nmae: "sch_chh"}

インターフェイスを実装する受信側の型とインターフェイスが初期化されるときの型の両方が 2 つの次元を持つため、4 つの異なるエンコーディングが生成されます。

  • | 値の受信者| ポインターの受信者
    — |—| —
    値の初期化| √ | ×
    ポインターの初期化| √ | √

×コンパイルが失敗したことを示します

次の 2 つの状況は、コンパイルによってよく理解できます。

  • メソッドレシーバーと初期化子の型は両方とも構造体の値です
  • メソッド レシーバーと初期化型は両方とも構造体ポインターです。

まず、コンパイルをパスできる状況、つまり、メソッドのレシーバーが構造体で、初期化された変数がポインター型である状況を見てみましょう。

type Runner interface {
    
    
    Run()
    Say()
}

type Person struct {
    
    
    Name string
}

func (p Person) Run() {
    
    
    fmt.Printf("%s is running\n", p.Name)
}

func (p *Person) Say() {
    
    
    fmt.Printf("hello, %s", p.Name)
}

func main() {
    
    
    var r Runner
    r = &Person{
    
    Name: "sch_chh"}
    r.Run()
    r.Say()
}

上記のコードでは、Person構造体ポインターとして、基になる構造体を暗黙的に取得できるため、構造体ポインターを直接呼び出すRunことSayができます。その後、対応するメソッドが構造体を通じて呼び出されます。

参照が削除された場合、変数の初期化では構造体型が使用されます

r = Person{
    
    Name: "sch_chh"}

コンパイルが失敗したことを示すプロンプトが表示されます

./pointer.go:24:4: cannot use Person literal (type Person) as type Runner in assignment:
        Person does not implement Runner (Say method has pointer receiver)

では、なぜコンパイルが失敗するのでしょうか? まず、Go言語ではパラメータ受け渡しは值传递

コード内の変数が&Person{}の、メソッド呼び出し中にパラメーターがコピーPersonされ、特定の構造体を指す新しい構造体ポインターが作成されるため、コンパイラーは暗黙的に変数を逆参照して構造体へのポインターを取得します。メソッド呼び出しを完了する

2

コード内の変数がPerson{}の、パラメーターはメソッド呼び出し中にコピーされます。つまり、新しい変数を受け入れますメソッドレシーバーが の場合、コンパイラーは構造に基づいて一意のポインターを見つけることができないため、コンパイラーはエラーを報告します。Run()Say()Person{}*Person

3

注: 特定の型 T の変数の場合、*T メソッドを直接呼び出すことも合法です。これは、コンパイラが暗黙的にアドレス取得操作を完了するためですが、これは単なる構文糖衣です。

2.5 nil と non-nil

別の例を見てください。まだ Runner インターフェイスと Person 構造体です。main() 関数本体に注意してください。最初にインターフェイス変数 r を宣言し、それが nil かどうかを出力し、次に *Person 型 p を定義し、p が nil かどうかを出力します、最後に p を r に代入し、この時点で r が nil かどうかを出力します

type Runner interface {
    
    
    Run()
}

type Person struct {
    
    
    Name string
}

func (p Person) Run() {
    
    
    fmt.Printf("%s is running\n", p.Name)
}

func main() {
    
    
    var r Runner
    fmt.Println("r:", r == nil)

    var p *Person
    fmt.Println("p:", p == nil)

    r = p 
    fmt.Println("r:", r == nil)
}

出力は何ですか?

r: true or false
p: true or false
r: true or false

実際の出力は次のとおりです。

r: true
p: true
r: false

インターフェイス型とポインタ型のゼロ値が nil であるため、最初の 2 つの出力 r が nil で p が nil であることは理解できます。したがって、p が r に割り当てられると、r は nil ではありませんか? 実はインターフェース値という概念があります

2.6 インターフェース値

概念的に言えば、インターフェイス タイプの値 (インターフェイス値と省略) には、実際に具体类型は と该类型的值合計と呼ばれます动态类型动态值そのため、インターフェイスの動的な型と動的な値が両方である場合に限ります。 nil、インターフェイス値は nil です

2.5 の例に戻ると、p が r インターフェイスに割り当てられている場合、r の実際の構造は図のようになります。

4

これが本当に当てはまるかどうかを確認するには、メイン関数本体の最後にコード行を追加します。

fmt.Printf("r type: %T, data: %v\n", r, r)

運用実績

r type: *main.Person, data: <nil>

動的な値が実際に nil であることがわかります

インターフェイス値の概念がわかったので、基礎となるインターフェイスの具体的な実装は何ですか?

3. 実施方針

Go 言語のインターフェース型は、メソッドのセットを含む構造体とメソッドをまったく含まない構造体是否包含一组方法という 2 つの異なる実装に分割されます。ifaceeface

3.1 イファス

iface の最下層は構造体であり、次のように定義されています。

//runtime/runtime2.go
type iface struct {
    
    
        tab  *itab
        data unsafe.Pointer
}

iface 内には 2 つのポインターがあり、1 つは itab 構造ポインターであり、もう 1 つはデータへのポインターです。

unsafe.Pointer 型は、任意の変数のアドレスを格納できる特別な型のポインターです (C の void* に似ています)。

//runtime/runtime2.go
type itab struct {
    
     
        inter *interfacetype
        _type *_type
        hash  uint32 // copy of _type.hash. Used for type switches.
        _     [4]byte
        fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}

itab は特定の型とインターフェイスの型の関係を示すために使用され、interはインターフェイスの型定義情報、_typeは特定の型の情報、hashは _type.hash のコピーです。インタフェースの型と一致するfun. メソッドのアドレスリスト. funは固定長1の配列だが, 実際は可変配列である. 格納要素数は不明. 複数のメソッドは辞書順にソートされる.

//runtime/type.go
type interfacetype struct {
    
    
        typ     _type
        pkgpath name
        mhdr    []imethod
}
```go
interfacetype是描述接口定义的信息,`_type`:接口的类型信息,`pkgpath`是定义接口的包名;,`mhdr`是接口中定义的函数表,按字典序排序

> 假设接口有ni个方法,实现接口的结构体有nt个方法,那么itab函数表生成时间复杂为O(ni*nt),如果接口方法列表和结构体方法列表有序,那么函数表生成时间复杂度为O(ni+nt)

```go
//runtime/type.go
type _type struct {
    
    
        size       uintptr
        ptrdata    uintptr // size of memory prefix holding all pointers
        hash       uint32
        tflag      tflag
        align      uint8
        fieldalign uint8
        kind       uint8
        alg        *typeAlg
        // gcdata stores the GC type data for the garbage collector.
        // If the KindGCProg bit is set in kind, gcdata is a GC program.
        // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
        gcdata    *byte
        str       nameOff
        ptrToThis typeOff
}

_type は、すべてのタイプに共通の説明です。これは、型のハッシュ値であるsize型のサイズです;リフレクションに関連する型のタグであり、メモリ配置に関連する型のタグであり、型番号です. 特定の定義は runtime/typekind にあります.go ( gc 関連の情報)hashtflagalignfieldalignkindgcdata

iface 全体の構造図は次のとおりです。

5

3.2 顔

iface と比較して、eface の構造は比較的単純です。

//runtime/runtime2.go
type eface struct {
    
    
        _type *_type
        data  unsafe.Pointer
}

eface 内には、特定の型情報 _type 構造体へのポインターとデータへのポインターの 2 つのポインターもあります。

6

3.3 インターフェイス型への具象型変換

これまでのところ、インターフェイスとは何か、インターフェイスの基礎となる構造、および特定の型がインターフェイス型に割り当てられたときにどのように変換が実行されるのでしょうか? インターフェイス実装の例を見てみましょう

  1 package main
  2 
  3 import "fmt"
  4 
  5 type Runner interface {
    
    
  6     Run()
  7 }
  8 
  9 type Person struct {
    
    
 10     Name string
 11 }
 12 
 13 func (p Person) Run() {
    
    
 14     fmt.Printf("%s is running\n", p.Name)
 15 }
 16 
 17 func main() {
    
    
 18     var r Runner
 19     r = Person{
    
    Name: "song_chh"}
 20     r.Run()
 21 }

Go が提供するツールを使用してアセンブリ コードを生成する

go tool compile -S interface.go

19 行目に関連するコードのみをインターセプトする

0x001d 00029 (interface.go:19)  PCDATA  $2, $0
0x001d 00029 (interface.go:19)  PCDATA  $0, $1
0x001d 00029 (interface.go:19)  XORPS   X0, X0
0x0020 00032 (interface.go:19)  MOVUPS  X0, ""..autotmp_1+32(SP)
0x0025 00037 (interface.go:19)  PCDATA  $2, $1
0x0025 00037 (interface.go:19)  LEAQ    go.string."song_chh"(SB), AX
0x002c 00044 (interface.go:19)  PCDATA  $2, $0
0x002c 00044 (interface.go:19)  MOVQ    AX, ""..autotmp_1+32(SP)
0x0031 00049 (interface.go:19)  MOVQ    $8, ""..autotmp_1+40(SP)
0x003a 00058 (interface.go:19)  PCDATA  $2, $1
0x003a 00058 (interface.go:19)  LEAQ    go.itab."".Person,"".Runner(SB), AX
0x0041 00065 (interface.go:19)  PCDATA  $2, $0
0x0041 00065 (interface.go:19)  MOVQ    AX, (SP)
0x0045 00069 (interface.go:19)  PCDATA  $2, $1
0x0045 00069 (interface.go:19)  PCDATA  $0, $0
0x0045 00069 (interface.go:19)  LEAQ    ""..autotmp_1+32(SP), AX
0x004a 00074 (interface.go:19)  PCDATA  $2, $0
0x004a 00074 (interface.go:19)  MOVQ    AX, 8(SP)
0x004f 00079 (interface.go:19)  CALL    runtime.convT2I(SB)
0x0054 00084 (interface.go:19)  MOVQ    16(SP), AX
0x0059 00089 (interface.go:19)  PCDATA  $2, $2
0x0059 00089 (interface.go:19)  MOVQ    24(SP), CX

itab を構築した後、コンパイラが変換関数を呼び出すことがわかりますruntime.convT2I(SB)。関数の実装を参照してください。

//runtime/iface.go
func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
    
    
        t := tab._type
        if raceenabled {
    
    
                raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2I))
        }
        if msanenabled {
    
    
                msanread(elem, t.size)
        }
        x := mallocgc(t.size, t, true)
        typedmemmove(t, x, elem)
        i.tab = tab
        i.data = x
        return
}

まず型のサイズに応じてmallocgcメモリ空間の断片を申請するelem呼び出しを行い、ポインタの内容を新しい空間にコピーし、タブをifaceのタブに割り当て、新しいメモリポインタをifaceのデータに割り当てます、ifaceが作成されるように

サンプル コードを少し変更して、構造体ポインター型の変数をインターフェイス変数に代入します。

 19     r = &Person{
    
    Name: "song_chh"}

ツールを使用してアセンブリ コードを再度生成する

go tool compile -S interface.go

次のアセンブリ コードを表示します。

0x001d 00029 (interface.go:19)  PCDATA  $2, $1
0x001d 00029 (interface.go:19)  PCDATA  $0, $0
0x001d 00029 (interface.go:19)  LEAQ    type."".Person(SB), AX
0x0024 00036 (interface.go:19)  PCDATA  $2, $0
0x0024 00036 (interface.go:19)  MOVQ    AX, (SP)
0x0028 00040 (interface.go:19)  CALL    runtime.newobject(SB)
0x002d 00045 (interface.go:19)  PCDATA  $2, $2
0x002d 00045 (interface.go:19)  MOVQ    8(SP), DI
0x0032 00050 (interface.go:19)  MOVQ    $8, 8(DI)
0x003a 00058 (interface.go:19)  PCDATA  $2, $-2
0x003a 00058 (interface.go:19)  PCDATA  $0, $-2
0x003a 00058 (interface.go:19)  CMPL    runtime.writeBarrier(SB), $0
0x0041 00065 (interface.go:19)  JNE     105
0x0043 00067 (interface.go:19)  LEAQ    go.string."song_chh"(SB), AX
0x004a 00074 (interface.go:19)  MOVQ    AX, (DI)

まず、コンパイラはPerson構造体型ポインタを取得し、runtime.newobject()関数をパラメータとして呼び出し、ソース コード内の関数定義もチェックします。

// runtime/malloc.go

// implementation of new builtin
// compiler (both frontend and SSA backend) knows the signature
// of this function
func newobject(typ *_type) unsafe.Pointer {
    
    
        return mallocgc(typ.size, typ, true)
}

newobject は *Person を入力パラメーターとして取り、新しいPerson構造ポインターを作成し、その変数を設定します。その後、コンパイラーは iface を生成します

関数に加えてconvT2I、実際にはruntime/runtime.go、ファイルには多くの変換関数の定義があります

// Non-empty-interface to non-empty-interface conversion.
func convI2I(typ *byte, elem any) (ret any)

// Specialized type-to-interface conversion.
// These return only a data pointer.
func convT16(val any) unsafe.Pointer     // val must be uint16-like (same size and alignment as a uint16)
func convT32(val any) unsafe.Pointer     // val must be uint32-like (same size and alignment as a uint32)
func convT64(val any) unsafe.Pointer     // val must be uint64-like (same size and alignment as a uint64 and contains no pointers)
func convTstring(val any) unsafe.Pointer // val must be a string
func convTslice(val any) unsafe.Pointer  // val must be a slice

// Type to empty-interface conversion.
func convT2E(typ *byte, elem *any) (ret any)
func convT2Enoptr(typ *byte, elem *any) (ret any)

// Type to non-empty-interface conversion.   
func convT2I(tab *byte, elem *any) (ret any)        //for the general case
func convT2Inoptr(tab *byte, elem *any) (ret any)   //for structs that do not contain pointers

convT2Inoptr構造体内部にポインタを持たない変換に使われます. noptrはポインタがないと理解できます.変換プロセスconvT2I
convT16convT32convT64convTstringconvTsliceconvT64

//runtime/iface.go
func convT64(val uint64) (x unsafe.Pointer) {
    
    
        if val == 0 {
    
    
                x = unsafe.Pointer(&zeroVal[0])
        } else {
    
    
                x = mallocgc(8, uint64Type, false)
                *(*uint64)(x) = val
        }
        return
}

convT2一連の関数と比較して、関数typedmemmoveの呼び出しがなくmemmove、メモリのコピーが削減されます。さらに、値がこの型のゼロ値の場合、新しいメモリを適用するmallocgcため、ポイントされたzeroVal[0]ポインターを直接返します。

空インターフェース変換関数を見てみましょうconvT2E

func convT2E(t *_type, elem unsafe.Pointer) (e eface) {
    
    
        if raceenabled {
    
    
                raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2E))
        }
        if msanenabled {
    
    
                msanread(elem, t.size)
        }
        x := mallocgc(t.size, t, true)
        // TODO: We allocate a zeroed object only to overwrite it with actual data.
        // Figure out how to avoid zeroing. Also below in convT2Eslice, convT2I, convT2Islice.
        typedmemmove(t, x, elem)
        e._type = t
        e.data = x
        return
}

convT2EconvT2Iと同様に*_typeeface への変換時にコンパイラによって生成され、入力パラメーターとして呼び出されます。convT2E

3.4 断言

前のセクションの内容は主に具象型をインターフェース型に変換する方法を紹介していますが、具象型をインターフェース型に変換する方法は? Go 言語には、类型断言との 2 つのメソッドがあります。类型分支

型アサーション

型アサーションを記述するには 2 つの方法があります

    v := x.(T)
v, ok := x.(T)
  • x: はインターフェイス タイプの式です
  • T: は既知の型です

1つ目の書き方に注意、型アサーションが失敗するとpaincが発動する

タイプスイッチ

switch x := x.(type) {
    
     /* ... */}

使用例

switch i.(type) {
    
    
case string:
    fmt.Println("i'm a string")
case int:
    fmt.Println("i'm a int")
default:
    fmt.Println("unknown")
} 

4.参考文献

【1】「囲碁プログラミング言語」機械工業新聞社

[2] 「golangにおけるインターフェース最下層の解析」

[3] 「Go言語の実装原理について」

[4] 「深層解読 Go 言語 インターフェイスに関する 10 の質問」

おすすめ

転載: blog.csdn.net/zhw21w/article/details/129488201