golang学习笔记--interface

350 阅读4分钟

动态类型

golang支持动态类型,类似c++的多态,它根据指定类型的方法是否被实现决定该类型是否是指定类型。举个例子

type A interface{
    Func1()
}

type B struct {
}

func (b B) Func1() {
}

类型B就是类型A,这种能力叫做duck typing。

但是golang是静态语言,不像python是动态的,也不像C++那样必须要显示的声明实现了A接口才能用B作为A的一个多态实例。

值接受者和指针接受者的区别

package main

import "fmt"

type Person struct {
    age int
}

func (p Person) howOld() int {
    return p.age
}

func (p *Person) growUp() {
    p.age += 1
}

func main() {
    // qcrao 是值类型
    qcrao := Person{age: 18}

    // 值类型 调用接收者也是值类型的方法
    fmt.Println(qcrao.howOld())

    // 值类型 调用接收者是指针类型的方法
    qcrao.growUp()
    fmt.Println(qcrao.howOld())

    // ----------------------

    // stefno 是指针类型
    stefno := &Person{age: 100}

    // 指针类型 调用接收者是值类型的方法
    fmt.Println(stefno.howOld())

    // 指针类型 调用接收者也是指针类型的方法
    stefno.growUp()
    fmt.Println(stefno.howOld())
}

-值接收者指针接收者
值类型调用者方法会使用调用者的一个副本,类似于“传值”使用值的引用来调用方法,上例中,qcrao.growUp() 实际上是 (&qcrao).growUp()
指针类型调用者指针被解引用为值,上例中,stefno.howOld() 实际上是 (*stefno).howOld()实际上也是“传值”,方法里的操作会影响到调用者,类似于指针传参,拷贝了一份指针


实现了接收者是值类型的方法,相当于自动实现了接收者是指针类型的方法;而实现了接收者是指针类型的方法,不会自动生成对应接收者是值类型的方法。

package main

import "fmt"

type coder interface {
    code()
    debug()
}

type Gopher struct {
    language string
}

func (p Gopher) code() {
    fmt.Printf("I am coding %s language\n", p.language)
}

func (p *Gopher) debug() {
    fmt.Printf("I am debuging %s language\n", p.language)
}

func main() {
    var c coder = &Gopher{"Go"}//Gopher的话,debug接口是未定义的
    c.code()
    c.debug()
}

而指针接收者调用一般使用的场景是:

1、接收者的值能被更改

2、大型结构体避免频繁复制,因为结构体大的话容易逃逸套堆上,增加GC

 iface和eface区别

//源码都摘自go1.13.x
iface源码摘要:
type iface struct {  
 tab  *itab   
 data unsafe.Pointer
}
eface源码摘要:
type eface struct {  
 _type *_type   
 data  unsafe.Pointer
}

从源码看到,都有一个data字段,含义是实际接口的值。

iface是有个tab字段,类型是*itab,而*itab包含接口的类型和接口实现的方法集合,对比eface的_type字段的类型*type就不一样了,_type只描述接口的类型,没有方法集合,所以iface可以理解为是具有指定方法集合的interface类型,eface是不实现指定方法集合的interface类型。

接口的动态类型和动态值

这里指的是iface的tab和data,如果在实际使用中,要让两个iterface相等,他们的该两个字段都要相等,特别是在进行==nil比较的时候,所以,为了避免采坑,一般不要赋值为nil,而是直接创建临时变量。下边是例子:

package main

import (
	"fmt"
)

type Person interface {
	Name()
}

type Engineer struct {
	name string
}

func (e Engineer) Name() {
	fmt.Printf("Name:%s\n", e.name)
}

func main() {
	var p1 Person
	var p2 Engineer
	var p3 *Engineer
	var p4 *Engineer = &Engineer{}
	fmt.Printf("p1: type:%T, value:%v, p1==nil:%t\n", p1, p1, p1==nil)
	fmt.Printf("p2: type:%T, value:%v\n", p2, p2)
	fmt.Printf("p3: type:%T, value:%v, p3==nil:%t\n", p3, p3, p3==nil)
	fmt.Printf("p4: type:%T, value:%v, p4==nil:%t\n", p4, p4, p4==nil)
	p1 = p3
	fmt.Printf("p1: type:%T, value:%v, p1==nil:%t\n", p1, p1, p1==nil)
}

输出:

p1: type:<nil>, value:<nil>, p1==nil:true
p2: type:main.Engineer, value:{}
p3: type:*main.Engineer, value:<nil>, p3==nil:true
p4: type:*main.Engineer, value:&{}, p4==nil:false
p1: type:*main.Engineer, value:<nil>, p1==nil:false


类型转换和断言的区别

两者都是类型转换,断言是针对接口类型的,类型转换要满足兼容,也可以转换接口,不过没人这么干。

p5 := Person(p4)
输出就是:
p5: type:*main.Engineer, value:&{}


而接口的类型转换的话,一般最好使用安全的方式进行转换:

目标类型, 布尔参数 := <表达式>.(目标类型)

或者是使用switch的方式

接口转换原理

接口的转换就是把待转换的实例实现的接口与目标接口的接口集合进行比对,如果包含目标接口集合,则会在全局的ifce的tab中记录这个转换。通过hash进行查找的。

go与c++的不同

golang中类似C++的多态功能就是通过interface实现同一个接口不同表现的功能,而实现方式和c++不同主要是因为golang是鸭子类型,所以它事先没法用一个类似虚函数表的东西提前记录,只能使用一个全局的itab去动态的增加。

参考资料

笔记中许多案例和分析都摘抄自:www.cnblogs.com/qcrao-2018/…