Go interface深入分析

栏目: C · 发布时间: 6年前

内容简介:版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/TurkeyCock/article/details/83317943

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/TurkeyCock/article/details/83317943

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其实就是两个指针,一个指向类型信息,一个指向实际的对象。

Go 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://blog.csdn.net/turkeycock

或关注飞久微信公众号:
Go interface深入分析

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

图解机器学习

图解机器学习

杉山将 / 许永伟 / 人民邮电出版社 / 2015-4 / 49

本书用丰富的图示,从最小二乘法出发,对基于最小二乘法实现的各种机器学习算法进行了详细的介绍。第Ⅰ部分介绍了机器学习领域的概况;第Ⅱ部分和第Ⅲ部分分别介绍了各种有监督的回归算法和分类算法;第Ⅳ部分介绍了各种无监督学习算法;第Ⅴ部分介绍了机器学习领域中的新兴算法。书中大部分算法都有相应的MATLAB程序源代码,可以用来进行简单的测试。 本书适合所有对机器学习有兴趣的初学者阅读。 187张图......一起来看看 《图解机器学习》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

SHA 加密
SHA 加密

SHA 加密工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具