Go interface深入分析

1.鸭子类型(Duck Typing)

  • If it walks like a duck and it quacks like a duck, then it must be a duck.
  • interface是一种鸭子类型
  • 无需显示声明,只要对象实现了接口声明的的全部方法,就实现了该接口
  • 把对象的类型检查从编译时推迟到运行时
  • 好处:
    • 松耦合
    • 可以先实现类型,再抽象接口

2.值receiver VS. 指针receiver

type T struct {}

func (t T) Value() {} //value receiver
func (t *T) Pointer() {} //pointer receiver
  • 值receiver会复制对象实例,而指针receiver不会
  • 把方法看作普通函数,receiver可以理解为传入的第一个参数
  • 只要receiver参数类型正确,方法就可以被执行

思考题:下面哪些语句在运行时会报错?

func main() {
    var p *T

    p.Pointer()
    (*T)(nil).Pointer()
    (*T).Pointer(nil)
    p.Value()
}

另外,map中的元素是不可寻址的(not addressable),简单来说就是不能取指针。所以如果map中存储struct元素的话,大部分情况都是以指针类型定义的。

func main() {
    m := make(map[string]T, 0)
    m["a"] = T{}
    m["a"].Value() // GOOD
    m["a"].Pointer() // BAD,编译错误
}
-----------------------------------------
func main() {
    m := make(map[string]*T, 0)
    m["a"] = T{}
    m["a"].Value() // GOOD
    m["a"].Pointer() // GOOD
}

3.方法集

  • 类型有一个与之相关的方法集,决定了它是否实现某个接口
  • 类型T的方法集包含所有receiver T的方法
  • 类型*T的方法集包含所有receiver T + *T的方法

可以通过反射进行验证:

func printMethodSet(obj interface{}) {
    t := reflect.TypeOf(obj)

    for i, n := 0, t.NumMethod(); i < n; i++ {
        m := t.Method(i)
        fmt.Println(t, m.Name, m.Type)
    }
}

func main() {
    var t T

    printMethodSet(t)
    fmt.Println("----------------")
    printMethodSet(&t)
}

输出结果:

main.T  Value func(main.T)
----------------
*main.T Pointer func(*main.T)
*main.T Value func(*main.T)

可以看到,*T类型包含了receiver T + *T的方法。但是,似乎Value()方法的receiver被改变了?

敲黑板:方法集仅仅用来验证接口实现,对象或对象指针会直接调用原实现,不会使用方法集

思考题:下面程序的输出是什么?

type T struct {
    x int
}

func (t T) Value() { //value receiver
    t.x++
}
func (t *T) Pointer() { //pointer receiver
    t.x++  //Go没有->运算符,编译器会自动把t转成(*t)
}

func main() {
    var t *T = &T{1}

    t.Value()
    fmt.Println(t.x)
    t.Pointer()
    fmt.Println(t.x)
}

4.什么是interface?

先看一下Go语言的实现,代码位于runtime/runtime2.go:

type iface struct {
        tab  *itab          //类型信息
        data unsafe.Pointer //实际对象指针
}

type itab struct {
        inter *interfacetype //接口类型
        _type *_type         //实际对象类型
        hash  uint32
        _     [4]byte
        fun   [1]uintptr     //实际对象方法地址
}

可以看到,interface其实就是两个指针,一个指向类型信息,一个指向实际的对象。
在这里插入图片描述
对象方法查找的两大阵营:

  • 静态类型语言:如C++/Java,在编译时生成完整的方法表
  • 动态类型语言:如Python/Javascript,在每次调用方法时进行查找(会使用cache)

Go采取了一种独有(折衷)的实现方式:

  • 在进行类型转换时计算itab,查找具体实现
  • itab类型只和interface相关,也就是说只包含接口声明的方法的具体实现(没有多余方法)

举例:

 1 type I interface {
 2     hello()
 3 }   
 4 
 5 type S struct {
 6     x int
 7 }   
 8 func (S) hello() {}
 9 
10 func main() {
11     s := S{1}
12     var iter I = s
13     for i := 0; i < 100; i++ {
14         iter.hello()
15     }   
16 }

Go会在第12行完成itable的计算,然后在第14行直接跳转。而在Python中则要到第14行才进行方法查找,虽然有cache的存在,仍然比直接一条跳转指令低效得多。

5.interface赋值

  • 将对象赋值给接口变量时,会复制该对象
  • 把指针赋值给接口变量则不会发生复制操作

可以用gdb查看接口内部数据。先用下面的命令阻止编译器优化:
go build -gcflags “-N -l”

从下面的例子可以看出,s的地址和i.data不同,发生了对象复制:

type I interface {
    hello()
}

type S struct {
    x int
}
func (S) hello() {}

func main() {
    s := S{100}
    var i I = s
    i.hello()
}
================= gdb调试信息 =======================
(gdb) i locals
i = {tab = 0x1071dc0 <S,main.I>, data = 0xc420012098}
s = {x = 100}
(gdb) p/x &s
$1 = 0xc420041f58

而下面这个例子中是指针赋值,因此s的地址和i.data是相同的。

type I interface {
    hello()
}

type S struct {
    x int
}
func (*S) hello() {}

func main() {
    s := S{100}
    var i I = &s
    i.hello()
}
================= gdb调试信息 =======================
(gdb) i locals
&s = 0xc420076000
i = {tab = 0x1071cc0 <S,main.I>, data = 0xc420076000}

6.interface何时等于nil?

  • 只有当接口变量中的itab和data指针都为nil时,接口才等于nil

常见错误:

type MyError struct{}

func (*MyError) Error() string {
    return "myerror"
}

func isPositive(x int) (int, error) {
    var err *MyError

    if (x <= 0) {
        err = new(MyError)
        return -x, err
    }

    return x, err //注意,err是有类型的!
}

func main() {
    _, err := isPositive(100)
    if err != nil {
        fmt.Println("ERROR!")
    }
}

可以看到,isPositive()函数返回err时相当于进行了一次类型转换,把*MyError对象转换为一个error接口。这个接口变量的data指针为nil,但itab指针不为空,指向MyError类型。

正确做法:直接返回nil即可

7.空接口interface{}

  • interface{}可以接受任意类型,会自动进行转换(类似于Java中的Object)
  • 例外:接口切片[]interface{}不会自动进行类型转换

看下面的例子:

func print(names []interface{}) {
    for _, n := range names {
        fmt.Println(n)
    }
}

func main() {
    names := []string {"star", "jivin", "sheng"}
    print(names)
}

编译后会报以下错误:

cannot use names (type []string) as type []interface {} in argument to print

原因解释:[]interface{}在编译时就有确定的内存布局,每个元素的大小是固定的(2个指针),而[]string的内存布局显然不同。至于为什么Go为什么不帮我们做这个转换,个人猜测可能是因为转换的开销比较大。

解决方案1: 使用interface{}代替[]interface{}作为参数

func print(names interface{}) {
    ns := names.([]string)
    for _, n := range ns {
        fmt.Println(n)
    }
}

解决方案2:手动做一次类型转换

func main() {
    inames := make([]interface{}, len(names))
    for i, n := range names {
        inames[i] = n 
    }   

    print(inames)
}

参考:
https://github.com/golang/go/wiki/MethodSets
https://research.swtch.com/interfaces
https://github.com/golang/go/wiki/InterfaceSlice

更多文章欢迎关注“鑫鑫点灯”专栏:https://blog.csdn.net/turkeycock
或关注飞久微信公众号:
在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值