Go 接口:深入内部原理

Go 接口:深入内部原理

接口的基本概念不在这里赘述,详情请看第十六章:接口

nil 非空?

package main

func main() {
   var obj interface{}
   obj = 1
   println(obj == 1)  // true
   obj = "hello"
   println(obj == "hello")  // true

   type User struct {

   }
   var u *User
   obj = u
   println(u == nil)  // true
   println(obj == nil)  // false
}

前面的只是对比,说明 interface can hold everything。我们需要注意的最后两个判断:

  • u是一个 User类型的空指针, println(u == nil)输出 true是意料之内;
  • u赋值给 obj后, println(obj == nil)输出的是 false, *意料之外

为什么把空指针 u赋值给 interface后, obj就不是nil了吗?那它会是什么呢?

通过 gdb工具调试,我们看到 interface原来是长这样的:

(gdb) ptype obj
type = struct runtime.eface {
    runtime._type *_type;
    void *data;
}

通过 goland断点看一下 obj里面到底了什么

Go 接口:深入内部原理

可以看出来 data是用来存储数据, _type用来存储类型:

  • obj = 1时,底层的 eface的两个属性都是有值的;
  • obj = u时,底层的 efacedata属性为空, _type属性非空
  • obj = nil时,底层的 efacedata_type属于都为空

