diva-notes
  • README
  • Ads
    • 定价策略
    • 广告层级
    • 归因模型
    • 买量
    • Chat GPT
    • Google
  • AI
    • 参考资料
    • Chat GPT
    • stable-diffusion-webui安装
  • Algorithm
    • 倍增
    • 并查集
    • 参考
    • 环的判断
    • 凸包
    • 蓄水池抽样
    • 最短路径
    • 最小生成树
    • KMP算法
    • Rabin-Karp算法
    • Tarjan桥算法
  • Architecture
    • Serverless
  • Career
  • CICD
    • 代码质量
    • CICD实践
  • Data Structure
    • 布谷鸟过滤器
    • 布隆过滤器
    • 浮点
    • 红黑树
    • 锁
    • LSM树
  • DB
    • My SQL
      • 隔离级别
      • 架构
      • 索引
      • 锁
      • 页结构
      • 主从同步
      • ACID
      • Log
      • MVCC
      • Questions
    • Postgres
      • 持久化
      • 对比MySQL
      • 隔离级别
      • 索引
      • Greenpulm
      • MVCC
    • 倒排索引
    • 列式存储
    • H Base
    • HDFS
    • MPP数据库选型
    • Questions
  • Distributed System
    • 分布式事务
    • 服务网格
    • BASE理论
    • CAP
    • Etcd
    • Raft协议
    • ZAB协议
  • Go
    • 1.语言基础
      • 1.CPU寄存器
      • 2-1.函数调用
      • 2-2.函数调用栈
      • 2.接口
      • 3.汇编
      • 4.调试
    • 2.编译
      • 1.编译
      • 2.词法与语法分析
      • 3.类型检查
      • 4.中间代码生成
      • 5.机器码生成
    • 3.数据结构
      • 1.数组array
      • 2.切片slice
      • 3.哈希表map
      • 4.字符串
    • 4.常用关键字
      • 1.循环
      • 2.defer
      • 3.panic和recover
      • 4.make和new
    • 5.并发编程
      • 1.上下文Context的实现
      • 2-1.runtime.sema信号量
      • 2-2.sync.Mutex的实现
      • 2-3.sync.WaitGroup
      • 2-4.sync.Once的实现
      • 2-5.sync.Map的实现
      • 2-6.sync.Cond
      • 2-7.sync.Pool的实现
      • 2-8.sync.Semaphore的实现
      • 2-9.sync.ErrGroup
      • 3.定时器Timer的实现
      • 4.Channel的实现
      • 5-1.调度-线程
      • 5-2.调度-MPG
      • 5-3.调度-程序及调度启动
      • 5-4.调度-调度策略
      • 5-5.调度-抢占
      • 6.netpoll实现
      • 7.atomic
    • 6.内存管理
      • 1-1.内存分配基础-TCmalloc
      • 1-2.内存分配
      • 2.垃圾回收
      • 3.栈内存管理
    • 参考
    • 各版本特性
    • 坑
    • Go程序性能优化
    • http.Client
    • net.http路由
    • profile采样的实现
    • Questions
    • time的设计
  • Kafka
    • 高可用
    • 架构
    • 消息队列选型
    • ISR
    • Questions
  • Network
    • ARP
    • DNS
    • DPVS
    • GET和POST
    • HTTP 2
    • HTTP 3
    • HTTPS
    • LVS的转发模式
    • NAT
    • Nginx
    • OSI七层模型
    • Protobuf
    • Questions
    • REST Ful
    • RPC
    • socket缓冲区
    • socket详解
    • TCP滑动窗口
    • TCP连接建立源码
    • TCP连接四元组
    • TCP三次握手
    • TCP数据结构
    • TCP四次挥手
    • TCP拥塞控制
    • TCP重传机制
    • UDP
  • OS
    • 磁盘IO
    • 调度
    • 进程VS线程
    • 零拷贝
    • 内存-虚拟内存
    • 内存分配
    • 用户态VS内核态
    • 中断
    • COW写时复制
    • IO多路复用
    • Questions
  • Redis
    • 安装
    • 参考
    • 高可用-持久化
    • 高可用-主从同步
    • 高可用-Cluster
    • 高可用-Sentinel
    • 缓存一致性
    • 事务
    • 数据结构-SDS
    • 数据结构-Skiplist
    • 数据结构-Ziplist
    • 数据结构
    • 数据类型-Hashtable
    • 数据类型-List
    • 数据类型-Set
    • 数据类型-Zset
    • 数据淘汰机制
    • 通信协议-RESP
    • Questions
    • Redis6.0多线程
    • Redis分布式锁
    • Redis分片
  • System Design
    • 本地缓存
    • 错误处理
    • 大文件处理
    • 点赞收藏关注
    • 短链接生成系统
    • 负载均衡
    • 高并发高可用
    • 规则引擎
    • 集卡活动
    • 秒杀系统
    • 评论系统
    • 熔断
    • 限流
    • 延迟队列
    • Docker
    • ES
    • K 8 S
    • Node.js
    • Questions
  • Work
    • Bash
    • Charles
    • Code Review
    • Ffmpeg
    • Git
    • intellij插件
    • I Term 2
    • Mac
    • mysql命令
    • Nginx
    • postgresql命令
    • Protoc
    • Ssh
    • Systemd
    • Tcp相关命令
    • Vim
