V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX 提问指南
sanbenweiyang
V2EX  ›  问与答

Golang 中 interface 内部构造与面试真题分析

  •  
  •   sanbenweiyang · 2020-04-16 10:19:05 +08:00 · 722 次点击
    这是一个创建于 1487 天前的主题,其中的信息可能已经有所发展或是发生改变。

    (1) interface 的赋值问题

    以下代码能编译过去吗?为什么?

    package main
    
    import (
    	"fmt"
    )
    
    type People interface {
    	Speak(string) string
    }
    
    type Stduent struct{}
    
    func (stu *Stduent) Speak(think string) (talk string) {
    	if think == "love" {
    		talk = "You are a good boy"
    	} else {
    		talk = "hi"
    	}
    	return
    }
    
    func main() {
    	var peo People = Stduent{}
    	think := "love"
    	fmt.Println(peo.Speak(think))
    }
    

    继承与多态的特点

    在 golang 中对多态的特点体现从语法上并不是很明显。

    我们知道发生多态的几个要素:

    1 、有 interface 接口,并且有接口定义的方法。

    2 、有子类去重写 interface 的接口。

    3 、有父类指针指向子类的具体对象

    那么,满足上述 3 个条件,就可以产生多态效果,就是,父类指针可以调用子类的具体方法。

    所以上述代码报错的地方在var peo People = Stduent{}这条语句, Student{}已经重写了父类People{}中的Speak(string) string方法,那么只需要用父类指针指向子类对象即可。

    所以应该改成var peo People = &Student{} 即可编译通过。( People 为 interface 类型,就是指针类型)

    (2) interface 的内部构造(非空接口 iface 情况)

    以下代码打印出来什么内容,说出为什么。

    package main
    
    import (
    	"fmt"
    )
    
    type People interface {
    	Show()
    }
    
    type Student struct{}
    
    func (stu *Student) Show() {
    
    }
    
    func live() People {
    	var stu *Student
    	return stu
    }
    
    func main() {
    	if live() == nil {
    		fmt.Println("AAAAAAA")
    	} else {
    		fmt.Println("BBBBBBB")
    	}
    }
    

    结果

    BBBBBBB
    

    分析:

    我们需要了解interface的内部结构,才能理解这个题目的含义。

    interface 在使用的过程中,共有两种表现形式

    一种为空接口(empty interface),定义如下:

    var MyInterface interface{}
    

    另一种为非空接口(non-empty interface), 定义如下:

    type MyInterface interface {
    		function()
    }
    

    这两种 interface 类型分别用两种struct表示,空接口为eface, 非空接口为iface. !]( https://upload-images.jianshu.io/upload_images/11093205-390416d69864055e.jpeg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)


    空接口 eface

    空接口 eface 结构,由两个属性构成,一个是类型信息_type,一个是数据信息。其数据结构声明如下:

    type eface struct {      //空接口
        _type *_type         //类型信息
        data  unsafe.Pointer //指向数据的指针(go 语言中特殊的指针类型 unsafe.Pointer 类似于 c 语言中的 void*)
    }
    

    _type 属性:是 GO 语言中所有类型的公共描述,Go 语言几乎所有的数据结构都可以抽象成 _type,是所有类型的公共描述,**type 负责决定 data 应该如何解释和操作,**type 的结构代码如下:

    type _type struct {
        size       uintptr  //类型大小
        ptrdata    uintptr  //前缀持有所有指针的内存大小
        hash       uint32   //数据 hash 值
        tflag      tflag
        align      uint8    //对齐
        fieldalign uint8    //嵌入结构体时的对齐
        kind       uint8    //kind 有些枚举值 kind 等于 0 是无效的
        alg        *typeAlg //函数指针数组,类型实现的所有方法
        gcdata    *byte
        str       nameOff
        ptrToThis typeOff
    }
    

    data 属性: 表示指向具体的实例数据的指针,他是一个unsafe.Pointer类型,相当于一个 C 的万能指针void*


    非空接口 iface

    iface 表示 non-empty interface 的数据结构,非空接口初始化的过程就是初始化一个 iface 类型的结构,其中data的作用同eface的相同,这里不再多加描述。

    type iface struct {
      tab  *itab
      data unsafe.Pointer
    }
    

    iface 结构中最重要的是 itab 结构(结构如下),每一个 itab 都占 32 字节的空间。itab 可以理解为pair<interface type, concrete type> 。itab 里面包含了 interface 的一些关键信息,比如 method 的具体实现。

    type itab struct {
      inter  *interfacetype   // 接口自身的元信息
      _type  *_type           // 具体类型的元信息
      link   *itab
      bad    int32
      hash   int32            // _type 里也有一个同样的 hash,此处多放一个是为了方便运行接口断言
      fun    [1]uintptr       // 函数指针,指向具体类型所实现的方法
    }
    

    其中值得注意的字段,个人理解如下:

    1. interface type包含了一些关于 interface 本身的信息,比如package path,包含的method。这里的 interfacetype 是定义 interface 的一种抽象表示。
    2. type表示具体化的类型,与 eface 的 type 类型相同。
    3. hash字段其实是对_type.hash的拷贝,它会在 interface 的实例化时,用于快速判断目标类型和接口中的类型是否一致。另,Go 的 interface 的 Duck-typing 机制也是依赖这个字段来实现。
    4. fun字段其实是一个动态大小的数组,虽然声明时是固定大小为 1,但在使用时会直接通过 fun 指针获取其中的数据,并且不会检查数组的边界,所以该数组中保存的元素数量是不确定的。

    所以,People 拥有一个 Show 方法的,属于非空接口,People 的内部定义应该是一个iface结构体

    type People interface {
        Show()  
    }  
    

    func live() People {
        var stu *Student
        return stu      
    }     
    

    stu 是一个指向 nil 的空指针,但是最后return stu 会触发匿名变量 People = stu值拷贝动作,所以最后live()放回给上层的是一个People insterface{}类型,也就是一个iface struct{}类型。stu 为 nil,只是iface中的 data 为 nil 而已。 但是iface struct{}本身并不为 nil.

    所以如下判断的结果为BBBBBBB

    func main() {   
        if live() == nil {  
            fmt.Println("AAAAAAA")      
        } else {
            fmt.Println("BBBBBBB")
        }
    }
    

    (3) interface 内部构造(空接口 eface 情况)

    下面代码结果为什么?

    func Foo(x interface{}) {
    	if x == nil {
    		fmt.Println("empty interface")
    		return
    	}
    	fmt.Println("non-empty interface")
    }
    func main() {
    	var p *int = nil
    	Foo(p)
    }
    

    结果

    non-empty interface
    

    分析

    不难看出,Foo()的形参x interface{}是一个空接口类型eface struct{}

    在执行Foo(p)的时候,触发x interface{} = p语句,所以此时 x 结构如下。

    所以 x 结构体本身不为 nil,而是 data 指针指向的 p 为 nil 。


    (4) inteface{}与*interface{}

    ABCD 中哪一行存在错误?

    type S struct {
    }
    
    func f(x interface{}) {
    }
    
    func g(x *interface{}) {
    }
    
    func main() {
    	s := S{}
    	p := &s
    	f(s) //A
    	g(s) //B
    	f(p) //C
    	g(p) //D
    }
    

    结果

    B 、D 两行错误
    B 错误为:cannot use s (type S) as type *interface {} in argument to g:
    	*interface {} is pointer to interface, not interface
    	
    D 错误为:cannot use p (type *S) as type *interface {} in argument to g:
    	*interface {} is pointer to interface, not interface
    
    

    看到这道题需要第一时间想到的是 Golang 是强类型语言,interface 是所有 golang 类型的父类 函数中func f(x interface{})interface{}可以支持传入 golang 的任何类型,包括指针,但是函数func g(x *interface{})只能接受*interface{}

    如果掌握interface构造,建议看下一篇文章 使用 Golang 的 interface 接口设计原则


    关于作者:

    mail: [email protected] github: https://github.com/aceld 原创书籍 gitbook: http://legacy.gitbook.com/@aceld

    创作不易, 共同学习进步, 欢迎关注作者, 回复"zinx"有好礼

    作者微信公众号


    文章推荐

    开源软件作品

    (原创开源)Zinx-基于 Golang 轻量级服务器并发框架-完整版(附教程视频)

    (原创开源)Lars-基于 C++负载均衡远程调度系统-完整版

    精选文章

    典藏版-Golang 调度器 GMP 原理与调度全分析

    典藏版-Golang 三色标记、混合写屏障 GC 模式图文全分析

    最常用的调试 golang 的 bug 以及性能问题的实践方法?

    Golang 中的 Defer 必掌握的 7 知识点

    Golang 中的局部变量“何时栈?何时堆?”

    使用 Golang 的 interface 接口设计原则

    流? I/O 操作?阻塞? epoll?

    深入浅出 Golang 的协程池设计

    Go 语言构建微服务一站式解决方案

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2840 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 34ms · UTC 10:09 · PVG 18:09 · LAX 03:09 · JFK 06:09
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.