ホームページ >バックエンド開発 >Golang >golang のインターフェースは何に役立ちますか?

golang のインターフェースは何に役立ちますか?

青灯夜游
青灯夜游オリジナル
2023-01-04 20:42:163752ブラウズ

golang では、インターフェースはメソッドをまとめるために使用される型であり、その機能は次のとおりです: 1. オブジェクト指向設計のためのメソッドのラッパーとして; 2. さまざまなメソッドとして関数パラメータなどを受け取るために使用されます。インターフェースの定義構文は「type インターフェース タイプ名 インターフェース{メソッド名(パラメータリスト1) 戻り値リスト}」となり、メソッド名の頭文字が大文字、インターフェースタイプ名の頭文字も大文字の場合、このメソッドは、インターフェイスが配置されているパッケージ (パッケージ) の外部のコード アクセスで使用できます。

golang のインターフェースは何に役立ちますか?

このチュートリアルの動作環境: Windows 7 システム、GO バージョン 1.18、Dell G3 コンピューター。

1. インターフェイスとは何ですか?

インターフェイスはメソッド シグネチャの組み合わせであり、オブジェクトの一連の動作を定義するためにインターフェイスを使用します。

(メソッドと通常の関数の違いに注意してください)

インターフェースは型であり、通常の言語のインターフェースとは異なります。メソッドのパックです。ただし、GO 言語に関数ベースのオブジェクト指向を持たせるのは、この種の収束です。

インターフェイスの主な機能:

1. オブジェクト指向設計のメソッド コレクターとして。

2. さまざまなデータのベアラーとして、関数パラメータの受信などに使用できます。

これも同様で、GO 言語は インターフェイス指向プログラミング を提唱しています。

2. インターフェースの定義を使用します

2.1 定義

同様の構造

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

もちろん、これはメソッドを使用したインターフェイス定義にすぎず、データ指向のインターフェイスではありません。

  • インターフェイス名: type を使用してインターフェイスをカスタム タイプ名として定義します。 Go 言語のインターフェイスに名前を付ける場合、通常、単語の後に er が追加されます。たとえば、書き込み操作を含むインターフェイスは Writer と呼ばれ、文字列関数を含むインターフェイスは Stringer と呼ばれます。 ##。 #待って。インターフェイス名は、インターフェイスの型の意味を強調するのが最適です。

  • メソッド名: メソッド名の最初の文字が大文字で、インターフェイス型名の最初の文字も大文字の場合、このメソッドにはパッケージ外のコードからアクセスできます。インターフェイスが配置されています。

  • #パラメータリスト、戻り値リスト:パラメータリスト、戻り値リスト内のパラメータ変数名は省略可能です。
## 2.2

を使用する オブジェクトがインターフェース内のすべてのメソッドを実装している限り、オブジェクトはインターフェースを実装します。つまり、インターフェイス

は、実装する必要があるメソッド

のリストです。

//定义接口
type FastfoodStore interface{
    MakeHamberger()
    MakeFriedChips()
    MakeSoftDrink()
}
//定义结构体
type KFC struct{}
type HambergerKing struct{}

//实现了接口中所有的方法
func (kfc KFC) MakeHamberger(){
    fmt.println("肯德基的汉堡")
}
func (kfc KFC) MakeFriedChips(){
    fmt.println("肯德基的薯条")
}
func (kfc KFC) MakeSoftDrink(){
    fmt.println("肯德基的饮料")
}

func (K *HambergerKing) MakeHameberger(){
    fmt.println("汉堡王的汉堡")
}
func (K *HambergerKing) MakeFriedChips(){
    fmt.println("汉堡王的薯条")
}
func (K *HambergerKing) MakeSoftDrink(){
    fmt.println("汉堡王的饮料")
}
Java のインターフェイスの明示的な実装とは異なり、Go 言語は暗黙的に実装されていることがわかります。

Java の場合: インターフェイスを実装するには、インターフェイスを明示的に宣言し、すべてのメソッドを実装する必要があります。

    Go の場合: インターフェイスを実装するすべてのメソッドは、暗黙的にインターフェイスを実装します。
  • #それでは、GO 言語は型がインターフェイスであるかどうかをどのようにチェックするのでしょうか?

回答: Go 言語は、パラメーターを渡すとき、パラメーターを返すとき、および変数の割り当てを行うときに、型がインターフェイスを実装しているかどうかのみをチェックします。型チェック プロセスの観点から見ると、コンパイラは必要な場合にのみ型をチェックします。型がインターフェイスを実装する場合、インターフェイス内のすべてのメソッドを実装するだけでよく、プログラミング言語のように明示的に宣言する必要はありません。 Javaなど。

