Home  >  Article  >  Backend Development  >  An in-depth analysis of the interface interface in golang

An in-depth analysis of the interface interface in golang

藏色散人
藏色散人forward
2021-01-28 17:36:451907browse

The following tutorial column of golang will give you an in-depth analysis of the interface interface in golang. I hope it will be helpful to friends in need!

An introduction to the interface

If gorountine and channel are the cornerstones that support the concurrency model of the Go language, let the Go language Nowadays, the era of clustering and multi-core has become a beautiful scenery, so interfaces are the cornerstone of the entire type series of Go language, allowing Go language to reach unprecedented heights in the exploration of basic programming philosophy. The Go language is a reformist rather than a reformist in programming philosophy. This is not because Go language has gorountine and channel, but more importantly because of the type system of Go language, and even more because of the interface of Go language. The programming philosophy of Go language tends to be perfect because of interfaces. C, Java uses "intrusive" interfaces, mainly because the implementation class needs to explicitly declare that it implements a certain interface. This mandatory interface inheritance method is a feature that has been subject to considerable doubts in the development of object-oriented programming ideas. Go language uses a "non-intrusive interface". The interface of Go language has its own uniqueness: as long as the public method of type T fully meets the requirements of interface I, objects of type T can be used where interface I is required. The so-called public methods of type T fully meet the requirements of interface I, that is, type T implements a set of members specified by interface I. The scientific name of this approach is Structural Typing, and some people also regard it as a kind of static Duck Typing.

You want this value to implement the interface method.

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)

One thing must be clear, no matter what value r saves, the type of r is always io.Reader, Go is a static type, and the static type of r is io .Reader. An extremely important example of an interface type is the empty interface interface{}, which represents an empty method set. Since any value has zero or more methods, any value can satisfy it. Some people also say that Go's interface is dynamically typed, but this is a misunderstanding. They are statically typed: variables of interface type always have the same static type, and the value always satisfies the empty interface, but the value stored in the interface variable may be changed at runtime. All of this must be treated with caution, since reflection and interfaces are closely related.

2 Interface type memory layout

There is an important category among types, which is the interface type, which expresses a fixed A collection of methods. An interface variable can store any actual value (non-interface), as long as the value implements the interface's method. The interface actually consists of two members in memory, as shown in the figure below, tab points to the virtual table, and data points to the actual referenced data. The virtual table depicts the actual type information and the set of methods required by the interface.

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()) 
}


Observe the structure of itable, first some metadata describing the type information, and then a list of function pointers that satisfy the Stringger interface (note that this is not the actual type Binary function pointer set). Therefore, if we make a function call through the interface, the actual operation is actually s.tab->fun[0](s.data) . Is it similar to C's virtual table? But they are fundamentally different. Let’s look at C first. It creates a method set, that is, a virtual table for each class. When a subclass overrides the virtual function of the parent class, it changes the corresponding function pointer in the table to the function implemented by the subclass itself. If not, then Pointing to the implementation of the parent class, when facing multiple inheritance, there will be multiple virtual table pointers in the C object structure, and each virtual table pointer points to a different part of the method set. Let's look at the implementation of golang. Like C, golang also creates a method set for each type. The difference is that the virtual table of the interface is specially generated at runtime, while the virtual table of c is generated at compile time. (But the polymorphism shown by the c virtual function table is determined at runtime). For example, when the statement s := Stringer(b) is encountered for the first time in the example, golang will generate the Stringer interface Corresponds to the virtual table of Binary type and caches it. So why doesn't go use c to implement it? This c is related to the object memory layout of 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来判断接口存储的类型。

Summary: The usage of querying whether the object pointed to by the interface is of a certain type can be considered a special case of interface query. An interface is an abstraction of the public characteristics of a group of types, so the difference between querying the interface and querying the specific type is like the difference between the following two questions:

Are you a doctor?

yes.

You are Momomo

Yes

The first question queries a group, which is the query interface; and the second question queries The two questions have reached specific individuals and are query specific types.

In addition, reflection can also be used for type query, which will be introduced in detail in reflection.

For more golang related technical articles, please visit the go language column!

The above is the detailed content of An in-depth analysis of the interface interface in golang. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:jb51.net. If there is any infringement, please contact admin@php.cn delete