分类目录归档:Golang

Golang channel 实现原理分析

Go channel 实现原理分析

channel一个类型管道,通过它可以在goroutine之间发送和接收消息。它是Golang在语言层面提供的goroutine间的通信方式。

众所周知,Go依赖于称为CSP(Communicating Sequential Processes)的并发模型,通过Channel实现这种同步模式。Go并发的核心哲学是不要通过共享内存进行通信; 相反,通过沟通分享记忆。

下面以简单的示例来演示Go如何通过channel来实现通信。

package main
import (
    "fmt"
    "time"
)
func goRoutineA(a <-chan int) {
    val := <-a
    fmt.Println("goRoutineA received the data", val)
}
func goRoutineB(b chan int) {
    val := <-b
    fmt.Println("goRoutineB  received the data", val)
}
func main() {
    ch := make(chan int, 3)
    go goRoutineA(ch)
    go goRoutineB(ch) 
    ch <- 3
    time.Sleep(time.Second * 1)
}

结果为:

goRoutineA received the data 3

上面只是个简单的例子,只输出goRoutineA ,没有执行goRoutineB,说明channel仅允许被一个goroutine读写。

接下来我们通过源代码分析程序执行过程

说道channel这里不得不提通道的结构hchan。

hchan

源代码在src/runtime/chan.go

type hchan struct {
   qcount   uint           // total data in the queue
   dataqsiz uint           // size of the circular queue
   buf      unsafe.Pointer // points to an array of dataqsiz elements
   elemsize uint16
   closed   uint32
   elemtype *_type // element type
   sendx    uint   // send index
   recvx    uint   // receive index
   recvq    waitq  // list of recv waiters
   sendq    waitq  // list of send waiters

   // lock protects all fields in hchan, as well as several
   // fields in sudogs blocked on this channel.
   //
   // Do not change another G's status while holding this lock
   // (in particular, do not ready a G), as this can deadlock
   // with stack shrinking.
   lock mutex
}
type waitq struct {
    first *sudog
    last  *sudog
}

说明:

  • qcount uint // 当前队列中剩余元素个数
  • dataqsiz uint // 环形队列长度,即缓冲区的大小,即make(chan T,N),N.
  • buf unsafe.Pointer // 环形队列指针
  • elemsize uint16 // 每个元素的大小
  • closed uint32 // 表示当前通道是否处于关闭状态。创建通道后,该字段设置为0,即通道打开; 通过调用close将其设置为1,通道关闭。
  • elemtype *_type // 元素类型,用于数据传递过程中的赋值;
  • sendx uint和recvx uint是环形缓冲区的状态字段,它指示缓冲区的当前索引 – 支持数组,它可以从中发送数据和接收数据。
  • recvq waitq // 等待读消息的goroutine队列
  • sendq waitq // 等待写消息的goroutine队列
  • lock mutex // 互斥锁,为每个读写操作锁定通道,因为发送和接收必须是互斥操作。

这里sudog代表goroutine。

创建channel 有两种,一种是带缓冲的channel,一种是不带缓冲的channel

// 带缓冲
ch := make(chan Task, 3)
// 不带缓冲
ch := make(chan int)

这里我们先讨论带缓冲

ch := make(chan int, 3)

创建通道后的缓冲通道结构

hchan struct {
    qcount uint : 0 //此时没有值,队列中剩余为0
    dataqsiz uint : 3 //队列长度,即缓冲区大小
    buf unsafe.Pointer : 0xc00007e0e0 //缓冲区指针地址
    elemsize uint16 : 8 //每个元素的大小,8个字节(64位)
    closed uint32 : 0 //0-通道打开,1-通道关闭
    elemtype *runtime._type : &{ //实体类型与interface,reflect中的type类似
        size:8 
        ptrdata:0 
        hash:4149441018 
        tflag:7 
        align:8 
        fieldalign:8 
        kind:130 
        alg:0x55cdf0 
        gcdata:0x4d61b4 
        str:1055 
        ptrToThis:45152
    }
    sendx uint : 0 //缓冲区发送索引位置,此时无值
    recvx uint : 0 //缓冲区接收索引位置,此时无值
    recvq runtime.waitq : 
        {first:<nil> last:<nil>}
    sendq runtime.waitq : 
        {first:<nil> last:<nil>}
    lock runtime.mutex : 
        {key:0}
}

源代码

func makechan(t *chantype, size int) *hchan {
   elem := t.elem
   ...
}

如果我们创建一个带buffer的channel,底层的数据模型如下图:

向channel写入数据

ch <- 3

底层hchan数据流程如图


发送操作概要

  • 1、锁定整个通道结构。
  • 2、确定写入。尝试recvq从等待队列中等待goroutine,然后将元素直接写入goroutine。
  • 3、如果recvq为Empty,则确定缓冲区是否可用。如果可用,从当前goroutine复制数据到缓冲区。
  • 4、如果缓冲区已满,则要写入的元素将保存在当前正在执行的goroutine的结构中,并且当前goroutine将在sendq中排队并从运行时挂起。
  • 5、写入完成释放锁。

这里我们要注意几个属性buf、sendx、lock的变化。

流程图

从channel读取操作

几乎和写入操作相同

func goRoutineA(a <-chan int) {
   val := <-a
   fmt.Println("goRoutineA received the data", val)
}

底层hchan数据流程如图


这里我们要注意几个属性buf、sendx、recvx、lock的变化。

读取操作概要

  • 1、先获取channel全局锁
  • 2、尝试sendq从等待队列中获取等待的goroutine,
  • 3、 如有等待的goroutine,没有缓冲区,取出goroutine并读取数据,然后唤醒这个goroutine,结束读取释放锁。
  • 4、如有等待的goroutine,且有缓冲区(此时缓冲区已满),从缓冲区队首取出数据,再从sendq取出一个goroutine,将goroutine中的数据存入buf队尾,结束读取释放锁。
  • 5、如没有等待的goroutine,且缓冲区有数据,直接读取缓冲区数据,结束读取释放锁。
  • 6、如没有等待的goroutine,且没有缓冲区或缓冲区为空,将当前的goroutine加入recvq排队,进入睡眠,等待被写goroutine唤醒。结束读取释放锁。

流程图

recvq和sendq 结构

recvq和sendq基本上是链表,看起来基本如下

select

select就是用来监听和channel有关的IO操作,当 IO 操作发生时,触发相应的动作。

一个简单的示例如下

package main

import (
   "fmt"
   "time"
)

func goRoutineD(ch chan int, i int) {
   time.Sleep(time.Second * 3)
   ch <- i
}
func goRoutineE(chs chan string, i string) {
   time.Sleep(time.Second * 3)
   chs <- i
}

func main() {
   ch := make(chan int, 5)
   chs := make(chan string, 5)

   go goRoutineD(ch, 5)
   go goRoutineE(chs, "ok")

    select {
    case msg := <-ch:
        fmt.Println(" received the data ", msg)
    case msgs := <-chs:
        fmt.Println(" received the data ", msgs)
    default:
        fmt.Println("no data received ")
        time.Sleep(time.Second * 1)
    }
}

运行程序,因为当前时间没有到3s,所以select 选择defult

no data received

修改程序,我们注释掉default,并多执行几次结果为

received the data 5
received the data ok
received the data ok
received the data ok

select语句会阻塞,直到监测到一个可以执行的IO操作为止,而这里goRoutineD和goRoutineE睡眠时间是相同的,都是3s,从输出可看出,从channel中读出数据的顺序是随机的。

再修改代码,goRoutineD睡眠时间改成4s

func goRoutineD(ch chan int, i int) {
   time.Sleep(time.Second * 4)
   ch <- i
}

此时会先执行goRoutineE,select 选择case msgs := <-chs。

range

可以持续从channel读取数据,一直到channel被关闭,当channel中没有数据时会阻塞当前goroutine,与读channel时阻塞处理机制一样。

package main

import (
   "fmt"
   "time"
)

func goRoutineD(ch chan int, i int) {
   for i := 1; i <= 5; i++{
      ch <- i
   }
}
func chanRange(chanName chan int) {
   for e := range chanName {
      fmt.Printf("Get element from chan: %d\n", e)
      if len(chanName) <= 0 { // 如果现有数据量为0,跳出循环
            break
      }
   }
}
func main() {
   ch := make(chan int, 5)
   go goRoutineD(ch, 5)
   chanRange(ch)
}

结果:

Get element from chan: 1
Get element from chan: 2
Get element from chan: 3
Get element from chan: 4
Get element from chan: 5

死锁(deadlock)

指两个或两个以上的协程的执行过程中,由于竞争资源或由于彼此通信而造成的一种阻塞的现象。

在非缓冲信道若发生只流入不流出,或只流出不流入,就会发生死锁。

下面是一些死锁的例子

1、案例1

package main

func main() {
   ch := make(chan int)
   ch <- 3
}

上面情况,向非缓冲通道写数据会发生阻塞,导致死锁。解决办法创建缓冲区 ch := make(chan int,3)

2、案例2

package main

import (
   "fmt"
)

func main() {
   ch := make(chan int)
   fmt.Println(<-ch)
}

向非缓冲通道读取数据会发生阻塞,导致死锁。 解决办法开启缓冲区,先向channel写入数据。

3、案例3

package main

func main() {
   ch := make(chan int, 3)
   ch <- 3
   ch <- 4
   ch <- 5
   ch <- 6
}

写入数据超过缓冲区数量也会发生死锁。解决办法将写入数据取走。

死锁的情况有很多这里不再赘述。
还有一种情况,向关闭的channel写入数据,不会产生死锁,产生panic。

package main

func main() {
    ch := make(chan int, 3)
    ch <- 1
    close(ch)
    ch <- 2
}

解决办法别向关闭的channel写入数据。

参考:

https://codeburst.io/diving-d…

https://speakerdeck.com/kavya…

[https://my.oschina.net/renhc/…](

Golang底层原理之Reflect

什么是反射

维基百科上反射的定义:

在计算机科学中,反射是指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力。用比喻来说,反射就是程序在运行的时候能够“观察”并且修改自己的行为。

难道不用反射就不能在运行时访问、检测和修改它本身的状态和行为吗?

问题的回答,其实要首先理解什么叫访问、检测和修改它本身状态或行为,它的本质是什么?

实际上,它的本质是程序在运行期探知对象的类型信息和内存结构。不用反射能行吗?可以的!使用汇编语言,直接和内层打交道,可以获取任何信息?但是,当编程迁移到高级语言上来之后,就不行了!只能通过反射来达到此项技能。

不同语言的反射模型不尽相同,有些语言还不支持反射。《Go 语言圣经》中是这样定义反射的:

Go 语言提供了一种机制在运行时更新变量和检查它们的值、调用它们的方法,但是在编译时并不知道这些变量的具体类型,这称为反射机制。

什么情况下需要使用反射

使用反射的常见场景有以下两种:

  1. 不能明确接口调用哪个函数,需要根据传入的参数在运行时决定。
  2. 不能明确传入函数的参数类型,需要在运行时处理任意对象。

【引申1】不推荐使用反射的理由有哪些?

  1. 与反射相关的代码,经常是难以阅读的。在软件工程中,代码可读性也是一个非常重要的指标。
  2. Go 语言作为一门静态语言,编码过程中,编译器能提前发现一些类型错误,但是对于反射代码是无能为力的。所以包含反射相关的代码,很可能会运行很久,才会出错,这时候经常是直接 panic,可能会造成严重的后果。
  3. 反射对性能影响还是比较大的,比正常代码运行速度慢一到两个数量级。所以,对于一个项目中处于运行效率关键位置的代码,尽量避免使用反射特性。

Go 语言中反射有哪些应用

Go 语言中反射的应用非常广:IDE 中的代码自动补全功能、对象序列化(encoding/json)、fmt 相关函数的实现、ORM(全称是:Object Relational Mapping,对象关系映射)……

Go 语言如何实现反射

interface,它是 Go 语言实现抽象的一个非常强大的工具。当向接口变量赋予一个实体类型的时候,接口会存储实体的类型信息,反射就是通过接口的类型信息实现的,反射建立在类型的基础上。

Go 语言在 reflect 包里定义了各种类型,实现了反射的各种函数,通过它们可以在运行时检测类型的信息、改变类型的值。

types 和 interface

Go 语言中,每个变量都有一个静态类型,在编译阶段就确定了的,比如 int, float64, []int 等等。注意,这个类型是声明时候的类型,不是底层数据类型。

Go 官方博客里就举了一个例子:

type MyInt int
var i int
var j MyInt

尽管 i,j 的底层类型都是 int,但我们知道,他们是不同的静态类型,除非进行类型转换,否则,i 和 j 不能同时出现在等号两侧。j 的静态类型就是 MyInt

反射主要与 interface{} 类型相关。关于 interface 的底层结构,可以参考前面有关 interface 章节的内容,这里复习一下。

type iface struct {
    tab  *itab
    data unsafe.Pointer
}
type itab struct {
    inter  *interfacetype
    _type  *_type
    link   *itab
    hash   uint32
    bad    bool
    inhash bool
    unused [2]byte
    fun    [1]uintptr
}

其中 itab 由具体类型 _type 以及 interfacetype 组成。_type 表示具体类型,而 interfacetype 则表示具体类型实现的接口类型。

实际上,iface 描述的是非空接口,它包含方法;与之相对的是 eface,描述的是空接口,不包含任何方法,Go 语言里有的类型都 “实现了” 空接口。

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

相比 ifaceeface 就比较简单了。只维护了一个 _type 字段,表示空接口所承载的具体的实体类型。data 描述了具体的值。

还是用 Go 官方关于反射的博客里的例子,当然,我会用图形来详细解释,结合两者来看会更清楚。顺便提一下,搞技术的不要害怕英文资料,要想成为技术专家,读英文原始资料是技术提高的一条必经之路。

先明确一点:接口变量可以存储任何实现了接口定义的所有方法的变量。

Go 语言中最常见的就是 ReaderWriter 接口:

type Reader interface {    
  Read(p []byte) (n int, err error)
}

type Writer interface {   
  Write(p []byte) (n int, err error)
}

接下来,就是接口之间的各种转换和赋值了:

var r io.Reader
tty, err := os.OpenFile("/Users/qcrao/Desktop/test", os.O_RDWR, 0)
if err != nil {    
  return nil, err
}
r = tty

首先声明 r 的类型是 io.Reader,注意,这是 r 的静态类型,此时它的动态类型为 nil,并且它的动态值也是 nil

之后,r = tty 这一语句,将 r 的动态类型变成 *os.File,动态值则变成非空,表示打开的文件对象。这时,r 可以用<value, type>对来表示为: <tty, *os.File>

注意看上图,此时虽然 fun 所指向的函数只有一个 Read 函数,其实 *os.File 还包含 Write 函数,也就是说 *os.File 其实还实现了 io.Writer 接口。因此下面的断言语句可以执行:

var w io.Writer
w = r.(io.Writer)

之所以用断言,而不能直接赋值,是因为 r 的静态类型是 io.Reader,并没有实现 io.Writer 接口。断言能否成功,看 r 的动态类型是否符合要求。

这样,w 也可以表示成 <tty, *os.File>,仅管它和 r 一样,但是 w 可调用的函数取决于它的静态类型 io.Writer,也就是说它只能有这样的调用形式: w.Write()w 的内存形式如下图:

r 相比,仅仅是 fun 对应的函数变了:Read -> Write

最后,再来一个赋值:

var empty interface{}
empty = w 

由于 empty 是一个空接口,因此所有的类型都实现了它,w 可以直接赋给它,不需要执行断言操作。

从上面的三张图可以看到,interface 包含三部分信息:_type 是类型信息,*data 指向实际类型的实际值,itab 包含实际类型的信息,包括大小、包路径,还包含绑定在类型上的各种方法(图上没有画出方法),补充一下关于 os.File 结构体的图:

这一节的最后,展示一个技巧:

先参考源码,分别定义一个“伪装”的 iface 和 eface 结构体。

type iface struct {
    tab  *itab
    data unsafe.Pointer
}
type itab struct {
    inter uintptr
    _type uintptr
    link uintptr
    hash  uint32
    _     [4]byte
    fun   [1]uintptr
}
type eface struct {
    _type uintptr
    data unsafe.Pointer
}

接着,将接口变量占据的内存内容强制解释成上面定义的类型,再打印出来:

package main
import (
    "os"
    "fmt"
    "io"
    "unsafe"
)
func main() {
    var r io.Reader
    fmt.Printf("initial r: %T, %v\n", r, r)
    tty, _ := os.OpenFile("/Users/qcrao/Desktop/test", os.O_RDWR, 0)
    fmt.Printf("tty: %T, %v\n", tty, tty)
    // 给 r 赋值
    r = tty
    fmt.Printf("r: %T, %v\n", r, r)
    rIface := (*iface)(unsafe.Pointer(&r))
    fmt.Printf("r: iface.tab._type = %#x, iface.data = %#x\n", rIface.tab._type, rIface.data)
    // 给 w 赋值
    var w io.Writer
    w = r.(io.Writer)
    fmt.Printf("w: %T, %v\n", w, w)
    wIface := (*iface)(unsafe.Pointer(&w))
    fmt.Printf("w: iface.tab._type = %#x, iface.data = %#x\n", wIface.tab._type, wIface.data)
    // 给 empty 赋值
    var empty interface{}
    empty = w
    fmt.Printf("empty: %T, %v\n", empty, empty)
    emptyEface := (*eface)(unsafe.Pointer(&empty))
    fmt.Printf("empty: eface._type = %#x, eface.data = %#x\n", emptyEface._type, emptyEface.data)
}

运行结果:

# 原作者运行结果
initial r: <nil>, <nil>
tty: *os.File, &{0xc4200820f0}
r: *os.File, &{0xc4200820f0}
r: iface.tab._type = 0x10bfcc0, iface.data = 0xc420080020
w: *os.File, &{0xc4200820f0}
w: iface.tab._type = 0x10bfcc0, iface.data = 0xc420080020
empty: *os.File, &{0xc4200820f0}
empty: eface._type = 0x10bfcc0, eface.data = 0xc420080020

# 自己的运行结果
initial r: <nil>, <nil>
tty: *os.File, <nil>
r: *os.File, <nil>
r: iface.tab._type = 0x10c8fc0, iface.data = 0x0
w: *os.File, <nil>
w: iface.tab._type = 0x10c8fc0, iface.data = 0x0
empty: *os.File, <nil>
empty: eface._type = 0x10c8fc0, eface.data = 0x0

r,w,empty 的动态类型和动态值都一样。不再详细解释了,结合前面的图可以看得非常清晰。

反射的基本函数

reflect 包里定义了一个接口和一个结构体,即 reflect.Typereflect.Value,它们提供很多函数来获取存储在接口里的类型信息。

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

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

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

TypeOf 函数用来提取一个接口中值的类型信息。由于它的输入参数是一个空的 interface{},调用此函数时,实参会先被转化为 interface{}类型。这样,实参的类型信息、方法集、值信息都存储到 interface{} 变量里了。

看下源码:

func TypeOf(i interface{}) Type {
    eface := *(*emptyInterface)(unsafe.Pointer(&i))
    return toType(eface.typ)
}

这里的 emptyInterface 和上面提到的 eface 是一回事(字段名略有差异,字段是相同的),并且在不同的源码包:前者在 reflect 包,后者在 runtime 包。 eface.typ 就是动态类型。

type emptyInterface struct {
    typ  *rtype
    word unsafe.Pointer
}

至于 toType 函数,只是做了一个类型转换:

func toType(t *rtype) Type {
    if t == nil {
        return nil
    }
    return t
}

注意,返回值 Type 实际上是一个接口,定义了很多方法,用来获取类型相关的各种信息,而 *rtype 实现了 Type 接口。

type Type interface {
    // 所有的类型都可以调用下面这些函数
    // 此类型的变量对齐后所占用的字节数
    Align() int
    // 如果是 struct 的字段,对齐后占用的字节数
    FieldAlign() int
    // 返回类型方法集里的第 `i` (传入的参数)个方法
    Method(int) Method
    // 通过名称获取方法
    MethodByName(string) (Method, bool)
    // 获取类型方法集里导出的方法个数
    NumMethod() int
    // 类型名称
    Name() string
    // 返回类型所在的路径,如:encoding/base64
    PkgPath() string
    // 返回类型的大小,和 unsafe.Sizeof 功能类似
    Size() uintptr
    // 返回类型的字符串表示形式
    String() string
    // 返回类型的类型值
    Kind() Kind
    // 类型是否实现了接口 u
    Implements(u Type) bool
    // 是否可以赋值给 u
    AssignableTo(u Type) bool
    // 是否可以类型转换成 u
    ConvertibleTo(u Type) bool
    // 类型是否可以比较
    Comparable() bool
    // 下面这些函数只有特定类型可以调用
    // 如:Key, Elem 两个方法就只能是 Map 类型才能调用
    // 类型所占据的位数
    Bits() int
    // 返回通道的方向,只能是 chan 类型调用
    ChanDir() ChanDir
    // 返回类型是否是可变参数,只能是 func 类型调用
    // 比如 t 是类型 func(x int, y ... float64)
    // 那么 t.IsVariadic() == true
    IsVariadic() bool
    // 返回内部子元素类型,只能由类型 Array, Chan, Map, Ptr, or Slice 调用
    Elem() Type
    // 返回结构体类型的第 i 个字段,只能是结构体类型调用
    // 如果 i 超过了总字段数,就会 panic
    Field(i int) StructField
    // 返回嵌套的结构体的字段
    FieldByIndex(index []int) StructField
    // 通过字段名称获取字段
    FieldByName(name string) (StructField, bool)
    // FieldByNameFunc returns the struct field with a name
    // 返回名称符合 func 函数的字段
    FieldByNameFunc(match func(string) bool) (StructField, bool)
    // 获取函数类型的第 i 个参数的类型
    In(i int) Type
    // 返回 map 的 key 类型,只能由类型 map 调用
    Key() Type
    // 返回 Array 的长度,只能由类型 Array 调用
    Len() int
    // 返回类型字段的数量,只能由类型 Struct 调用
    NumField() int
    // 返回函数类型的输入参数个数
    NumIn() int
    // 返回函数类型的返回值个数
    NumOut() int
    // 返回函数类型的第 i 个值的类型
    Out(i int) Type
    // 返回类型结构体的相同部分
    common() *rtype
    // 返回类型结构体的不同部分
    uncommon() *uncommonType
}

可见 Type 定义了非常多的方法,通过它们可以获取类型的一切信息,大家一定要完整的过一遍上面所有的方法。

注意到 Type 方法集的倒数第二个方法 common 返回的 rtype类型,它和上一篇文章讲到的 _type 是一回事,而且源代码里也注释了:两边要保持同步:

// rtype must be kept in sync with ../runtime/type.go:/^type._type.
type rtype struct {
    size       uintptr
    ptrdata    uintptr
    hash       uint32
    tflag      tflag
    align      uint8
    fieldAlign uint8
    kind       uint8
    alg        *typeAlg
    gcdata     *byte
    str        nameOff
    ptrToThis  typeOff
}

所有的类型都会包含 rtype 这个字段,表示各种类型的公共信息;另外,不同类型包含自己的一些独特的部分。

比如下面的 arrayTypechanType 都包含 rytpe,而前者还包含 slice,len 等和数组相关的信息;后者则包含 dir 表示通道方向的信息。

// arrayType represents a fixed array type.
type arrayType struct {
    rtype `reflect:"array"`
    elem  *rtype // array element type
    slice *rtype // slice type
    len   uintptr
}
// chanType represents a channel type.
type chanType struct {
    rtype `reflect:"chan"`
    elem  *rtype  // channel element type
    dir   uintptr // channel direction (ChanDir)
}

注意到,Type 接口实现了 String() 函数,满足 fmt.Stringer 接口,因此使用 fmt.Println 打印的时候,输出的是 String() 的结果。另外,fmt.Printf() 函数,如果使用 %T 来作为格式参数,输出的是 reflect.TypeOf 的结果,也就是动态类型。例如:

fmt.Printf("%T", 3) // int

讲完了 TypeOf 函数,再来看一下 ValueOf 函数。返回值 reflect.Value 表示 interface{} 里存储的实际变量,它能提供实际变量的各种信息。相关的方法常常是需要结合类型信息和值信息。例如,如果要提取一个结构体的字段信息,那就需要用到 _type (具体到这里是指 structType) 类型持有的关于结构体的字段信息、偏移信息,以及 *data 所指向的内容 —— 结构体的实际值。

源码如下:

func ValueOf(i interface{}) Value {
    if i == nil {
        return Value{}
    }
   // ……
    return unpackEface(i)
}
// 分解 eface
func unpackEface(i interface{}) Value {
    e := (*emptyInterface)(unsafe.Pointer(&i))
    t := e.typ
    if t == nil {
        return Value{}
    }
    f := flag(t.Kind())
    if ifaceIndir(t) {
        f |= flagIndir
    }
    return Value{t, e.word, f}
}

从源码看,比较简单:将先将 i 转换成 *emptyInterface 类型, 再将它的 typ 字段和 word 字段以及一个标志位字段组装成一个 Value 结构体,而这就是 ValueOf 函数的返回值,它包含类型结构体指针、真实数据的地址、标志位。

Value 结构体定义了很多方法,通过这些方法可以直接操作 Value 字段 ptr 所指向的实际数据:

// 设置切片的 len 字段,如果类型不是切片,就会panic
 func (v Value) SetLen(n int)
 // 设置切片的 cap 字段
 func (v Value) SetCap(n int)
 // 设置字典的 kv
 func (v Value) SetMapIndex(key, val Value)
 // 返回切片、字符串、数组的索引 i 处的值
 func (v Value) Index(i int) Value
 // 根据名称获取结构体的内部字段值
 func (v Value) FieldByName(name string) Value
 // ……

Value 字段还有很多其他的方法。例如:

// 用来获取 int 类型的值
func (v Value) Int() int64
// 用来获取结构体字段(成员)数量
func (v Value) NumField() int
// 尝试向通道发送数据(不会阻塞)
func (v Value) TrySend(x reflect.Value) bool
// 通过参数列表 in 调用 v 值所代表的函数(或方法
func (v Value) Call(in []Value) (r []Value) 
// 调用变参长度可变的函数
func (v Value) CallSlice(in []Value) []Value

不一一列举了,反正是非常多。可以去 src/reflect/value.go 去看看源码,搜索 func (v Value) 就能看到。

另外,通过 Type() 方法和 Interface() 方法可以打通 interfaceTypeValue 三者。Type() 方法也可以返回变量的类型信息,与 reflect.TypeOf() 函数等价。Interface() 方法可以将 Value 还原成原来的 interface。

总结一下:TypeOf() 函数返回一个接口,这个接口定义了一系列方法,利用这些方法可以获取关于类型的所有信息; ValueOf() 函数返回一个结构体变量,包含类型信息以及实际值。

用一张图来串一下:

上图中,rtye 实现了 Type 接口,是所有类型的公共部分。emptyface 结构体和 eface 其实是一个东西,而 rtype 其实和 _type 是一个东西,只是一些字段稍微有点差别,比如 emptyface 的 word 字段和 eface 的 data 字段名称不同,但是数据型是一样的。

反射的三大定律

根据 Go 官方关于反射的博客,反射有三大定律:

  1. Reflection goes from interface value to reflection object.
  2. Reflection goes from reflection object to interface value.
  3. To modify a reflection object, the value must be settable.

第一条是最基本的:反射是一种检测存储在 interface 中的类型和值机制。这可以通过 TypeOf 函数和 ValueOf 函数得到。

第二条实际上和第一条是相反的机制,它将 ValueOf 的返回值通过 Interface() 函数反向转变成 interface 变量。

前两条就是说 接口型变量反射类型对象 可以相互转化,反射类型对象实际上就是指的前面说的 reflect.Typereflect.Value

第三条不太好懂:如果需要操作一个反射变量,那么它必须是可设置的。反射变量可设置的本质是它存储了原变量本身,这样对反射变量的操作,就会反映到原变量本身;反之,如果反射变量不能代表原变量,那么操作了反射变量,不会对原变量产生任何影响,这会给使用者带来疑惑。所以第二种情况在语言层面是不被允许的。

举一个经典例子:

var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.

执行上面的代码会产生 panic,原因是反射变量 v 不能代表 x 本身,为什么?因为调用 reflect.ValueOf(x) 这一行代码的时候,传入的参数在函数内部只是一个拷贝,是值传递,所以 v 代表的只是 x 的一个拷贝,因此对 v 进行操作是被禁止的。

可设置是反射变量 Value 的一个性质,但不是所有的 Value 都是可被设置的。

就像在一般的函数里那样,当我们想改变传入的变量时,使用指针就可以解决了。

var x float64 = 3.4
p := reflect.ValueOf(&x)
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())

