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深入分析

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

查看所有标签

猜你喜欢:

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

Computational Geometry

Computational Geometry

Mark de Berg、Otfried Cheong、Marc van Kreveld、Mark Overmars / Springer / 2008-4-16 / USD 49.95

This well-accepted introduction to computational geometry is a textbook for high-level undergraduate and low-level graduate courses. The focus is on algorithms and hence the book is well suited for st......一起来看看 《Computational Geometry》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具