对应结构体类型的比较,要求结构体中的所有字段都相等时两个变量才是相等的,因为 eface_type属于非空,所以当将 u赋值给 obj后, println(obj == nil输出的是 false

这就引出了另一个问题,当执行 obj = u这行代码时,golang runtime是如何把静态类型的值 u转换成 eface结构的呢?

当给接口赋值时

接着上面的问题,我们通过下面这段简单代码,看看是如何把一个静态类型值转换成 eface

package main

import "fmt"

func main() {
   var a int64 = 123
   var i interface{} = a  // 这一行进行转换
   fmt.Println(i)
}

通过命令 go tool compile -N -l -S main.go将其转成汇编代码

Go 接口:深入内部原理
红框内的正是第 7 行对应的汇编指 CALL runtime.convT64(SB)(汇编代码可以直接调用 Go func),我们可以在 runtime包中找到对应的函数函数
// runtime/iface.go
func convT64(val uint64) (x unsafe.Pointer) {
   if val < uint64(len(staticuint64s)) {
      x = unsafe.Pointer(&staticuint64s[val])
   } else {
      x = mallocgc(8, uint64Type, false) // 分配内存,(size, _type, needzero)
      *(*uint64)(x) = val // 复制
   }
   return
}

eface, iface

通过上面的实验,我们了解了接口的底层结构是 eface。实际上,Golang 根据接口是否包含方法,将接口分为两类:

  • eface:不包含任何绑定方法的接口
  • 比如:空接口 interface{}
  • iface:包含绑定方法的接口
  • 比如:os.Writer
    type Writer interface {
       Write(p []byte) (n int, err error)
    }

eface

eface的数据结构:

type eface struct {
   _type *_type
   data  unsafe.Pointer
}

这个我们应该比较熟悉了,在上面的实验中我们已经见过了: _typedata 属性,分别代表底层的指向的类型信息和指向的值信息指针。

我们在看一下 _type属性,它的类型是又是一个结构体:

type _type struct {
   size       uintptr // 类型的大小
   ptrdata    uintptr // 包含所有指针的内存前缀的大小
   hash       uint32  // 类型的 hash 值,此处提前计算好,可以避免在哈希表中计算
   tflag      tflag   // 额外的类型信息标志,此处为类型的 flag 标志,主要用于反射
   align      uint8   // 对应变量与该类型的内存对齐大小
   fieldAlign uint8   // 对应类型的结构体的内存对齐大小
   kind       uint8   // 类型的枚举值, 包含 Go 语言中的所有类型,例如:kindBoolkindIntkindInt8kindInt16 等
   equal func(unsafe.Pointer, unsafe.Pointer) bool  // 用于比较此对象的回调函数
   gcdata    *byte    // 存储垃圾收集器的 GC 类型数据
   str       nameOff
   ptrToThis typeOff
}

总结来说:runtime 只需在这里查询,就能得到与类型相关的所有信息(字节大小、类型标志、内存对齐等)。

iface

iface的数据结构:

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

iface相比,它们的 data属性是一样的,用于存储数据;不同的是,因为 iface不仅要存储类型信息,还要存储接口绑定的方法,所有需要使用 itab结构来存储两者信息。我们看一下 itab

type itab struct {
   inter *interfacetype  // 接口的类型信息
   _type *_type          // 具体类型信息
   hash  uint32          // _type.hash 的副本,用于目标类型和接口变量的类型对比判断
   _     [4]byte
   fun   [1]uintptr      // 存储接口的方法集的具体实现的地址,其包含一组函数指针,实现了接口方法的动态分派,且每次在接口发生变更时都会更
}

总结来讲,接口的数据结构基本表示形式比较简单,就是类型和值描述。再根据其具体的区别,例如是否包含方法集,具体的接口类型等进行组合使用。

Go 接口:深入内部原理

iface,接口绑定的 method 你存到了哪里?

通过上节,我们知道 iface可以存储接口绑定的方法。从其结构体也能看出来 iface.tab.fun字段就是用来干这个事。但是,我有一个疑问: fun类型是长度为 1 的指针数组,难道它就只能存一个 method?

type Animal interface {
   Speak () string
   Move()
   Attack()
}

type Lion struct {

}

func (l Lion) Speak() string {
   return "Uh....."
}

func (l Lion) Move() {
}

func (l Lion) Attack() {
}

func main() {
    lion := Lion{}
    var obj interface{} = lion
    cc, _ := obj.(Animal)
    fmt.Println(cc.Speak()) // Un....

}

Lion是一个实现了接口 Animal所有方法的结构体,所以一个接口 obj尝试通过类型断言转换成 Animal接口是,是可以成功的。通过 Debug 调试,当我执行 cc, _ := obj.(Animal)这行代码时,内部回去调 assertE2I2方法然后返回

func assertE2I2(inter *interfacetype, e eface) (r iface, b bool) {
   t := e._type
   if t == nil {
      return
   }
   tab := getitab(inter, t, true)
   if tab == nil {
      return
   }
   r.tab = tab
   r.data = e.data
   b = true
   return
}

所以返回的 cc变量实际上是一个 iface结构体,因为 iface无法导出我们看不到内部数据,但我们可以通过在 main 程序中把 iface结构体定义一封,通过指针操作进行转换:

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

type itab struct {
   inter *interfacetype
   _type *_type
   hash  uint32 // copy of _type.hash. Used for type switches.

   _     [4]byte
   fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.

}
...

func main() {
   lion := Lion{}
   var obj interface{} = lion
   cc, _ := obj.(Animal)
   fmt.Println(cc.Speak())  // Uh.....

   dd := *(*iface)(unsafe.Pointer(&cc))  // 当cc转成 iface 接口体
   fmt.Printf("%v\n", dd)
   fmt.Printf("%+V", cc)
}

通过 debug 可以看到,接口 Animal对应的 eface的一个完整的数据

Go 接口:深入内部原理

tab里面保存了类型和绑定方法的数据: inter.mhdr的长度为 3,看起来是存储了 3 个方法的名字和类型, fun里存储了一个指针,应该就是第一个方法的地址了。下面这段代码可以证实:

// itab 的初始化
func (m *itab) init() string {
   inter := m.inter
   typ := m._type
   x := typ.uncommon()

   // ni的值为接口绑定的方法数量
   ni := len(inter.mhdr)
   nt := int(x.mcount)
   // 我猜 xmhdr 是真实存储接口的方法的地方
   xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt]
   j := 0
   methods := (*[1 << 16]unsafe.Pointer)(unsafe.Pointer(&m.fun[0]))[:ni:ni]
   var fun0 unsafe.Pointer
imethods:
   // 遍历3个方案
   for k := 0; k < ni; k++ {
      i := &inter.mhdr[k]
      itype := inter.typ.typeOff(i.ityp)
      name := inter.typ.nameOff(i.name)
      iname := name.name()
      ipkg := name.pkgPath()
      if ipkg == "" {
         ipkg = inter.pkgpath.name()
      }
      for ; j < nt; j++ {
         t := &xmhdr[j]
         tname := typ.nameOff(t.name)
         // 通过遍历 xmhdr,如果和mhrd[k]的名字、类型并且pkgpath都相等,就找到了
         if typ.typeOff(t.mtyp) == itype && tname.name() == iname {
            pkgPath := tname.pkgPath()
            if pkgPath == "" {
               pkgPath = typ.nameOff(x.pkgpath).name()
            }
            if tname.isExported() || pkgPath == ipkg {
               if m != nil {
                  // 获取方法的地址
                  ifn := typ.textOff(t.ifn)
                  if k == 0 {
                     // 记录第一个方法的地址
                     fun0 = ifn // we'll set m.fun[0] at the end
                  } else {
                     methods[k] = ifn
                  }
               }
               continue imethods
            }
         }
      }
      // didn't find method
      m.fun[0] = 0
      return iname
   }
   // func[0] = 第一个方法的地址
   m.fun[0] = uintptr(fun0)
   return ""
}