输出是这样的:

type of p: *float64
settability of p: false

p 还不是代表 xp.Elem() 才真正代表 x,这样就可以真正操作 x 了:

v := p.Elem()
v.SetFloat(7.1)
fmt.Println(v.Interface()) // 7.1
fmt.Println(x) // 7.1

关于第三条,记住一句话:如果想要操作原变量,反射变量 Value 必须要 hold 住原变量的地址才行。

如何比较两个对象完全相同

Go 语言中提供了一个函数可以完成此项功能:

func DeepEqual(x, y interface{}) bool

DeepEqual 函数的参数是两个 interface,实际上也就是可以输入任意类型,输出 true 或者 flase 表示输入的两个变量是否是“深度”相等。

先明白一点,如果是不同的类型,即使是底层类型相同,相应的值也相同,那么两者也不是“深度”相等。

type MyInt int
type YourInt int
func main() {
    m := MyInt(1)
    y := YourInt(1)
    fmt.Println(reflect.DeepEqual(m, y)) // false
}

上面的代码中,m, y 底层都是 int,而且值都是 1,但是两者静态类型不同,前者是 MyInt,后者是 YourInt,因此两者不是“深度”相等。

在源码里,有对 DeepEqual 函数的非常清楚地注释,列举了不同类型,DeepEqual 的比较情形,这里做一个总结:

类型 深度相等情形
Array 相同索引处的元素“深度”相等
Struct 相应字段,包含导出和不导出,“深度”相等
Func 只有两者都是 nil 时
Interface 两者存储的具体值“深度”相等
Map 1、都为 nil;2、非空、长度相等,指向同一个 map 实体对象,或者相应的 key 指向的 value “深度”相等
Pointer 1、使用 比较的结果相等;2、指向的实体“深度”相等
Slice 1、都为 nil;2、非空、长度相等,首元素指向同一个底层数组的相同元素,即 &x[0] &y[0] 或者 相同索引处的元素“深度”相等
numbers, bools, strings, and channels 使用 比较的结果为真

一般情况下,DeepEqual 的实现只需要递归地调用 就可以比较两个变量是否是真的“深度”相等。

但是,有一些异常情况:比如 func 类型是不可比较的类型,只有在两个 func 类型都是 nil 的情况下,才是“深度”相等;float 类型,由于精度的原因,也是不能使用 比较的;包含 func 类型或者 float 类型的 struct, interface, array 等。

对于指针而言,当两个值相等的指针就是“深度”相等,因为两者指向的内容是相等的,即使两者指向的是 func 类型或者 float 类型,这种情况下不关心指针所指向的内容。

同样,对于指向相同 slice, map 的两个变量也是“深度”相等的,不关心 slice, map 具体的内容。

对于“有环”的类型,比如循环链表,比较两者是否“深度”相等的过程中,需要对已比较的内容作一个标记,一旦发现两个指针之前比较过,立即停止比较,并判定二者是深度相等的。这样做的原因是,及时停止比较,避免陷入无限循环。

来看源码:

func DeepEqual(x, y interface{}) bool {
    if x == nil || y == nil {
        return x == y
    }
    v1 := ValueOf(x)
    v2 := ValueOf(y)
    if v1.Type() != v2.Type() {
        return false
    }
    return deepValueEqual(v1, v2, make(map[visit]bool), 0)
}

首先查看两者是否有一个是 nil 的情况,这种情况下,只有两者都是 nil,函数才会返回 true

接着,使用反射,获取x,y 的反射对象,并且立即比较两者的类型,根据前面的内容,这里实际上是动态类型,如果类型不同,直接返回 false。

最后,最核心的内容在子函数 deepValueEqual 中。

代码比较长,思路却比较简单清晰:核心是一个 switch 语句,识别输入参数的不同类型,分别递归调用 deepValueEqual 函数,一直递归到最基本的数据类型,比较 int,string 等可以直接得出 true 或者 false,再一层层地返回,最终得到“深度”相等的比较结果。

实际上,各种类型的比较套路比较相似,这里就直接节选一个稍微复杂一点的 map 类型的比较:

// deepValueEqual 函数
// ……
case Map:
    if v1.IsNil() != v2.IsNil() {
        return false
    }
    if v1.Len() != v2.Len() {
        return false
    }
    if v1.Pointer() == v2.Pointer() {
        return true
    }
    for _, k := range v1.MapKeys() {
        val1 := v1.MapIndex(k)
        val2 := v2.MapIndex(k)
        if !val1.IsValid() || !val2.IsValid() || !deepValueEqual(v1.MapIndex(k), v2.MapIndex(k), visited, depth+1) {
            return false
        }
    }
    return true
// ……

和前文总结的表格里,比较 map 是否相等的思路比较一致,也不需要多说什么。说明一点,visited 是一个 map,记录递归过程中,比较过的“对”:

type visit struct {
    a1  unsafe.Pointer
    a2  unsafe.Pointer
    typ Type
}
map[visit]bool

比较过程中,一旦发现比较的“对”,已经在 map 里出现过的话,直接判定“深度”比较结果的是 true

应用案例1

概述

在程序运行期对程序动态的进行访问和修改
reflect godoc:https://golang.org/pkg/reflect/

reflect包有两个数据类型:

  • Type:数据类型 【reflect.TypeOf():是获取Type的方法】
  • Value:值的类型【reflect.ValueOf():是获取Value的方法】

语法

1、基本操作
  • 获取变量类型
func TypeOf(i interface{}) Type   //Type是interface{}的别名

例子

reflect.TypeOf(10)  //int
reflect.TypeOf(struct{ age int }{10})  //struct { age int }
  • 获取变量的种类
reflect.TypeOf(struct{ age int }{10}).Kind()  //reflect.Struct
reflect.ValueOf("hello word").Kind()  //reflect.String
  • 获取变量值
func ValueOf(i interface{}) Value   //value是struct {}别名

例子

reflect.ValueOf("hello word")  //hello word
reflect.ValueOf(struct{ age int }{10})   //{10}
2、修改目标对象
  • 修改普通类型
str := "hello word"
reflect.ValueOf(&str).Elem().SetString("张三")
  • 修改结构体
//第一步:ValueOf():传入一个变量的地址,返回是变量的地址 
//Elem():返回的是变量的原始值
elem:=reflect.ValueOf(&变量名).Elem()

//第二步 FieldByName():传入结构体字段名称   
//SetString():传入你要修改的变量值
elem.FieldByName("Name").SetString("李四")
//定义一个User结构体
type User struct {
    Name string
    Age  int
}

user := User{Name: "张三", Age: 10}
//Elem() 获取user原始的值
elem := reflect.ValueOf(&user).Elem()
//FieldByName() 通过Name返回具有给定名称的结构字段 通过SetString 修改原始的值
elem.FieldByName("Name").SetString("李四")
elem.FieldByName("Age").SetInt(18)
3、动态调用方法
  • 无参方法
//MethodByName():传方法名,方法名必须大小  
//Call():方法的形参
reflect.ValueOf(变量名).MethodByName(方法名).Call([]reflect.Value{})
reflect.ValueOf(变量名).MethodByName(方法名).Call(make([]reflect.Value, 0))
type User struct {
    Name string `json:"name" name:"张三"`
    Age  int
}

func (_ User) Say() {
    fmt.Println("user 说话")
}

user := User{Name: "张三", Age: 10}
reflect.ValueOf(&user).MethodByName("Say").Call([]reflect.Value{})
reflect.ValueOf(user).MethodByName("Say").Call(make([]reflect.Value, 0))
  • 有参方法
reflect.ValueOf(变量名).MethodByName(方法名).Call([]reflect.Value{reflect.ValueOf("该说话了"), reflect.ValueOf(1)})
type User struct {
  Name string `json:"name" name:"张三"`
  Age  int
}

func (_ User) SayContent(content string, a int) {
    fmt.Println("user", content, a)
}

user := User{Name: "张三", Age: 10}
reflect.ValueOf(user).MethodByName("SayContent").Call([]reflect.Value{reflect.ValueOf("该说话了"), reflect.ValueOf(1)})

总结

  • 反射调用struct的方法必须是公有的
  • 反射调用无参方法时必修传 nil 或者 []reflect.Value{}

示例

package main

import (
    "fmt"
    "reflect"
)

func main() {
    //1. 获取变量类型
    fmt.Println("获取变量类型")

    fmt.Println(reflect.TypeOf(10))                          //int
    fmt.Println(reflect.TypeOf(10.0))                        //float64
    fmt.Println(reflect.TypeOf(struct{ age int }{10}))       //struct { age int }
    fmt.Println(reflect.TypeOf(map[string]string{"a": "a"})) //map[string]string
    fmt.Println("")

    //2. 获取变量值
    fmt.Println("获取变量值")

    fmt.Println(reflect.ValueOf("hello word"))                //hello word
    fmt.Println(reflect.ValueOf(struct{ age int }{10}))       //{10}
    fmt.Println(reflect.TypeOf(struct{ age int }{10}).Kind()) //struct
    //类型判断
    if t := reflect.TypeOf(struct{ age int }{10}).Kind(); t == reflect.Struct {
        fmt.Println("是结构体") //是结构体
    } else {
        fmt.Println("不是结构体")
    }

    //修改目标对象
    str := "hello word"
    //普通变量修改
    reflect.ValueOf(&str).Elem().SetString("张三")
    fmt.Println(str) //张三

    //结构体变量修改
    user := User{Name: "张三", Age: 10}
    //Elem() 获取user原始的值
    elem := reflect.ValueOf(&user).Elem()

    //FieldByName() 通过Name返回具有给定名称的结构字段 通过SetString 修改原始的值
    elem.FieldByName("Name").SetString("李四")
    elem.FieldByName("Age").SetInt(18)

    fmt.Println(user) //{{} 李四 18}

    //获取结构体的标签的值
    fmt.Println(reflect.TypeOf(&user).Elem().Field(0).Tag.Get("name")) //无

    //调用无参方法
    reflect.ValueOf(&user).MethodByName("Say").Call([]reflect.Value{})       //user 说话
    reflect.ValueOf(user).MethodByName("Say").Call(make([]reflect.Value, 0)) //user 说话

    //调用有参方法
    reflect.ValueOf(user).MethodByName("SayContent").Call([]reflect.Value{reflect.ValueOf("该说话了"), reflect.ValueOf(1)}) //user 该说话了 1

    //调用本地的方法
    reflect.ValueOf(Hello).Call([]reflect.Value{}) //hello
    reflect.ValueOf(Hello).Call(nil)               //hello

    fmt.Printf("%#v\n", reflect.TypeOf(user).Field(0)) //reflect.StructField{Name:"Person", PkgPath:"", Type:(*reflect.rtype)(0x10dc9e0), Tag:"", Offset:0x0, Index:[]int{0}, Anonymous:true}
}

func Hello() {
    fmt.Println("hello")
}

type Person struct {
    Name string
}

type User struct {
    Person        // //反射会将匿名字段作为一个独立字段来处理
    Name   string `json:"name" name:"张三"`
    Age    int
}

func (user User) Say() {
    fmt.Println("user 说话")
}

func (user User) SayContent(content string, a int) {
    fmt.Println("user", content, a)
}

应用案例2

1.反射定律

  • 反射可以将“接口类型变量”转换为“反射类型对象”。
  • 反射可以将“反射类型对象”转换为“接口类型变量”。
  • 如果要修改“反射类型对象”,其值必须是“可写的”。
定律1 “接口类型变量”=>“反射类型对象”

所谓的反射类型,就是reflect.Type和reflect.Value

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var a int = 30

    v := reflect.ValueOf(a) //返回Value类型对象,值为30
    t := reflect.TypeOf(a)  //返回Type类型对象,值为int
    fmt.Println(v)
    fmt.Println(t)

    v = reflect.ValueOf(&a) //返回Value类型对象,值为0xc0000b4008,变量a的地址
    t = reflect.TypeOf(&a)  //返回Type类型对象,值为*int
    fmt.Println(v)
    fmt.Println(t)
}

上面的案例通过使用reflect.ValueOf和reflect.TypeOf将接口类型变量分别转换为反射类型对象value和type。

  • value中包含了接口中的实际值。
  • type中包含了接口中的实际类型。

大家可能对上面的案例感到疑惑,程序里没有接口类型变量啊,哪来的接口类型变量到反射类型对象的转换啊?事实上,reflect.ValueOf和reflect.TypeOf的参数类型都是interface{},空接口类型,而返回值的类型是reflect.Value和reflect.Type,中间的转换由reflect包来实现。

定律2 “反射类型对象”=>“接口类型变量”

基本方法是:

reflectValue值.Interface.(要转化为的类型)

根据一个reflect.Value类型(注意没有reflect.Type)的变量,我们可以使用 Interface方法恢复其接口类型的值。事实上,这个方法会把 type 和 value信息打包并填充到一个接口变量中,然后返回。

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var a int = 30
    value := reflect.ValueOf(&a)  //返回Value类型对象,值为&a,变量a的地址
    t := value.Interface().(*int) //类型断言,断定v1中type=*int
    fmt.Printf("%T %v\n", t, t)   // *int 0xc420086008
    fmt.Println(*t)               // 30

    cir := 6.28
    value2 := reflect.ValueOf(cir)
    t2 := value2.Interface().(float64)
    fmt.Printf("%T %v\n", t2, t2) // float64 6.28
    fmt.Println(t2)               // 6.28

    t3 := value2.Interface().(int) //panic: interface conversion: interface {} is float64, not int
    fmt.Println(t3)
}

最关键的两步

v1 := value.Interface()  // 返回的是一个接口变量
v2 := v1.(float64)       // 再判断这个接口变量是否能转化为某类型变量
定律3 修改“反射类型对象”

基本方法:

指针类型的value.Elem().SetFloat(待赋的新值)

注意这里用到的也是reflect.ValueOf

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var circle float64 = 6.28
    value := reflect.ValueOf(circle)
    fmt.Println(value.CanSet()) // false
    fmt.Println(value)          // 6.28

    value2 := reflect.ValueOf(&circle)
    fmt.Println(value2.CanSet()) // false
    fmt.Println(value2)          // 0xc420086008

    value3 := reflect.ValueOf(&circle).Elem() //Elem可拿到了真实值
    fmt.Println(value3.CanSet()) // true
    fmt.Println(value3)          // 6.28

    value3.SetFloat(3.14)
    fmt.Println(value3) // 3.14
}

2. reflect.Type

reflect.TypeOf(i interface{}) Type

因为reflect.Typeof的参数是空接口类型,因此可以接收任意类型的数据。 TypeOf()的返回值是这个接口类型对应的reflect.Type对象。

reflect.Type的定义方法,比如,对于一个结构体

package main

import (
    "fmt"
    "reflect"
)

type User struct {
    Name string
    Age  int
    Sex  string
}

func (user User) Say(msg string) error {
    fmt.Println("say", msg)
    return nil
}

func main() {
    user := User{"wang", 1, "m"}
    rType := reflect.TypeOf(user)
    fmt.Println(rType)                                    //main.User
    fmt.Printf("%T,%T,%v,%v\n", user, rType, user, rType) //main.User,*reflect.rtype,{wang 1 m},main.User

    // 数据类名
    fmt.Println(rType) //  main.User
    // 数据类型
    fmt.Println(rType.Kind()) //  struct
    //数据的类名
    fmt.Println(rType.Name()) //  User
    //数据的完整全名(包含包)
    fmt.Println(rType.String()) // main.User
    //数据对象的字段个数
    fmt.Println(rType.NumField()) // 3
    //数据对象的方法个数
    fmt.Println(rType.NumMethod()) // 1

    rValue := reflect.ValueOf(user)
    fmt.Println(rValue)             // {wang 1 m}
    fmt.Println(rValue.Kind())      // struct
    fmt.Println(rValue.String())    // <main.User Value>
    fmt.Println(rValue.NumMethod()) // 1
    fmt.Println(rValue.NumField())  // 3
}

3. reflect.Value

reflect.ValueOf(i interface{}) Value
  • reflect.ValueOf()的返回值类型为reflect.Value,它实现了interface{}参数到reflect.Value的反射
  • reflact.Value对象可以通过调用Interface()方法,再反射回interface{}对象
  • 修改变量数据,通过变量指针类型的value.Elem(),可调用Set…
  • 结构体属性的修改和方法的调用
对于一般类型

见上面反射定律的例子

对于结构体类型

可以通过Field和Method方法修改属性和调用方法

Field

  • 修改变量一定需要通过Elem()
  • 可使用FieldByName 或者Field(index)
package main

import (
    "fmt"
    "reflect"
)

type User struct {
    Name string
    Age  int
    Sex  string
}

func main() {
    user := User{"小小", 23, "Man"}
    rvalue := reflect.ValueOf(&user)
    rvalue.Elem().FieldByName("Sex").SetString("Woman")
    fmt.Println(user) // {小小 23 Woman}
    rvalue.Elem().Field(1).SetInt(10)
    fmt.Println(user) // {小小 10 Woman}
}

Method

  • 函数的调用用MethodByName()或者Method(index)的call方法
  • 返回值是一个数组
package main

import (
    "fmt"
    "reflect"
)

type User struct {
    Name string
    Age  int
    Sex  string
}

func (user User) Say(msg string) int {
    fmt.Println("say", msg)
    return 1234
}

func main() {

    args := []reflect.Value{reflect.ValueOf("hello 1")}

    user := User{Name: "张三", Age: 10}
    rvalue := reflect.ValueOf(user)

    method := rvalue.Method(0)
    method.Call(args) // say hello 1

    method1 := rvalue.MethodByName("Say")
    t := method1.Call(args) // say hello 1
    fmt.Println(t)          // [<int Value>]
    fmt.Println(t[0])       // 1234
}

用面向对象设计原则理解Golang中Interface

Interface接口

interface 是GO语言的基础特性之一。可以理解为一种类型的规范或者约定。它跟java,C# 不太一样,不需要显示说明实现了某个接口,它没有继承或子类或“implements”关键字,只是通过约定的形式,隐式的实现interface 中的方法即可。因此,Golang 中的 interface 让编码更灵活、易扩展。

如何理解go 语言中的interface ?只需记住以下三点即可。

  • interface是方法声明的集合
  • 任何类型的对象实现了在interface接口中声明的全部方法,则表明该类型实现了接口。
  • interface可以作为一种数据类型,实现了该接口的任何对象都可以给对应的接口类型变量赋值。

注意:

  • interface可以被任意对象实现,一个类型/对象也可以实现多个interface.
  • 方法不能重载,如eat(), eat(s string)不能同时存在
package main

import "fmt"

type Phone interface {
    call()
}

