Go语言 context包源码学习

你必须非常努力,才能看起来毫不费力!
微信搜索公众号[ 漫漫Coding路 ],一起From Zero To Hero !

日常 Go 开发中,Context 包是用的最多的一个了,几乎所有函数的第一个参数都是 ctx,那么我们为什么要传递 Context 呢,Context 又有哪些用法,底层实现是如何呢?相信你也一定会有探索的欲望,那么就跟着本篇文章,一起来学习吧!

开发中肯定会调用别的函数,比如 A 调用 B,在调用过程中经常会设置超时时间,比如超过2s 就不等待 B 的结果了,直接返回,那么我们需要怎么做呢?

// 睡眠5s,模拟长时间操作
func FuncB() (interface{}, error) {
    time.Sleep(5 * time.Second)
    return struct{}{}, nil
}

func FuncA() (interface{}, error) {

    var res interface{}
    var err error
    ch := make(chan interface{})

  // 调用FuncB(),将结果保存至 channel 中
    go func() {
        res, err = FuncB()
        ch

上面我们的实现,可以实现超过等待时间后,A 不等待 B,但是 B 并没有感受到取消信号,如果 B 是个计算密度型的函数,我们也希望B 感知到取消信号,及时取消计算并返回,减少资源浪费。

另一种情况,如果存在多层调用,比如A 调用 B、C,B 调用 D、E,C调用 E、F,在超过 A 的超时时间后,我们希望取消信号能够一层层的传递下去,后续所有被调用到的函数都能感知到,及时返回。

在多层调用的时候,A->B->C->D,有些数据需要固定传输,比如 LogID,通过打印相同的 LogID,我们就能够追溯某一次调用,方便问题的排查。如果每次都需要传参的话,未免太麻烦了,我们可以使用 Context 来保存。通过设置一个固定的 Key,打印日志时从中取出 value 作为 LogID。

const LogKey = "LogKey"

// 模拟一个日志打印,每次从 Context 中取出 LogKey 对应的 Value 作为LogID
type Logger struct{}
func (logger *Logger) info(ctx context.Context, msg string) {
    logId, ok := ctx.Value(LogKey).(string)
    if !ok {
        logId = uuid.New().String()
    }
    fmt.Println(logId + " " + msg)
}
var logger Logger

// 日志打印 并 调用 FuncB
func FuncA(ctx context.Context) {
    logger.info(ctx, "FuncA")
    FuncB(ctx)
}

func FuncB(ctx context.Context) {
    logger.info(ctx, "FuncB")
}

// 获取初始化的,带有 LogID 的 Context,一般在程序入口做
func getLogCtx(ctx context.Context) context.Context {
    logId, ok := ctx.Value(LogKey).(string)
    if ok {
        return ctx
    }
    logId = uuid.NewString()
    return context.WithValue(ctx, LogKey, logId)
}

func main() {
    ctx = getLogCtx(context.Background())
    FuncA(ctx)
}

这利用到了本篇文章讲到的 valueCtx,继续往下看,一起来学习 valueCtx 是怎么实现的吧!

Context 接口

type Context interface {

    Deadline() (deadline time.Time, ok bool)

    Done()

Context 接口比较简单,定义了四个方法:

  • Deadline() 方法返回两个值,deadline 表示 Context 将会在什么时间点取消,ok 表示是否设置了deadline。当 ok=false 时,表示没有设置deadline,那么此时 deadline 将会是个零值。多次调用这个方法返回同样的结果。
  • Done() 返回一个只读的 channel,类型为 chan struct{},如果当前的 Context 不支持取消,Done 返回 nil。我们知道,如果一个 channel 中没有数据,读取数据会阻塞;而如果channel被关闭,则可以读取到数据,因此可以监听 Done 返回的 channel,来获取 Context 取消的信号。
  • Err() 返回 Done 返回的 channel 被关闭的原因。当 channel 未被关闭时,Err() 返回 nil;channel 被关闭时则返回相应的值,比如 Canceled 、DeadlineExceeded。Err() 返回一个非 nil 值之后,后面再次调用会返回相同的值。
  • Value() 返回 Context 保存的键值对中,key 对应的 value,如果 key 不存在则返回 nil。

Done() 是一个比较常用的方法,下面是一个比较经典的流式处理任务的示例:监听 ctx.Done() 是否被关闭来判断任务是否需要取消,需要取消则返回相应的原因;没有取消则将计算的结果写入到 out channel中。

 func Stream(ctx context.Context, out chan

Value() 也是一个比较常用的方法,用于在上下文中传递一些数据。使用 context.WithValue() 方法存入 key 和 value,通过 Value() 方法则可以根据 key 拿到 value。

func main() {
    ctx := context.Background()
    c := context.WithValue(ctx, "key", "value")
    v, ok := c.Value("key").(string)
    fmt.Println(v, ok)
}

emptyCtx

Context 接口并不需要我们自己去手动实现,一般我们都是直接使用 context 包中提供的 Background() 方法和 TODO() 方法,来获取最基础的 Context。

var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)

func Background() Context {
    return background
}

func TODO() Context {
    return todo
}

Background() 方法一般用在 main 函数,或者程序的初始化方法中;在我们不知道使用哪个 Context,或者上文没有传递 Context时,可以使用 TODO()。

Background() 和 TODO() 都是基于 emptyCtx 生成的,从名字可以看出来,emptyCtx 是一个空的Context,没有 deadline、不能被取消、没有键值对。

type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
    return
}

func (*emptyCtx) Done()

除了上面两个最基本的 Context 外,context 包中提供了功能更加丰富的 Context,包括 valueCtx、cancelCtx、timerCtx,下面我们就挨个来看下。

valueCtx

使用示例

我们一般使用 context.WithValue() 方法向 Context 存入键值对,然后通过 Value() 方法根据 key 得到 value,此种功能的实现就依赖 valueCtx。

func main() {
    ctx := context.Background()
    c := context.WithValue(ctx, "myKey", "myValue")

    v1 := c.Value("myKey")
    fmt.Println(v1.(string))

    v2 := c.Value("hello")
    fmt.Println(v2) //  nil
}

valueCtx 结构体中嵌套了 Context,使用 key 、value 来保存键值对:

type valueCtx struct {
    Context
    key, val interface{}
}

context包 对外暴露了 WithValue 方法,基于一个 parent context 来创建一个 valueCtx。从下面的源码中可以看出,key 必须是可比较的!

func WithValue(parent Context, key, val interface{}) Context {
    if parent == nil {
        panic("cannot create context from nil parent")
    }
    if key == nil {
        panic("nil key")
    }
    if !reflectlite.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}

*valueCtx 实现了 Value(),可以根据 key 得到 value。这是一个向上递归寻找的过程,如果 key 不在当前 valueCtx 中,会继续向上找 parent Context,直到找到最顶层的 Context,一般最顶层的是 emptyCtx,而 emtpyCtx.Value() 返回 nil。

func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
    return c.Context.Value(key)
}

cancelCtx

cancelCtx 是一个用于取消任务的 Context,任务通过监听 Context 是否被取消,来决定是否继续处理任务还是直接返回。

如下示例中,我们在 main 函数定义了一个 cancelCtx,并在 2s 后调用 cancel() 取消 Context,即我们希望 doSomething() 在 2s 内完成任务,否则就可以直接返回,不需要再继续计算浪费资源了。

doSomething() 方法内部,我们使用 select 监听任务是否完成,以及 Context 是否已经取消,哪个先到就执行哪个分支。方法模拟了一个 5s 的任务,main 函数等待时间是2s,因此没有完成任务;如果main函数等待时间改为10s,则任务完成并会返回结果。

这只是一层调用,真实情况下可能会有多级调用,比如 doSomething 可能又会调用其他任务,一旦 parent Context 取消,后续的所有任务都应该取消。

func doSomething(ctx context.Context) (interface{}, error) {
    res := make(chan interface{})
    go func() {
        fmt.Println("do something")
        time.Sleep(time.Second * 5)
        res

接下来就让我们来研究下,cancelCtx 是如何实现取消的吧

  • canceler 接口包含 cancel() 和 Done() 方法,cancelCtx 和 timerCtx 均实现了这个接口。
  • closedchan 是一个被关闭的channel,可以用于后面 Done() 返回
  • canceled 是一个 err,用于 Context 被取消的原因
type canceler interface {
    cancel(removeFromParent bool, err error)
    Done()

CancelFunc 是一个函数类型定义,是一个取消函数,有如下规范:

  • CancelFunc 告诉一个任务停止工作
  • CancelFunc 不会等待任务结束
  • CancelFunc 支持并发调用
  • 第一次调用后,后续的调用不会产生任何效果
type CancelFunc func()

&cancelCtxKey 是一个固定的key,用来返回 cancelCtx 自身

var cancelCtxKey int

cancelCtx 是可以被取消的,它嵌套了 Context 接口,实现了 canceler 接口。cancelCtx 使用 children 字段保存同样实现 canceler 接口的子节点,当 cancelCtx 被取消时,所有的子节点也会取消。

type cancelCtx struct {
    Context

    mu       sync.Mutex            // 保护如下字段,保证线程安全
    done     atomic.Value          // 保存 channel,懒加载,调用 cancel 方法时会关闭这个 channel
    children map[canceler]struct{} // 保存子节点,第一次调用 cancel 方法时会置为 nil
    err      error                 // 保存为什么被取消,默认为nil,第一次调用 cancel 会赋值
}

cancelCtx 的 Value() 方法 和 valueCtx 的 Value() 方法类似,只不过加了个固定的key: &cancelCtxKey。当key 为 &cancelCtxKey 时返回自身

func (c *cancelCtx) Value(key interface{}) interface{} {
    if key == &cancelCtxKey {
        return c
    }
    return c.Context.Value(key)
}

*cancelCtx 的 done 字段是懒加载的,只有在调用 Done() 方法 或者 cancel() 时才会赋值。

func (c *cancelCtx) Done()

Err 方法返回 cancelCtx 的 err 字段

func (c *cancelCtx) Err() error {
   c.mu.Lock()
   err := c.err
   c.mu.Unlock()
   return err
}

那么我们如何新建一个 cancelCtx呢?context 包提供了 WithCancel() 方法,让我们基于一个 Context 来创建一个 cancelCtx。WithCancel() 方法返回两个字段,一个是基于传入的 Context 生成的 cancelCtx,另一个是 CancelFunc。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    if parent == nil {
        panic("cannot create context from nil parent")
    }
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}

WithCancel 调用了两个外部方法:newCancelCtx 、propagateCancel。newCancelCtx 比较简单,根据传入的 context,返回了一个 cancelCtx 结构体。

func newCancelCtx(parent Context) cancelCtx {
    return cancelCtx{Context: parent}
}

propagateCancel 从名字可以看出,就是将 cancel 传播。如果父Context支持取消,那么我们需要建立一个通知机制,这样父节点取消的时候,通知子节点也取消,层层传播。

在 propagateCancel 中,如果 父Context 是 cancelCtx 类型且未取消,会将 子Context 挂在它下面,形成一个树结构;其余情况都不会挂载。

func propagateCancel(parent Context, child canceler) {

  // 如果 parent 不支持取消,那么就不支持取消传播,直接返回
    done := parent.Done()
    if done == nil {
        return
    }

  // 到这里说明 done 不为 nil,parent 支持取消

    select {
    case

cancel 方法就是来取消 cancelCtx,主要的工作是:关闭c.done 中的channel,给 err 赋值,然后级联取消所有 子Context。如果 removeFromParent 为 true,会从父节点中删除以该节点为树顶的树。

cancel() 方法只负责自己管辖的范围,即自己以及自己的子节点,然后根据配置判断是否需要从父节点中移除自己为顶点的树。如果子节点还有子节点,那么由子节点负责处理,不用自己负责了。

propagateCancel() 中有三处调用了 cancel() 方法,传入的 removeFromParent 都为 false,是因为当时根本没有挂载,不需要移除。而 WithCancel 返回的 CancelFunc ,传入的 removeFromParent 为 true,是因为调用 propagateCancel 有可能产生挂载,当产生挂载时,调用 cancel() 就需要移除了。

func (c *cancelCtx) cancel(removeFromParent bool, err error) {

  // err 是指取消的原因,必传,cancelCtx 中是 errors.New("context canceled")
    if err == nil {
        panic("context: internal error: missing cancel error")
    }

  // 涉及到保护字段值的修改,都需要加锁
    c.mu.Lock()

  // 如果该Context已经取消过了,直接返回。多次调用cancel,不会产生额外效果
    if c.err != nil {
        c.mu.Unlock()
        return
    }

  // 给 err 赋值,这里 err 一定不为 nil
    c.err = err

  // close channel
    d, _ := c.done.Load().(chan struct{})
  // 因为c.done 是懒加载,有可能存在 nil 的情况
  // 如果 c.done 中没有值,直接赋值 closedchan;否则直接 close
    if d == nil {
        c.done.Store(closedchan)
    } else {
        close(d)
    }

  // 遍历当前 cancelCtx 所有的子Context,让子节点也 cancel
  // 因为当前的Context 会主动把子Context移除,子Context 不用主动从parent中脱离
  // 因此 child.cancel 传入的 removeFromParent 为false
    for child := range c.children {
        child.cancel(false, err)
    }
  // 将 children 置空,相当于移除自己的所有子Context
    c.children = nil
    c.mu.Unlock()

  // 如果当前 cancelCtx 需要从上层的 cancelCtx移除,调用removeChild方法
  // c.Context 就是自己的父Context
    if removeFromParent {
        removeChild(c.Context, c)
    }
}

从propagateCancel方法中可以看到,只有parent 属于 cancelCtx 类型 ,才会将自己挂载。因此 removeChild 会再次判断 parent 是否为 cancelCtx,和之前的逻辑保持一致。找到的话,再将自己移除,需要注意的是,移除会把自己及其自己下面的所有子节点都移除。

如果上一步 propagateCancel 方法将自己挂载到了 A 上,但是在调用 cancel() 时,A 已经取消过了,此时 parentCancelCtx() 会返回 false。不过这没有关系,A 取消时已经将挂载的子节点移除了,当前的子节点不用将自己从 A 中移除了。

func removeChild(parent Context, child canceler) {
  // parent 是否为未取消的 cancelCtx
    p, ok := parentCancelCtx(parent)
    if !ok {
        return
    }
  // 获取 parent cancelCtx 的锁,修改保护字段 children
    p.mu.Lock()
  // 将自己从 parent cancelCtx 的 children 中删除
    if p.children != nil {
        delete(p.children, child)
    }
    p.mu.Unlock()
}

parentCancelCtx 判断 parent 是否为 未取消的 cancelCtx。取消与否容易判断,难判断的是 parent 是否为 cancelCtx,因为有可能其他结构体内嵌了 cancelCtx,比如 timerCtx,会通过比对 channel 来确定。

func parentCancelCtx(parent Context) (*cancelCtx, bool) {

  // 如果 parent context 的 done 为 nil, 说明不支持 cancel,那么就不可能是 cancelCtx
    // 如果 parent context 的 done 为 closedchan, 说明 parent context 已经 cancel 了
    done := parent.Done()
    if done == closedchan || done == nil {
        return nil, false
    }

  // 到这里说明支持取消,且没有被取消

    // 如果 parent context 属于原生的 *cancelCtx 或衍生类型,需要继续进行后续判断
    // 如果 parent context 无法转换到 *cancelCtx,则认为非 cancelCtx,返回 nil,fasle
    p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
    if !ok {
        return nil, false
    }

  // 经过上面的判断后,说明 parent context 可以被转换为 *cancelCtx,这时存在多种情况:
    //   - parent context 就是 *cancelCtx
    //   - parent context 是标准库中的 timerCtx
    //   - parent context 是个自己自定义包装的 cancelCtx
    //
    // 针对这 3 种情况需要进行判断,判断方法就是:
    //   判断 parent context 通过 Done() 方法获取的 done channel 与 Value 查找到的 context 的 done channel 是否一致
    //
    // 一致情况说明 parent context 为 cancelCtx 或 timerCtx 或 自定义的 cancelCtx 且未重写 Done(),
    // 这种情况下可以认为拿到了底层的 *cancelCtx
    //
    // 不一致情况说明 parent context 是一个自定义的 cancelCtx 且重写了 Done() 方法,并且并未返回标准 *cancelCtx 的
    // 的 done channel,这种情况需要单独处理,故返回 nil, false
    pdone, _ := p.done.Load().(chan struct{})
    if pdone != done {
        return nil, false
    }
    return p, true
}

timerCtx

timerCtx 嵌入了 cancelCtx,并新增了一个 timer 和 deadline 字段。timerCtx 的取消能力是复用 cancelCtx 的,只是在这个基础上增加了定时取消而已。

在我们的使用过程中,有可能还没到 deadline,任务就提前完成了,此时需要手动调用 CancelFunc。

func slowOperationWithTimeout(ctx context.Context) (Result, error) {
        ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
    defer cancel()  // 如果未到截止时间,slowOperation就完成了,尽早调用 cancel() 释放资源
        return slowOperation(ctx)
}
type timerCtx struct {
   cancelCtx // 内嵌 cancelCtx
   timer *time.Timer // 受 cancelCtx.mu 互斥锁的保护

   deadline time.Time // 截止时间
}

Deadline() 返回 deadline 字段的值

func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
    return c.deadline, true
}

WithDeadline 基于parent Context 和 时间点 d,返回了一个定时取消的 Context,以及一个 CancelFunc。返回的Context 有三种情况被取消:1. 到达了指定时间,就会主动取消;2. 手动调用了 CancelFunc;3. 父Context取消,导致该Context被取消。这三种情况哪种先到,就会首次触发取消操作,后续的再次取消不会产生任何效果。

如果传入 parent Context 的 deadline 比指定的时间 d 还要早,此时 d 就没用处了,直接依赖 parent 取消传播就可以了。

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {

  // 传入的 parent 不能为 nil
    if parent == nil {
        panic("cannot create context from nil parent")
    }

  // parent 也有 deadline,并且比 d 还要早,直接依赖 parent 的取消传播即可
    if cur, ok := parent.Deadline(); ok && cur.Before(d) {
        // The current deadline is already sooner than the new one.
        return WithCancel(parent)
    }

  // 定义 timerCtx 接口
    c := &timerCtx{
        cancelCtx: newCancelCtx(parent),
        deadline:  d,
    }

  // 设置传播,如果parent 属于 cancelCtx,会挂载到 children 字段上
    propagateCancel(parent, c)

  // 距离截止时间 d 还有多久
    dur := time.Until(d)
    if dur
func (c *timerCtx) cancel(removeFromParent bool, err error) {
    c.cancelCtx.cancel(false, err)

  // 从父节点中移除
    if removeFromParent {
        removeChild(c.cancelCtx.Context, c)
    }

  // 把定时器停了,释放资源
  // 有可能还没到deadline,手动触发了 CancelFunc,此时把 timer 停了
    c.mu.Lock()
    if c.timer != nil {
        c.timer.Stop()
        c.timer = nil
    }
    c.mu.Unlock()
}

WithTimeout 就是基于 WithDeadline,deadline 就是基于当前时间计算的

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

本篇文章,我们通过源码+示例的方式,一起学习了 context 包相关的结构以及实现逻辑,包括如下内容

Context 接口:定义了一些接口方法和规范

emptyCtx:空的Context,Background() 和 TODO() 方法就是使用的 emptyCtx

valueCtx:用于保存键值对,查询时是递归查询,可以用于 LogID 这种全局 id 的保存

cancelCtx:可以取消的Context,用于取消信号的传递

timerCtx:定时取消的 cancelCtx

微信公众号:漫漫Coding路

Original: https://www.cnblogs.com/lifelmy/p/16585351.html
Author: 漫漫Coding路
Title: Go语言 context包源码学习

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

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

(0)

大家都在看

  • 第十五章:指针类型

    本篇翻译自《Practical Go Lessons》 Chapter 15: Pointer type 1 你将在本章将学到什么? 什么是指针? 什么时指针类型? 如何去创建并使…

    Go语言 2023年5月25日
    058
  • go微服务框架Kratos笔记(四)使用nacos作为远端配置中心

    初识nacos nacos是阿里开源的一款用于动态服务发现、配置管理和服务管理的平台。 官方介绍,Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特…

    Go语言 2023年5月25日
    076
  • Excelize 2.5.0 正式发布,这些新增功能值得关注

    Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准。可以使用它来读取、写入由 Mic…

    Go语言 2023年5月25日
    063
  • go语言异常处理

    go语言异常处理 go语言引入了一个关于错误错里的标准模式,即error接口,该接口的定义如下: type error interface{ Error() string } 对于…

    Go语言 2023年5月29日
    060
  • Go语言中单个字符char rune

    Go 语言的字符使用UTF-8 编码 *英文字母 1个 字节, 汉子 3个 字节 golang的字符称为rune,等价于C中的char,…

    Go语言 2023年5月29日
    051
  • golang 实现一个简单的命令行进度条

    由于有时候跑脚本几个小时看不到进度,所以想着写一个简单的命令行的进度条。类似下面这样的 其中的原理主要是\r回车符(将光标移动到行首)。这样的话就可以重新打印一行以覆盖之前的那一行…

    Go语言 2023年5月25日
    049
  • 一次Kafka内存泄露排查经过

    一、现象 服务部署后内存总体呈上升趋势 二、排查过程 通过go tool pprof收集了三天内存数据 2月11号数据: 2月14号数据: 2月15号数据: 我们使用sarama客…

    Go语言 2023年5月25日
    053
  • Go 简单入门

    GO的环境配置? GOPATH GOROOT 都是干嘛用的? 配置环境跟java对比有点奇怪 https://blog.csdn.net/weixin_40563757/artic…

    Go语言 2023年5月25日
    074
  • RSA.js_公钥加密_NoPadding_golang实现_n_e_pem格式互转

    转载注明来源:本文链接 来自osnosn的博客,写于 2021-09-13. 参考 PKCS1【Golang 实现RSA加密解密】 PKCS1,密钥格式转换(需第三方包)【RSA非…

    Go语言 2023年5月25日
    061
  • 【golang】 关于for range中只存储最后一个元素的问题

    前言: 今天用for range写了个demo,发现无论怎么运行,最后的结果是一直是最后一个。自己思考过后,又和其他伙伴商量了下,最终算是解决了自己的疑惑。 正文: 下面我们来看个…

    Go语言 2023年5月25日
    055
  • 编译kubeadm使生成证书有效期为100年

    问题 编译 检查结果 问题 当我使用kubeadm部署成功k8s集群时在想默认生成的证书有效期是多久,如下所示 /etc/kubernetes/pki/apiserver.crt …

    Go语言 2023年5月25日
    052
  • Go语言的goroutine

    Go世界里,每一个并发执行的活动成为goroutine。 通过创建goroutine,就可以实现并行运算,十分方便。 如果有函数f(),那么: f():调用函数f(),并且等待它返…

    Go语言 2023年5月29日
    062
  • go 错误处理设计思考

    前段时间准备对线上一个golang系统服务进行内部开源,对代码里面的错误处理进行了一波优化。 优化的几个原因: 错误处理信息随意,未分类未定义。看到错误日志不能第一时间定位 错误的…

    Go语言 2023年5月25日
    048
  • 将百度万年历存入自己的数据库

    前言 最近有需要研究阴历和阳历互相转换的问题。因此找到两个库carbon和solarlunar但是感觉计算出来的总是不太放心,而且也会占用计算资源。我的想法是通过接口获取现成的阴历…

    Go语言 2023年5月25日
    059
  • Golang的GC回收机制

    GC触发的条件 v1.3版本 标记清除法 第一步,找出不可达的对象,做上标记。 第二部,回收没有被标记的对象。 缺点:在标记的时候会进行STW(Stop the world) St…

    Go语言 2023年5月25日
    049
  • 举个栗子之gorpc – 消息的编码和解码

    2022年的第一个rpc,比以往来的更早一些… 留杭过年…写点东西 初始化项目gorpc 借助go module我们可以轻易创建一个新的项目 mkdir g…

    Go语言 2023年5月25日
    059
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球