总结一下,在将一个不确定的 interface{}类型断言成某个特定接口时,runtime 会将原来的数据、方法以 iface的数据结构进行返回。 iface实际上只保存第一个方法的地址, &#x5176;&#x4ED6;&#x7684;&#x65B9;&#x6CD5;&#x901A;&#x8FC7;&#x504F;&#x79FB;&#x91CF;&#x5C31;&#x80FD;&#x627E;&#x5230;&#xFF0C;&#x504F;&#x79FB;&#x7684;&#x4FE1;&#x606F;&#x4FDD;&#x5B58;&#x5728; mhdr &#x4E2D;&#xFF08;&#x5F85;&#x9A8C;&#x8BC1;&#xFF09;

类型断言是怎么做到的

Go 是强类型的语言,变量类型、函数传参的类型一定定义就不能变换。这为程序的类型提供了安全稳定的保证,但也为程序的编码带来更多的工作量。比如我们去是实现一个加法函数,需要对不同的类型都写一遍,并且使用起来也不方便:

func addInt(a, b int) int { return a + b }
func addInt32(a, b int32) int32 { return a + b }
func addInt64(a, b int64) int64 { return a + b }
func addFloat32(a, b float32) float32 { return a + b }
func addFloat64(a, b float64) float64 { return a + b }

基于 interface can hold everything,我们通过使用 interface{}当入参类型,用一个函数来实现:

func add(a, b interface{}) interface{} {
   switch av := a.(type) {
   case int:
      if bv, ok := b.(int); ok {
         return av + bv
      }
      panic("bv is not int")
   case int32:
      if bv, ok := b.(int32); ok {
         return av + bv
      }
      panic("bv is not int32")
   ...

   case float64:
      if bv, ok := b.(float64); ok {
         return av + bv
      }
      panic("bv is not float64")

   }

   panic("illegal a and b")
}

func main() {
    var a int64 = 1
    var b int64 = 4
    c := add(a, b)
    fmt.Println(c)  // 5
}