Powered by GitBook
On this page
  • 类型转换
  • 类型断言
  • 动态派发
  1. Go
  2. 1.语言基础

2.接口

Go 语言中的接口是一组方法的签名。接口的本质是引入一个新的中间层,调用方可以通过接口与具体实现分离,解除上下游的耦合,上层的模块不再需要依赖下层的具体模块,只需要依赖一个约定好的接口。

实现上,interface 实际结构为动态类型 + 动态值,根据是否包含方法,又分为 iface 和 eface 两种,eface 多带一个函数地址列表

这种面向接口的编程方式有着非常强大的生命力,无论是在框架还是操作系统中我们都能够找到接口的身影。可移植操作系统接口(Portable Operating System Interface,POSIX)就是一个典型的例子,它定义了应用程序接口和命令行等标准,为计算机软件带来了可移植性 — 只要操作系统实现了 POSIX,计算机软件就可以直接在不同操作系统上运行。

除了解耦有依赖关系的上下游,接口还能够帮助我们隐藏底层实现,减少关注点。人能够同时处理的信息非常有限,定义良好的接口能够隔离底层的实现,让我们将重点放在当前的代码片段中。SQL 就是接口的一个例子,当我们使用 SQL 语句查询数据时,其实不需要关心底层数据库的具体实现,我们只在乎 SQL 返回的结果是否符合预期。

隐式接口

很多面向对象语言都有接口这一概念,例如 Java 和 C#。这里简单介绍一下 Java 中的接口:

// 接口定义
public interface MyInterface {
    public void sayHello();
}

// 实现接口的类
public class MyInterfaceImpl implements MyInterface {
    public void sayHello() {
        System.out.println("Hello");
    }
}

Java 中的类必须通过上述方式显式地声明实现的接口,但是在 Go 语言中实现接口就不需要使用类似的方式。一个常见的 Go 语言接口是这样的:

// 接口定义
type error interface {
	  Error() string
}

// 实现接口的类
type RPCError struct {
	  Code    int64
	  Message string
}

func (e *RPCError) Error() string {
  	return fmt.Sprintf("%s, code=%d", e.Message, e.Code)
}

Go 语言实现接口的方式与 Java 完全不同:

  • 在 Java 中:实现接口需要显式地声明接口 (implements 关键字) 并实现所有方法;

  • 在 Go 中:实现接口的所有方法就隐式地实现了接口;

我们使用上述 RPCError 结构体时并不关心它实现了哪些接口,Go 语言只会在传递参数、返回参数以及变量赋值时才会对某个类型是否实现接口进行检查,这里举几个例子来演示发生接口类型检查的时机:

func main() {
    var rpcErr error = NewRPCError(400, "unknown err") // typecheck1
    err := AsErr(rpcErr) // typecheck2
    println(err)
}

func NewRPCError(code int64, msg string) error {
    return &RPCError{ // typecheck3
        Code:    code,
        Message: msg,
    }
}

func AsErr(err error) error {
    return err
}
  1. 将 *RPCError 类型的变量赋值给 error 类型的变量 rpcErr;

  2. 将 *RPCError 类型的变量 rpcErr 传递给签名中参数类型为 error 的 AsErr 函数;

  3. 将 *RPCError 类型的变量从函数签名的返回值类型为 error 的 NewRPCError 函数中返回;

从类型检查的过程来看,编译器仅在需要时才检查类型,类型实现接口时只需要实现接口中的全部方法,不需要像 Java 等编程语言中一样显式声明。

数据结构

Go 语言中有两种略微不同的接口,一种是带方法的,Go语言中使用 runtime.iface 结构;一种是不带方法的,使用 runtime.eface 结构。两者都为动态类型 + 动态值,eface 还会多一个方法列表

eface (empty face)

type eface struct { // 16 字节
    _type *_type
    data  unsafe.Pointer
}

// 类型的运行时表示
// 包含了很多类型的元信息,例如:类型的大小、哈希、对齐以及种类等
type _type struct {
    size       uintptr // 类型占用的内存空间,为内存空间的分配提供信息
	  ptrdata    uintptr 
  	hash       uint32  // 快速确定类型是否相等
	  tflag      tflag
  	align      uint8
	  fieldAlign uint8
  	kind       uint8
	  equal      func(unsafe.Pointer, unsafe.Pointer) bool // 判断当前类型的多个对象是否相等,该字段是为了减少 Go 语言二进制包大小从 typeAlg 结构体中迁移过来的
  	gcdata     *byte
	  str        nameOff
  	ptrToThis  typeOff
}

