一文理解Go语言中接口interface(一文理解Go语言中接口interface)
Go推荐使用面向组合编程,而interface是实现组合编程的重要组成部分,本篇文章将对interface底层实现原理,interface如何设计,如何正确使用接口类型。
什么是接口?接口就是规范好一些行为但是不具体去实现,就好像编程一样任何语言都有自己的语法规范,但是具体怎么写还是得码农去做。
Go中的接口具有动态和静态两种特性
- 动态特性
动态特性就是在程序运行时存储在接口类型变量中的真实类型,这样可以实现不同数据类型调用同一个interface实现不同的功能来支持多态的特性。
- 静态特性
接口类型变量被赋值之后检查赋值的类型是否实现了interface定义的所有方法
interface底层怎么实现的?那必须得阅读go runtime的源码找到interface的结构体类型, 先看下源码具体的代码
type iface struct {
tab *itab
data unsafe.Pointer
}
type eface struct{
_type *_type
data unsafe.Pointer
}
从代码中可以看出接口有iface跟eface两个不一样的结构体类型,eface表示空接口(即接口类型没有定义方法),那iface就表示带有方法的接口类型。这两种结构的共同点是都有两个指针字段,并且第二个指针字段的功用相同,都指向当前赋值给该接口类型变量的动态类型变量的值。
不同的是eface表示的空接口类型并无方法列表,因此它的第一个指针字段指向一个_type类型结构,该结构为该接口类型变量的动态类型的信息,下面是源码_type类型的结构体
iface除了要存储动态类型信息之外,还要存储接口本身的信息(接口的类型信息、方法列表信息等)以及动态类型所实现的方法的信息,因此iface的第一个字段指向一个itab类型结构,下面是iface第一个字段itab的类型结构
- 字段_type是存储改接口类型变量的动态类型信息
- 字段fun是动态类型已经实现的接口方法的调用地址数组
上面itabl结构中的第一个字段inter指向interfacetype类型结构,这个结构存储着改接口类型自身的信息,interfacetype类型定义如下。
- typ是类型信息
- pkgpath是包的路径名
- mhdr是接口方法集合切片
- eface表示空接口类型变量的例子
package main
import "fmt"
type efaceType struct {
n int
s string
}
func main() {
var t = efaceType{
n: 20,
s: "jack",
}
var i interface{} = t
fmt.Println(i)
}
// result {20 jack}
- iface表示非空接口类型变量例子
package main
import "fmt"
type ifaceType struct {
n int
s string
}
func (i ifaceType) method1() {}
func (i ifaceType) method2() {}
type ifaceTypeInterface interface {
method1()
method2()
}
func main() {
var t = ifaceType{
n: 20,
s: "jack",
}
var i ifaceTypeInterface = t
fmt.Println(i)
}
- 结论:根据图我们可以看出每个接口类型变量在运行时的表示都是由两部分组成的,这两种接口类型可以分别为eface(_type, data)和iface(tab, data)。
与结构体struct 类似,我们需要创建一个派生类型来使用关键字interface简化接口声明。
type Shape interface {
Area() float64
Perimeter() float64
}
在上面的代码示例中,定义了Shape接口,它有两个方法 Area 和 Permeter,它们不接受任何参数并返回 float64值。实现这些方法的任何类型(具有精确的方法签名)也将实现塑形接口。由于接口是类似于 struct 的类型,所以我们可以创建其类型的变量。在上面的例子中,我们可以创建一个类型为接口形状的变量 s。
接口有两种类型。接口的静态类型是接口本身,例如上面程序中的形状接口没有静态值,而是指向动态值。接口类型的变量可以保存实现该接口的类型的值。该类型的值成为接口的动态值,该类型成为接口的动态类型。从上面的示例中,我们可以看到接口的值和类型为零。这是因为,此时,我们已经声明了变量 s 的形状类型,但没有赋予任何值。当我们使用带接口参数的 fmt 包中的 Println 函数时,它指向接口的动态值,而 Printf 函数中的%T 语法指的是接口的动态类型。
实现接口我们使用Shape接口提供的签名来声明 Area 和 Permeter 方法。另外让我们创建 Rect struct 并让它实现 Shape接口方法。
在上面的程序中,我们创建了Shape接口和结构类型 Rect。然后我们定义了属于 Rect 类型的面积和周长方法,因此 Rect 实现了这些方法。这些方法是由Shape接口定义的,所以 Rect 结构类型实现了Shape接口。因为我们还没有强制 Rect 实现Shape接口,所以这一切都是自动发生的。因此我们可以说 Go 中的接口是隐式实现的。
当类型实现接口时,该类型的变量也可以表示为接口的类型。我们可以确认,通过创建一个 nil 接口的形状类型和指定一个结构类型 Rect。
因为 Rect 实现了Shape接口,所以这是完全有效的。从上面的结果,我们可以看到,动态类型的 s 现在是 Rect 和动态值的 s 是结构 Rect 的值是{5,4}。
动态类型的接口也称为具体类型,因为当我们访问接口类型时,它返回其底层动态值的类型,而其静态类型保持隐藏。
我们可以在 变量s 上调用 Area 方法,因为接口Shape定义了 Area 方法,而 s 的具体类型是 Rect,它实现了 Area 方法。此方法将在动态值接口的持有者上调用。
另外我们可以看到比较 s 和 r,因为这两个变量拥有相同的动态类型(Rect 类型的结构)和动态值{5,4}。
在上面的程序中,我们移除了Perimeter方法。这个程序不会编译,编译器会抛出一个错误。
program.go:22: cannot use Rect literal (type Rect) as type Shape in assignment:
Rect does not implement Shape (missing Perimeter method)
从上面的错误可以明显看出,为了成功地实现一个接口,您需要实现该接口声明的所有的方法。
空接口当一个接口的方法为零时,它被称为空接口。这由interface{}表示。由于空接口没有方法,所以所有类型都隐式实现此接口。fmt 内置包的 Println 函数如何接受不同类型的值作为参数?因为它是一个空接口。让我们看看 Println 函数。
func Println(a ...interface{}) (n int, err error)
在上面的程序中,我们创建了一个自定义字符串类型 MyString 和一个结构类型 Rect。explain函数接受空接口类型的参数,因此可以传递 MyString、 Rect 或其他类型的变量。
多接口一个类型可以实现多个接口。
在上面的程序中,我们创建了面积方法的Shape接口和Area方法的对象接口。由于 struct 类型 Cube 实现了这两个方法,因此它实现了这两个接口。因此我们可以将 struct 类型 Cube 的值赋给 Shape 或 Object 类型的变量。
如果我们使用变量s去调用接口Object里面的Volume方法,变量o去调用Shape接口里面的Area方法会发生什么?
program.go:31: s.Volume undefined (type Shape has no field or method Volume)
program.go:32: o.Area undefined (type Object has no field or method Area)
程序不能编译,因为 s 的静态类型是 Shape,o 的静态类型是 Object。由于Shape没有定义的Volume方法和Object没有定义的Area方法,我们得到上述错误。
为了让代码顺利编译,我们需要以某种方式提取这些接口的动态值可以通过使用类型断言来完成。
类型断言我们可以使用语法 i.(Type)查找接口的底层动态值,其中 i 是类型接口的变量,Type 是实现接口的类型。Go 将检查动态类型 i 是否与 Type 相同。类型断言用于断言判断一个变量是某种类型。类型断言只能在接口上发生。
断言interface i的值类型是T,如果是,t则为interface i的值,并且t的类型也为interface i的值类型;如果不是,则panic报错,类似:panic: interface conversion: interface {} is int, not float64
- 断言语法
t, ok := i.(T)
- 断言例子
package main
import "fmt"
func main() {
var i interface{} = 100
s, ok := i.(int)
fmt.Println(s, ok)
fmt.Printf("%T\n", s)
f, ok := i.(float64)
fmt.Println(f, ok)
fmt.Printf("%T\n", f)
}
// result
// 100 true
// int
// 0 false
// float64
注意:我们需要使用类型断言来获取接口的动态值,这样我们就可以访问该动态值的属性。例如,您不能访问类型接口对象上的结构字段,即使它具有结构的动态值。简而言之,访问接口类型没有表示的任何内容都会导致运行时panic。因此,确保在需要时使用类型断言。
类型转换类型切换的语法类似于类型断言,它是 i.(type) ,其中 i 是接口,type 是固定的关键字。使用它我们可以得到接口的动态类型,而不是动态值。
在上面的程序中,explain函数以使用类型转换。当使用任何类型调用explain函数时,i 接收它的动态值和动态类型。
通过在 switch 中使用 i.(type)语句,我们可以访问该动态类型。使用switch块中的case,我们可以根据接口 i 的动态类型执行条件操作。
接口嵌套在 Go 中,一个接口不能实现或扩展其他接口,但是我们可以通过合并两个或多个接口来创建一个新接口。让我们重写我们的Shape-Cube程序。
在上面的程序中,由于 Cube 实现了 Area 方法和Volume方法,所以它实现了Shape和Object接口。但是由于接口 Material 是这些接口的嵌入式接口,Cube 也必须实现Shape跟Object所有方法。匿名嵌套结构一样,嵌套接口的所有方法都被提升到父接口。
指针与值接收器
在上面的程序中,Area 方法属于 Rect 指针类型,因此它的接收者将获得 Area 类型变量的指针。但是上面的程序不会编译,Go 会抛出编译错误。
program.go:27: cannot use Rect literal (type Rect) as type Shape in
assignment: Rect does not implement Shape (Area method has pointer receiver)
上面的代码编码报错是因为发生了值传递才会导致出现这个问题。实际上不管接收者类型是值类型还是指针类型,都可以通过值类型或指针类型调用,这里面实际上通过语法糖起作用的。实现了接收者是值类型的方法,相当于自动实现了接收者是指针类型的方法;而实现了接收者是指针类型的方法,不会自动生成对应接收者是值类型的方法。
上面的代码我们使用指向 r 的指针,而不是 r 的值,这样 s 的动态类型就变成了 * Rect,它实现了 Area 方法。然而,即使使用 * Area 没有实现 Permeter,s.Permeter ()调用也没有失败。是因为如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法
接口对比可以将两个接口使用== 和!= 运算符进行对比。如果底层动态值为 nil,则两个接口始终相等,这意味着两个 nil 接口始终相等,因此 = = 返回 true。
var a, b interface{}
fmt.Println( a == b ) // true
如果这些接口不为空,那么它们的动态类型(具体值的类型)应该相同,具体值应该相等。
如果接口的动态类型不具有可比性,比如切片、映射和函数,或者接口的具体值是包含这些不可比性值的复杂数据结构,比如切片或数组,那么 == 或!= 操作将导致运行时panic。如果一个接口为 nil,那么 == 操作将总是返回 false。
,免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com