ホームページ  >  記事  >  バックエンド開発  >  golang のインターフェースインターフェースの詳細な分析

golang のインターフェースインターフェースの詳細な分析

藏色散人
藏色散人転載
2021-01-28 17:36:451958ブラウズ

#golang の次のチュートリアル コラムでは、golang のインターフェイス インターフェイスについて詳しく説明します。困っている友人の役に立てば幸いです。

インターフェイスの概要

ゴロンチンとチャネルが Go 言語の同時実行モデルをサポートする基礎である場合、次のようにします。 Go 言語 現在、クラスタリングとマルチコアの時代が美しい風景となっているため、インターフェイスは Go 言語のタイプ シリーズ全体の基礎となっており、基本的なプログラミング哲学の探求において Go 言語が前例のない高みに達することを可能にしています。 Go 言語はプログラミング哲学の改革者というよりむしろ改革者です。これは、Go 言語に gorountine とチャネルがあるからではなく、より重要なのは Go 言語の型システム、さらには Go 言語のインターフェースによるものです。 Go 言語のプログラミング哲学は、インターフェイスのおかげで完璧になる傾向があります。 C、Java は「侵入型」インターフェイスを使用します。これは主に、実装クラスが特定のインターフェイスを実装することを明示的に宣言する必要があるためです。この必須のインターフェイス継承方法は、オブジェクト指向プログラミングのアイデアの開発においてかなりの疑問の対象となってきた機能です。 Go 言語は「非侵入型インターフェイス」を使用します。Go 言語のインターフェイスには独自の独自性があります。型 T のパブリック メソッドがインターフェイス I の要件を完全に満たしている限り、インターフェイス I が必要な場所で型 T のオブジェクトを使用できます。型 T のいわゆるパブリック メソッドは、インターフェイス I の要件を完全に満たしています。つまり、型 T はインターフェイス I で指定されたメンバーのセットを実装します。このアプローチの学名は Structural Typing であり、静的なダック タイピングの一種であると考える人もいます。


この値はインターフェイス メソッドを実装するために必要です。

type Reader interface { 
 Read(p []byte) (n int, err os.Error) 
} 
 
// Writer 是包裹了基础 Write 方法的接口。 
type Writer interface { 
 Write(p []byte) (n int, err os.Error) 
} 
 
var r io.Reader 
r = os.Stdin 
r = bufio.NewReader(r) 
r = new(bytes.Buffer)

1 つ明確にしておきたいのは、r がどのような値を保存しても、r の型は常に

io.Reader であり、Go は静的型であり、r の静的型は io です。 。読者。インターフェイス タイプの非常に重要な例は、空のメソッド セットを表す空のインターフェイス {} です。任意の値には 0 個以上のメソッドがあるため、任意の値でそれを満たすことができます。 Go のインターフェースは動的に型付けされていると言う人もいますが、これは誤解です。これらは静的に型付けされます。インターフェイス型の変数は常に同じ静的型を持ち、値は常に空のインターフェイスを満たしますが、インターフェイス変数に格納されている値は実行時に変更される可能性があります。リフレクションとインターフェイスは密接に関連しているため、これらすべては注意して扱う必要があります。

2 インターフェイス タイプのメモリ レイアウト

タイプの中にはインターフェイス タイプという重要なカテゴリがあります。メソッドの固定されたコレクションを表します。インターフェイス変数は、その値がインターフェイスのメソッドを実装している限り、任意の実際の値 (インターフェイス以外) を格納できます。実際、インターフェイスは、以下の図に示すように、メモリ内の 2 つのメンバーで構成されます。タブは仮想テーブルを指し、データは実際の参照データを指します。仮想テーブルは、インターフェイスに必要な実際の型情報とメソッドのセットを表します。

type Stringer interface { 
 String() string 
} 
 
type Binary uint64 
 
func (i Binary) String() string { 
 return strconv.FormatUint(i.Get(), 2) 
} 
 
func (i Binary) Get() uint64 { 
 return uint64(i) 
} 
 
func main() { 
 var b Binary = 32 
 s := Stringer(b) 
 fmt.Print(s.String()) 
}


itable の構造を観察します。最初に型情報を記述するメタデータがあり、次に Stringger インターフェイスを満たす関数ポインターのリストがあります (これは実際のタイプのバイナリ関数ポインター セットではありません)。したがって、インターフェイスを通じて関数呼び出しを行う場合、実際の操作は