可能会有人问: add函数的参数变量类型是 interface{}了, 它在函数里面是后如何把从 interface{}中的带变量?(答案就是 eface

  1. 第一步 int64 -> eface 注意这行代码 c := add(a, b),翻译成汇编的话:
0x002f 00047 (main.go:132)      FUNCDATA      $2, "".main.stkobj(SB)
0x002f 00047 (main.go:142)      MOVQ    $1, "".a+56(SP)
0x0038 00056 (main.go:143)      MOVQ    $4, "".b+48(SP)
0x0041 00065 (main.go:144)      MOVQ    "".a+56(SP), AX
0x0046 00070 (main.go:144)      MOVQ    AX, (SP)
0x004a 00074 (main.go:144)      PCDATA  $1, $0
0x004a 00074 (main.go:144)      CALL    runtime.convT64(SB)

注意最后一行 runtime.convT64,上面提到过,这里的操作就拷贝一份值给到函数 add

func convT64(val uint64) (x unsafe.Pointer) {
    if val < uint64(len(staticuint64s)) {
      x = unsafe.Pointer(&staticuint64s[val])
    } else {
      x = mallocgc(8, uint64Type, false)
      *(*uint64)(x) = val
    }
    return
}
  1. 第二步从 eface中得到类型信息 为了验证我们的猜想,我们在 add函数入口处通过类型转换把 interface{} a转成 eface dd来看一它的具体数据长什么样
func add(a, b interface{}) interface{} {
    dd := *(*eface)(unsafe.Pointer(&a))
    fmt.Println(dd)
    switch av := a.(type) {
    case int:
      if bv, ok := b.(int); ok {
         return av + bv
      }
      panic("bv is not int")
   }
   ...

通过 debug 看到的 dd 数据如下:

Go 接口:深入内部原理
注意 dd._type.kind字段的只为 6,在 src/runtime/typekind.go文件中,维护了每个类型对应一个常量
const (
   kindBool = 1 + iota
   kindInt
   kindInt8
   kindInt16
   kindInt32
   kindInt64 // 6
   kindUint
   kindUint8
   kindUint16
   kindUint32
   kindUint64
   kindUintptr
   kindFloat32
   ...

)

可以看到, int64对应的常量值正好是 6。这也就解释通过类型断言获取将 interface{}转成具体类型的原理。

总结

接口的作用

  • 在 Go 运行时,为方便内部传递数据、操作数据,使用 interface{}作为存储数据的媒介,大大降低了开发成本。这个媒介存储了 &#x6570;&#x636E;&#x7684;&#x4F4D;&#x7F6E;&#x6570;&#x636E;&#x7684;&#x7C7B;&#x578B;,有这两个信息,就能代表一切变量,即 interface can hold everything
  • 接口也作为一种抽象的能力,通过定义一个接口所需实现的方法,等同于对 &#x5982;&#x4F55;&#x5224;&#x5B9A;&#x8FD9;&#x4E2A; struct &#x662F;&#x4E0D;&#x662F;&#x8FD9;&#x7C7B;&#x63A5;&#x53E3;完成了明确的定义,即必须是接口绑定的所有方法。通过这种能力,可以在编码上做到很大程度的解耦,接口就好比上下游开发者之间协议。

接口的内部存储有两类

Golang 根据接口是否包含方法,将接口分为两类:

  • eface:不包含任何绑定方法的接口
  • 比如:空接口 interface{}
  • iface:包含绑定方法的接口
  • 比如:os.Writer

二者之间的差别在与 eface多存了接口绑定的方法信息。

当心,变成接口后,判空不准

判空的条件是结构体的所有字段都为 nil才行,当 nil的固定类型值转成接口后,接口的数据值为 nil,但是 &#x7C7B;&#x578B;值不为 nil会导致判空失败。

Go 接口:深入内部原理

解决的方案是:函数返回参数不要写出接口类型,在外部先做判空,在转成接口。

Original: https://www.cnblogs.com/Zioyi/p/16511256.html
Author: Zioyi
Title: Go 接口:深入内部原理

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/575842/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • java基础

    java基础知识图解 软件开发 软件开发 软件,即一系列按照特定顺序组织的计算机数据和指令的集合。有系统软件和应用软件之分。 人机交互方式 图形化界面(Graphical User…

    数据库 2023年6月16日
    058
  • windows安装mysql8.0.29(ZIP解压安装版本)

    一. 下载mysql 8.0.29软件包 二. 解压,初始化安装 1,打开下载后文件所在目录,使用解压软件解压,打开文件夹!(如图,文件路径不要出现中文!) 2,创建my.ini文…

    数据库 2023年6月16日
    073
  • 数据库设计案例

    简单构建设计数据库 数据库设计案例 描述:简单构建设计数据库 sql代码实现 /* 数据库设计案例 */ — 音乐表 CREATE TABLE Music ( title VAR…

    数据库 2023年6月16日
    092
  • Redis 哈希Hash底层数据结构

    Redis 底层数据结构 Redis数据库就像是一个哈希表,首先对key进行哈希运算得到哈希值再取模得到一个下标,每个元素是一个节点,节点之间形成链表。这感觉有点像Java中的Ha…

    数据库 2023年6月14日
    096
  • 一份超长的MySQL学习笔记

    前言 最近系统地学习了一边MySQL数据库的基础知识,巩固了一下以前学习的数据库查询基础,又新学习了关于索引、事务等的新内容,做了一些学习笔记。因为MySQL的学习,实操性比较强,…

    数据库 2023年5月24日
    082
  • Mysql 的Innodb引擎和Myisam数据结构和区别

    先大体看一下MySQL的SQL layer层的一个架构流程: 对一些关键模块做一下简单的描述: 初始模块:初始一些参数,比如初始myinit配置文件(在安装的根目录下)里的一些参数…

    数据库 2023年6月16日
    084
  • 23种设计模式之责任链模式

    文章目录 概览 责任链模式的优缺点 责任链模式的结构和实现 * 模式的结构 模式的实现 总结 ; 概览 责任链模式(Chain of Responsibility Pattern)…

    数据库 2023年6月6日
    092
  • 锁定文件失败 打不开磁盘“D:Windows7Windows7 64 位.vmdk”或它所依赖的某个快照磁盘。 模块“Disk”启动失败。

    Windows7虚拟机非正常关闭,再次打开有时候会出现”锁定文件失败,打不开磁盘……”的错误提示解决办法:打开虚拟机所在路径删除.v…

    数据库 2023年6月14日
    079
  • js异步编程终级解决方案 async/await

    在最新的ES7(ES2017)中提出的前端异步特性:async、await。 async、await是什么 async顾名思义是”异步”的意思,async用…

    数据库 2023年6月9日
    077
  • 4、异常

    一、异常的体系结构: java.lang.Throwable |—–java.lang.Error:一般不编写针对性的代码进行处理。 |—&#8…

    数据库 2023年6月6日
    084
  • leetcode 144. Binary Tree Preorder Traversal 二叉树展开为链表(中等)

    一、题目大意 给你二叉树的根节点 root ,返回它节点值的 前序 遍历。 示例 1: 输入:root = [1,null,2,3]输出:[1,2,3] 示例 2: 输入:root…

    数据库 2023年6月16日
    073
  • 自定义 systemd service

    Red Hat Linux 自 7 版本后 采用systemd 形式取代原先 init ,用户可以参考 系统service 创建自己的service ,以便于日常统一管理,系统se…

    数据库 2023年6月15日
    081
  • 【01】Maven依赖插件之maven-dependency-plugin

    1、analyze:分析项目依赖,确定哪些是已使用已声明的,哪些是已使用未声明的,哪些是未使用已声明的 2、analyze-dep-mgt:分析项目依赖,列出已解析的依赖项与dep…

    数据库 2023年6月9日
    078
  • django中的JsonRseponse对象

    json格式的数据 在进行前后端数据交互的时候,我们需要使用json格式的数据作为过渡,实现跨语言传输数据! django中的JsonResponse对象 在django中Json…

    数据库 2023年6月14日
    075
  • 多商户商城系统功能拆解23讲-平台端分销等级

    多商户商城系统,也称为B2B2C(BBC)平台电商模式多商家商城系统。可以快速帮助企业搭建类似拼多多/京东/天猫/淘宝的综合商城。 多商户商城系统支持商家入驻加盟,同时满足平台自营…

    数据库 2023年6月14日
    079
  • idea的使用技巧和必要的设置

    idea 如何开启多个线程 打开下面按钮,然后运行相同的代码即可 打开idea需要选择打开哪一个项目 设置如下,关闭下面选项即可 posted @2022-06-17 21:07 …

    数据库 2023年6月14日
    081
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球