写在开头
非原创,知识搬运工,本节介绍了GO语言接口,接口的类型,接口方法接收者的差异,接口和nil的对比,接口的隐式转换
demo代码地址
往期回顾
带着问题去阅读
- GO的接口是隐式的,还是和java一样显式的
- 接口方法的值接收者和指针接收者有啥区别
- 接口的实现一定要用指针吗?为什么
- 接口如何和nil做比较
- 接口和接口之间如何比较
- 空接口是和JS的Object一样的任意类型吗
- 接口底层是怎么实现的
1.隐式接口/鸭子类型
在前文结构体中大致提到了一下接口,我们说空接口interface{}
是类似js的Object,任何结构体都实现了该接口。
那有了结构体和实例不就行了,为什么还需要接口呢?接口的本质就是引入一个新的中间层,调用方可以通过接口与具体实现分离,解耦,即上层模块不再依赖下层模块的具体实现,只需要约定一个接口即可,隐藏底层实现,减少关注点。
比如sql语句查询,我们不关心底层数据库的实现,只关心利用接口返回的结果
GO中的接口都是隐式的,只要结构体实现了接口的所有方法,那么就隐式地实现了该接口,这是不是听起来很像我们学习面向对象常听到的一句话
If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.
某个东西长得像鸭子,像鸭子一样游泳,能嘎嘎叫,那它就可以被看做鸭子
GO通过接口的方式完美支持鸭子类型(Duck Typing)(实际上是编译器在其中做了隐匿的转换工作)
2.值接收者和指针接收者
一段很简单的demo
type Languge interface {
sayHello()
code()
}
type Go struct{}
func (g Go) sayHello() {
fmt.Println("hello world go")
}
func (g *Go) code() {
fmt.Println("code go")
}
func sayHello(l Languge) {
l.sayHello()
l.code()
}
func TestInterface(t *testing.T) {
g := new(Go)
sayHello(g)
}
接口/结构体方法和函数的区别就是方法有一个接收者,接收者可以是值接收者,也可以是指针接收者(code)
区别:
- 结构体章中我们也聊过只有指针接收者可以改变结构体内部成员(实际上也是编译器完成实现的)
- 实现了值接收者的方法,相当于自动实现了指针接收者的方法,但是实现了指针接收者的方法,不会自动实现对应值接收者类型的方法
第二个区别听起来比较乱,用上面的例子体验一下
func TestDiff(t *testing.T) {
//var g Languge = &Go{} 这个能通过
var g Languge = Go{} //这个不能通过
g.code()
g.sayHello()
}
这是一段一眼错误的代码,因为没用实例的指针(*GO)赋值给接口类型,那为什么不用指针就会报错呢?
cannot use Go{} (value of type Go) as Languge value in variable declaration: Go does not implement Languge (method code has pointer receiver)
错误提示告诉我们结构体实例(GO)之所以不能作为接口的实现,是没有实现方法code!(method code has pointer receiver),我们再回头看上面的第二个观点恍然大悟:
GO{}类型实现了sayhello(值接收者),所以让*Go{}类型自动拥有了sayhello方法,但是*Go类型实现了code方法,却不能让Go{}类型拥有code方法
还看不懂?再总结一下
如果实现了接收者是值类型的方法,会隐式地实现接收者是指针类型的对应方法,反之则无
所以当我们将指针接收者方法code注释掉,就能通过
type Languge interface {
sayHello()
}
type Go struct{}
func (g Go) sayHello() {
fmt.Println("hello world go")
}
func sayHello(l Languge) {
l.sayHello()
}
func TestInterface(t *testing.T) {
g := new(Go)
sayHello(g)
}
func TestDiff(t *testing.T) {
var g Languge = Go{}
g.sayHello()
}
所以并不是什么接口的实现必须是指针,只是某个方法没实现而已,但是接口的实例还是遇事不决用指针总没错。
3.接口的类型
先梳理一下几个概念
type a interface{} //这种叫接口
type b struct{} //这种叫结构体
a := b{} //a被称为b的实体或实例,b就是a的实体类型
没错,接口也有类型,一种是老朋友空接口,另一种是带有方法的接口。go程序在运行时使用runtime.iface表示后者,runtime.eface表示空接口
type iface struct {
tab *itab
data unsafe.Pointer
}
type eface struct {
_type *_type
data unsafe.Pointer
}
iface:
- tab指向一个itab实体,表示接口的类型以及赋给这个接口的实体类型
-
data则指向接口具体的值,一般是指向堆的内存指针
type itab struct {
inter *interfacetype
_type *_type
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
- _type是实体的类型(上文Language的Go),包括内存对齐的方式,大小
- inter是接口的类型
- fun放置和接口方法对应的具体数据类型的方法地址,数组大小为1是因为只存储第一个方法的函数指针,更多方法则在之后的内存空间存储,这些方法是按照函数名称的字典序进行排序的
eface就维护_type字段存放实体类型,没有方法就不需要存储。
另外这个_type,GO中的各种数据类型都是在这个字段基础上,增加一些额外字段进行管理的,这也是反射实现的基础
最后需要注意的是空接口不意味着任意类型,将类型转为inteface类型,变量在运行期间类型也会发生变化
func Print(v interface{}) {
println(v)
}
这里调用Print时会对v进行类型转换为interface{}
3.1接口的动态类型和动态值
继续对iface进行探索,tab指向类型信息(接口类型和实例类型),data是数据指针(实例),分别被称为动态类型T和动态值V,
只有当动态类型和动态值都为nil,该接口值==nil,下面有三个例子
例1
func TestCompareNil(t *testing.T) {
var g Languge
t.Log(g == nil)
fmt.Printf("g:%T,%v\n", g, g)
var g1 *Go
t.Log(g1 == nil)
g = g1
t.Log(g == nil)
fmt.Printf("g:%T,%v", g, g)
}
true
g:<nil>,<nil>
true
false
g:*main.Go,<nil>
刚声明时都为动态类型T和值V都为nil,g==nil成立
之后赋值操作g=g1,动态类型T成为*main.Go,虽然动态值V=g1为nil,但是g==nil不成立(两个条件都要成立)
例2
func TestCompareNil2(t *testing.T) {
var p func() Languge
p = func() Languge {
var g *Go = nil
return g
}
g1 := p()
t.Log(g1)
t.Log(g1 == nil)
}
nil
false
第一个nil很容易理解,值为nil,(打印是获取值嘛)
第二个为false是因为p函数返回Language接口,发生隐式类型转换,动态值为nil,但是动态类型是*Go,所以为false
例3
func TestCompareNil3(t *testing.T) {
var p func(v interface{}) bool
p = func(v interface{}) bool {
return v == nil
}
var g *Go
t.Log(g == nil)
t.Log(p(g))
}
true
false
这个是空接口隐式类型转换的例子,*GO转为interface{}类型,包含了动态类型信息GO,所以不成立
综上,这一类的判断主要考察的是对T有没有进行赋值,即使是其他类型转为eface空接口也能对T赋值
4.利用编译器自动检测类型是否实现接口
一个小技巧
var _ Language = (*Go)(nil)
var _ Language = GO{}
func TestCompareInterface(t *testing.T) {
var g1 Languge = &Go{}
var g2 Languge = &Go{}
t.Log(g1 == g2)
var t1 interface{} = int(1)
var t2 interface{} = int(1)
t.Log(t1 == t2)
fmt.Printf("%T,%T,%v,%v\n", t1, t2, t1, t2)
var t3 interface{} = int64(1)
t.Log(t3 == t1)
fmt.Printf("%T,%T,%v,%v\n", t1, t3, t1, t3)
}
true
true
false
T和V都相等时才相等
5.断言和类型转换
5.1类型断言
type Person interface {
Say() string
}
type Man struct {
Name string
}
func (m *Man) Say() string {
return "Man"
}
func main() {
var p Person
m := &Man{Name: "hhf"}
p = m
if m1, ok := p.(*Man); ok {
fmt.Println(m1.Name)
}
}
断言处对应的汇编代码如下
0x0087 00135 (main.go:23) MOVQ "".p+104(SP), AX
0x008c 00140 (main.go:23) MOVQ "".p+112(SP), CX
0x0091 00145 (main.go:23) LEAQ go.itab.*"".Man,"".Person(SB), DX
0x0098 00152 (main.go:23) CMPQ DX, AX
0x0091将go.itab.*"".Man和"".Person(SB)放入DX,比较判断两者是否相等,来判断person的真实类型是否是Man,本质上就是内存排布的比较,同理类型转换也是如此
6.VScode中寻找实现的接口
以gin框架的核心结构体Engine为例
想要查看该实例实现了哪个接口:
- 快捷键 ctrl+f12
-
Go->Go to Implementation,中文界面就是"转到"->“转到实现”,同理查看接口实例也可以用这种办法
参考:
1.深度解密接口
2.GO接口
3.GO面试题
4.go interface原理剖析