iface

// runtime/runtime2.go
type iface struct { // 16 字节
    tab  *itab
    data unsafe.Pointer
}

type itab struct { // 32 字节
    inter *interfacetype // 接口定义的类型信息
    _type *_type         // 接口实际指向值的类型信息
    hash  uint32         // 用于快速判断目标类型和具体类型 runtime._type 是否一致
    _     [4]byte
    fun   [1]uintptr     // 接口方法实现列表,即函数地址列表,按字典序排序
}

// runtime/type.go
type interfacetype struct {
    typ     _type
    pkgpath name
    mhdr    []imethod      // 接口方法声明列表,按字典序排序
}
// 接口的方法声明 
type imethod struct {
    name nameOff           // 方法名
    ityp typeOff           // 描述方法参数返回值等细节
}

类型转换

以如下代码为例:

var c Duck = &Cat{Name: "draven"}
c.Quack()

可以拆解为三步:

  1. 结构体 Cat 的初始化 (&Cat{Name: "draven"}`)

  2. 赋值触发的类型转换过程

    1. 先构建 itab 对象,其内容包含 Cat 的类型、函数方法

    2. 再调用 runtime.convT2I 方法构建 iface 对象,其 itab 字段为上面一步的结果,data 字段为指向 Cat 的指针

  3. 调用接口的方法 Quack()

    1. 其 itab 结构内的 fun 字段存储了 Cat 的函数索引

类型断言

func main() {
    var c Duck = &Cat{Name: "draven"}
    switch c.(type) {
    case *Cat:
        cat := c.(*Cat)
        cat.Quack()
	}
}
00058 CMPL  go.itab.*"".Cat,"".Duck+16(SB), $593696792
                                        ;; if (c.tab.hash != 593696792) {
00068 JEQ   80                          ;;
00070 MOVQ  24(SP), BP                  ;;      BP = SP+24
00075 ADDQ  $32, SP                     ;;      SP += 32
00079 RET                               ;;      return
                                        ;; } else {
00080 LEAQ  ""..autotmp_4+8(SP), AX     ;;      AX = &Cat{Name: "draven"}
00085 MOVQ  AX, (SP)                    ;;      SP = AX
00089 CALL  "".(*Cat).Quack(SB)         ;;      SP.Quack()
00094 JMP   70                          ;;      ...
                                        ;;      BP = SP+24
                                        ;;      SP += 32
                                        ;;      return
                                        ;; }

switch语句生成的汇编指令会将目标类型的 hash 与接口变量中的 itab.hash 进行比较:

  • 如果两者相等意味着变量的具体类型是 Cat,我们会跳转到 0080 所在的分支完成类型转换。

    1. 获取 SP+8 存储的 Cat 结构体指针;

    2. 将结构体指针拷贝到栈顶;

    3. 调用 Quack 方法;

    4. 恢复函数的栈并返回;

  • 如果接口中存在的具体类型不是 Cat,就会直接恢复栈指针并返回到调用方

动态派发

动态派发(Dynamic dispatch)是在运行期间选择具体多态操作(方法或者函数)执行的过程,它是面向对象语言中的常见特性6。Go 语言虽然不是严格意义上的面向对象语言,但是接口的引入为它带来了动态派发这一特性,调用接口类型的方法时,如果编译期间不能确认接口的类型,Go 语言会在运行期间决定具体调用该方法的哪个实现。

在如下所示的代码中,main 函数调用了两次 Quack 方法:

  1. 第一次以 Duck 接口类型的身份调用,调用时需要经过【运行时】的动态派发;

  2. 第二次以 *Cat 具体类型的身份调用,【编译期】就会确定调用的函数:

func main() {
	var c Duck = &Cat{Name: "draven"}
	c.Quack()
	c.(*Cat).Quack()
}

reflect

reflect 包基本是依赖 interface 来实现的。里面定义了一个接口和一个结构体,即 reflect.Type 和 reflect.Value,提供很多函数来获取存储在接口里的类型信息。

reflect.Type 主要提供关于类型相关的信息,所以它和 _type 关联比较紧密;reflect.Value 则结合 _type 和 data 两者,因此程序员可以获取甚至改变类型的值。

reflect 包中提供了两个基础的关于反射的函数来获取上述的接口和结构体:

func TypeOf(i interface{}) Type 
func ValueOf(i interface{}) Value

调用这两个函数时,实参会先被值拷贝,转为 interface{} 类型。这样,实参的类型信息、方法集、值信息都存储到 interface{} 变量里了。

参考

Previous2-2.函数调用栈Next3.汇编

Last updated 2 years ago

Go 语言在对代码进行类型检查,上述代码总共触发了三次类型检查:

编译期间
draveness - 4.2 接口