s.tab->fun[0](s.data) になります。 C の仮想テーブルに似ていますか?しかし、それらは根本的に異なります。まず C を見てみましょう。これはメソッド セット、つまり各クラスの仮想テーブルを作成します。サブクラスが親クラスの仮想関数をオーバーライドすると、テーブル内の対応する関数ポインタがサブクラスによって実装された関数に変更されます。そうでない場合は、親クラスの実装を指し、多重継承に直面すると、C オブジェクト構造内に複数の仮想テーブル ポインターが存在し、各仮想テーブル ポインターはメソッド セットの異なる部分を指します。 golang の実装を見てみましょう。C と同様に、golang も型ごとにメソッド セットを作成します。違いは、インターフェイスの仮想テーブルが実行時に特別に生成されるのに対し、c の仮想テーブルはコンパイル時に生成されることです。(ただし、c 仮想関数テーブルによって示される多態性は実行時に決定されます) たとえば、この例で初めてステートメント s := Stringer(b) が出現すると、golang は Stringer を生成します。インターフェース Binary型の仮想テーブルに対応し、キャッシュします。では、なぜ c を使用して実装しないのでしょうか?この c は、golang のオブジェクト メモリ レイアウトに関係します。

首先c++的动态多态是以继承为基础的,在对象构造初始化的时首先会初始化父类,其次是子类,也就是说一个对象的内存布局是虚表,父类部分,子类部分(编译器不同可能会有差异),当一个父类指针指向子类时,会发生内存的截断,截断子类部分(内存地址偏移),但是此时子类的虚表中的函数指针实际上还是指向了自己的实现,所以此时的指针才会调用到子类的虚函数,如果不是虚函数,因为内存已经截断没有子类的非虚函数信息了,所以只能调用父类的了,这种继承关系让c++的虚表的初始化非常清晰,在一个对象初始化时先调用父类的构造此时虚表跟父类是一样的,接下来初始化子类,此时编译器就会去识别子类有没有覆盖父类的虚函数,如果有则虚表中相应的函数指针改成自己的虚函数实现指针。

那么go有什么不同呢,首先我们很清楚go是没有严格意义上的继承的,go的接口不存在继承关系,只要实现了接口定义的方法都可以成为接口类型,这给go的虚表初始化带来很大的麻烦,到底有多少类型实现了这个接口,一个类型到底实现了多少接口这让编译器很confused。举个例子,某个类型有m个方法,某接口有n个方法,则很容易知道这种判定的时间复杂度为O(mXn),不过可以使用预先排序的方式进行优化,实际的时间复杂度为O(m+n)这样看来其实还行那为什么要在运行时生成虚表呢,这不是会拖慢程序的运行速度吗,注意我们这里是某个类型,某个接口,是1对1的关系,如果有n个类型,n个接口呢,编译器难道要把之间所有的关系都理清吗?退一步说就算编译器任劳任怨把这事干了,可是你在写过程中你本来就不想实现那个接口,而你无意中给这个类型实现的方法中包含了某些接口的方法,你根本不需要这个接口(况且go的接口机制会导致很多这种无意义的接口实现),你欺负编译器就行了,这也太欺负人了吧。如果我们放到运行时呢,我们只要在需要接口的去分析一下类型是否实现了接口的所有方法就行了很简单的一件事。

三 空接口

接口类型的一个极端重要的例子是空接口:interface{} ,它表示空的方法集合,由于任何值都有零个或者多个方法,所以任何值都可以满足它。 注意,[]T不能直接赋值给[]interface{}

//t := []int{1, 2, 3, 4} wrong 
//var s []interface{} = t 
t := []int{1, 2, 3, 4} //right 
s := make([]interface{}, len(t)) 
for i, v := range t { 
 s[i] = v 
}
str, ok := value.(string) 
if ok { 
 fmt.Printf("string value is: %q\n", str) 
} else { 
 fmt.Printf("value is not a string\n") 
}

在Go语言中,我们可以使用type switch语句查询接口变量的真实数据类型,语法如下:

type Stringer interface { 
  String() string 
} 
 
var value interface{} // Value provided by caller. 
switch str := value.(type) { 
case string: 
  return str //type of str is string 
case Stringer: //type of str is Stringer 
  return str.String() 
}

也可以使用“comma, ok”的习惯用法来安全地测试值是否为一个字符串:

str, ok := value.(string) 
if ok { 
  fmt.Printf("string value is: %q\n", str) 
} else { 
  fmt.Printf("value is not a string\n") 
}

四 接口赋值

package main 
 
import ( 
"fmt" 
) 
 
type LesssAdder interface { 
  Less(b Integer) bool 
  Add(b Integer) 
} 
 
type Integer int 
 
func (a Integer) Less(b Integer) bool { 
  return a < b 
} 
 
func (a *Integer) Add(b Integer) { 
  *a += b 
} 
 
func main() { 
 
  var a Integer = 1 
  var b LesssAdder = &a 
  fmt.Println(b) 
 
  //var c LesssAdder = a 
  //Error:Integer does not implement LesssAdder  
  //(Add method has pointer receiver) 
}

go语言可以根据下面的函数:

func (a Integer) Less(b Integer) bool

自动生成一个新的Less()方法

func (a *Integer) Less(b Integer) bool

这样,类型*Integer就既存在Less()方法,也存在Add()方法,满足LessAdder接口。 而根据

func (a *Integer) Add(b Integer)

这个函数无法生成以下成员方法:

func(a Integer) Add(b Integer) { 
  (&a).Add(b) 
}

因为(&a).Add()改变的只是函数参数a,对外部实际要操作的对象并无影响(值传递),这不符合用户的预期。所以Go语言不会自动为其生成该函数。因此类型Integer只存在Less()方法,缺少Add()方法,不满足LessAddr接口。(可以这样去理解:指针类型的对象函数是可读可写的,非指针类型的对象函数是只读的)将一个接口赋值给另外一个接口 在Go语言中,只要两个接口拥有相同的方法列表(次序不同不要紧),那么它们就等同的,可以相互赋值。 如果A接口的方法列表时接口B的方法列表的子集,那么接口B可以赋值给接口A,但是反过来是不行的,无法通过编译。

五 接口查询

接口查询是否成功,要在运行期才能够确定。他不像接口的赋值,编译器只需要通过静态类型检查即可判断赋值是否可行。

var file1 Writer = ...
if file5,ok := file1.(two.IStream);ok {
...
}

这个if语句检查file1接口指向的对象实例是否实现了two.IStream接口,如果实现了,则执行特定的代码。

在Go语言中,你可以询问它指向的对象是否是某个类型,比如,

var file1 Writer = ...
if file6,ok := file1.(*File);ok {
...
}

这个if语句判断file1接口指向的对象实例是否是*File类型,如果是则执行特定的代码。

slice := make([]int, 0)
slice = append(slice, 1, 2, 3)

var I interface{} = slice


if res, ok := I.([]int);ok {
  fmt.Println(res) //[1 2 3]
}

这个if语句判断接口I所指向的对象是否是[]int类型,如果是的话输出切片中的元素。

func Sort(array interface{}, traveser Traveser) error {

  if array == nil {
    return errors.New("nil pointer")
  }
  var length int //数组的长度
  switch array.(type) {
  case []int:
    length = len(array.([]int))
  case []string:
    length = len(array.([]string))
  case []float32:
    length = len(array.([]float32))

  default:
    return errors.New("error type")
  }

  if length == 0 {
    return errors.New("len is zero.")
  }

  traveser(array)

  return nil
}

通过使用.(type)方法可以利用switch来判断接口存储的类型。

概要: インターフェイスが指すオブジェクトが特定のタイプであるかどうかをクエリする使用法は、インターフェイス クエリの特殊なケースと考えることができます。インターフェイスは型のグループの公開特性を抽象化したものであるため、インターフェイスのクエリと特定の型のクエリの違いは、次の 2 つの質問の違いに似ています。

あなたは医者ですか? #########はい。

あなたは Momomo です

#Yes


最初の質問は、クエリ インターフェイスであるグループをクエリし、2 番目の質問は、 2 つの質問は特定の個人に届いており、質問固有のタイプです。


さらに、リフレクションは型クエリにも使用できます。これについては、リフレクションで詳しく紹介します。

golang 関連の技術記事については、

go language

列をご覧ください。

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

声明:
この記事はjb51.netで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。