type NokiaPhone struct {
}

func (nokiaPhone NokiaPhone) call() {
    fmt.Println("I am Nokia, I can call you!")
}

type ApplePhone struct {
}

func (iPhone ApplePhone) call() {
    fmt.Println("I am Apple Phone, I can call you!")
}

func main() {
    var phone Phone
    phone = new(NokiaPhone)
    phone.call()

    phone = new(ApplePhone)
    phone.call()
}

输出结果

I am Nokia, I can call you!
I am Apple Phone, I can call you!

上述中体现了interface接口的语法,在main函数中,也体现了多态的特性。
同样一个phone的抽象接口,分别指向不同的实体对象,调用的call()方法,打印的效果不同,那么就是体现出了多态的特性。

面向对象中的开闭原则

平铺式的模块设计

那么作为interface数据类型,他存在的意义在哪呢?实际上是为了满足一些面向对象的编程思想。我们知道,软件设计的最高目标就是高内聚,低耦合。那么其中有一个设计原则叫开闭原则。什么是开闭原则呢,接下来我们看一个例子:

package main

import "fmt"

//我们要写一个类,Banker银行业务员
type Banker struct {
}

//存款业务
func (this *Banker) Save() {
    fmt.Println( "进行了 存款业务...")
}

//转账业务
func (this *Banker) Transfer() {
    fmt.Println( "进行了 转账业务...")
}

//支付业务
func (this *Banker) Pay() {
    fmt.Println( "进行了 支付业务...")
}

func main() {
    banker := &Banker{}

    banker.Save()
    banker.Transfer()
    banker.Pay()
}

输出结果

进行了 存款业务...
进行了 转账业务...
进行了 支付业务...

代码很简单,就是一个银行业务员,他可能拥有很多的业务,比如Save()存款、Transfer()转账、Pay()支付等。那么如果这个业务员模块只有这几个方法还好,但是随着我们的程序写的越来越复杂,银行业务员可能就要增加方法,会导致业务员模块越来越臃肿。

这样的设计会导致,当我们去给Banker添加新的业务的时候,会直接修改原有的Banker代码,那么Banker模块的功能会越来越多,出现问题的几率也就越来越大,假如此时Banker已经有99个业务了,现在我们要添加第100个业务,可能由于一次的不小心,导致之前99个业务也一起崩溃,因为所有的业务都在一个Banker类里,他们的耦合度太高,Banker的职责也不够单一,代码的维护成本随着业务的复杂正比成倍增大。

开闭设计原则

那么,如果我们拥有接口, interface这个东西,那么我们就可以抽象一层出来,制作一个抽象的Banker模块,然后提供一个抽象的方法。分别根据这个抽象模块,去实现支付Banker(实现支付方法),转账Banker(实现转账方法)如下:

那么依然可以搞定程序的需求。然后,当我们想要给Banker添加额外功能的时候,之前我们是直接修改Banker的内容,现在我们可以单独定义一个股票Banker(实现股票方法),到这个系统中。而且股票Banker的实现成功或者失败都不会影响之前的稳定系统,他很单一,而且独立。

所以以上,当我们给一个系统添加一个功能的时候,不是通过修改代码,而是通过增添代码来完成,那么就是开闭原则的核心思想了。所以要想满足上面的要求,是一定需要interface来提供一层抽象的接口的。

package main

import "fmt"

//抽象的银行业务员
type AbstractBanker interface {
    DoBusi() //抽象的处理业务接口
}

//存款的业务员
type SaveBanker struct {
    //AbstractBanker
}

func (sb *SaveBanker) DoBusi() {
    fmt.Println("进行了存款")
}

//转账的业务员
type TransferBanker struct {
    //AbstractBanker
}

func (tb *TransferBanker) DoBusi() {
    fmt.Println("进行了转账")
}

//支付的业务员
type PayBanker struct {
    //AbstractBanker
}

func (pb *PayBanker) DoBusi() {
    fmt.Println("进行了支付")
}

func main() {
    //进行存款
    sb := &SaveBanker{}
    sb.DoBusi()

    //进行转账
    tb := &TransferBanker{}
    tb.DoBusi()

    //进行支付
    pb := &PayBanker{}
    pb.DoBusi()
}

输出结果

进行了存款
进行了转账
进行了支付

当然我们也可以根据AbstractBanker设计一个小框架

//实现架构层(基于抽象层进行业务封装-针对interface接口进行封装)
func BankerBusiness(banker AbstractBanker) {
    //通过接口来向下调用,(多态现象)
    banker.DoBusi()
}

那么main中可以如下实现业务调用:

func main() {
    //进行存款
    BankerBusiness(&SaveBanker{})
    //进行存款
    BankerBusiness(&TransferBanker{})
    //进行存款
    BankerBusiness(&PayBanker{})
}

开闭原则定义:

​ 一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
​ 简单的说就是在修改需求的时候,应该尽量通过扩展来实现变化,而不是通过修改已有代码来实现变化。

接口的意义

好了,现在interface已经基本了解,那么接口的意义最终在哪里呢,想必现在你已经有了一个初步的认知,实际上接口的最大的意义就是实现多态的思想,就是我们可以根据interface类型来设计API接口,那么这种API接口的适应能力不仅能适应当下所实现的全部模块,也适应未来实现的模块来进行调用。 调用未来可能就是接口的最大意义所在吧,这也是为什么架构师那么值钱,因为良好的架构师是可以针对interface设计一套框架,在未来许多年却依然适用。

面向对象中的依赖倒转原则

耦合度极高的模块设计

代码实现如下

package main

import "fmt"

// === > 奔驰汽车 <===
type Benz struct {
  //...
}

func (this *Benz) Run() {
    fmt.Println("Benz is running...")
}

// === > 宝马汽车  <===
type BMW struct {
  //...
}

func (this *BMW) Run() {
    fmt.Println("BMW is running ...")
}

//===> 司机张三  <===
type Zhang3 struct {
    //...
}

func (zhang3 *Zhang3) DriveBenZ(benz *Benz) {
    fmt.Println("zhang3 Drive Benz")
    benz.Run()
}

func (zhang3 *Zhang3) DriveBMW(bmw *BMW) {
    fmt.Println("zhang3 drive BMW")
    bmw.Run()
}

//===> 司机李四 <===
type Li4 struct {
    //...
}

func (li4 *Li4) DriveBenZ(benz *Benz) {
    fmt.Println("li4 Drive Benz")
    benz.Run()
}

func (li4 *Li4) DriveBMW(bmw *BMW) {
    fmt.Println("li4 drive BMW")
    bmw.Run()
}

func main() {
    //业务1 张3开奔驰
    benz := &Benz{}
    zhang3 := &Zhang3{}
    zhang3.DriveBenZ(benz)

    //业务2 李四开宝马
    bmw := &BMW{}
    li4 := &Li4{}
    li4.DriveBMW(bmw)
}

输出结果:

zhang3 Drive Benz
Benz is running...
li4 drive BMW
BMW is running ...

我们来看上面的代码和图中每个模块之间的依赖关系,实际上并没有用到任何的interface接口层的代码,显然最后我们的两个业务 张三开奔驰, 李四开宝马,程序中也都实现了。但是这种设计的问题就在于,小规模没什么问题,但是一旦程序需要扩展,比如我现在要增加一个丰田汽车 或者 司机王五, 那么模块和模块的依赖关系将成指数级递增,想蜘蛛网一样越来越难维护和捋顺。

面向抽象层的依赖倒转设计

如上图所示,如果我们在设计一个系统的时候,将模块分为3个层次,抽象层、实现层、业务逻辑层。那么,我们首先将抽象层的模块和接口定义出来,这里就需要了interface接口的设计,然后我们依照抽象层,依次实现每个实现层的模块,在我们写实现层代码的时候,实际上我们只需要参考对应的抽象层实现就好了,实现每个模块,也和其他的实现的模块没有关系,这样也符合了上面介绍的开闭原则。这样实现起来每个模块只依赖对象的接口,而和其他模块没关系,依赖关系单一。系统容易扩展和维护。
我们在指定业务逻辑也是一样,只需要参考抽象层的接口来业务就好了,抽象层暴露出来的接口就是我们业务层可以使用的方法,然后可以通过多态的线下,接口指针指向哪个实现模块,调用了就是具体的实现方法,这样我们业务逻辑层也是依赖抽象成编程。
我们就将这种的设计原则叫做依赖倒转原则

来一起看一下修改的代码:

package main

import "fmt"

// ===== >   抽象层  < ========
type Car interface {
    Run()
}

type Driver interface {
    Drive(car Car)
}

// ===== >   实现层  < ========
type BenZ struct {
    //...
}

func (benz *BenZ) Run() {
    fmt.Println("Benz is running...")
}

type Bmw struct {
    //...
}

func (bmw *Bmw) Run() {
    fmt.Println("Bmw is running...")
}

type Zhang_3 struct {
    //...
}

func (zhang3 *Zhang_3) Drive(car Car) {
    fmt.Println("Zhang3 drive car")
    car.Run()
}

type Li_4 struct {
    //...
}

func (li4 *Li_4) Drive(car Car) {
    fmt.Println("li4 drive car")
    car.Run()
}

// ===== >   业务逻辑层  < ========
func main() {
    //张3 开 宝马
    var bmw Car
    bmw = &Bmw{}

    var zhang3 Driver
    zhang3 = &Zhang_3{}

    zhang3.Drive(bmw)

    //李4 开 奔驰
    var benz Car
    benz = &BenZ{}

    var li4 Driver
    li4 = &Li_4{}

    li4.Drive(benz)
}

输出结果

Zhang3 drive car
Bmw is running...
li4 drive car
Benz is running...

依赖倒转小案例

模拟组装2台电脑

  • 抽象层

​ 有显卡Card 方法display,有内存Memory 方法storage,有处理器CPU 方法calculate

  • 实现层

​ 有 Intel因特尔公司 、产品有(显卡、内存、CPU),有 Kingston 公司, 产品有(内存3),有 NVIDIA 公司, 产品有(显卡)

  • 逻辑层
  1. 组装一台Intel系列的电脑,并运行
  2. 组装一台 Intel CPU Kingston内存 NVIDIA显卡的电脑,并运行

代码实现

package main

import "fmt"

//------  抽象层 -----
type Card interface {
    Display()
}

type Memory interface {
    Storage()
}

type CPU interface {
    Calculate()
}

type Computer struct {
    cpu  CPU
    mem  Memory
    card Card
}

func NewComputer(cpu CPU, mem Memory, card Card) *Computer {
    return &Computer{
        cpu:  cpu,
        mem:  mem,
        card: card,
    }
}

func (this *Computer) DoWork() {
    this.cpu.Calculate()
    this.mem.Storage()
    this.card.Display()
}

//------  实现层 -----
//intel
type IntelCPU struct {
    CPU
}

func (this *IntelCPU) Calculate() {
    fmt.Println("Intel CPU 开始计算了...")
}

type IntelMemory struct {
    Memory
}

func (this *IntelMemory) Storage() {
    fmt.Println("Intel Memory 开始存储了...")
}

type IntelCard struct {
    Card
}

func (this *IntelCard) Display() {
    fmt.Println("Intel Card 开始显示了...")
}

//kingston
type KingstonMemory struct {
    Memory
}

func (this *KingstonMemory) Storage() {
    fmt.Println("Kingston memory storage...")
}

//nvidia
type NvidiaCard struct {
    Card
}

func (this *NvidiaCard) Display() {
    fmt.Println("Nvidia card display...")
}

//------  业务逻辑层 -----
func main() {
    //intel系列的电脑
    com1 := NewComputer(&IntelCPU{}, &IntelMemory{}, &IntelCard{})
    com1.DoWork()

    //杂牌子
    com2 := NewComputer(&IntelCPU{}, &KingstonMemory{}, &NvidiaCard{})
    com2.DoWork()
}

输出结果

Intel CPU 开始计算了...
Intel Memory 开始存储了...
Intel Card 开始显示了...
Intel CPU 开始计算了...
Kingston memory storage...
Nvidia card display...

Golang底层原理之Interface

Go 接口与 C++ 接口有何异同

接口定义了一种规范,描述了类的行为和功能,而不做具体实现。

C++ 的接口是使用抽象类来实现的,如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用 “= 0” 来指定的。例如:

class Shape{
  public:      // 纯虚函数
  virtual double getArea() = 0;
  private:      string name;      // 名称
};

设计抽象类的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。

派生类需要明确地声明它继承自基类,并且需要实现基类中所有的纯虚函数。

C++ 定义接口的方式称为“侵入式”,而 Go 采用的是 “非侵入式”,不需要显式声明,只需要实现接口定义的函数,编译器自动会识别。

C++ 和 Go 在定义接口方式上的不同,也导致了底层实现上的不同。C++ 通过虚函数表来实现基类调用派生类的函数;而 Go 通过 itab 中的 fun 字段来实现接口变量调用实体类型的函数。C++ 中的虚函数表是在编译期生成的;而 Go 的 itab 中的 fun 字段是在运行期间动态生成的。原因在于,Go 中实体类型可能会无意中实现 N 多接口,很多接口并不是本来需要的,所以不能为类型实现的所有接口都生成一个 itab, 这也是“非侵入式”带来的影响;这在 C++ 中是不存在的,因为派生需要显示声明它继承自哪个基类。

参考资料
【和 C++ 的对比】https://www.jianshu.com/p/b38b1719636e

Go 语言与鸭子类型的关系

先直接来看维基百科里的定义:

If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.

翻译过来就是:如果某个东西长得像鸭子,像鸭子一样游泳,像鸭子一样嘎嘎叫,那它就可以被看成是一只鸭子。

Duck Typing,鸭子类型,是动态编程语言的一种对象推断策略,它更关注对象能如何被使用,而不是对象的类型本身。Go 语言作为一门静态语言,它通过通过接口的方式完美支持鸭子类型。

例如,在动态语言 python 中,定义一个这样的函数:

def hello_world(coder):    
        coder.say_hello()

当调用此函数的时候,可以传入任意类型,只要它实现了 say_hello() 函数就可以。如果没有实现,运行过程中会出现错误。

而在静态语言如 Java, C++ 中,必须要显示地声明实现了某个接口,之后,才能用在任何需要这个接口的地方。如果你在程序中调用 hello_world 函数,却传入了一个根本就没有实现 say_hello() 的类型,那在编译阶段就不会通过。这也是静态语言比动态语言更安全的原因。

动态语言和静态语言的差别在此就有所体现。静态语言在编译期间就能发现类型不匹配的错误,不像动态语言,必须要运行到那一行代码才会报错。插一句,这也是我不喜欢用 python 的一个原因。当然,静态语言要求程序员在编码阶段就要按照规定来编写程序,为每个变量规定数据类型,这在某种程度上,加大了工作量,也加长了代码量。动态语言则没有这些要求,可以让人更专注在业务上,代码也更短,写起来更快,这一点,写 python 的同学比较清楚。

Go 语言作为一门现代静态语言,是有后发优势的。它引入了动态语言的便利,同时又会进行静态语言的类型检查,写起来是非常 Happy 的。Go 采用了折中的做法:不要求类型显示地声明实现了某个接口,只要实现了相关的方法即可,编译器就能检测到。

来看个例子:

先定义一个接口,和使用此接口作为参数的函数:

type IGreeting interface {    
  sayHello()
}

func sayHello(i IGreeting) {    
  i.sayHello()
}

再来定义两个结构体:

type Go struct {}
func (g Go) sayHello() {    
  fmt.Println("Hi, I am GO!")
}

type PHP struct {}
func (p PHP) sayHello() {    
  fmt.Println("Hi, I am PHP!")
}

最后,在 main 函数里调用 sayHello() 函数:

func main() {    
  golang := Go{}    
  php := PHP{}    
  sayHello(golang)    
  sayHello(php)
}

程序输出:

Hi, I am GO!
Hi, I am PHP!

在 main 函数中,调用调用 sayHello() 函数时,传入了 golang, php 对象,它们并没有显式地声明实现了 IGreeting 类型,只是实现了接口所规定的 sayHello() 函数。实际上,编译器在调用 sayHello() 函数时,会隐式地将 golang, php 对象转换成 IGreeting 类型,这也是静态语言的类型检查功能。

顺带再提一下动态语言的特点:

变量绑定的类型是不确定的,在运行期间才能确定函数和方法可以接收任何类型的参数,且调用时不检查参数类型不需要实现接口

总结一下,鸭子类型是一种动态语言的风格,在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由它”当前方法和属性的集合”决定。Go 作为一种静态语言,通过接口实现了 鸭子类型,实际上是 Go 的编译器在其中作了隐匿的转换工作。

参考资料
【wikipedia】https://en.wikipedia.org/wiki/Duck_test
【Golang 与鸭子类型,讲得比较好】https://blog.csdn.net/cszhouwei/article/details/33741731
【各种面向对象的名词】https://cyent.github.io/golang/other/oo/
【多态、鸭子类型特性】https://www.jb51.net/article/116025.htm
【鸭子类型、动态静态语言】https://www.jianshu.com/p/650485b78d11

iface 和 eface 的区别是什么

ifaceeface 都是 Go 中描述接口的底层结构体,区别在于 iface 描述的接口包含方法,而 eface 则是不包含任何方法的空接口:interface{}

从源码层面看一下:

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

type itab struct {    
  inter  *interfacetype    
  _type  *_type   
  link   *itab    
  hash   uint32 // copy of _type.hash. Used for type switches.    
  bad    bool   // type does not implement interface   
  inhash bool   // has this itab been added to hash?   
  unused [2]byte    
  fun    [1]uintptr // variable sized
}

iface 内部维护两个指针,tab 指向一个 itab 实体, 它表示接口的类型以及赋给这个接口的实体类型。data 则指向接口具体的值,一般而言是一个指向堆内存的指针。

再来仔细看一下 itab 结构体:_type 字段描述了实体的类型,包括内存对齐方式,大小等;inter 字段则描述了接口的类型。fun 字段放置和接口方法对应的具体数据类型的方法地址,实现接口调用方法的动态分派,一般在每次给接口赋值发生转换时会更新此表,或者直接拿缓存的 itab。

这里只会列出实体类型和接口相关的方法,实体类型的其他方法并不会出现在这里。如果你学过 C++ 的话,这里可以类比虚函数的概念。

另外,你可能会觉得奇怪,为什么 fun 数组的大小为 1,要是接口定义了多个方法可怎么办?实际上,这里存储的是第一个方法的函数指针,如果有更多的方法,在它之后的内存空间里继续存储。从汇编角度来看,通过增加地址就能获取到这些函数指针,没什么影响。顺便提一句,这些方法是按照函数名称的字典序进行排列的。

再看一下 interfacetype 类型,它描述的是接口的类型:

type interfacetype struct {    
  typ     _type    
  pkgpath name    
  mhdr    []imethod
}

可以看到,它包装了 _type 类型,_type 实际上是描述 Go 语言中各种数据类型的结构体。我们注意到,这里还包含一个 mhdr 字段,表示接口所定义的函数列表, pkgpath 记录定义了接口的包名。

这里通过一张图来看下 iface 结构体的全貌:

接着来看一下 eface 的源码:

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

相比 ifaceeface 就比较简单了。只维护了一个 _type 字段,表示空接口所承载的具体的实体类型。data 描述了具体的值。

我们来看个例子:

package main

import "fmt"

func main() {
    x := 200
    var any interface{} = x
    fmt.Println(any)
    g := Gopher{"Go"}
    var c coder = g
    fmt.Println(c)
}

type coder interface {
    code()
    debug()
}
type Gopher struct {
    language string
}

func (p Gopher) code() {
    fmt.Printf("I am coding %s language\n", p.language)
}
func (p Gopher) debug() {
    fmt.Printf("I am debuging %s language\n", p.language)
}

执行命令,打印出汇编语言:

go tool compile -S ./src/main.go

可以看到,main 函数里调用了两个函数:

func convT2E64(t *_type, elem unsafe.Pointer) (e eface)
func convT2I(tab *itab, elem unsafe.Pointer) (i iface)

上面两个函数的参数和 ifaceeface 结构体的字段是可以联系起来的:两个函数都是将参数组装一下,形成最终的接口。

作为补充,我们最后再来看下 _type 结构体:

type _type struct {
    // 类型大小
    size       uintptr
    ptrdata    uintptr
    // 类型的 hash 值
    hash       uint32
    // 类型的 flag,和反射相关
    tflag      tflag
    // 内存对齐相关
    align      uint8
    fieldalign uint8
    // 类型的编号,有bool, slice, struct 等等等等
    kind       uint8
    alg        *typeAlg
    // gc 相关
    gcdata    *byte
    str       nameOff
    ptrToThis typeOff
}

Go 语言各种数据类型都是在 _type 字段的基础上,增加一些额外的字段来进行管理的:

type arraytype struct {
    typ   _type
    elem  *_type
    slice *_type
    len   uintptr
}
type chantype struct {
    typ  _type
    elem *_type
    dir  uintptr
}
type slicetype struct {
    typ  _type
    elem *_type
}
type structtype struct {
    typ     _type
    pkgPath name
    fields  []structfield
}

这些数据类型的结构体定义,是反射实现的基础。

参考资料
【有汇编分析,不错】http://legendtkl.com/2017/07/01/golang-interface-implement/
【interface 源码解读 很不错 包含反射】http://wudaijun.com/2018/01/go-interface-implement/

值接收者和指针接收者的区别

方法

方法能给用户自定义的类型添加新的行为。它和函数的区别在于方法有一个接收者,给一个函数添加一个接收者,那么它就变成了方法。接收者可以是值接收者,也可以是指针接收者

在调用方法的时候,值类型既可以调用值接收者的方法,也可以调用指针接收者的方法;指针类型既可以调用指针接收者的方法,也可以调用值接收者的方法。

也就是说,不管方法的接收者是什么类型,该类型的值和指针都可以调用,不必严格符合接收者的类型。

来看个例子:

package main
import "fmt"
type Person struct {
    age int
}
func (p Person) howOld() int {
    return p.age
}
func (p *Person) growUp() {
    p.age += 1
}
func main() {
    // qcrao 是值类型
    qcrao := Person{age: 18}
    // 值类型 调用接收者也是值类型的方法
    fmt.Println(qcrao.howOld())
    // 值类型 调用接收者是指针类型的方法
    qcrao.growUp()
    fmt.Println(qcrao.howOld())
    // ----------------------
    // stefno 是指针类型
    stefno := &Person{age: 100}
    // 指针类型 调用接收者是值类型的方法
    fmt.Println(stefno.howOld())
    // 指针类型 调用接收者也是指针类型的方法
    stefno.growUp()
    fmt.Println(stefno.howOld())
}

上例子的输出结果是:

18
19
100
101

调用了 growUp 函数后,不管调用者是值类型还是指针类型,它的 Age 值都改变了。

实际上,当类型和方法的接收者类型不同时,其实是编译器在背后做了一些工作,用一个表格来呈现:

值接收者 指针接收者
值类型调用者 方法会使用调用者的一个副本,类似于“传值” 使用值的引用来调用方法,上例中,qcrao.growUp() 实际上是 (&qcrao).growUp()
指针类型调用者 指针被解引用为值,上例中,stefno.howOld() 实际上是 (*stefno).howOld() 实际上也是“传值”,方法里的操作会影响到调用者,类似于指针传参,拷贝了一份指针

值接收者和指针接收者

前面说过,不管接收者类型是值类型还是指针类型,都可以通过值类型或指针类型调用,这里面实际上通过语法糖起作用的。

先说结论:实现了接收者是值类型的方法,相当于自动实现了接收者是指针类型的方法;而实现了接收者是指针类型的方法,不会自动生成对应接收者是值类型的方法。

