Interface实现原理分析

本文探讨了Go语言中的鸭子类型,通过接口实现了动态类型检查。介绍了接口的定义、实现、接口与指针的关系,以及接口转换和断言等核心概念。文章深入分析了Go接口的实现原理,包括iface和eface结构,展示了具体类型如何转换为接口类型,并详细解释了接口间的转换和类型断言的过程。
摘要由CSDN通过智能技术生成

Duck Typing

什么是鸭子类型

在这里插入图片描述
图中的大黄鸭是一只鸭子吗?如果从传统角度来看,图中的大黄鸭并非是一只鸭子,因为它即不会叫也不会跑,甚至连生命都没有

首先看下鸭子类型的定义

If it walks like a duck and it quacks like a duck, then it must be a duck
如果某个东西像鸭子一样走,像鸭子一样嘎嘎叫,那它一定是鸭子

所以,从Duck Typing角度来看,图中的大黄鸭是一只鸭子

鸭子类型,是程序设计中的一种类型推断风格,它描述事物的外部行为而非内部结构

Go语言的鸭子类型

Go语言通过接口的方式实现Duck Typing。不像其他动态语言那样,只能在运行时才能检查到类型不匹配,也不像大多数静态语言那样,需要显示声明实现哪个接口,Go语言接口的独特之处在于它是隐式实现。

概述

接口类型

接口是一种抽象类型,它没有暴露所含数据的布局或者内部结构,当然也没有哪些数据的基本操作,所提供的仅仅是一些方法。当你拿到一个接口类型的变量,你无从知道它是什么,但你能知道它能做什么,或者更精确地讲,仅仅是它提供了哪些方法。

接口定义

Go语言提供了 interface关键字,接口中只能定义需要实现的方法,不能包含任何的变量

type 接口类型名 interface{
   
    方法名1( 参数列表1 ) 返回值列表1
    方法名2( 参数列表2 ) 返回值列表2}

例如 io.Writer 其实就是接口类型

type Writer interface {
   
    Write(p []byte) (n int, err error)
}

接口与接口间可以嵌套得到新接口,如 io.ReadWriter


type Reader interface {
   
    Read(p []byte) (n int, err error)
}

type ReadWriter interface{
   
    Reader
    Writer
}

不包含方法的接口,叫做空接口类型

interface{
   }

实现接口

如果一个具体类型实现了一个接口要求的所有方法,那么这个类型实现了这个接口。当具体类型实现了一个接口时,这个具体类型才可以赋值给该接口

如下示例中,定义一个 Runner 接口,只包含一个 run() 方法, Person 结构体实现了 Run() 方法,那么就实现了 Runner 接口

type Runner interface {
   
    Run()
}

type Person struct {
   
    Name string
}

func (p Person) Run() {
   
    fmt.Printf("%s is running\n", p.Name)
}

func main() {
   
    var r Runner
    r = Person{
   Name: "song_chh"}
    r.Run()
}

另外,因为空接口类型是没有定义任何方法的接口,因此所有类型都实现了空接口,也就是说可以把任何类型赋给空接口类型

接口和指针

接口在定义一组方法时,没有对实现的接收者做限制,所以有两种实现方式的接收者,一种是指针接收者,另一种是值接收者

同一个方法不能两种实现同时存在

为Runner接口增加一个 Say() 方法,Person结构体类型使用指针接收者实现 Say() 方法

type Runner interface {
   
    Run()
    Say()
}

type Person struct {
   
    Name string
}

func (p Person) Run() {
   
    fmt.Printf("%s is running\n", p.Name)
}

func (p *Person) Say() {
   
    fmt.Printf("hello, %s", p.Name)
}

在对接口变量进行初始化时,可以使用结构体或者结构体指针

var r Runner
r = &Person{
   Name: "song_chh"}
r = Person{
   Name: "song_chh"}

因为实现接口的接受者类型和接口初始化时的类型都有两个维度,就会产生四种不同情况的编码
在这里插入图片描述

  • × 表示编译不通过

下面两种情况能够通过编译很好理解:

  • 方法接受者和初始化类型都是结构体值
  • 方法接受者和初始化类型都是结构体指针

首先,我们来看一下能够通过编译的情况,也就是方法接收者是结构体,而初始化的变量是指针类型


type Runner interface {
   
    Run()
    Say()
}

type Person struct {
   
    Name string
}

func (p Person) Run() {
   
    fmt.Printf("%s is running\n", p.Name)
}

func (p *Person) Say() {
   
    fmt.Printf("hello, %s", p.Name)
}

func main() {
   
    var r Runner
    r = &Person{
   Name: "song_chh"}
    r.Run()
    r.Say()
}

上述代码中,Person结构体指针是能够直接调用Run和Say,因为作为结构体指针,能够隐式获取到底层的结构体,然后在通过结构体调用对应的方法
如果将引用去掉,即变量初始化使用结构体类型

r = Person{
   Name: "song_chh"}

则会提示编译不通过

./pointer.go:24:4: cannot use Person literal (type Person) as type Runner in assignment:
  Person does not implement Runner (Say method has pointer receiver)

那么为什么会编译不通过呢?首先在Go语言在进行参数传递都是 值传递
当代码中的变量是 &Person{} 时,在方法调用的过程中会对参数进行复制,创建一个新的 Person 结构体指针,指针指向一个确定的结构体,所以编译器会隐式的对变量解引用获取指针指向的结构体,完成方法的调用
在这里插入图片描述
当代码中的变量是 Person{}时,在方法调用的过程中会对参数进行复制,也就是 Run() 和 Say() 会接受一个新的 Person{} 变量。如果方法接收者是 *Person ,编译器无法根据结构体找到一个唯一的指针,所以编译器会报错
在这里插入图片描述

注意:一个具体类型T的变量,直接调用*T的方法也是合法的,因为编译器会隐式的帮你完成取地址操作,但这仅仅是一个语法糖

nil和non-nil

再看一段示例,还是Runner接口和Person结构体,注意看main()函数体,首先声明一个接口变量r,打印是否为nil,紧接着定义一个*Person类型的p,打印p是否为nil,最后将p赋值给r,打印此时的r是否为nil


type Runner interface {
   
    Run()
}

type Person struct {
   
    Name string
}

func (p Person) Run() {
   
    fmt.Printf("%s is running\n", p.Name)
}

func main() {
   
    var r Runner
    fmt.Println("r:", r == nil)

    var p *Person
    fmt.Println("p:", p == nil)

    r = p 
    fmt.Println("r:", r == nil)
}

输出结果是什么?

r: true or false
p: 
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值