上記のインターフェイスを実装すると、KFC は構造体オブジェクトを使用して実装され、ハンバーガー キングはポインターを使用して実装されることがわかります。この 2 つの違いは何ですか?

答え: 違いは、インターフェイスを初期化するときです。

//结构体初始化和指针初始化
var f faststore = KFC{}             //可以通过编译
var f faststore = &KFC{}            //可以通过编译

var f faststore = HambergerKing{}    //无法通过编译
var f faststore = &HambergerKing{}    //可以通过编译
では、ポインターを使用して構造体を実装および初期化すると、なぜ機能しないのでしょうか?

回答:

Go 言語はパラメータを渡すときに値を使用します。

#上の図に示すように、上記のコードで初期化された変数ポインターや構造体に関係なく、メソッドの呼び出し時に値のコピーが発生します。

如上图左侧,对于 &HambergerKing{} 来说,这意味着拷贝一个新的 &HambergerKing{} 指针,这个指针与原来的指针指向一个相同并且唯一的结构体,所以编译器可以隐式的对变量解引用(dereference)获取指针指向的结构体;
如上图右侧,对于 HambergerKing{} 来说,这意味着方法会接受一个全新的 HambergerKing{},因为方法的参数是*HambergerKing,编译器不会无中生有创建一个新的指针;即使编译器可以创建新指针,这个指针指向的也不是最初调用该方法的结构体;
上面的分析解释了指针类型的现象,当我们使用指针实现接口时,只有指针类型的变量才会实现该接口;当我们使用结构体实现接口时,指针类型和结构体类型都会实现该接口。当然这并不意味着我们应该一律使用结构体实现接口,这个问题在实际工程中也没那么重要,在这里我们只想解释现象背后的原因。

在上面我们说过,interface有两种用法,现在介绍了其中一种就是作为方法的收束器。那么第二种就是作为数据的承载者

2.3 数据承载者

作为数据容器时,接口就是一个“空”接口,这个空来形容没有Method。空interface(interface{})不包含任何的method,正因为如此,所有的类型都实现了空interface。空interface对于描述起不到任何的作用(因为它不包含任何的method),但是空interface在我们需要存储任意类型的数值的时候相当有用,因为它可以存储任意类型的数值。它有点类似于C语言的void*类型。

需要注意的是,与 C 语言中的 void * 不同,interface{} 类型不是任意类型。如果我们将类型转换成了 interface{} 类型,变量在运行期间的类型也会发生变化,获取变量类型时会得到 interface{}。

我们尝试从底层实现来解释两种用法的不同,你会好理解一些。Go 语言使用 runtime.iface 表示第一种接口,使用 runtime.eface 表示第二种不包含任何方法的接口 interface{},两种接口虽然都使用 interface 声明,但是由于后者在 Go 语言中很常见,所以在实现时使用了特殊的类型。

golang のインターフェースは何に役立ちますか?

空接口作为函数的参数

使用空接口实现可以接收任意类型的函数参数。

// 空接口作为函数参数
func show(a interface{}) {
    fmt.Printf("type:%T value:%v\n", a, a)
}

空接口作为map的值

使用空接口实现可以保存任意值的字典。

// 空接口作为map值
    var studentInfo = make(map[string]interface{})
    studentInfo["name"] = "Wilen"
    studentInfo["age"] = 18
    studentInfo["married"] = false
    fmt.Println(studentInfo)
//gin框架的gin.H{}

三、关于接口类型转换

interface 可以存储所有的值,那么自然会涉及到类型转换这个话题。与此同时,我们也将在这节细说类型转换中,因为结构体实现和结构体指针实现的接口的异同。

3.1结构体指针实现接口

//我们仍然运用上面快餐店的例子
type Store interface{
    MakeHamberger()
}
type KFC struct{
    name string
}
func (k *KFC) MakeHamberger(){
    fmt.println(k.name+"制作了一个汉堡")
}
func main(){
    var s store = &KFC{name:"东街店"}
    store.MakeHamberger()
}

这里将上述代码生成的汇编指令拆分成三部分分析:

1.结构体 KFC 的初始化;

KFC的初始化又可以分为下面几步:

  • 获取 KFC 结构体类型指针并将其作为参数放到栈上;

  • 通过 CALL 指定调用 runtime.newobject函数,这个函数会以 KFC 结构体类型指针作为入参,分配一片新的内存空间并将指向这片内存空间的指针返回到 SP+8 上;

  • SP+8 现在存储了一个指向 KFC 结构体的指针,我们将栈上的指针拷贝到寄存器 DI 上方便操作;

  • 由于 Cat 中只包含一个字符串类型的 Name 变量,所以在这里会分别将字符串地址 &"东街店" 和字符串长度 6 设置到结构体上。

golang のインターフェースは何に役立ちますか?

2.赋值触发的类型转换过程;

因为 KFC 结构体的定义中只包含一个字符串,而字符串在 Go 语言中总共占 16 字节,所以每一个 KFC 结构体的大小都是 16 字节。初始化 KFC 结构体之后就进入了将 *KFC 转换成 Store 类型的过程了:

类型转换的过程比较简单,Store 作为一个包含方法的接口,它在底层使用 [runtime.iface] 结构体表示。runtime.iface 结构体包含两个字段,其中一个是指向数据的指针,另一个是表示接口和结构体关系的 tab 字段,我们已经通过上一段代码 SP+8 初始化了 KFC 结构体指针,这段代码只是将编译期间生成的 runtime.itab 结构体指针复制到 SP 上:

golang のインターフェースは何に役立ちますか?

到这里,我们会发现 SP ~ SP+16 共同组成了 runtime.iface 结构体。

3.调用接口的方法 Quack();

栈上的这个 runtime.iface 也是 MakeHamberger() 方法的第一个入参。通过CALL()完成方法的调用。

3.2 结构体实现接口

//我们仍然运用上面快餐店的例子
type Store interface{
    MakeHamberger()
}
type KFC struct{
    name string
}
func (k KFC) MakeHamberger(){
    fmt.println(k.name+"制作了一个汉堡")
}
func main(){
    var s store = KFC{name:"东街店"}
    store.MakeHamberger()
}

如果我们在初始化变量时使用指针类型 &KFC{Name: "东街店"} 也能够通过编译,不过生成的汇编代码和上一节中的几乎完全相同,所以这里也就不分析这个情况了。

初始化 KFC 结构体;

在栈上初始化 KFC 结构体,而上一节的代码在堆上申请了 16 字节的内存空间,栈上只有一个指向 KFC 的指针。

完成从 KFC 到 Store 接口的类型转换;

初始化结构体后会进入类型转换的阶段,编译器会将 go.itab."".KFC,"".Store 的地址和指向 KFC 结构体的指针作为参数一并传入 runtime.convT2I 函数:这个函数会获取 runtime.itab 中存储的类型,根据类型的大小申请一片内存空间并将 elem 指针中的内容拷贝到目标的内存中:

func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
    t := tab._type
    x := mallocgc(t.size, t, true)
    typedmemmove(t, x, elem)
    i.tab = tab
    i.data = x
    return
}

runtime.convT2I 会返回一个 runtime.iface,其中包含 runtime.itab 指针和 KFC 变量。当前函数返回之后,main 函数的栈上会包含以下数据:

golang のインターフェースは何に役立ちますか?

SP 和 SP+8 中存储的 runtime.itab 和 KFC 指针是 runtime.convT2I 函数的入参,这个函数的返回值位于 SP+16,是一个占 16 字节内存空间的 runtime.iface 结构体,SP+32 存储的是在栈上的 KFC 结构体,它会在 runtime.convT2I 执行的过程中拷贝到堆上。

3.3类型断言

如何将一个接口类型转换成具体类型?

x.(T)

非空接口

func main() {
    var c Store = &KFC{Name: "东街店"}
    switch c.(type) {
    case *KFC:
        kfc := c.(*KFC)
        kfc.MakeHamberger()
    }
}

因为 Go 语言的编译器做了一些优化,所以代码中没有runtime.iface 的构建过程,不过对于这一节要介绍的类型断言和转换没有太多的影响。

switch语句生成的汇编指令会将目标类型的 hash 与接口变量中的 itab.hash 进行比较

空接口

func main() {
    var c interface{} = &KFC{Name: "东街店"}
    switch c.(type) {
    case *KFC:
        kfc := c.(*KFC)
        kfc.MakeHamberger()
    }
}

上述代码会在类型断言时就不是直接获取变量中具体类型的 runtime._type,而是从 eface._type 中获取,汇编指令仍然会使用目标类型的 hash 与变量的类型比较.

【相关推荐:Go视频教程编程教学

以上がgolang のインターフェースは何に役立ちますか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。