来看一个例子,就会完全明白:

package main
import "fmt"
type coder interface {
    code()
    debug()
}
type Gopher struct {
    language string
}
func (p Gopher) code() {
    fmt.Printf("I am coding %s language\n", p.language)
}
func (p *Gopher) debug() {
    fmt.Printf("I am debuging %s language\n", p.language)
}
func main() {
    var c coder = &Gopher{"Go"}
    c.code()
    c.debug()
}

上述代码里定义了一个接口 coder,接口定义了两个函数:

code()
debug()

接着定义了一个结构体 Gopher,它实现了两个方法,一个值接收者,一个指针接收者。

最后,我们在 main 函数里通过接口类型的变量调用了定义的两个函数。

运行一下,结果:

I am coding Go language
I am debuging Go language

但是如果我们把 main 函数的第一条语句换一下:

 func main() {    
   var c coder = Gopher{"Go"}
   c.code()
   c.debug()
 }

运行一下,报错:

src/main.go:23:6: cannot use Gopher literal (type Gopher) as type coder in assignment:    Gopher does not implement coder (debug method has pointer receiver)

看出这两处代码的差别了吗?第一次是将 &Gopher 赋给了 coder;第二次则是将 Gopher 赋给了 coder

第二次报错是说,Gopher 没有实现 coder。很明显了吧,因为 Gopher 类型并没有实现 debug 方法;表面上看, *Gopher 类型也没有实现 code 方法,但是因为 Gopher 类型实现了 code 方法,所以让 *Gopher 类型自动拥有了 code 方法。

当然,上面的说法有一个简单的解释:接收者是指针类型的方法,很可能在方法中会对接收者的属性进行更改操作,从而影响接收者;而对于接收者是值类型的方法,在方法中不会对接收者本身产生影响。

所以,当实现了一个接收者是值类型的方法,就可以自动生成一个接收者是对应指针类型的方法,因为两者都不会影响接收者。但是,当实现了一个接收者是指针类型的方法,如果此时自动生成一个接收者是值类型的方法,原本期望对接收者的改变(通过指针实现),现在无法实现,因为值类型会产生一个拷贝,不会真正影响调用者。

最后,只要记住下面这点就可以了:

如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法。

两者分别在何时使用

如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者;如果方法的接收者是指针类型,则调用者修改的是指针指向的对象本身。

使用指针作为方法的接收者的理由:

  • 方法能够修改接收者指向的值。
  • 避免在每次调用方法时复制该值,在值的类型为大型结构体时,这样做会更加高效。

是使用值接收者还是指针接收者,不是由该方法是否修改了调用者(也就是接收者)来决定,而是应该基于该类型的本质

如果类型具备“原始的本质”,也就是说它的成员都是由 Go 语言里内置的原始类型,如字符串,整型值等,那就定义值接收者类型的方法。像内置的引用类型,如 slice,map,interface,channel,这些类型比较特殊,声明他们的时候,实际上是创建了一个 header, 对于他们也是直接定义值接收者类型的方法。这样,调用函数时,是直接 copy 了这些类型的 header,而 header 本身就是为复制设计的。

如果类型具备非原始的本质,不能被安全地复制,这种类型总是应该被共享,那就定义指针接收者的方法。比如 go 源码里的文件结构体(struct File)就不应该被复制,应该只有一份实体

这一段说的比较绕,大家可以去看《Go 语言实战》5.3 那一节。

参考资料
【飞雪无情 Go实战笔记】https://www.flysnow.org/2017/04/03/go-in-action-go-interface.html
【何时使用指针接收者】http://ironxu.com/711
【理解Go Interface】http://lanlingzi.cn/post/technical/2016/0803_go_interface/
【Go语言实战 类型的本置】 图书《Go In Action》

如何用 interface 实现多态

Go 语言并没有设计诸如虚函数、纯虚函数、继承、多重继承等概念,但它通过接口却非常优雅地支持了面向对象的特性。

多态是一种运行期的行为,它有以下几个特点:

  1. 一种类型具有多种类型的能力
  2. 允许不同的对象对同一消息做出灵活的反应
  3. 以一种通用的方式对待个使用的对象
  4. 非动态语言必须通过继承和接口的方式来实现

看一个实现了多态的代码例子:

package main

import "fmt"

func main() {
    qcrao := Student{age: 18}
    whatJob(&qcrao) //这里必须是引用,growUp用指针实现了
    growUp(&qcrao)  //这里必须是引用,growUp用指针实现了
    fmt.Println(qcrao)
    stefno := Programmer{age: 100}
    whatJob(stefno)
    growUp(stefno)
    fmt.Println(stefno)
}
func whatJob(p Person) {
    p.job()
}
func growUp(p Person) {
    p.growUp()
}

type Person interface {
    job()
    growUp()
}
type Student struct {
    age int
}

func (p Student) job() {
    fmt.Println("I am a student.")
}
func (p *Student) growUp() {
    p.age += 1
}

type Programmer struct {
    age int
}

func (p Programmer) job() {
    fmt.Println("I am a programmer.")
}
func (p Programmer) growUp() {
    // 程序员老得太快 ^_^
    p.age += 10
}

代码里先定义了 1 个 Person 接口,包含两个函数:

job()
growUp()

然后,又定义了 2 个结构体,StudentProgrammer,同时,类型 *StudentProgrammer 实现了 Person 接口定义的两个函数。注意,*Student 类型实现了接口, Student 类型却没有。

之后,我又定义了函数参数是 Person 接口的两个函数:

func whatJob(p Person)
func growUp(p Person)

main 函数里先生成 StudentProgrammer 的对象,再将它们分别传入到函数 whatJobgrowUp。函数中,直接调用接口函数,实际执行的时候是看最终传入的实体类型是什么,调用的是实体类型实现的函数。于是,不同对象针对同一消息就有多种表现,多态就实现了。

更深入一点来说的话,在函数 whatJob() 或者 growUp() 内部,接口 person 绑定了实体类型 *Student 或者 Programmer。根据前面分析的 iface 源码,这里会直接调用 fun 里保存的函数,类似于: s.tab->fun[0],而因为 fun 数组里保存的是实体类型实现的函数,所以当函数传入不同的实体类型时,调用的实际上是不同的函数实现,从而实现多态。

运行一下代码:

I am a student.{19}
I am a programmer.{100}

参考资料
【各种面向对象的名词】https://cyent.github.io/golang/other/oo/
【多态与鸭子类型】https://www.jb51.net/article/116025.htm

接口的动态类型和动态值

从源码里可以看到:iface包含两个字段:tab 是接口表指针,指向类型信息;data 是数据指针,则指向具体的数据。它们分别被称为动态类型动态值。而接口值包括动态类型动态值

【引申1】接口类型和 nil 作比较

接口值的零值是指动态类型动态值都为 nil。当仅且当这两部分的值都为 nil 的情况下,这个接口值就才会被认为 接口值 == nil

来看个例子:

package main
import "fmt"
type Coder interface {
    code()
}
type Gopher struct {
    name string
}
func (g Gopher) code() {
    fmt.Printf("%s is coding\n", g.name)
}
func main() {
    var c Coder
    fmt.Println(c == nil)
    fmt.Printf("c: %T, %v\n", c, c)
    var g *Gopher
    fmt.Println(g == nil)
    c = g
    fmt.Println(c == nil)
    fmt.Printf("c: %T, %v\n", c, c)
}

输出:

true
c: <nil>, <nil>
true
false
c: *main.Gopher, <nil>

一开始,c 的 动态类型和动态值都为 nilg 也为 nil,当把 g 赋值给 c 后,c 的动态类型变成了 *main.Gopher,仅管 c 的动态值仍为 nil,但是当 cnil 作比较的时候,结果就是 false 了。

【引申2】来看一个例子,看一下它的输出:

package main
import "fmt"
type MyError struct {}
func (i MyError) Error() string {
    return "MyError"
}
func main() {
    err := Process()
    fmt.Println(err)
    fmt.Println(err == nil)
}
func Process() error {
    var err *MyError = nil
    return err
}

函数运行结果:

<nil>
false

这里先定义了一个 MyError 结构体,实现了 Error 函数,也就实现了 error 接口。Process 函数返回了一个 error 接口,这块隐含了类型转换。所以,虽然它的值是 nil,其实它的类型是 *MyError,最后和 nil 比较的时候,结果为 false

【引申3】如何打印出接口的动态类型和值?

直接看代码:

package main
import (
    "unsafe"
    "fmt"
)
type iface struct {
    itab, data uintptr
}
func main() {
    var a interface{} = nil
    var b interface{} = (*int)(nil)
    x := 5
    var c interface{} = (*int)(&x)
    ia := *(*iface)(unsafe.Pointer(&a))
    ib := *(*iface)(unsafe.Pointer(&b))
    ic := *(*iface)(unsafe.Pointer(&c))
    fmt.Println(ia, ib, ic)
    fmt.Println(*(*int)(unsafe.Pointer(ic.data)))
}

代码里直接定义了一个 iface 结构体,用两个指针来描述 itabdata,之后将 a, b, c 在内存中的内容强制解释成我们自定义的 iface。最后就可以打印出动态类型和动态值的地址。

运行结果如下:

{0 0} {17426912 0} {17426912 842350714568}
5

a 的动态类型和动态值的地址均为 0,也就是 nil;b 的动态类型和 c 的动态类型一致,都是 *int;最后,c 的动态值为 5。

参考资料
【一个包含NIL指针的接口不是NIL接口】https://i6448038.github.io/2018/07/18/golang-mistakes/

接口的构造过程是怎样的

我们已经看过了 ifaceeface 的源码,知道 iface 最重要的是 itab_type

为了研究清楚接口是如何构造的,接下来我会拿起汇编的武器,还原背后的真相。

来看一个示例代码:

package main
import "fmt"
type Person interface {
    growUp()
}
type Student struct {
    age int
}
func (p Student) growUp() {
    p.age += 1
    return
}
func main() {
    var qcrao = Person(Student{age: 18})
    fmt.Println(qcrao)
}

执行命令:

go tool compile -S main.go

得到 main 函数的汇编代码如下:

0x0000 00000 (./src/main.go:30) TEXT    "".main(SB), 80-0
0x0000 00000 (./src/main.go:30) MOVQ    (TLS), CX
0x0009 00009 (./src/main.go:30) CMPQ    SP, 16(CX)
0x000d 00013 (./src/main.go:30) JLS     157
0x0013 00019 (./src/main.go:30) SUBQ80, SP
0x0017 00023 (./src/main.go:30) MOVQ    BP, 72(SP)
0x001c 00028 (./src/main.go:30) LEAQ    72(SP), BP
0x0021 00033 (./src/main.go:30) FUNCDATA0, gclocals·69c1753bd5f81501d95132d08af04464(SB)
0x0021 00033 (./src/main.go:30) FUNCDATA1, gclocals·e226d4ae4a7cad8835311c6a4683c14f(SB)
0x0021 00033 (./src/main.go:31) MOVQ    18, ""..autotmp_1+48(SP)
0x002a 00042 (./src/main.go:31) LEAQ    go.itab."".Student,"".Person(SB), AX
0x0031 00049 (./src/main.go:31) MOVQ    AX, (SP)
0x0035 00053 (./src/main.go:31) LEAQ    ""..autotmp_1+48(SP), AX
0x003a 00058 (./src/main.go:31) MOVQ    AX, 8(SP)
0x003f 00063 (./src/main.go:31) PCDATA0, 0
0x003f 00063 (./src/main.go:31) CALL    runtime.convT2I64(SB)
0x0044 00068 (./src/main.go:31) MOVQ    24(SP), AX
0x0049 00073 (./src/main.go:31) MOVQ    16(SP), CX
0x004e 00078 (./src/main.go:33) TESTQ   CX, CX
0x0051 00081 (./src/main.go:33) JEQ     87
0x0053 00083 (./src/main.go:33) MOVQ    8(CX), CX
0x0057 00087 (./src/main.go:33) MOVQ0, ""..autotmp_2+56(SP)
0x0060 00096 (./src/main.go:33) MOVQ    0, ""..autotmp_2+64(SP)
0x0069 00105 (./src/main.go:33) MOVQ    CX, ""..autotmp_2+56(SP)
0x006e 00110 (./src/main.go:33) MOVQ    AX, ""..autotmp_2+64(SP)
0x0073 00115 (./src/main.go:33) LEAQ    ""..autotmp_2+56(SP), AX
0x0078 00120 (./src/main.go:33) MOVQ    AX, (SP)
0x007c 00124 (./src/main.go:33) MOVQ1, 8(SP)
0x0085 00133 (./src/main.go:33) MOVQ    1, 16(SP)
0x008e 00142 (./src/main.go:33) PCDATA0, 1
0x008e 00142 (./src/main.go:33) CALL    fmt.Println(SB)
0x0093 00147 (./src/main.go:34) MOVQ    72(SP), BP
0x0098 00152 (./src/main.go:34) ADDQ80, SP
0x009c 00156 (./src/main.go:34) RET
0x009d 00157 (./src/main.go:34) NOP
0x009d 00157 (./src/main.go:30) PCDATA  0,-1
0x009d 00157 (./src/main.go:30) CALL    runtime.morestack_noctxt(SB)
0x00a2 00162 (./src/main.go:30) JMP     0

我们从第 10 行开始看

汇编行数 操作
10-14 构造调用 runtime.convT2I64(SB) 的参数

我们来看下这个函数的参数形式:

func convT2I64(tab *itab, elem unsafe.Pointer) (i iface) {
    // ……
}

convT2I64 会构造出一个 inteface,也就是我们的 Person 接口。

第一个参数的位置是 (SP),这里被赋上了 go.itab."".Student,"".Person(SB) 的地址。

我们从生成的汇编找到:

go.itab."".Student,"".Person SNOPTRDATA dupok size=40
        0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  
        0x0010 00 00 00 00 00 00 00 00 da 9f 20 d4              
        rel 0+8 t=1 type."".Person+0
        rel 8+8 t=1 type."".Student+0

size=40 大小为40字节,回顾一下:

type itab struct {
    inter  *interfacetype // 8字节
    _type  *_type // 8字节
    link   *itab // 8字节
    hash   uint32 // 4字节
    bad    bool   // 1字节
    inhash bool   // 1字节
    unused [2]byte // 2字节
    fun    [1]uintptr // variable sized // 8字节
}

把每个字段的大小相加,itab 结构体的大小就是 40 字节。上面那一串数字实际上是 itab 序列化后的内容,注意到大部分数字是 0,从 24 字节开始的 4 个字节 da 9f 20 d4 实际上是 itabhash 值,这在判断两个类型是否相同的时候会用到。

下面两行是链接指令,简单说就是将所有源文件综合起来,给每个符号赋予一个全局的位置值。这里的意思也比较明确:前8个字节最终存储的是 type."".Person 的地址,对应 itab 里的 inter 字段,表示接口类型;8-16 字节最终存储的是 type."".Student 的地址,对应 itab_type 字段,表示具体类型。

第二个参数就比较简单了,它就是数字 18 的地址,这也是初始化 Student 结构体的时候会用到。

汇编行数 操作
15 调用 runtime.convT2I64(SB)

具体看下代码:

func convT2I64(tab *itab, elem unsafe.Pointer) (i iface) {
    t := tab._type
    //...
    var x unsafe.Pointer
    if *(*uint64)(elem) == 0 {
        x = unsafe.Pointer(&zeroVal[0])
    } else {
        x = mallocgc(8, t, false)
        *(*uint64)(x) = *(*uint64)(elem)
    }
    i.tab = tab
    i.data = x
    return
}

这块代码比较简单,把 tab 赋给了 ifacetab 字段;data 部分则是在堆上申请了一块内存,然后将 elem 指向的 18 拷贝过去。这样 iface 就组装好了。

汇编行数 操作
17 i.tab 赋给 CX
18 i.data 赋给 AX
19-21 检测 i.tab 是否是 nil,如果不是的话,把 CX 移动 8 个字节,也就是把 itab_type 字段赋给了 CX,这也是接口的实体类型,最终要作为 fmt.Println 函数的参数

后面,就是调用 fmt.Println 函数及之前的参数准备工作了,不再赘述。

这样,我们就把一个 interface 的构造过程说完了。

【引申1】如何打印出接口类型的 Hash 值?

这里参考曹大神翻译的一篇文章,参考资料里会写上。具体做法如下:

type iface struct {
    tab  *itab
    data unsafe.Pointer
}
type itab struct {
    inter uintptr
    _type uintptr
    link uintptr
    hash  uint32
    _     [4]byte
    fun   [1]uintptr
}
func main() {
    var qcrao = Person(Student{age: 18})
    iface := (*iface)(unsafe.Pointer(&qcrao))
    fmt.Printf("iface.tab.hash = %#x\n", iface.tab.hash)
}

定义了一个山寨版ifaceitab,说它山寨是因为 itab 里的一些关键数据结构都不具体展开了,比如 _type,对比一下正宗的定义就可以发现,但是山寨版依然能工作,因为 _type 就是一个指针而已嘛。

main 函数里,先构造出一个接口对象 qcrao,然后强制类型转换,最后读取出 hash 值,非常妙!你也可以自己动手试一下。

运行结果:

iface.tab.hash = 0xd4209fda

值得一提的是,构造接口 qcrao 的时候,即使我把 age 写成其他值,得到的 hash 值依然不变的,这应该是可以预料的,hash 值只和他的字段、方法相关。

参考资料
【曹大神翻译的文章,非常硬核】http://xargin.com/go-and-interface/#reconstructing-an-itab-from-an-executable

接口转换的原理

通过前面提到的 iface 的源码可以看到,实际上它包含接口的类型 interfacetype 和 实体类型的类型 _type,这两者都是 iface 的字段 itab 的成员。也就是说生成一个 itab 同时需要接口的类型和实体的类型。

->itable

当判定一种类型是否满足某个接口时,Go 使用类型的方法集和接口所需要的方法集进行匹配,如果类型的方法集完全包含接口的方法集,则可认为该类型实现了该接口。

例如某类型有 m 个方法,某接口有 n 个方法,则很容易知道这种判定的时间复杂度为 O(mn),Go 会对方法集的函数按照函数名的字典序进行排序,所以实际的时间复杂度为 O(m+n)

这里我们来探索将一个接口转换给另外一个接口背后的原理,当然,能转换的原因必然是类型兼容。

直接来看一个例子:

package main
import "fmt"
type coder interface {
    code()
    run()
}
type runner interface {
    run()
}
type Gopher struct {
    language string
}
func (g Gopher) code() {
    return
}
func (g Gopher) run() {
    return
}
func main() {
    var c coder = Gopher{}
    var r runner
    r = c
    fmt.Println(c, r)
}

简单解释下上述代码:定义了两个 interface: coderrunner。定义了一个实体类型 Gopher,类型 Gopher 实现了两个方法,分别是 run()code()。main 函数里定义了一个接口变量 c,绑定了一个 Gopher 对象,之后将 c 赋值给另外一个接口变量 r 。赋值成功的原因是 c 中包含 run() 方法。这样,两个接口变量完成了转换。

执行命令:

go tool compile -S ./src/main.go

得到 main 函数的汇编命令,可以看到: r = c 这一行语句实际上是调用了 runtime.convI2I(SB),也就是 convI2I 函数,从函数名来看,就是将一个 interface 转换成另外一个 interface,看下它的源代码:

func convI2I(inter *interfacetype, i iface) (r iface) {
    tab := i.tab
    if tab == nil {
        return
    }
    if tab.inter == inter {
        r.tab = tab
        r.data = i.data
        return
    }
    r.tab = getitab(inter, tab._type, false)
    r.data = i.data
    return
}

代码比较简单,函数参数 inter 表示接口类型,i 表示绑定了实体类型的接口,r 则表示接口转换了之后的新的 iface。通过前面的分析,我们又知道, iface 是由 tabdata 两个字段组成。所以,实际上 convI2I 函数真正要做的事,找到新 interfacetabdata,就大功告成了。

我们还知道,tab 是由接口类型 interfacetype 和 实体类型 _type。所以最关键的语句是 r.tab = getitab(inter, tab._type, false)

因此,重点来看下 getitab 函数的源码,只看关键的地方:

func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
    // ……
    // 根据 inter, typ 计算出 hash 值
    h := itabhash(inter, typ)
    // look twice - once without lock, once with.
    // common case will be no lock contention.
    var m *itab
    var locked int
    for locked = 0; locked < 2; locked++ {
        if locked != 0 {
            lock(&ifaceLock)
        }
        // 遍历哈希表的一个 slot
        for m = (*itab)(atomic.Loadp(unsafe.Pointer(&hash[h]))); m != nil; m = m.link {
            // 如果在 hash 表中已经找到了 itab(inter 和 typ 指针都相同)
            if m.inter == inter && m._type == typ {
                // ……
                if locked != 0 {
                    unlock(&ifaceLock)
                }
                return m
            }
        }
    }
    // 在 hash 表中没有找到 itab,那么新生成一个 itab
    m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys))
    m.inter = inter
    m._type = typ
    // 添加到全局的 hash 表中
    additab(m, true, canfail)
    unlock(&ifaceLock)
    if m.bad {
        return nil
    }
    return m
}

简单总结一下:getitab 函数会根据 interfacetype_type 去全局的 itab 哈希表中查找,如果能找到,则直接返回;否则,会根据给定的 interfacetype_type 新生成一个 itab,并插入到 itab 哈希表,这样下一次就可以直接拿到 itab

这里查找了两次,并且第二次上锁了,这是因为如果第一次没找到,在第二次仍然没有找到相应的 itab 的情况下,需要新生成一个,并且写入哈希表,因此需要加锁。这样,其他协程在查找相同的 itab 并且也没有找到时,第二次查找时,会被挂住,之后,就会查到第一个协程写入哈希表的 itab

再来看一下 additab 函数的代码:

// 检查 _type 是否符合 interface_type 并且创建对应的 itab 结构体 将其放到 hash 表中
func additab(m *itab, locked, canfail bool) {
    inter := m.inter
    typ := m._type
    x := typ.uncommon()
    // both inter and typ have method sorted by name,
    // and interface names are unique,
    // so can iterate over both in lock step;
    // the loop is O(ni+nt) not O(ni*nt).
    // 
    // inter 和 typ 的方法都按方法名称进行了排序
    // 并且方法名都是唯一的。所以循环的次数是固定的
    // 只用循环 O(ni+nt),而非 O(ni*nt)
    ni := len(inter.mhdr)
    nt := int(x.mcount)
    xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt]
    j := 0
    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)
            // 检查方法名字是否一致
            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 {
                        // 获取函数地址,并加入到itab.fun数组中
                        ifn := typ.textOff(t.ifn)
                        *(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*sys.PtrSize)) = ifn
                    }
                    goto nextimethod
                }
            }
        }
        // ……
        m.bad = true
        break
    nextimethod:
    }
    if !locked {
        throw("invalid itab locking")
    }
    // 计算 hash 值
    h := itabhash(inter, typ)
    // 加到Hash Slot链表中
    m.link = hash[h]
    m.inhash = true
    atomicstorep(unsafe.Pointer(&hash[h]), unsafe.Pointer(m))
}

additab 会检查 itab 持有的 interfacetype_type 是否符合,就是看 _type 是否完全实现了 interfacetype 的方法,也就是看两者的方法列表重叠的部分就是 interfacetype 所持有的方法列表。注意到其中有一个双层循环,乍一看,循环次数是 ni * nt,但由于两者的函数列表都按照函数名称进行了排序,因此最终只执行了 ni + nt 次,代码里通过一个小技巧来实现:第二层循环并没有从 0 开始计数,而是从上一次遍历到的位置开始。

求 hash 值的函数比较简单:

func itabhash(inter *interfacetype, typ *_type) uint32 {
    h := inter.typ.hash
    h += 17 * typ.hash
    return h % hashSize
}

hashSize 的值是 1009。

更一般的,当把实体类型赋值给接口的时候,会调用 conv 系列函数,例如空接口调用 convT2E 系列、非空接口调用 convT2I 系列。这些函数比较相似:

  • 具体类型转空接口时,_type 字段直接复制源类型的 _type;调用 mallocgc 获得一块新内存,把值复制进去,data 再指向这块新内存。

  • 具体类型转非空接口时,入参 tab 是编译器在编译阶段预先生成好的,新接口 tab 字段直接指向入参 tab 指向的 itab;调用 mallocgc 获得一块新内存,把值复制进去,data 再指向这块新内存。

  • 而对于接口转接口,itab 调用 getitab 函数获取。只用生成一次,之后直接从 hash 表中获取。

参考资料
【接口赋值、反射】http://wudaijun.com/2018/01/go-interface-implement/
【itab】http://legendtkl.com/2017/07/01/golang-interface-implement/
【和 C++ 的对比】https://www.jianshu.com/p/b38b1719636e
【itab 原理】https://ninokop.github.io/2017/10/29/Go-%E6%96%B9%E6%B3%95%E8%B0%83%E7%94%A8%E4%B8%8E%E6%8E%A5%E5%8F%A3/
【getitab源码说明】https://www.twblogs.net/a/5c245d59bd9eee16b3db561d

类型转换和断言的区别

我们知道,Go 语言中不允许隐式类型转换,也就是说 = 两边,不允许出现类型不相同的变量。

类型转换类型断言本质都是把一个类型转换成另外一个类型。不同之处在于,类型断言是对接口变量进行的操作。

类型转换

对于类型转换而言,转换前后的两个类型要相互兼容才行。类型转换的语法为:

<结果类型> := <目标类型> ( <表达式> )

package main
import "fmt"
func main() {
    var i int = 9
    var f float64
    f = float64(i)
    fmt.Printf("%T, %v\n", f, f) //float64, 9
    f = 10.8
    a := int(f)
    fmt.Printf("%T, %v\n", a, a) //int, 10
    // s := []int(i)
}

上面的代码里,我定义了一个 int 型和 float64 型的变量,尝试在它们之前相互转换,结果是成功的:int 型和 float64 是相互兼容的。

如果我把最后一行代码的注释去掉,编译器会报告类型不兼容的错误:

cannot convert i (type int) to type []int

断言

前面说过,因为空接口 interface{} 没有定义任何函数,因此 Go 中所有类型都实现了空接口。当一个函数的形参是 interface{},那么在函数中,需要对形参进行断言,从而得到它的真实类型。

断言的语法为:

<目标类型的值>,<布尔参数> := <表达式>.( 目标类型 ) // 安全类型断言<目标类型的值> := <表达式>.( 目标类型 )  //非安全类型断言

类型转换和类型断言有些相似,不同之处,在于类型断言是对接口进行的操作。

还是来看一个简短的例子:

package main
import "fmt"
type Student struct {
    Name string
    Age int
}

func main() {
    var i interface{} = new(Student)
    s := i.(Student)
    fmt.Println(s)
}

运行一下:

panic: interface conversion: interface {} is *main.Student, not main.Student

直接 panic 了,这是因为 i*Student 类型,并非 Student 类型,断言失败。这里直接发生了 panic,线上代码可能并不适合这样做,可以采用“安全断言”的语法:

func main() {    
  var i interface{} = new(Student)   
  s, ok := i.(Student)    
  if ok {        
    fmt.Println(s)    
  }
}

这样,即使断言失败也不会 panic

断言其实还有另一种形式,就是用在利用 switch 语句判断接口的类型。每一个 case 会被顺序地考虑。当命中一个 case 时,就会执行 case 中的语句,因此 case 语句的顺序是很重要的,因为很有可能会有多个 case 匹配的情况。

代码示例如下:

package main

import "fmt"

func main() {
    //var i interface{} = new(Student)
    //var i interface{} = (*Student)(nil)
    var i interface{}
    fmt.Printf("%p %v\n", &i, i)
    judge(i)//地址值拷贝
}

func judge(v interface{}) {
    fmt.Printf("%p %v\n", &v, v)
    switch v := v.(type) {
    case nil:
        fmt.Printf("%p %v\n", &v, v)
        fmt.Printf("nil type[%T] %v\n", v, v)
    case Student:
        fmt.Printf("%p %v\n", &v, v)
        fmt.Printf("Student type[%T] %v\n", v, v)
    case *Student:
        fmt.Printf("%p %v\n", &v, v)
        fmt.Printf("*Student type[%T] %v\n", v, v)
    default:
        fmt.Printf("%p %v\n", &v, v)
        fmt.Printf("unknow\n")
    }
}

type Student struct {
    Name string
    Age  int
}

main 函数里有三行不同的声明,每次运行一行,注释另外两行,得到三组运行结果:

// --- var i interface{} = new(Student)
0xc4200701b0 [Name: ], [Age: 0]
0xc4200701d0 [Name: ], [Age: 0]
0xc420080020 [Name: ], [Age: 0]
*Student type[*main.Student] [Name: ], [Age: 0]
// --- var i interface{} = (*Student)(nil)
0xc42000e1d0 <nil>
0xc42000e1f0 <nil>
0xc42000c030 <nil>
*Student type[*main.Student] <nil>
// --- var i interface{}
0xc42000e1d0 <nil>
0xc42000e1e0 <nil>
0xc42000e1f0 <nil>
nil type[<nil>] <nil>

对于第一行语句:

var i interface{} = new(Student)

i 是一个 *Student 类型,匹配上第三个 case,从打印的三个地址来看,这三处的变量实际上都是不一样的。在 main 函数里有一个局部变量 i;调用函数时,实际上是复制了一份参数,因此函数里又有一个变量 v,它是 i 的拷贝;断言之后,又生成了一份新的拷贝。所以最终打印的三个变量的地址都不一样。

对于第二行语句:

var i interface{} = (*Student)(nil)

这里想说明的其实是 i 在这里动态类型是 (*Student), 数据为 nil,它的类型并不是 nil,它与 nil 作比较的时候,得到的结果也是 false

最后一行语句:

var i interface{}

这回 i 才是 nil 类型。

【引申1】fmt.Println 函数的参数是 interface

对于内置类型,函数内部会用穷举法,得出它的真实类型,然后转换为字符串打印。而对于自定义类型,首先确定该类型是否实现了 String() 方法,如果实现了,则直接打印输出 String() 方法的结果;否则,会通过反射来遍历对象的成员进行打印。

再来看一个简短的例子,比较简单,不要紧张:

package main
import "fmt"
type Student struct {
    Name string
    Age int
}
func main() {
    var s = Student{
        Name: "qcrao",
        Age: 18,
    }
    fmt.Println(s)
}

因为 Student 结构体没有实现 String() 方法,所以 fmt.Println 会利用反射挨个打印成员变量:

{qcrao 18}

增加一个 String() 方法的实现:

func (s Student) String() string {
    return fmt.Sprintf("[Name: %s], [Age: %d]", s.Name, s.Age)
}

打印结果:

[Name: qcrao], [Age: 18]

按照我们自定义的方法来打印了。

【引申2】针对上面的例子,如果改一下:

func (s *Student) String() string {
    return fmt.Sprintf("[Name: %s], [Age: %d]", s.Name, s.Age)
}

注意看两个函数的接受者类型不同,现在 Student 结构体只有一个接受者类型为 指针类型String() 函数,打印结果:

{qcrao 18}

为什么?

类型 T 只有接受者是 T 的方法;而类型 *T 拥有接受者是 T*T 的方法。语法上 T 能直接调 *T 的方法仅仅是 Go 的语法糖。

所以, Student 结构体定义了接受者类型是值类型的 String() 方法时,通过

fmt.Println(s)
fmt.Println(&s)

均可以按照自定义的格式来打印。

如果 Student 结构体定义了接受者类型是指针类型的 String() 方法时,只有通过

fmt.Println(&s)

才能按照自定义的格式打印。

参考资料
【类型转换和断言】https://www.cnblogs.com/zrtqsk/p/4157350.html
【断言】https://studygolang.com/articles/11419

编译器自动检测类型是否实现接口

经常看到一些开源库里会有一些类似下面这种奇怪的用法:

var _ io.Writer = (*myWriter)(nil)

这时候会有点懵,不知道作者想要干什么,实际上这就是此问题的答案。编译器会由此检查 *myWriter 类型是否实现了 io.Writer 接口。

来看一个例子:

package main
import "io"
type myWriter struct {
}
/*func (w myWriter) Write(p []byte) (n int, err error) {
    return
}*/
func main() {
    // 检查 *myWriter 类型是否实现了 io.Writer 接口
    var _ io.Writer = (*myWriter)(nil)
    // 检查 myWriter 类型是否实现了 io.Writer 接口
    var _ io.Writer = myWriter{}
}

注释掉为 myWriter 定义的 Write 函数后,运行程序:

src/main.go:14:6: cannot use (*myWriter)(nil) (type *myWriter) as type io.Writer in assignment:
    *myWriter does not implement io.Writer (missing Write method)
src/main.go:15:6: cannot use myWriter literal (type myWriter) as type io.Writer in assignment:
    myWriter does not implement io.Writer (missing Write method)

报错信息:*myWriter/myWriter 未实现 io.Writer 接口,也就是未实现 Write 方法。

解除注释后,运行程序不报错。

实际上,上述赋值语句会发生隐式地类型转换,在转换的过程中,编译器会检测等号右边的类型是否实现了等号左边接口所规定的函数。

总结一下,可通过在代码中添加类似如下的代码,用来检测类型是否实现了接口:

var _ io.Writer = (*myWriter)(nil)
var _ io.Writer = myWriter{}

Golang的汇编过程

golang的汇编过程

在程序编译的时候,汇编的目的是把汇编代码转化为机器指令,因为几乎每一条汇编指令都对应着一条机器指令,所以汇编的过程相对而言非常的简单。

汇编操作所生成的文件叫做目标文件(Object File),目标文件的结构与可执行文件是一致的,它们之间只存在着一些细微的差异。目标文件是无法被执行的,它还需要经过链接这一步操作,目标文件被链接之后才可以产生可执行文件。

Golang原生支持用户级协程,交叉编译,跨平台部署运行, 但是go在编译成机器语言交付给CPU执行的过程中,汇编也只是一个中间状态,汇编指令相对于以上的高级语言而言则显得十分拗口。在大部分强类型的语言中,基本上代码在执行前会经历几个阶段:

语法分析--->词法分析--->目标码生成

Go的汇编是怎么样的?

Go汇编器所用的指令,一部分与目标机器的指令一一对应,而另外一部分则不是。这是因为编译器套件不需要汇编器直接参与常规的编译过程。相反,编译器使用了一种半抽象的指令集,并且部分指令是在代码生成后才被选择的。

汇编器基于这种半抽象的形式工作,所以虽然你看到的是一条MOV指令,但是工具链针对这条指令实际生成可能完全不是一个移动指令,也许会是清除或者加载。也有可能精确的对应目标平台上同名的指令。
由于这种汇编并不对应某种真实的硬件架构,Go编译器会输出一种抽象可移植的汇编代码。

接着我们看一个应用示例:

package main 

//go:noinline
func add(a, b int) (int, bool) { 
  return a + b, true 
}

func main() { 
  add(5, 10) 
}

其中 //go:noinline 为编译器指令,不是注释,这里应该意为禁止内联,这部分在scan后形成ast树时也会scan到这个记录,在汇编的过程中会读取这个标记,从而控制一些汇编行为。

然后,我们将这段代码编译到汇编:

> GOOS=linux GOARCH=amd64 go tool compile -S main.go
"".add STEXT nosplit size=20 args=0x10 locals=0x0 funcid=0x0
  0x0000 00000 (main.go:4)        TEXT    "".add(SB), NOSPLIT|ABIInternal, 0-16
  0x0000 00000 (main.go:4)        FUNCDATA0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
  0x0000 00000 (main.go:4)        FUNCDATA        1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
  0x0000 00000 (main.go:5)        MOVL    "".b+12(SP), AX
  0x0004 00004 (main.go:5)        MOVL    "".a+8(SP), CX
  0x0008 00008 (main.go:5)        ADDL    CX, AX
  0x000a 00010 (main.go:5)        MOVL    AX, "".~r2+16(SP)
  0x000e 00014 (main.go:5)        MOVB1, "".~r3+20(SP)
  0x0013 00019 (main.go:5)        RET

"".main STEXT size=66 args=0x0 locals=0x18 funcid=0x0
  0x0000 00000 (main.go:8)        TEXT    "".main(SB), ABIInternal, 24-0
  0x0000 00000 (main.go:8)        MOVQ    (TLS), CX
  0x0009 00009 (main.go:8)        CMPQ    SP, 16(CX)
  0x000d 00013 (main.go:8)        PCDATA0, -2
  0x000d 00013 (main.go:8)        JLS     58
  0x000f 00015 (main.go:8)        PCDATA0, -1
  0x000f 00015 (main.go:8)        SUBQ24, SP
  0x0013 00019 (main.go:8)        MOVQ    BP, 16(SP)
  0x0018 00024 (main.go:8)        LEAQ    16(SP), BP
  0x001d 00029 (main.go:8)        FUNCDATA        0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
  0x001d 00029 (main.go:8)        FUNCDATA1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
  0x001d 00029 (main.go:9)        MOVQ    42949672965, AX
  0x0027 00039 (main.go:9)        MOVQ    AX, (SP)
  0x002b 00043 (main.go:9)        PCDATA1, 0
  0x002b 00043 (main.go:9)        CALL    "".add(SB)
  0x0030 00048 (main.go:10)       MOVQ    16(SP), BP
  0x0035 00053 (main.go:10)       ADDQ24, SP
  0x0039 00057 (main.go:10)       RET
  0x003a 00058 (main.go:10)       NOP
  0x003a 00058 (main.go:8)        PCDATA  1,-1
  0x003a 00058 (main.go:8)        PCDATA  0,-2
  0x003a 00058 (main.go:8)        CALL    runtime.morestack_noctxt(SB)
  0x003f 00063 (main.go:8)        PCDATA  0,-1
  0x003f 00063 (main.go:8)        NOP
  0x0040 00064 (main.go:8)        JMP     0

这里的add:

0x0000 00000 (main.go:4)        TEXT    "".add(SB), NOSPLIT|ABIInternal, 0-16

* 0x0000: 当前指令相对于当前函数的偏移量。

* TEXT "".add: TEXT 指令声明了 "".add 是 .text 段(程序代码在运行期会放在内存的 .text 段中)的一部分,并表明跟在这个声明后的是函数的函数体。 在链接期,"" 这个空字符会被替换为当前的包名: 也就是说,"".add 在链接到二进制文件后会变成 main.add。

* (SB): SB 是一个虚拟寄存器,保存了静态基地址(static-base) 指针,即我们程序地址空间的开始地址。 "".add(SB) 表明我们的符号位于某个固定的相对地址空间起始处的偏移位置 (最终是由链接器计算得到的)。换句话来讲,它有一个直接的绝对地址: 是一个全局的函数符号。

* NOSPLIT: 向编译器表明不应该插入 stack-split 的用来检查栈需要扩张的前导指令。 在我们 add 函数的这种情况下,编译器自己帮我们插入了这个标记: 它足够聪明地意识到,由于 add 没有任何局部变量且没有它自己的栈帧,所以一定不会超出当前的栈,因此每次调用函数时在这里执行栈检查就是完全浪费 CPU 循环了。

*0-16: 0 代表即将分配的栈帧大小;而 16 指定了调用方传入的参数大小。

* 通常帧大小后一般都跟随着一个参数大小,用-分隔。(这不是一个减法操作,只是一种特殊的语法)
帧大小24-8 意味着这个函数有24个字节的帧以及8个字节的参数,位于调用者的帧上。如果NOSPLIT没有在TEXT中指定,则必须提供参数大小。

对于Go原型的汇编函数,go vet会检查参数大小是否正确。Go是一个具备gc机制的语言,因此在C,C++里担心的那些问题在Go这都不是问题!

* Go 的调用规约要求每一个参数都通过栈来传递,这部分空间由 caller 在其栈帧(stack frame)上提供。调用其它函数之前,caller 就需要按照参数和返回变量的大小来对应地增长(返回后收缩)栈。
0x0000 00000 (main.go:4)        FUNCDATA        0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (main.go:4)        FUNCDATA1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)

FUNCDATA以及PCDATA指令包含有被gc回收所使用的信息,这些指令是被编译器加入的。

0x0000 00000 (main.go:5)        MOVL    "".b+12(SP), AX
0x0004 00004 (main.go:5)        MOVL    "".a+8(SP), CX

Go的调用要求每一个参数都通过栈来传递,这部分空间由caller在其栈帧(stack frame)上提供。调用其它过程之前,caller就需要按照参数和返回变量的大小来对应地增长(返回后收缩)栈。Go编译器不会生成任何 PUSH或POP 族的指令: 栈的增长和收缩是通过在栈指针寄存器 SP 上分别执行减法和加法指令来实现的。

SP伪寄存器是虚拟的栈指针,用于引用帧局部变量以及为函数调用准备的参数,它指向局部栈帧的顶部。
"".b+12(SP) 和 "".a+8(SP) 分别指向栈的低12字节和低8字节位置(栈是向低位地址方向增长的!)。
 0x0008 00008 (main.go:5)        ADDL    CX, AX
 0x000a 00010 (main.go:5)        MOVL    AX, "".~r2+16(SP)
 0x000e 00014 (main.go:5)        MOVB    $1, "".~r3+20(SP)

其中,第一个变量 a 的地址并不是 0(SP),而是在 8(SP),这是因为调用方通过使用 CALL 伪指令,把其返回地址保存在了 0(SP) 位置。参数是反序传入的,也就是说,第一个参数和栈顶距离最近。

ADDL 进行实际的加法操作,L 这里代表 Long,4 字节的值(int32),其将保存在 AX 和 CX 寄存器中的值进行相加,然后再保存进 AX 寄存器中。 这个结果之后被移动到 "".~r2+16(SP) 地址处,这是之前调用方专门为返回值预留的栈空间。这一次 "".~r2 同样没什么语义上的含义。

为了弄清楚Go 是如何处理多返回值,我们可以同时返回了一个 bool 常量 true。 返回这个 bool 值的方法和之前返回数值的方法是一样的,只是相对于 SP 寄存器的偏移量发生了变化。

最后:

 0x0013 00019 (main.go:5)        RET

最后的 RET 伪指令告诉 Go汇编器插入一些指令,这些指令是对应的目标平台中的调用规约所要求的,从子过程中返回时所需要的指令。 一般情况下这样的指令会使在 0(SP) 寄存器中保存的函数返回地址被 pop 出栈,并跳回到该地址。

接着我们看下main:

"".main STEXT size=66 args=0x0 locals=0x18 funcid=0x0
        0x0000 00000 (main.go:8)        TEXT    "".main(SB), ABIInternal, 24-0
        0x0000 00000 (main.go:8)        MOVQ    (TLS), CX
        0x0009 00009 (main.go:8)        CMPQ    SP, 16(CX)
        0x000d 00013 (main.go:8)        PCDATA0, -2
        0x000d 00013 (main.go:8)        JLS     58
        0x000f 00015 (main.go:8)        PCDATA0, -1
        0x000f 00015 (main.go:8)        SUBQ24, SP
        0x0013 00019 (main.go:8)        MOVQ    BP, 16(SP)
        0x0018 00024 (main.go:8)        LEAQ    16(SP), BP
        0x001d 00029 (main.go:8)        FUNCDATA        0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x001d 00029 (main.go:8)        FUNCDATA1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x001d 00029 (main.go:9)        MOVQ    42949672965, AX
        0x0027 00039 (main.go:9)        MOVQ    AX, (SP)
        0x002b 00043 (main.go:9)        PCDATA1, 0
        0x002b 00043 (main.go:9)        CALL    "".add(SB)
        0x0030 00048 (main.go:10)       MOVQ    16(SP), BP
        0x0035 00053 (main.go:10)       ADDQ24, SP
        0x0039 00057 (main.go:10)       RET
        0x003a 00058 (main.go:10)       NOP
        0x003a 00058 (main.go:8)        PCDATA  1,-1
        0x003a 00058 (main.go:8)        PCDATA  0,-2
        0x003a 00058 (main.go:8)        CALL    runtime.morestack_noctxt(SB)
        0x003f 00063 (main.go:8)        PCDATA  0,-1
        0x003f 00063 (main.go:8)        NOP
        0x0040 00064 (main.go:8)        JMP     0
"".main (被链接之后名字会变成 main.main) 是一个全局的函数符号,存储在 .text 段中,该函数的地址是相对于整个地址空间起始位置的一个固定的偏移量。
它分配了 24 字节的栈帧,且不接收参数,不返回值。
* main 作为调用者,通过对虚拟栈指针(stack-pointer)寄存器做减法,将其栈帧大小增加了24个字节(回忆一下栈是向低地址方向增长,所以这里的 SUBQ 指令是将栈帧的大小调整得更大了)。 这 24个字节中:

* 8 个字节(16(SP)-24(SP)) 用来存储当前帧指针 BP (这是一个实际存在的寄存器)的值,以支持栈的展开和方便调试
* 1+3 个字节(12(SP)-16(SP)) 是预留出的给第二个返回值 (bool) 的空间,除了类型本身的 1 个字节,在 amd64 平台上还额外需要 3 个字节来做对齐
* 4 个字节(8(SP)-12(SP)) 预留给第一个返回值 (int32)
* 4 个字节(4(SP)-8(SP)) 是预留给传给被调用函数的参数 b (int32)
* 4 个字节(0(SP)-4(SP)) 预留给传入参数 a (int32)

最后,跟着栈的增长,LEAQ 指令计算出帧指针的新地址,并将其存储到 BP 寄存器中。

 0x001d 00029 (main.go:9)        MOVQ    $42949672965, AX
 0x0027 00039 (main.go:9)        MOVQ    AX, (SP)

调用方将被调用方需要的参数作为一个 Quad word(8 字节值,对应$42949672965)推到了刚刚增长的栈的栈顶。

尽管指令里出现的 42949672965 这个值看起来像是随机的垃圾值,实际上这个值对应的就是 10 和 32 这两个 4 字节值,它们两被连接成了一个 8 字节值。

> echo 'obase=2;42949672965' | bc 
101000000000000000000000000000000101

我们使用相对于 static-base 指针的偏移量,来对 add 函数进行 CALL 调用: 这种调用实际上相当于直接跳到一个指定的地址。

注意 CALL 指令还会将函数的返回地址(8 字节值)也推到栈顶;所以每次我们在 add 函数中引用 SP 寄存器的时候还需要额外偏移 8 个字节! 例如,"".a 现在不是 0(SP) 了,而是在 8(SP) 位置。

  0x0030 00048 (main.go:10)       MOVQ    16(SP), BP
  0x0035 00053 (main.go:10)       ADDQ    $24, SP
  0x0039 00057 (main.go:10)       RET

这里的3个指令对应:

  • 将帧指针(frame-pointer)下降一个栈帧(stack-frame)的大小(就是“向下”一级).
  • 将栈收缩 24 个字节,回收之前分配的栈空间.
  • 请求Go汇编器插入子过程返回相关的指令.

Golang逃逸分析

Golang的逃逸分析

所谓逃逸分析(Escape analysis)是指由编译器决定内存分配的位置,不需要程序员指定。 函数中申请一个新的对象如果分配在栈中,则函数执行结束可自动将内存回收;如果分配在堆中,则函数执行结束可交给GC(垃圾回收)处理.

每当函数中申请新的对象,编译器会跟据该对象是否被函数外部引用来决定是否逃逸:

  1. 如果函数外部没有引用,则优先放到栈中;

  2. 如果函数外部存在引用,则必定放到堆中.

注意,对于函数外部没有引用的对象,也有可能放到堆中,比如内存过大超过栈的存储能力。

逃逸分析通常有四种情况:

  • 指针逃逸.

  • 栈空间不足逃逸.

  • 动态类型逃逸.

  • 闭包引用对象逃逸.

逃逸总结

  • 栈上分配内存比在堆中分配内存有更高的效率.
  • 栈上分配的内存不需要GC处理.
  • 堆上分配的内存使用完毕会交给GC处理.
  • 逃逸分析目的是决定内分配地址是栈还是堆.
  • 逃逸分析在编译阶段完成.

什么情况下会发生内存逃逸?

golang程序变量会携带有一组校验数据,用来证明它的整个生命周期是否在运行时完全可知。如果变量通过了这些校验,它就可以在栈上分配。否则就说它 逃逸 了,必须在堆上分配。
能引起变量逃逸到堆上的典型情况:
– 在方法内把局部变量指针返回 局部变量原本应该在栈中分配,在栈中回收。但是由于返回时被外部引用,因此其生命周期大于栈,则溢出。
– 发送指针或带有指针的值到 channel 中。 在编译时,是没有办法知道哪个 goroutine 会在 channel 上接收数据。所以编译器没法知道变量什么时候才会被释放。
– 在一个切片上存储指针或带指针的值。 一个典型的例子就是 []*string 。这会导致切片的内容逃逸。尽管其后面的数组可能是在栈上分配的,但其引用的值一定是在堆上。
– slice 的背后数组被重新分配了,因为 append 时可能会超出其容量( cap )。 slice 初始化的地方在编译时是可以知道的,它最开始会在栈上分配。如果切片背后的存储要基于运行时的数据进行扩充,就会在堆上分配。
– 在 interface 类型上调用方法。 在 interface 类型上调用方法都是动态调度的 —— 方法的真正实现只能在运行时知道。想像一个 io.Reader 类型的变量 r , 调用 r.Read(b) 会使得 r 的值和切片b 的背后存储都逃逸掉,所以会在堆上分配。

Golang的堆栈

Go的堆栈

在理解Go的堆栈分配前,我们先理解下什么是堆栈?在计算机中堆栈的概念分为:数据结构的堆栈和内存分配中堆栈。

数据结构的堆栈:

堆: 堆可以被看成是一棵树,如:堆排序。在队列中,调度程序反复提取队列中第一个作业并运行,因为实际情况中某些时间较短的任务将等待很长时间才能结束,或者某些不短小,但具有重要性的作业,同样应当具有优先权。堆即为解决此类问题设计的一种数据结构。

栈: 一种先进后出的数据结构。

在内存分配中的堆和栈:

栈(操作系统): 由操作系统自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

堆(操作系统): 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。

堆栈缓存方式

栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放。

堆则是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收),所以调用这些对象的速度要相对来得低一些。

变量是堆(heap)还是堆栈(stack)

官方给出的解释如下:

How do I know whether a variable is allocated on the heap or the stack?

From a correctness standpoint, you don't need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language.

The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function's stack frame. However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.

In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.

从上面可以了解到, 您不需要知道。Go中的每个变量都存在,只要有对它的引用即可。实现选择的存储位置与语言的语义无关。

存储位置确实会影响编写高效的程序。如果可能,Go编译器将为该函数的堆栈帧中的函数分配本地变量。

但是,如果编译器在函数返回后无法证明变量未被引用,则编译器必须在垃圾收集堆上分配变量以避免悬空指针错误。此外,如果局部变量非常大,将它存储在堆而不是堆栈上可能更有意义。

在当前的编译器中,如果变量具有其地址,则该变量是堆上分配的候选变量。但是,基础的逃逸分析可以将那些生存不超过函数返回值的变量识别出来,并且因此可以分配在栈上。

Go的编译器会决定在哪(堆or栈)分配内存,保证程序的正确性。

Go的堆栈分配

  • 每个goroutine维护着一个栈空间,默认最大为4KB.
  • 当goroutine的栈空间不足时,golang会调用runtime.morestack(汇编实现:asm_xxx.s)来进行动态扩容.
  • 连续栈是当栈空间不足的时候申请一个2倍于当前大小的新栈,并把所有数据拷贝到新栈,接下来的所有调用执行都发生在新栈上.
  • 每个function维护着各自的栈帧(stack frame),当function退出时会释放栈帧.

Go function内的栈操作

用一段简单的代码来说明Go函数调用及传参时的栈操作:

package main

func g(p int) int {
     return p+1;
}

func main() {
     c := g(4) + 1
     _ = c
}

执行go tool compile -S main.go生成汇编,并截取其中的一部分来说明一下程序调用时的栈操作.

"".g t=1 size=17 args=0x10 locals=0x0
    // 初始化函数的栈地址
    // 0-16表示函数初始地址为0,数据大小为16字节(input: 8字节,output: 8字节)
    // SB是函数寄存器
    0x0000 00000 (test_stack.go:3)  TEXT    "".g(SB), 0-16
    // 函数的gc收集提示。提示0和1是用于局部函数调用参数,需要进行回收
    0x0000 00000 (test_stack.go:3)  FUNCDATA0, gclocals·aef1f7ba6e2630c93a51843d99f5a28a(SB)
    0x0000 00000 (test_stack.go:3)  FUNCDATA    1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
    // FP(frame point)指向栈底
    // 将FP+8位置的数据(参数p)放入寄存器AX
    0x0000 00000 (test_stack.go:4)  MOVQ    "".p+8(FP), AX
    0x0005 00005 (test_stack.go:4)  MOVQ    (AX), AX
    // 寄存器值自增
    0x0008 00008 (test_stack.go:4)  INCQ    AX
    // 从寄存器中取出值,放入FP+16位置(返回值)
    0x000b 00011 (test_stack.go:4)  MOVQ    AX, "".~r1+16(FP)
    // 返回,返回后程序栈的空间会被回收
    0x0010 00016 (test_stack.go:4)  RET
    0x0000 48 8b 44 24 08 48 8b 00 48 ff c0 48 89 44 24 10  H.D.H..H..H.D.
    0x0010 c3                                               .
"".main t=1 size=32 args=0x0 locals=0x10
    0x0000 00000 (test_stack.go:7)  TEXT    "".main(SB),16-0
    0x0000 00000 (test_stack.go:7)  SUBQ    16, SP
    0x0004 00004 (test_stack.go:7)  MOVQ    BP, 8(SP)
    0x0009 00009 (test_stack.go:7)  LEAQ    8(SP), BP
    0x000e 00014 (test_stack.go:7)  FUNCDATA0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
    0x000e 00014 (test_stack.go:7)  FUNCDATA    1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
    // SP(stack point)指向栈顶
    // 把4存入SP的位置
    0x000e 00014 (test_stack.go:8)  MOVQ4, "".c(SP)
    // 这里会看到没有第9行`call g()`的调用出现,这是因为go汇编编译器会把一些短函数变成内嵌函数,减少函数调用
    0x0016 00022 (test_stack.go:10) MOVQ    8(SP), BP
    0x001b 00027 (test_stack.go:10) ADDQ    $16, SP
    0x001f 00031 (test_stack.go:10) RET

事实上,即便我定义了指针调用,以上的数据也都是在栈上拷贝的;那么Golang中的数据什么时候会被分配到堆上呢?

Golang逃逸分析

  • 在编译程序优化理论中,逃逸分析是一种确定指针动态范围的方法,用于分析在程序的哪些地方可以访问到指针。
  • Golang在编译时的逃逸分析可以减少gc的压力,不逃逸的对象分配在栈上,当函数返回时就回收了资源,不需要gc标记清除。
  • 如果你定义的对象的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行,提高效率。

还是上面的那段程序代码,我们可以执行 go build -gcflags '-m -l' test_stack.go来进行逃逸分析,输出结果如下

# command-line-arguments
./test_stack.go:3: g p does not escape
./test_stack.go:9: main &c does not escape

可以看到,对象c是没有逃逸的,还是分配在栈上。

即便在一开始定义的时候直接把c定义为指针:

package main

func g(p *int) int {
    return *p + 1
}

func main() {
    c := new(int)
    (*c) = 4
    _ = g(c)
}

逃逸分析的结果仍然不会改变:

# command-line-arguments
./test_stack.go:3: g p does not escape
./test_stack.go:8: main new(int) does not escape

那么,在什么时候指针对象才会逃逸呢?

那就是在按值传递和按址传递时候.

  • 按值传递
package main

func g(p int) int {
    ret := p + 1
    return ret
}

func main() {
    c := 4
    _ = g(c)
}

返回值ret是按值传递的,执行的是栈拷贝,不存在逃逸.

  • 按址传递
package main

func g(p *int) *int {
    ret := *p + 1
    return &ret
}

func main() {
    c := new(int)
    *c = 4
    _ = g(c)
}

返回值&ret是按址传递,传递的是指针对象,发生了逃逸,将对象存放在堆上以便外部调用.

# command-line-arguments
./test_stack.go:5:9: &ret escapes to heap
./test_stack.go:4:14: moved to heap: ret
./test_stack.go:3:17: g p does not escape
./test_stack.go:9:10: main new(int) does not escape

golang只有在function内的对象可能被外部访问时,才会把该对象分配在堆上.

  • 在g()方法中,ret对象的引用被返回到了方法外,因此会发生逃逸;而p对象只在g()内被引用,不会发生逃逸.
  • 在main()方法中,c对象虽然被g()方法引用了,但是由于引用的对象c没有在g()方法中发生逃逸,因此对象c的生命周期还是在main()中的,不会发生逃逸.
package main

type Result struct {
    Data *int
}

func g(p *int) *Result {
    var ret Result
    ret.Data = p
    return &ret
}

func main() {
    c := new(int)
    *c = 4
    _ = g(c)
}

逃逸分析结果

# command-line-arguments
./test_stack.go:10:9: &ret escapes to heap
./test_stack.go:8:6: moved to heap: ret
./test_stack.go:7:17: leaking param: p to result ~r1 level=-1
./test_stack.go:14:10: new(int) escapes to heap
  • 可以看到,ret和2.2中一样,存在外部引用,发生了逃逸.
  • 由于ret.Data是一个指针对象,p赋值给ret.Data后,也伴随p发生了逃逸.
  • main()中的对象c,由于作为参数p传入g()后发生了逃逸,因此c也发生了逃逸.
  • 当然,如果定义ret.Data为int(instead of *int)的话,对象p也是不会逃逸的(执行了拷贝).

开发建议大对象按址传递,小对象按值传递

  • 按址传递更高效,按值传递更安全(from William Kennedy).
  • 90%的bug都来自于指针调用.

初始化一个结构体,使用引用的方式来传递指针

func r() *Result{
    var ret Result
    ret.Data = ...
    ...
    return &ret
}

只有返回ret对象的引用时才会把对象分配在堆上,我们不必要在一开始的时候就显式地把ret定义为指针,因为这样会对阅读代码也会容易产生误导.

ret = &Result{}
...
return ret

参考链接

Golang runtime的调度

Golang runtime的调度

Golang作为一个为并发而产生的语言, 从Golang产生的那一刻就注定它具有高并发的特性,而 Go 语言中的并发(并行)编程是经由 goroutine 实现的,goroutine 是 Golang 最重要的特性之一,具有使用成本低、消耗资源低、能效高等特点,官方宣称原生 goroutine 并发成千上万不成问题,于是它也成为 Gopher 们经常使用的特性。

Goroutine,Go 语言基于并发(并行)编程的核心。那么 Goroutine 是什么?

通常 goroutine 会被当做 coroutine(协程)的 golang 实现,从比较粗浅的层面来看,这种认知也算是合理.

但实际上,goroutine 并非传统意义上的协程,现在主流的线程模型分三种:内核级线程模型、用户级线程模型和两级线程模型(也称混合型线程模型),传统的协程库属于用户级线程模型.

而 goroutine 和它的Go Scheduler在底层实现上其实是属于两级线程模型,通常 goroutine 会被当做 coroutine(协程)的 golang 实现,从比较粗浅的层面来看,这种认知也算是合理.

但是,goroutine 并非传统意义上的协程,目前主流的线程模型主要分为三种:

  1. 内核级线程模型.
  2. 用户级线程模型和两级线程模型(也称混合型线程模型).
  3. 传统的协程库属于用户级线程模型.

因此,有时候为了方便理解可以简单把 goroutine 类比成协程,但心里一定要有个清晰的认知goroutine并不等同于协程。

线程

计算机在是早期的单进程操作系统,这样就面临2个问题:

  1. 单一的执行流程,计算机只能一个任务一个任务处理。

  2. 进程阻塞所带来的CPU时间浪费。

随着技术的发展,后面的操作系统就具有了最早的并发能力:多进程并发,当一个进程阻塞的时候,切换到另外等待执行的进程,这样就能尽量把CPU利用起来,CPU就不浪费了.

在多进程/多线程的操作系统中,就是为了解决在单线程系统中的阻塞的问题,因为一个进程阻塞cpu可以立刻切换到其他进程中去执行,而且调度cpu的算法可以保证在运行的进程都可以被分配到cpu的运行时间片。这样从宏观来看,似乎多个进程是在同时被运行。

但新的问题就又出现了,进程拥有太多的资源,进程的创建、切换、销毁,都会占用很长的时间,CPU虽然利用起来了,但如果进程过多,CPU有很大的一部分都被用来进行进程调度了。

但是要怎么才能提高CPU的利用率呢?对于Linux操作系统来讲,cpu对进程的态度和线程的态度是一样的。

很明显,CPU调度切换的是进程和线程。尽管线程看起来很美好,但实际上多线程开发设计会变得更加复杂,要考虑很多同步竞争等问题,如锁、竞争冲突等。

随着时间的发展,工程师们发现,其实一个线程分为“内核态“线程和”用户态“线程。 即一个“用户态线程”必须要绑定一个“内核态线程”,但是CPU并不知道有“用户态线程”的存在,它只知道它运行的是一个“内核态线程”(系统的PCB进程控制块)。

这样,我们可以分类一下,内核线程依然叫“线程(thread)”,用户线程就叫“协程(co-routine)”.

线程的实现模型主要有 3 种:内核级线程模型、用户级线程模型和两级线程模型(也称混合型线程模型),它们之间最大的差异就在于用户线程与内核调度实体(KSE,Kernel Scheduling Entity)之间的对应关系上。而所谓的内核调度实体 KSE 就是指可以被操作系统内核调度器调度的对象实体。

简单来说 KSE 就是内核级线程,是操作系统内核的最小调度单元,也就是我们写代码的时候通俗理解上的线程了。

用户级线程模型

用户线程与内核线程 KSE 是多对一(N:1)的映射模型,多个用户线程的一般从属于单个进程并且多线程的调度是由用户自己的线程库来完成,线程的创建、销毁以及多线程之间的协调等操作都是由用户自己的线程库来负责而无须借助系统调用来实现。一个进程中所有创建的线程都只和同一个 KSE 在运行时动态绑定,也就是说,操作系统只知道用户进程而对其中的线程是无感知的,内核的所有调度都是基于用户进程。许多语言实现的 协程库 基本上都属于这种方式(比如 python 的 gevent)。

由于线程调度是在用户层面完成的,也就是相较于内核调度不需要让 CPU 在用户态和内核态之间切换,这种实现方式相比内核级线程可以做的很轻量级,对系统资源的消耗会小很多,因此可以创建的线程数量与上下文切换所花费的代价也会小得多。但该模型有个原罪:并不能做到真正意义上的并发,假设在某个用户进程上的某个用户线程因为一个阻塞调用(比如 I/O 阻塞)而被 CPU 给中断(抢占式调度)了,那么该进程内的所有线程都被阻塞(因为单个用户进程内的线程自调度是没有 CPU 时钟中断的,从而没有轮转调度),整个进程被挂起。即便是多 CPU 的机器,也无济于事,因为在用户级线程模型下,一个 CPU 关联运行的是整个用户进程,进程内的子线程绑定到 CPU 执行是由用户进程调度的,内部线程对 CPU 是不可见的,此时可以理解为 CPU 的调度单位是用户进程。

所以很多的协程库会把自己一些阻塞的操作重新封装为完全的非阻塞形式,然后在以前要阻塞的点上,主动让出自己,并通过某种方式通知或唤醒其他待执行的用户线程在该 KSE 上运行,从而避免了内核调度器由于 KSE 阻塞而做上下文切换,这样整个进程也不会被阻塞了。

  • 特点:

    N个协程绑定1个线程,优点就是协程在用户态线程即完成切换,不会陷入到内核态,这种切换非常的轻量快速。但也有很大的缺点,1个进程的所有协程都绑定在1个线程上.

    • 缺点
  1. 某个程序用不了硬件的多核加速能力.

  2. 一旦某协程阻塞,造成线程阻塞,本进程的其他协程都无法执行了,无并发能力.

内核级线程模型

用户线程与内核线程 KSE 是一对一(1 : 1)的映射模型,也就是每一个用户线程绑定一个实际的内核线程,而线程的调度则完全交付给操作系统内核去做,应用程序对线程的创建、终止以及同步都基于内核提供的系统调用来完成,大部分编程语言的线程库(比如 Java 的 java.lang.Thread、C++11 的 std::thread 等等)都是对操作系统的线程(内核级线程)的一层封装,创建出来的每个线程与一个独立的 KSE 静态绑定,因此其调度完全由操作系统内核调度器去做,也就是说,一个进程里创建出来的多个线程每一个都绑定一个 KSE。这种模型的优势和劣势同样明显:优势是实现简单,直接借助操作系统内核的线程以及调度器,所以 CPU 可以快速切换调度线程,于是多个线程可以同时运行,因此相较于用户级线程模型它真正做到了并行处理;但它的劣势是,由于直接借助了操作系统内核来创建、销毁和以及多个线程之间的上下文切换和调度,因此资源成本大幅上涨,且对性能影响很大。

  • 特点:

1个协程绑定1个线程,这种最容易实现。协程的调度都由CPU完成了,不存在N:1缺点。

  • 缺点

协程的创建、删除和切换的代价都由CPU完成,有点略显昂贵了。

两级线程模型

两级线程模型是博采众长之后的产物,充分吸收前两种线程模型的优点且尽量规避它们的缺点。在此模型下,用户线程与内核 KSE 是多对多(N : M)的映射模型:首先,区别于用户级线程模型,两级线程模型中的一个进程可以与多个内核线程 KSE 关联,也就是说一个进程内的多个线程可以分别绑定一个自己的 KSE,这点和内核级线程模型相似;其次,又区别于内核级线程模型,它的进程里的线程并不与 KSE 唯一绑定,而是可以多个用户线程映射到同一个 KSE,当某个 KSE 因为其绑定的线程的阻塞操作被内核调度出 CPU 时,其关联的进程中其余用户线程可以重新与其他 KSE 绑定运行。

所以,两级线程模型既不是用户级线程模型那种完全靠自己调度的也不是内核级线程模型完全靠操作系统调度的,而是中间态(自身调度与系统调度协同工作),因为这种模型的高度复杂性,操作系统内核开发者一般不会使用,所以更多时候是作为第三方库的形式出现,而 Go 语言中的 runtime 调度器就是采用的这种实现方案,实现了 Goroutine 与 KSE 之间的动态关联,不过 Go 语言的实现更加高级和优雅;该模型为何被称为两级?即用户调度器实现用户线程到 KSE 的『调度』,内核调度器实现 KSE 到 CPU 上的调度

  • 特点

G只能运行在M中,一个M必须持有一个P,M与P是1:1的关系。M会从P的本地队列弹出一个可执行状态的G来执行,如果P的本地队列为空,就会向其他的MP组合偷取一个可执行的G来执行, 即M个协程绑定1个线程,是N:1和1:1类型的结合,克服了以上2种模型的缺点,但实现起来最为复杂。

协程跟线程是有区别的,线程由CPU调度是抢占式的,协程由用户态调度是协作式的,一个协程让出CPU后,才执行下一个协程。

Go的协程Goroutine

Go为了提供更容易使用的并发方法,使用了goroutine和channel。goroutine来自协程的概念,让一组可复用的函数运行在一组线程之上,即使有协程阻塞,该线程的其他协程也可以被runtime调度,转移到其他可运行的线程上。最关键的是,开发人员是看不到这些底层的细节,这就降低了编程的难度,提供了更容易的并发。

Go中,协程被称为goroutine,它非常轻量,一个goroutine只占几KB,并且这几KB就足够goroutine运行完,这就能在有限的内存空间内支持大量goroutine,支持了更多的并发。虽然一个goroutine的栈只占几KB,但实际是可伸缩的,如果需要更多内容,runtime会自动为goroutine分配。

Goroutine特点:

  • 占用内存更小(几Kb).

  • 调度更灵活(runtime调度).

GPM模型

在Go中,线程是运行goroutine的实体,调度器的功能是把可运行的goroutine分配到工作线程上。

每一个 OS 线程都有一个固定大小的内存块(一般会是 2MB)来做栈,这个栈会用来存储当前正在被调用或挂起(指在调用其它函数时)的函数的内部变量。这个固定大小的栈同时很大又很小。因为 2MB 的栈对于一个小小的 goroutine 来说是很大的内存浪费,而对于一些复杂的任务(如深度嵌套的递归)来说又显得太小。因此,Go 语言做了它自己的线程

在 Go 语言中,每一个 goroutine 是一个独立的执行单元,相较于每个 OS 线程固定分配 2M 内存的模式,goroutine 的栈采取了动态扩容方式, 初始时仅为 2KB,随着任务执行按需增长,最大可达 1GB(64 位机器最大是 1G,32 位机器最大是 256M),且完全由 golang 自己的调度器 Go Scheduler 来调度。

此外,GC 还会周期性地将不再使用的内存回收,收缩栈空间。 因此,Go 程序可以同时并发成千上万个 goroutine 是得益于它强劲的调度器和高效的内存模型。Go 的创造者大概对 goroutine 的定位就是屠龙刀,因为他们不仅让 goroutine 作为 golang 并发编程的最核心组件(开发者的程序都是基于 goroutine 运行的)而且 golang 中的许多标准库的实现也到处能见到 goroutine 的身影,比如 net/http 这个包,甚至语言本身的组件 runtime 运行时和 GC 垃圾回收器都是运行在 goroutine 上的,作者对 goroutine 的厚望可见一斑。

任何用户线程最终肯定都是要交由 OS 线程来执行的,goroutine(称为 G)也不例外,但是 G 并不直接绑定 OS 线程运行,而是由 Goroutine Scheduler 中的 P – Logical Processor (逻辑处理器)来作为两者的传递者,P 可以看作是一个抽象的资源或者一个上下文,一个 P 绑定一个 OS 线程.

在 golang 的实现里把 OS 线程抽象成一个数据结构:M,G 实际上是由 M 通过 P 来进行调度运行的,但是在 G 的层面来看,P 提供了 G 运行所需的一切资源和环境,因此在 G 看来 P 就是运行它的 “CPU”,由 G、P、M 这三种由 Go 抽象出来的实现,最终形成了 Go 调度器的基本结构:

  • G: 表示 Goroutine,每个 Goroutine 对应一个 G 结构体,G 存储 Goroutine 的运行堆栈、状态以及任务函数,可重用。G 并非执行体,每个 G 需要绑定到 P 才能被调度执行。

  • P: Processor,表示逻辑处理器, 对 G 来说,P 相当于 CPU 核,G 只有绑定到 P(在 P 的 local runq 中)才能被调度。对 M 来说,P 提供了相关的执行环境(Context),如内存分配状态(mcache),任务队列(G)等,P 的数量决定了系统内最大可并行的 G 的数量(前提:物理 CPU 核数 >= P 的数量),P 的数量由用户设置的 GOMAXPROCS 决定,但是不论 GOMAXPROCS 设置为多大,P 的数量最大为 256。

  • M: Machine,OS 线程抽象,负责调度任务, 代表着真正执行计算的资源,在绑定有效的 P 后,进入 schedule 循环;而 schedule 循环的机制大致是从 Global 队列、P 的 Local 队列以及 wait 队列中获取 G,切换到 G 的执行栈上并执行 G 的函数,调用 goexit 做清理工作并回到 M,如此反复。M 并不保留 G 状态,这是 G 可以跨 M 调度的基础,M 的数量是不定的,由 Go Runtime 调整,为了防止创建过多 OS 线程导致系统调度不过来,目前默认最大限制为 10000 个。

在新的版本1.13.6中Go的GPM的模型的源码位于src/runtime/runtime2.go. 至于为什么M的的最大数量限制在10000,在这里可以查看

关于 P, 其实在 Go 1.0 发布的时候,它的调度器其实 G-M 模型,也就是没有 P 的,调度过程全由 G 和 M 完成,这个模型暴露出一些问题:

单一全局互斥锁(Sched.Lock)和集中状态存储的存在导致所有 goroutine 相关操作,比如:创建、重新调度等都要上锁;

  • goroutine 传递问题:M 经常在 M 之间传递可运行的 goroutine,这导致调度延迟增大以及额外的性能损耗;

  • 每个 M 做内存缓存,导致内存占用过高,数据局部性较差;

  • 由于 syscall 调用而形成的剧烈的 worker thread 阻塞和解除阻塞,导致额外的性能损耗。

这些问题实在太严重了,导致 Go1.0 虽然号称原生支持并发,却在并发性能上一直饱受诟病,于是Dmitry Vyukov在Scalable Go Scheduler Design Doc提出该模型在并发伸缩性方面的问题,并通过加入P(Processors)来改进该问题。

在重新设计和实现了 Go 调度器(在原有的 G-M 模型中引入了 P)并且实现了一个叫做 work-stealing 的调度算法:

  • 每个 P 维护一个 G 的本地队列;

  • 当一个 G 被创建出来,或者变为可执行状态时,就把他放到 P 的可执行队列中;

  • 当一个 G 在 M 里执行结束后,P 会从队列中把该 G 取出;如果此时 P 的队列为空,即没有其他 G 可以执行, M 就随机选择另外一个 P,从其可执行的 G 队列中取走一半。

该算法避免了在 goroutine 调度时使用全局锁。

GPM调度流程

Go 调度器工作时会维护两种用来保存 G 的任务队列:一种是一个 Global 任务队列,一种是每个 P 维护的 Local 任务队列。

当通过go关键字创建一个新的 goroutine 的时候,它会优先被放入 P 的本地队列。为了运行 goroutine,M 需要持有(绑定)一个 P,接着 M 会启动一个 OS 线程,循环从 P 的本地队列里取出一个 goroutine 并执行。

当然上面提到的 work-stealing调度算法:当 M 执行完了当前 P 的 Local 队列里的所有 G 后,P 也不会就这么在那干等着啥都不干,它会先尝试从 Global 队列寻找 G 来执行,如果 Global 队列为空,它会随机挑选另外一个 P,从它的队列里中拿走一半的 G 到自己的队列中执行。

// go1.13.6 src/runtime/proc.go

// 省略了GC检查等其它细节,只保留了主要流程
// g:       G结构体定义
// sched:   Global队列
// 获取一个待执行的G
// 尝试从其他P中steal,从全局队列中获取g,轮询网络。
func findrunnable() (gp *g, inheritTime bool) {
   // 获取当前的G对象
    _g_ := getg()

    // The conditions here and in handoffp must agree: if
    // findrunnable would return a G to run, handoffp must start
    // an M.

top:
    // 获取当前P对象
    _p_ := _g_.m.p.ptr()
    if sched.gcwaiting != 0 {
        gcstopm()
        goto top
    }
    if _p_.runSafePointFn != 0 {
        runSafePointFn()
    }
    if fingwait && fingwake {
        if gp := wakefing(); gp != nil {
            ready(gp, 0, true)
        }
    }
    if *cgo_yield != nil {
        asmcgocall(*cgo_yield, nil)
    }

    // 1. 尝试从P的Local队列中取得G 优先_p_.runnext 然后再从Local队列中取
    if gp, inheritTime := runqget(_p_); gp != nil {
        return gp, inheritTime
    }

    // 2. 尝试从Global队列中取得G
    if sched.runqsize != 0 {
        lock(&sched.lock)
        // globrunqget从Global队列中获取G 并转移一批G到_p_的Local队列
        gp := globrunqget(_p_, 0)
        unlock(&sched.lock)
        if gp != nil {
            return gp, false
        }
    }

    // Poll network.
    // This netpoll is only an optimization before we resort to stealing.
    // We can safely skip it if there are no waiters or a thread is blocked
    // in netpoll already. If there is any kind of logical race with that
    // blocked thread (e.g. it has already returned from netpoll, but does
    // not set lastpoll yet), this thread will do blocking netpoll below
    // anyway.

    // 3. 检查netpoll任务
    if netpollinited() && atomic.Load(&netpollWaiters) > 0 && atomic.Load64(&sched.lastpoll) != 0 {
        if list := netpoll(false); !list.empty() { // non-blocking
            gp := list.pop()
            // netpoll返回的是G链表,将其它G放回Global队列
            injectglist(&list)
            casgstatus(gp, _Gwaiting, _Grunnable)
            if trace.enabled {
                traceGoUnpark(gp, 0)
            }
            return gp, false
        }
    }

     // 4. 尝试从其它P窃取任务
    procs := uint32(gomaxprocs)
    if atomic.Load(&sched.npidle) == procs-1 {
        // Either GOMAXPROCS=1 or everybody, except for us, is idle already.
        // New work can appear from returning syscall/cgocall, network or timers.
        // Neither of that submits to local run queues, so no point in stealing.
        goto stop
    }
    // If number of spinning M's >= number of busy P's, block.
    // This is necessary to prevent excessive CPU consumption
    // when GOMAXPROCS>>1 but the program parallelism is low.
    if !_g_.m.spinning && 2*atomic.Load(&sched.nmspinning) >= procs-atomic.Load(&sched.npidle) {
        goto stop
    }
    if !_g_.m.spinning {
        _g_.m.spinning = true
        atomic.Xadd(&sched.nmspinning, 1)
    }
    for i := 0; i < 4; i++ {
         // 随机P的遍历顺序
        for enum := stealOrder.start(fastrand()); !enum.done(); enum.next() {
            if sched.gcwaiting != 0 {
                goto top
            }
            stealRunNextG := i > 2 // first look for ready queues with more than 1 g
            // runqsteal执行实际的steal工作,从目标P的Local队列转移一般的G过来
            // stealRunNextG指是否steal目标P的p.runnext G
            if gp := runqsteal(_p_, allp[enum.position()], stealRunNextG); gp != nil {
                return gp, false
            }
        }
    }

stop:

    // 我们没事做如果我们处于GC标记阶段,可以安全地扫描和三色法标记对象为黑色并进行工作,请运行空闲时间标记,而不是放弃P
    // 当没有G可被执行时,M会与P解绑,然后进入休眠(idle)状态。

    if gcBlackenEnabled != 0 && _p_.gcBgMarkWorker != 0 && gcMarkWorkAvailable(_p_) {
        _p_.gcMarkWorkerMode = gcMarkWorkerIdleMode
        gp := _p_.gcBgMarkWorker.ptr()
        casgstatus(gp, _Gwaiting, _Grunnable)
        if trace.enabled {
            traceGoUnpark(gp, 0)
        }
        return gp, false
    }

    // wasm only:
    // If a callback returned and no other goroutine is awake,
    // then pause execution until a callback was triggered.
    if beforeIdle() {
        // At least one goroutine got woken.
        goto top
    }

    // Before we drop our P, make a snapshot of the allp slice,
    // which can change underfoot once we no longer block
    // safe-points. We don't need to snapshot the contents because
    // everything up to cap(allp) is immutable.
    allpSnapshot := allp

    // return P and block
    lock(&sched.lock)
    if sched.gcwaiting != 0 || _p_.runSafePointFn != 0 {
        unlock(&sched.lock)
        goto top
    }
    if sched.runqsize != 0 {
        gp := globrunqget(_p_, 0)
        unlock(&sched.lock)
        return gp, false
    }
    if releasep() != _p_ {
        throw("findrunnable: wrong p")
    }
    pidleput(_p_)
    unlock(&sched.lock)

    // Delicate dance: thread transitions from spinning to non-spinning state,
    // potentially concurrently with submission of new goroutines. We must
    // drop nmspinning first and then check all per-P queues again (with
    // #StoreLoad memory barrier in between). If we do it the other way around,
    // another thread can submit a goroutine after we've checked all run queues
    // but before we drop nmspinning; as the result nobody will unpark a thread
    // to run the goroutine.
    // If we discover new work below, we need to restore m.spinning as a signal
    // for resetspinning to unpark a new worker thread (because there can be more
    // than one starving goroutine). However, if after discovering new work
    // we also observe no idle Ps, it is OK to just park the current thread:
    // the system is fully loaded so no spinning threads are required.
    // Also see "Worker thread parking/unparking" comment at the top of the file.
    wasSpinning := _g_.m.spinning
    if _g_.m.spinning {
        _g_.m.spinning = false
        if int32(atomic.Xadd(&sched.nmspinning, -1)) < 0 {
            throw("findrunnable: negative nmspinning")
        }
    }

    // check all runqueues once again
    for _, _p_ := range allpSnapshot {
        if !runqempty(_p_) {
            lock(&sched.lock)
            _p_ = pidleget()
            unlock(&sched.lock)
            if _p_ != nil {
                acquirep(_p_)
                if wasSpinning {
                    _g_.m.spinning = true
                    atomic.Xadd(&sched.nmspinning, 1)
                }
                goto top
            }
            break
        }
    }

    // Check for idle-priority GC work again.
    if gcBlackenEnabled != 0 && gcMarkWorkAvailable(nil) {
        lock(&sched.lock)
        _p_ = pidleget()
        if _p_ != nil && _p_.gcBgMarkWorker == 0 {
            pidleput(_p_)
            _p_ = nil
        }
        unlock(&sched.lock)
        if _p_ != nil {
            acquirep(_p_)
            if wasSpinning {
                _g_.m.spinning = true
                atomic.Xadd(&sched.nmspinning, 1)
            }
            // Go back to idle GC check.
            goto stop
        }
    }

    // poll network
    if netpollinited() && atomic.Load(&netpollWaiters) > 0 && atomic.Xchg64(&sched.lastpoll, 0) != 0 {
        if _g_.m.p != 0 {
            throw("findrunnable: netpoll with p")
        }
        if _g_.m.spinning {
            throw("findrunnable: netpoll with spinning")
        }
        list := netpoll(true) // block until new work is available
        atomic.Store64(&sched.lastpoll, uint64(nanotime()))
        if !list.empty() {
            lock(&sched.lock)
            _p_ = pidleget()
            unlock(&sched.lock)
            if _p_ != nil {
                acquirep(_p_)
                gp := list.pop()
                injectglist(&list)
                casgstatus(gp, _Gwaiting, _Grunnable)
                if trace.enabled {
                    traceGoUnpark(gp, 0)
                }
                return gp, false
            }
            injectglist(&list)
        }
    }
    stopm()
    goto top
}

GPM模型调度

如果一切正常,调度器会以上述的那种方式顺畅地运行,但总是有特殊的情况存在, 下面分析 goroutine 在两种例外情况下的行为。

Go runtime 会在下面的 goroutine 被阻塞的情况下运行另外一个 goroutine:

然而在通常情况下, Go runtime 会在下面的 goroutine 被阻塞的情况下运行另外一个 goroutine:

  • blocking syscall (for example opening a file)
  • network input
  • channel operations
  • primitives in the sync package

这里其实可以看做两个情况,即用户态阻塞/唤醒系统调用阻塞.

  • 用户态阻塞/唤醒

当 goroutine 因为 channel 操作或者 network I/O 而阻塞时(实际上 golang 已经用 netpoller 实现了 goroutine 网络 I/O 阻塞不会导致 M 被阻塞,仅阻塞 G),对应的 G 会被放置到某个 wait 队列(如 channel 的 waitq),该 G 的状态由_Gruning变为_Gwaitting,而 M 会跳过该 G 尝试获取并执行下一个 G.如果此时没有 runnable 的 G 供 M 运行,那么 M 将解绑 P,并进入 sleep 状态.

当阻塞的 G 被另一端的 G2 唤醒时(比如 channel 的可读/写通知),G 被标记为 runnable,尝试加入 G2 所在 P 的 runnext,然后再是 P 的 Local 队列和 Global 队列。

  • syscall 系统调用阻塞

当 G 被阻塞在某个系统调用上时,此时 G 会阻塞在_Gsyscall状态,M 也处于 block on syscall 状态,此时的 M 可被抢占调度:执行该 G 的 M 会与 P 解绑,而 P 则尝试与其它 idle 的 M 绑定,继续执行其它 G。如果没有其它 idle 的 M,但 P 的 Local 队列中仍然有 G 需要执行,则创建一个新的 M.

当系统调用完成后,G 会重新尝试获取一个 idle 的 P 进入它的 Local 队列恢复执行,如果没有 idle 的 P,G 会被标记为 runnable 加入到 Global 队列。

系统调用能被调度的关键有两点:

runtime/syscall 包中,将系统调用分为 SysCallRawSysCallSysCallRawSysCall的区别是 SysCall 会在系统调用前后分别调用entersyscallexitsyscall(位于src/runtime/proc.go),做一些现场保存和恢复操作,这样才能使P安全地与M解绑,并在其它M上继续执行其它G。

某些系统调用本身可以确定会长时间阻塞(比如锁),会调用 entersyscallblock 在发起系统调用前直接让P和M解绑。

这里的关键点是sysmon,它负责检查所有系统调用的执行时间,判断是否需要解绑。

sysmon是一个由runtime启动的M,也叫监控线程,它无需P也可以运行,它每20us~10ms唤醒一次,主要执行:

  1. 释放闲置超过5分钟的span物理内存;
  2. 如果超过2分钟没有垃圾回收,强制执行;
  3. 将长时间未处理的netpoll结果添加到任务队列;
  4. 向长时间运行的G任务发出抢占调度;
  5. 收回因syscall长时间阻塞的P;

sysmon 它通过retake实现对syscall和长时间运行的G进行调度:

// src/runtime/proc.go:sysmon

type sysmontick struct {
    schedtick   uint32
    schedwhen   int64
    syscalltick uint32
    syscallwhen int64
}

// forcePreemptNS is the time slice given to a G before it is
// preempted.
const forcePreemptNS = 10 * 1000 * 1000 // 10ms

func retake(now int64) uint32 {
    n := 0
    // Prevent allp slice changes. This lock will be completely
    // uncontended unless we're already stopping the world.
    lock(&allpLock)
    // We can't use a range loop over allp because we may
    // temporarily drop the allpLock. Hence, we need to re-fetch
    // allp each time around the loop.
    for i := 0; i < len(allp); i++ {
        _p_ := allp[i]
        if _p_ == nil {
            // This can happen if procresize has grown
            // allp but not yet created new Ps.
            continue
        }
        pd := &_p_.sysmontick
        s := _p_.status
        sysretake := false
        if s == _Prunning || s == _Psyscall {
            // Preempt G if it's running for too long.
            t := int64(_p_.schedtick)
            if int64(pd.schedtick) != t {
                pd.schedtick = uint32(t)
                pd.schedwhen = now
            } else if pd.schedwhen+forcePreemptNS <= now {
               // 如果当前G执行时间超过10ms,则抢占(preemptone)
               // 执行抢占

                preemptone(_p_)
                // In case of syscall, preemptone() doesn't
                // work, because there is no M wired to P.
                sysretake = true
            }
        }
        if s == _Psyscall {
            // Retake P from syscall if it's there for more than 1 sysmon tick (at least 20us).
            t := int64(_p_.syscalltick)
            if !sysretake && int64(pd.syscalltick) != t {
                pd.syscalltick = uint32(t)
                pd.syscallwhen = now
                continue
            }
            // 如果当前P Local队列没有其它G,当前有其它P处于Idle状态,并且syscall执行事件不超过10ms,则不用解绑当前P(handoffp)
            if runqempty(_p_) && atomic.Load(&sched.nmspinning)+atomic.Load(&sched.npidle) > 0 && pd.syscallwhen+10*1000*1000 > now {
                continue
            }
            // Drop allpLock so we can take sched.lock.
            unlock(&allpLock)
            // Need to decrement number of idle locked M's
            // (pretending that one more is running) before the CAS.
            // Otherwise the M from which we retake can exit the syscall,
            // increment nmidle and report deadlock.

            incidlelocked(-1)
            if atomic.Cas(&_p_.status, s, _Pidle) {
                if trace.enabled {
                    traceGoSysBlock(_p_)
                    traceProcStop(_p_)
                }
                n++
                _p_.syscalltick++
                handoffp(_p_)
            }
            incidlelocked(1)
            lock(&allpLock)
        }
    }
    unlock(&allpLock)
    return uint32(n)
}

抢占式调度

当某个goroutine执行超过10ms,sysmon会向其发起抢占调度请求,由于Go调度不像OS调度那样有时间片的概念,因此实际抢占机制要弱很多: Go中的抢占实际上是为G设置抢占标记(g.stackguard0),当G调用某函数时(更确切说,在通过newstack分配函数栈时),被编译器安插的指令会检查这个标记,并且将当前G以runtime.Goched的方式暂停,并加入到全局队列。

源代码如下:

// Called from runtime·morestack when more stack is needed.
// Allocate larger stack and relocate to new stack.
// Stack growth is multiplicative, for constant amortized cost.
//
// g->atomicstatus will be Grunning or Gscanrunning upon entry.
// If the GC is trying to stop this g then it will set preemptscan to true.
//
// ctxt is the value of the context register on morestack. newstack
// will write it to g.sched.ctxt.

func newstack() {
    thisg := getg()
    // TODO: double check all gp. shouldn't be getg().
    if thisg.m.morebuf.g.ptr().stackguard0 == stackFork {
        throw("stack growth after fork")
    }
    if thisg.m.morebuf.g.ptr() != thisg.m.curg {
        print("runtime: newstack called from g=", hex(thisg.m.morebuf.g), "\n"+"\tm=", thisg.m, " m->curg=", thisg.m.curg, " m->g0=", thisg.m.g0, " m->gsignal=", thisg.m.gsignal, "\n")
        morebuf := thisg.m.morebuf
        traceback(morebuf.pc, morebuf.sp, morebuf.lr, morebuf.g.ptr())
        throw("runtime: wrong goroutine in newstack")
    }

    gp := thisg.m.curg

    if thisg.m.curg.throwsplit {
        // Update syscallsp, syscallpc in case traceback uses them.
        morebuf := thisg.m.morebuf
        gp.syscallsp = morebuf.sp
        gp.syscallpc = morebuf.pc
        pcname, pcoff := "(unknown)", uintptr(0)
        f := findfunc(gp.sched.pc)
        if f.valid() {
            pcname = funcname(f)
            pcoff = gp.sched.pc - f.entry
        }
        print("runtime: newstack at ", pcname, "+", hex(pcoff),
            " sp=", hex(gp.sched.sp), " stack=[", hex(gp.stack.lo), ", ", hex(gp.stack.hi), "]\n",
            "\tmorebuf={pc:", hex(morebuf.pc), " sp:", hex(morebuf.sp), " lr:", hex(morebuf.lr), "}\n",
            "\tsched={pc:", hex(gp.sched.pc), " sp:", hex(gp.sched.sp), " lr:", hex(gp.sched.lr), " ctxt:", gp.sched.ctxt, "}\n")

        thisg.m.traceback = 2 // Include runtime frames
        traceback(morebuf.pc, morebuf.sp, morebuf.lr, gp)
        throw("runtime: stack split at bad time")
    }

    morebuf := thisg.m.morebuf
    thisg.m.morebuf.pc = 0
    thisg.m.morebuf.lr = 0
    thisg.m.morebuf.sp = 0
    thisg.m.morebuf.g = 0

    // NOTE: stackguard0 may change underfoot, if another thread
    // is about to try to preempt gp. Read it just once and use that same
    // value now and below.
    preempt := atomic.Loaduintptr(&gp.stackguard0) == stackPreempt

    // Be conservative about where we preempt.
    // We are interested in preempting user Go code, not runtime code.
    // If we're holding locks, mallocing, or preemption is disabled, don't
    // preempt.
    // This check is very early in newstack so that even the status change
    // from Grunning to Gwaiting and back doesn't happen in this case.
    // That status change by itself can be viewed as a small preemption,
    // because the GC might change Gwaiting to Gscanwaiting, and then
    // this goroutine has to wait for the GC to finish before continuing.
    // If the GC is in some way dependent on this goroutine (for example,
    // it needs a lock held by the goroutine), that small preemption turns
    // into a real deadlock.
    if preempt {
        if thisg.m.locks != 0 || thisg.m.mallocing != 0 || thisg.m.preemptoff != "" || thisg.m.p.ptr().status != _Prunning {
            // Let the goroutine keep running for now.
            // gp->preempt is set, so it will be preempted next time.
            gp.stackguard0 = gp.stack.lo + _StackGuard
            gogo(&gp.sched) // never return
        }
    }

    if gp.stack.lo == 0 {
        throw("missing stack in newstack")
    }
    sp := gp.sched.sp
    if sys.ArchFamily == sys.AMD64 || sys.ArchFamily == sys.I386 || sys.ArchFamily == sys.WASM {
        // The call to morestack cost a word.
        sp -= sys.PtrSize
    }
    if stackDebug >= 1 || sp < gp.stack.lo {
        print("runtime: newstack sp=", hex(sp), " stack=[", hex(gp.stack.lo), ", ", hex(gp.stack.hi), "]\n",
            "\tmorebuf={pc:", hex(morebuf.pc), " sp:", hex(morebuf.sp), " lr:", hex(morebuf.lr), "}\n",
            "\tsched={pc:", hex(gp.sched.pc), " sp:", hex(gp.sched.sp), " lr:", hex(gp.sched.lr), " ctxt:", gp.sched.ctxt, "}\n")
    }
    if sp < gp.stack.lo {
        print("runtime: gp=", gp, ", goid=", gp.goid, ", gp->status=", hex(readgstatus(gp)), "\n ")
        print("runtime: split stack overflow: ", hex(sp), " < ", hex(gp.stack.lo), "\n")
        throw("runtime: split stack overflow")
    }

    if preempt {
        if gp == thisg.m.g0 {
            throw("runtime: preempt g0")
        }
        if thisg.m.p == 0 && thisg.m.locks == 0 {
            throw("runtime: g is running but p is not")
        }
        // Synchronize with scang.
        casgstatus(gp, _Grunning, _Gwaiting)
        if gp.preemptscan {
            for !castogscanstatus(gp, _Gwaiting, _Gscanwaiting) {
                // Likely to be racing with the GC as
                // it sees a _Gwaiting and does the
                // stack scan. If so, gcworkdone will
                // be set and gcphasework will simply
                // return.
            }
            if !gp.gcscandone {
                // gcw is safe because we're on the
                // system stack.
                gcw := &gp.m.p.ptr().gcw
                scanstack(gp, gcw)
                gp.gcscandone = true
            }
            gp.preemptscan = false
            gp.preempt = false
            casfrom_Gscanstatus(gp, _Gscanwaiting, _Gwaiting)
            // This clears gcscanvalid.
            casgstatus(gp, _Gwaiting, _Grunning)
            gp.stackguard0 = gp.stack.lo + _StackGuard
            gogo(&gp.sched) // never return
        }

        // Act like goroutine called runtime.Gosched.
        casgstatus(gp, _Gwaiting, _Grunning)
        gopreempt_m(gp) // never return
    }

    // Allocate a bigger segment and move the stack.
    // 扩容至现在的2倍
    oldsize := gp.stack.hi - gp.stack.lo
    newsize := oldsize * 2
    if newsize > maxstacksize {
        print("runtime: goroutine stack exceeds ", maxstacksize, "-byte limit\n")
        throw("stack overflow")
    }

    // The goroutine must be executing in order to call newstack,
    // so it must be Grunning (or Gscanrunning).
    casgstatus(gp, _Grunning, _Gcopystack)

    // The concurrent GC will not scan the stack while we are doing the copy since
    // the gp is in a Gcopystack status.
    // 拷贝栈数据后切换到新栈
    copystack(gp, newsize, true)
    if stackDebug >= 1 {
        print("stack grow done\n")
    }

    // 恢复执行
    casgstatus(gp, _Gcopystack, _Grunning)
    gogo(&gp.sched)
}

// Copies gp's stack to a new stack of a different size.
// Caller must have changed gp status to Gcopystack.
//
// If sync is true, this is a self-triggered stack growth and, in
// particular, no other G may be writing to gp's stack (e.g., via a
// channel operation). If sync is false, copystack protects against
// concurrent channel operations.
func copystack(gp *g, newsize uintptr, sync bool) {
    if gp.syscallsp != 0 {
        throw("stack growth not allowed in system call")
    }
    old := gp.stack
    if old.lo == 0 {
        throw("nil stackbase")
    }
    used := old.hi - gp.sched.sp

    // allocate new stack
    // 从缓存或堆分配新栈
    new := stackalloc(uint32(newsize))
    if stackPoisonCopy != 0 {
        fillstack(new, 0xfd)
    }
    if stackDebug >= 1 {
        print("copystack gp=", gp, " [", hex(old.lo), " ", hex(old.hi-used), " ", hex(old.hi), "]", " -> [", hex(new.lo), " ", hex(new.hi-used), " ", hex(new.hi), "]/", newsize, "\n")
    }

    // Compute adjustment.
    var adjinfo adjustinfo
    adjinfo.old = old
    adjinfo.delta = new.hi - old.hi

    // Adjust sudogs, synchronizing with channel ops if necessary.
    ncopy := used
    if sync {
        adjustsudogs(gp, &adjinfo)
    } else {
        // sudogs can point in to the stack. During concurrent
        // shrinking, these areas may be written to. Find the
        // highest such pointer so we can handle everything
        // there and below carefully. (This shouldn't be far
        // from the bottom of the stack, so there's little
        // cost in handling everything below it carefully.)
        adjinfo.sghi = findsghi(gp, old)

        // Synchronize with channel ops and copy the part of
        // the stack they may interact with.
        ncopy -= syncadjustsudogs(gp, used, &adjinfo)
    }

    // Copy the stack (or the rest of it) to the new location
    // 拷贝栈到新的位置
    memmove(unsafe.Pointer(new.hi-ncopy), unsafe.Pointer(old.hi-ncopy), ncopy)

    // Adjust remaining structures that have pointers into stacks.
    // We have to do most of these before we traceback the new
    // stack because gentraceback uses them.
    adjustctxt(gp, &adjinfo)
    adjustdefers(gp, &adjinfo)
    adjustpanics(gp, &adjinfo)
    if adjinfo.sghi != 0 {
        adjinfo.sghi += adjinfo.delta
    }

    // Swap out old stack for new one
    // 切换到新栈
    gp.stack = new
    gp.stackguard0 = new.lo + _StackGuard // NOTE: might clobber a preempt request
    gp.sched.sp = new.hi - used
    gp.stktopsp += adjinfo.delta

    // Adjust pointers in the new stack.
    gentraceback(^uintptr(0), ^uintptr(0), 0, gp, 0, nil, 0x7fffffff, adjustframe, noescape(unsafe.Pointer(&adjinfo)), 0)

    // free old stack
    // 释放旧栈
    if stackPoisonCopy != 0 {
        fillstack(old, 0xfc)
    }
    stackfree(old)
}

go在1.3之前栈扩容采用的是分段栈(Segemented Stack),在栈空间不够的时候新申请一个栈空间用于被调用函数的执行, 执行后销毁新申请的栈空间并回到老的栈空间继续执行,当函数出现频繁调用(递归)时可能会引发hot split。

为了避免hot split, 1.3之后采用的是连续栈(Contiguous Stack),栈空间不足的时候申请一个2倍于当前大小的新栈,并把所有数据拷贝到新栈, 接下来的所有调用执行都发生在新栈上。

看完了扩容,我们来看看缩容。一些long running的goroutine可能由于某次函数调用中引发了栈的扩容, 被调用函数返回后很大部分空间都未被利用,为了解决这样的问题,需要能够对栈进行收缩,以节约内存提高利用率。

栈收缩不是在函数调用时发生的,是由垃圾回收器在垃圾回收时主动触发的。基本过程是计算当前使用的空间,小于栈空间的1/4的话, 执行栈的收缩,将栈收缩为现在的1/2,否则直接返回。

// runtime/stack.go
// Maybe shrink the stack being used by gp.
// Called at garbage collection time.
// gp must be stopped, but the world need not be.
func shrinkstack(gp *g) {
    gstatus := readgstatus(gp)
    if gp.stack.lo == 0 {
        throw("missing stack in shrinkstack")
    }
    if gstatus&_Gscan == 0 {
        throw("bad status in shrinkstack")
    }

    if debug.gcshrinkstackoff > 0 {
        return
    }
    f := findfunc(gp.startpc)
    if f.valid() && f.funcID == funcID_gcBgMarkWorker {
        // We're not allowed to shrink the gcBgMarkWorker
        // stack (see gcBgMarkWorker for explanation).
        return
    }

    // 收缩目标是一半大小
    oldsize := gp.stack.hi - gp.stack.lo
    newsize := oldsize / 2
    // Don't shrink the allocation below the minimum-sized stack
    // allocation.
    if newsize < _FixedStack {
        return
    }
    // Compute how much of the stack is currently in use and only
    // shrink the stack if gp is using less than a quarter of its
    // current stack. The currently used stack includes everything
    // down to the SP plus the stack guard space that ensures
    // there's room for nosplit functions.
    // 如果使用空间超过1/4, 则不收缩
    avail := gp.stack.hi - gp.stack.lo
    if used := gp.stack.hi - gp.sched.sp + _StackLimit; used >= avail/4 {
        return
    }

    // We can't copy the stack if we're in a syscall.
    // The syscall might have pointers into the stack.
    if gp.syscallsp != 0 {
        return
    }
    if sys.GoosWindows != 0 && gp.m != nil && gp.m.libcallsp != 0 {
        return
    }

    if stackDebug > 0 {
        print("shrinking stack ", oldsize, "->", newsize, "\n")
    }

    // 用较小的栈替换当前的栈
    copystack(gp, newsize, false)
}

这里只是对Go的调度器进行一个分析, 当然,Go 的调度中更复杂的抢占式调度、阻塞调度的更多细节,大家可以自行去找相关资料深入理解,这里只讲到 Go 调度器的基本调度过程,所以想了解更多细节的同学可以去看看 Go 调度器 G-P-M 模型的设计者 Dmitry Vyukov 写的该模型的设计文档《Go Preemptive Scheduler Design》 以及直接去看源码,G-P-M 模型的定义放在 src/runtime/runtime2.go 里面,而调度过程则放在了 src/runtime/proc.go 里。

在Go的最新1.14源码中优化了调度器,后续我们继续分析.

资料参考

Golang-GC

Golang GC

Golang的 GC全称 GarbageCollection,即垃圾回收,是一种自动内存管理的机制。

当程序向操作系统申请的内存不再需要时,垃圾回收主动将其回收并供其他代码进行内存申请时候复用,或者将其归还给操作系统,这种针对内存级别资源的自动回收过程,即为垃圾回收。而负责垃圾回收的程序组件,即为垃圾回收器。

垃圾回收其实是一个完美的 “Simplicity is Complicated” 的例子。一方面,开发者受益于GC,不用关心、也不需要对内存进行手动的申请和释放操作,GC在程序运行时自动释放残留的内存。另一方面,GC 对开发者几乎不可见,仅在程序需要进行特殊优化时,通过提供可调控的 API,对 GC 的运行时机、运行开销进行把控的时候才得以现身。

通常,垃圾回收器的执行过程会被划分为两个半独立的组件:

赋值器(Mutator):这一名称本质上是在指代用户态的代码。因为对垃圾回收器而言,用户态的代码仅仅只是在修改对象之间的引用关系,也就是在对象图(对象之间引用关系的一个有向图)上进行操作。

回收器(Collector):负责执行垃圾回收的代码。

Golang 中的defer性能提升

Golang 中的defer性能提升

在Golang 1.14中新加入了开放编码(Open-coded)defer类型,编译器在ssa过程中会把被延迟的方法直接插入到函数的尾部,避免了运行时的deferproc及deferprocStack操作。

避免了在没有运行时判断下的defer return调用。如有运行时判断的逻辑,则 defer return 也进一步优化,开放编码下的 defer return 不会进行jmpdefer的尾递归调用,而直接在一个循环里遍历执行。

在1.14中defer的实现原理,共有三种defer模式类型,编译后一个函数里只会一种defer模式。

堆上分配

在 Golang 1.13 之前的版本中,所有 defer 都是在堆上分配 (deferProc),该机制在编译时会进行两个步骤:

  1. 在 defer 语句的位置插入 runtime.deferproc,当被执行时,延迟调用会被保存为一个 _defer 记录,并将被延迟调用的入口地址及其参数复制保存,存入 Goroutine 的调用链表中。

  2. 在函数返回之前的位置插入 runtime.deferreturn,当被执行时,会将延迟调用从 Goroutine 链表中取出并执行,多个延迟调用则以 jmpdefer 尾递归调用方式连续执行。

这种机制的主要性能问题存在于每个 defer 语句产生记录时的内存分配,以及记录参数和完成调用时参数移动的系统调用开销。

栈上分配

在Golang 1.13 版本中新加入 deferprocStack 实现了在栈上分配的形式来取代 deferproc,相比后者,栈上分配在函数返回后 _defer 便得到释放,省去了内存分配时产生的性能开销,只需适当维护 _defer 的链表即可。

编译器可以去选择使用deferproc 还是 deferprocStack,通常情况下都会使用deferprocStack,性能会提升约 30%。不过在 defer 语句出现在了循环语句里,或者无法执行更高阶的编译器优化时,亦或者同一个函数中使用了过多的 defer 时,依然会使用 deferproc

栈上分配 (deferprocStack),基本跟堆上差不多,只是分配方式改为在栈上分配,压入的函数调用栈存有_defer记录,另外编译器在ssa过程中会预留defer空间。

SSA 代表 static single-assignment,是一种IR(中间表示代码),要保证每个变量只被赋值一次。这个能帮助简化编译器的优化算法。简单来说,使用ssa可以使二进制文件大小减少了30%,性能提升5%-35%等.

// buildssa builds an SSA function for fn.
// worker indicates which of the backend workers is doing the processing.
func buildssa(fn *Node, worker int) *ssa.Func {
    name := fn.funcname()
    printssa := name == ssaDump
    var astBuf *bytes.Buffer
    if printssa {
        astBuf = &bytes.Buffer{}
        fdumplist(astBuf, "buildssa-enter", fn.Func.Enter)
        fdumplist(astBuf, "buildssa-body", fn.Nbody)
        fdumplist(astBuf, "buildssa-exit", fn.Func.Exit)
        if ssaDumpStdout {
            fmt.Println("generating SSA for", name)
            fmt.Print(astBuf.String())
        }
    }

    var s state
    s.pushLine(fn.Pos)
    defer s.popLine()

    s.hasdefer = fn.Func.HasDefer()
    if fn.Func.Pragma&CgoUnsafeArgs != 0 {
        s.cgoUnsafeArgs = true
    }

    fe := ssafn{
        curfn: fn,
        log:   printssa && ssaDumpStdout,
    }
    s.curfn = fn

    s.f = ssa.NewFunc(&fe)
    s.config = ssaConfig
    s.f.Type = fn.Type
    s.f.Config = ssaConfig
    s.f.Cache = &ssaCaches[worker]
    s.f.Cache.Reset()
    s.f.DebugTest = s.f.DebugHashMatch("GOSSAHASH", name)
    s.f.Name = name
    s.f.PrintOrHtmlSSA = printssa
    if fn.Func.Pragma&Nosplit != 0 {
        s.f.NoSplit = true
    }
    s.panics = map[funcLine]*ssa.Block{}
    s.softFloat = s.config.SoftFloat

    if printssa {
        s.f.HTMLWriter = ssa.NewHTMLWriter(ssaDumpFile, s.f.Frontend(), name, ssaDumpCFG)
        // TODO: generate and print a mapping from nodes to values and blocks
        dumpSourcesColumn(s.f.HTMLWriter, fn)
        s.f.HTMLWriter.WriteAST("AST", astBuf)
    }

    // Allocate starting block
    s.f.Entry = s.f.NewBlock(ssa.BlockPlain)

    // Allocate starting values
    s.labels = map[string]*ssaLabel{}
    s.labeledNodes = map[*Node]*ssaLabel{}
    s.fwdVars = map[*Node]*ssa.Value{}
    s.startmem = s.entryNewValue0(ssa.OpInitMem, types.TypeMem)

    s.hasOpenDefers = Debug['N'] == 0 && s.hasdefer && !s.curfn.Func.OpenCodedDeferDisallowed()
    switch {
    case s.hasOpenDefers && (Ctxt.Flag_shared || Ctxt.Flag_dynlink) && thearch.LinkArch.Name == "386":
        // Don't support open-coded defers for 386 ONLY when using shared
        // libraries, because there is extra code (added by rewriteToUseGot())
        // preceding the deferreturn/ret code that is generated by gencallret()
        // that we don't track correctly.
        s.hasOpenDefers = false
    }
    if s.hasOpenDefers && s.curfn.Func.Exit.Len() > 0 {
        // Skip doing open defers if there is any extra exit code (likely
        // copying heap-allocated return values or race detection), since
        // we will not generate that code in the case of the extra
        // deferreturn/ret segment.
        s.hasOpenDefers = false
    }
    if s.hasOpenDefers &&
        s.curfn.Func.numReturns*s.curfn.Func.numDefers > 15 {
        // Since we are generating defer calls at every exit for
        // open-coded defers, skip doing open-coded defers if there are
        // too many returns (especially if there are multiple defers).
        // Open-coded defers are most important for improving performance
        // for smaller functions (which don't have many returns).
        s.hasOpenDefers = false
    }

    s.sp = s.entryNewValue0(ssa.OpSP, types.Types[TUINTPTR]) // TODO: use generic pointer type (unsafe.Pointer?) instead
    s.sb = s.entryNewValue0(ssa.OpSB, types.Types[TUINTPTR])

    s.startBlock(s.f.Entry)
    s.vars[&memVar] = s.startmem
    if s.hasOpenDefers {
        // Create the deferBits variable and stack slot.  deferBits is a
        // bitmask showing which of the open-coded defers in this function
        // have been activated.
        deferBitsTemp := tempAt(src.NoXPos, s.curfn, types.Types[TUINT8])
        s.deferBitsTemp = deferBitsTemp
        // For this value, AuxInt is initialized to zero by default
        startDeferBits := s.entryNewValue0(ssa.OpConst8, types.Types[TUINT8])
        s.vars[&deferBitsVar] = startDeferBits
        s.deferBitsAddr = s.addr(deferBitsTemp, false)
        s.store(types.Types[TUINT8], s.deferBitsAddr, startDeferBits)
        // Make sure that the deferBits stack slot is kept alive (for use
        // by panics) and stores to deferBits are not eliminated, even if
        // all checking code on deferBits in the function exit can be
        // eliminated, because the defer statements were all
        // unconditional.
        s.vars[&memVar] = s.newValue1Apos(ssa.OpVarLive, types.TypeMem, deferBitsTemp, s.mem(), false)
    }

    // Generate addresses of local declarations
    s.decladdrs = map[*Node]*ssa.Value{}
    for _, n := range fn.Func.Dcl {
        switch n.Class() {
        case PPARAM, PPARAMOUT:
            s.decladdrs[n] = s.entryNewValue2A(ssa.OpLocalAddr, types.NewPtr(n.Type), n, s.sp, s.startmem)
            if n.Class() == PPARAMOUT && s.canSSA(n) {
                // Save ssa-able PPARAMOUT variables so we can
                // store them back to the stack at the end of
                // the function.
                s.returns = append(s.returns, n)
            }
        case PAUTO:
            // processed at each use, to prevent Addr coming
            // before the decl.
        case PAUTOHEAP:
            // moved to heap - already handled by frontend
        case PFUNC:
            // local function - already handled by frontend
        default:
            s.Fatalf("local variable with class %v unimplemented", n.Class())
        }
    }

    // Populate SSAable arguments.
    for _, n := range fn.Func.Dcl {
        if n.Class() == PPARAM && s.canSSA(n) {
            v := s.newValue0A(ssa.OpArg, n.Type, n)
            s.vars[n] = v
            s.addNamedValue(n, v) // This helps with debugging information, not needed for compilation itself.
        }
    }

    // Convert the AST-based IR to the SSA-based IR
    s.stmtList(fn.Func.Enter)
    s.stmtList(fn.Nbody)

    // fallthrough to exit
    if s.curBlock != nil {
        s.pushLine(fn.Func.Endlineno)
        s.exit()
        s.popLine()
    }

    for _, b := range s.f.Blocks {
        if b.Pos != src.NoXPos {
            s.updateUnsetPredPos(b)
        }
    }

    s.insertPhis()

    // Main call to ssa package to compile function
    ssa.Compile(s.f)

    if s.hasOpenDefers {
        s.emitOpenDeferInfo()
    }

    return s.f
}

如果在构建ssa时如发现gcflags有N禁止优化的参数 或者 return数量 * defer数量超过了15不适用open-coded模式。

此外逃逸分析会判断循序的层数,如果有轮询,那么强制使用栈分配模式。

// augmentParamHole augments parameter holes as necessary for use in
// go/defer statements.
func (e *Escape) augmentParamHole(k EscHole, call, where *Node) EscHole {
    k = k.note(call, "call parameter")
    if where == nil {
        return k
    }

    // Top level defers arguments don't escape to heap, but they
    // do need to last until end of function. Tee with a
    // non-transient location to avoid arguments from being
    // transiently allocated.
    if where.Op == ODEFER && e.loopDepth == 1 {
        // force stack allocation of defer record, unless open-coded
        // defers are used (see ssa.go)
        where.Esc = EscNever
        return e.later(k)
    }

    return e.heapHole().note(where, "call parameter")
}

开放编码

Golang 1.14 版本继续加入了开发编码(open coded),该机制会将延迟调用直接插入函数返回之前,省去了运行时的 deferprocdeferprocStack 操作,在运行时的 deferreturn 也不会进行尾递归调用,而是直接在一个循环中遍历所有延迟函数执行。

这种机制使得 defer 的开销几乎可以忽略,唯一的运行时成本就是存储参与延迟调用的相关信息,不过使用这个机制还需要三个条件:

  1. 没有禁用编译器优化,即没有设置 -gcflags "-N".
  2. 函数内 defer 的数量不超过 8 个,且返回语句与延迟语句个数的乘积不超过 15.
  3. defer 不是在循环语句中。

此外该机制还引入了一种元素 —— 延迟比特(defer bit),用于运行时记录每个 defer 是否被执行(尤其是在条件判断分支中的 defer),从而便于判断最后的延迟调用该执行哪些函数。

延迟比特的原理:

同一个函数内每出现一个 defer 都会为其分配 1个比特,如果被执行到则设为 1,否则设为 0,当到达函数返回之前需要判断延迟调用时,则用掩码判断每个位置的比特,若为 1 则调用延迟函数,否则跳过。

为了轻量,官方将延迟比特限制为 1 个字节,即 8 个比特,这就是为什么不能超过 8 个 defer 的原因,若超过依然会选择堆栈分配,但显然大部分情况不会超过 8 个。

// The constant is known to runtime.
const tmpstringbufsize = 32
const zeroValSize = 1024 // must match value of runtime/map.go:maxZero

func walk(fn *Node) {
    Curfn = fn

    if Debug['W'] != 0 {
        s := fmt.Sprintf("\nbefore walk %v", Curfn.Func.Nname.Sym)
        dumplist(s, Curfn.Nbody)
    }

    lno := lineno

    // Final typecheck for any unused variables.
    for i, ln := range fn.Func.Dcl {
        if ln.Op == ONAME && (ln.Class() == PAUTO || ln.Class() == PAUTOHEAP) {
            ln = typecheck(ln, ctxExpr|ctxAssign)
            fn.Func.Dcl[i] = ln
        }
    }

    // Propagate the used flag for typeswitch variables up to the NONAME in its definition.
    for _, ln := range fn.Func.Dcl {
        if ln.Op == ONAME && (ln.Class() == PAUTO || ln.Class() == PAUTOHEAP) && ln.Name.Defn != nil && ln.Name.Defn.Op == OTYPESW && ln.Name.Used() {
            ln.Name.Defn.Left.Name.SetUsed(true)
        }
    }

    for _, ln := range fn.Func.Dcl {
        if ln.Op != ONAME || (ln.Class() != PAUTO && ln.Class() != PAUTOHEAP) || ln.Sym.Name[0] == '&' || ln.Name.Used() {
            continue
        }
        if defn := ln.Name.Defn; defn != nil && defn.Op == OTYPESW {
            if defn.Left.Name.Used() {
                continue
            }
            yyerrorl(defn.Left.Pos, "%v declared but not used", ln.Sym)
            defn.Left.Name.SetUsed(true) // suppress repeats
        } else {
            yyerrorl(ln.Pos, "%v declared but not used", ln.Sym)
        }
    }

    lineno = lno
    if nerrors != 0 {
        return
    }
    walkstmtlist(Curfn.Nbody.Slice())
    if Debug['W'] != 0 {
        s := fmt.Sprintf("after walk %v", Curfn.Func.Nname.Sym)
        dumplist(s, Curfn.Nbody)
    }

    zeroResults()
    heapmoves()
    if Debug['W'] != 0 && Curfn.Func.Enter.Len() > 0 {
        s := fmt.Sprintf("enter %v", Curfn.Func.Nname.Sym)
        dumplist(s, Curfn.Func.Enter)
    }
}

在使用open code的模式的时候,默认open coded最多支持8个defer,超过则取消。

const maxOpenDefers = 8

func walkstmt(n *Node) *Node {
    ...
    switch n.Op {
    case ODEFER:
        Curfn.Func.SetHasDefer(true)
        Curfn.Func.numDefers++
        if Curfn.Func.numDefers > maxOpenDefers {
            Curfn.Func.SetOpenCodedDeferDisallowed(true)
        }

        if n.Esc != EscNever {
            Curfn.Func.SetOpenCodedDeferDisallowed(true)
        }
    ...
}

因此 open coded的使用条件是,最多8个defer,而且 return * defer < 15,无循环,gcflags无 “N” 并且取消优化。