Golang中的Interface(接口),全面解析
什么是接口
interface就是字面意思——接口,C++中可以用虚基类表示;Java中就是interface。interface则是Golang更接近面向对象编程范式的另一个难点
interface是方法签名的一个集合,这些方法可以被任一类型通过方法实现。因此接口就是对象行为的申明(不是定义,仅仅表示方法签名,也可以称作函数原型)。
💡注意:我多次强调 任一类型 ,Golang中所有类型都可以实现自己的方法,为了便于理解,本文还是使用 struct(结构体)来做示例
举个例子🌰,狗会走路也会汪汪叫,如果在一个
所以,
如果你是一个OOP(面向对象)类型的程序员,你可能用过implement关键字来实现一个
💡注意:一个类型定义了一个或多个方法,方法的名字,参数,返回值和接口中的方法签名完全一致,并且接口中的所有方法签名在这个类型中都存在,那么我们称为实现了这个接口
举例子🌰,如果我们说一个东西,走起来像鸭子,游泳像鸭子,并且叫起来像鸭子,那么在Golang中我们就认为它就是一只鸭子🦆!!
申明一个接口
类似struct的申明方法,我们需要使用interface关键字来定义
type Shape interface {
Area() float64 //面积
Perimeter float64 //周长
}
在上面的例子中,我们定义了一个Shape接口,其中包含了两个方法签名:Area, Perimeter,无形参,返回float64值。任何类型,如果实现了和这两个签名形式一样
由于

上面的例子虽然简单,但是包含了很多信息,让我来理理
类型呢,一种是静态类型,另一种就是动态类型。静态类型就是指接口本身的类型,比如上图中的Shape 就是静态类型。
值只有动态值(没有静态值)。一个接口类型的变量只能表示实现了这个接口的动态类型的值。这个动态类型的变量就是这个接口的动态值。(ok,不知道有没有被我绕晕,晕了没事,这里做好笔记,继续往下看,回头再来理解这里的概念就明白了。^.^)
从上面的例子中,我们可以发现,接口类型变量值和类型都是
当我们用fmt.Println函数的时候,它接受的就是接口类型的参数,第一个Println的参数就是指向了这个接口的动态值,第二个Println的参数指向的就是这个接口的动态类型。
💡 事实上, 接口 s 是有个静态的类型的:Shape
接口的实现
接着上例子🌰,我们先定义一个包含Area和*

在上面的程序中,我们定义了一个Shape接口和Rect结构体。然后Rect实现了Area和*
当一个类型实现了某个接口,这个类型的变量也可以用它所实现的接口类型表示(或者说用接口类型的变量去存放)。我们可以声明一个Shape接口类型的便令
💡 其实上面我们已经使用了多态的特性
因为Rect实现了Shape接口,所以第25行代码是完全有效的。我们可以看到,接口变量
💡 我们用动态这个词,是因为我们也可以给接口变量 s 赋值另一个实现了 Shape接口的结构体类型, 所以 s 实际指向的对象类型不是固定的,是动态的
有时候
我们可以用
我们也可以比较变量
让我们改变

我们定义了一个新的结构体Circle,它也实现了Shape接口,所以我们可以给变量
猜猜下面程序会发生什么?

上面程序中,我们删除了Perimeter方法,这个程序就编译不过并且抛出一个错误。
program.go:22: cannot use Rect literal (type Rect) as type Shape in assignment:
Rect does not implement Shape (missing Perimeter method)
从上面的错误中我们可以很容易的理解实现接口的要求:我们需要实现接口中申明的所有方法签名。这也解释了我之前说的要实现接口中的所有方法,看到这里应该有了清晰的理解。
空接口
当一个接口没有申明任何方法签名,它就是空接口,用*
现在你知道标准库fmt中的*
func Println(a ...interface{}) (n int, err error)
如你所见,
我们创建一个explain函数,有一个空接口类型的输入参数,无返回值。用它来解释动态类型和空接口。

上面的程序中,我们创建了一个自定义字符串类型MyString和一个结构体Rect。因为explain函数接收的
多接口
一个类型可以实现多个接口,也可以理解为多继承。直接例子🌰🌰🌰🌰

上面的程序中,我们创建了一个有Area方法*
我们指定变量
fmt.Println("volume of s of interface type Shape is ", s.Volume())
fmt.Println("area of o of interface type Object is", o.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)
这个程序是编译不过的,因为
为了让上面程序正常运行,我们需要设法获取这些接口的动态值——Cube类型对象(实现了这些接口的类型)。这时候我们就要用到类型断言,下面有请类型断言。
类型断言
我们创建一个变量
i.(Type), Type代表的目标类型,这个类型实现了
例子🌰来了:

上面的程序中,Shape类型的变量
注意,如果使用类型断言语法:
impossible type assertion:
Type does not implement i (missing Area method)
但是,即使 Type 实现了
panic: interface conversion: main.Shape is nil, not main.Cube
但是呢,我们还是有方法能避免运行错误的,需要用另一种 类型断言 语法:
value, ok := i.(Type)
在上面的语法中,我们可以用bool类型的变量

因为接口变量
但是,因为接口变量
panic: interface conversion: main.Cube is not main.Skin: missing method Color
⚠️ 注意:我们使用类型断言获取到该接口的动态值后,我们就能通过它获取这个动态值的属性和方法。但是你不能直接通过接口类型对象直接去获取它指向的动态值的属性信息。
>
简而言之,获取任何接口类型没有定义的属性或者方法,都会引起运行时错误。所以在必要的时候,记得使用类型断言进行转换和判断。
类型断言 不仅仅只是用来判断某个接口是否指向了某个具体值。我们也用来从一个接口类型到另一个接口类型的转换(请看上面的例子:
Switch
package main
import fmt
type MyString string
type Rect struct {
Width float
Height float
}
func explain(i interface{}) {
fmt.Printf("value give to explain function is of type:%T with value:%v", i, i)
}
func main() {
myString := MyString{"hello, world"}
r := Rect{5.5, 4.5}
explain(myString)
explain(r)
}
现在,让我们看看空接口对的作用,上面的例子是之间空接口那一节中用的例子,
但是如果有一个string类型的参数,我们把这个参数传入
我们可以用 string 包的 ToUpper 这个函数,但是这个函数只接受字符串类型的参数,在
这时候我们就可以用到 Switch 关键字了。Switch 语法很类似之前用的 类型断言 的语法:
💡 注意 i.(type) 这个语法只用在 switch 语句中
让我们来看个例子:

在上面的例子中,我们用 Switch 修改了
在 Switch 中使用 i.(type),我们可以获取 i 的动态类型。然后在 Switch 中使用:case 关键字 + 类型,这种方式来判断是否符合 i 的动态类型。
在 case string 块中,我们使用
## 接口的嵌套
在Go语言中,一个接口不能实现或者扩展另一个接口,只能通过组合的方式,把多个接口组合成一个新的接口。下面重写Shape—Cube程序来感受一下:

上面的程序中,因为Cube实现了
就像匿名嵌套结构体,这是可行的,所有内嵌接口中的方法签名也都属于父接口,父接口可以随意访问。
指针接收者和值接收者
本文直到这里,所有方法都是用的值接收者的方式。如果使用指针接收者,这些程序还是否可行?让我们来检验一下:
[

](https://imgchr.com/i/GSTVSJ)
在上面的程序中,
program.go:27: cannot use Rect literal (type Rect) as type Shape in assignment: Rect does not implement Shape (Area method has pointer receiver)
这是什么鬼!Rect 明明已经实现了 Shape 接口的所有方法签名,这是怎么回事呢,Rect表示不服!凭什么提示
在仔细品品上面的错误,后面还有一句
一个结构类型的方法(比如上面的Rect结构体的Area方法),它的接收者无论是这个结构体的指针类型,还是值类型,我们都可以用
但是,在实现接口的情况下,可能会有一点点的不同。上面程序中,接口
💡 在Golang中 Rect 类型 和 *Rect 类型是两种不同的类型,在使用Rect类型变量调用方法的时候,Golang底层会去自动转换成指定的接收者类型去调用该方法。但是,在接口的实现中,Rect实现的方法,不代表 *Rect 就实现了该方法。就像上面的程序,*Rect实现了 *Area* 方法,但是 Rect 没有实现
Area 方法,Golang底层不会默认 Rect 也实现了Area 方法。
所以让上面的程序可以顺利编译通过,我们可以给
用上面的方法重写程序:

仅仅改变了第25行代码,编译完美通过并成功执行。
但是,有个问题,虽然 *Rect 没有实现
我还是希望Go语言在处理接口实现的时候能统一一下这方面的细节,既然已经做到语法格式如此的严格,那么就请严格到底吧!
接口的比较
两个接口可以通过比较运算符 == ,*
var a, b interface{}
fmt.Println(a==b) // true
fmt.Println(a!=b) // false
如果两个不是空接口,那么只有当他们的动态类型和动态值都相等的情况下,他们才相等(==操作符返回true)。
上面的情况都是基于动态类型都能比较的情况下进行的,如果动态类型不能比较呢(比如:slice,map,array, function和结构体等),那么,执行比较操作的时候会抛出运行时异常。
接口的用处
到此为止,接口的特性和使用方法基本都讲完了,总结一下接口的用处吧: