注:写帖子时go的版本是1.12.7 Context的github地址

go语言中实现一个interface不用像其他语言一样需要显示的声明实现接口。go语言只要实现了某interface的方法就可以做类型转换。go语言没有继承的概念,只有Embedding的概念。想深入学习这些用法,阅读源码是最好的方式.Context的源码非常推荐阅读,从中可以领悟出go语言接口设计的精髓。

对外暴露Context接口

Context源码中只对外显露出一个Context接口

type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}

对于Context的实现源码里有一个最基本的实现,就是私有的emptyCtx,他也就是我们经常使用的context.Background()底层的实现,他是一个int类型,实现了Context接口的所有方法,但都是没有做任何处理,都是返回的默认空值。只有String()方法,里有几行代码,去判断emptyCtx的类型来进行相应的字符串输出,String()方法其实是实现了接口StringeremptyCtx是整个Context灵魂,为什么这么说,因为你对context的所有的操作都是基于他去做的再次封装。

注意一下Value(key interface{}) interface{} ,因为还没有泛型,所以能用的做法就是传递或者返回interface{}。不知道Go2会不会加入泛型,说是会加入,但是还没有出最终版,一切都是未知的,因为前一段时间还说会加入try,后来又宣布放弃。

type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}
func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)

在使用Context时我们能直接得到就是backgroundtodo

func Background() Context {
return background
}
func TODO() Context {
return todo
}

其他所有对外公开的方法都必须传入一个Context做为parent,这里设计的很巧妙,为什么要有parent后面我会详细说。

可以cancel掉的Context

可以cancel掉的context有三个公开的方法,也就是,是否带过期时间的Context

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

Context只用关心自己是否Done(),具体这个是怎么完成的他并不关心,是否可以cancel掉也不是他的业务,所以源码中把这部分功能分开来。

Context最常用的功能就是去监控他的Done()是否已完成,然后判断完成的原因,根据自己的业务展开相应的操作。要提一下Context是线程安全的,他在必要的地方都加了锁处理。Done()的原理:其实是close掉了channel所以所有监控Done()方法都能知道这个Context执行完了。

ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer cancel()
v, err := DoSomething(ctx)
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case out <- v:
}

我这里不缀述Context是如何使用的。这篇帖子主要分析的是源码。

Context可以被cancel掉需要考虑几个问题:

  • 如何处理父或子Contextcancel
  • cancelContext是否也应该删除掉。

我们从源码中来找到答案。

看一下canceler的接口,这是一个独立的私有接口,和Context接口独立开来,Context只做自己的事,并不用关心自己有啥附加的功能,比如现在说的cancel功能,这也是一个很好的例子,如果有需要对Context进行扩展,可以参考他们的代码。

type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}

和两个错误

var Canceled = errors.New("context canceled")
var DeadlineExceeded error = deadlineExceededError{}

是个是被主动Cancel的错误和一个超时的错误,这两个错误是对外显露的,我们也是根据这两个Error判断Done()是如何完成的。

实现canceler接口的是结构体cancelCtx

// that implement canceler.
type cancelCtx struct {
Context mu sync.Mutex // protects following fields
done chan struct{} // created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}

注意:cancelCtxContext接口Embedding进去了,也就是说cancelCtx多重实现接口,不但是个canceler类型也是一个Context类型。

源码中cancelCtx并没有实现Context接口中的所有的方法,这就是Embedding的强大之处,Context接口的具体实现都是外部传进来的具体Context实现类型来实现的eg: cancelCtx{Context: xxxx}

还要注意一点就是这两个接口都有各自的Done()方法,cancelCtx有实现自己的Done()方法,也就是说无论转换成canceler接口类型还是Context类型调用Done()方法时,都是他自己的实现

cancelCtx 为基础还有一个是带过期时间的实现timerCtx

type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu. deadline time.Time
}

timerCtxWithDeadlineWithTimeout方法的基础。

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithCancel 需要调用者主动去调用cancel,其他的两个,就是有过期时间,如果不主动去调用cancel到了过期时间系统会自动调用。

上面我有说过context包中Background()TODO()方法,是其他所有公开方法的基础,因为其他所有的公开方法都需要传递进来一个Context接口做为parent。这样我们所有创建的新的Context都是以parent为基础来进行封装和操作

看一下cancelCtx的是如何初始化的

func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}

propagateCancel回答了我们第一个问题

如何处理父或子Contextcancel

func propagateCancel(parent Context, child canceler) {
if parent.Done() == nil {
return // parent is never canceled
}
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}

propagateCancel做了以下几件事

  1. 检查parent是否可以cancel
  2. 检查parent是否是cancelCtx类型

    2.1. 如果是,再检查是否已经cancel掉,是则cancel掉child,否则加入child

    2.2. 如果不是,则监控parentchild 的Done()

我们看一下timerCtx的具体实现

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock() if removeFromParent {
removeChild(c.Context, c)
}
}

我们去查看所有对cancel的调用会发现

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
propagateCancel(parent, c)
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(false, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}

返回的cancel方法都是func() { c.cancel(true, Canceled) }

回答了我们的第二个问题

cancelContext是否也应该删除掉。

所有创建的可以cancel掉的方法都会被从parent上删除掉

保存key/value信息的Context

Context还有一个功能就是保存key/value的信息,从源码中我们可以看出一个Context只能保存一对,但是我们可以调用多次WithValue创建多个Context

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

在查询key的时候,是一个向上递归的过程:

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

总结一下

  • 接口要有边界,要简洁。
  • 对外公开的部分要简单明了。
  • 提炼边界方法和辅助实现部分,隐藏细节。

golang从context源码领悟接口的设计的更多相关文章

  1. ibatis源码学习1_整体设计和核心流程

    背景介绍ibatis实现之前,先来看一段jdbc代码: Class.forName("com.mysql.jdbc.Driver"); String url = "jdb ...

  2. 10个带源码的充满活力的Web设计教程

    10个带源码的充满活力的Web设计教程 2013-08-02 16:47 佚名 OSCHINA编译 我要评论(0) 字号:T | T Web设计师必须了解各种各样的Web设计风格,这才能让他或者她在设 ...

  3. Golang Http Server源码阅读

    建议看这篇文章前先看一下net/http文档 http://golang.org/pkg/net/http/ net.http包里面有很多文件,都是和http协议相关的,比如设置cookie,head ...

  4. 【协作式原创】查漏补缺之Golang中mutex源码实现(预备知识)

    预备知识 CAS机制 1. 是什么 参考附录3 CAS 是项乐观锁技术,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是 ...

  5. 【协作式原创】查漏补缺之Golang中mutex源码实现

    概览最简单版的mutex(go1.3版本) 预备知识 主要结构体 type Mutex struct { state int32 // 指代mutex锁当前的状态 sema uint32 // 信号量 ...

  6. golang学习笔记----源码文件

    GO源码文件

  7. 读EntityFramework.DynamicFilters源码_心得_设计思想_04

    前几次,我们从说明文档,示例,单元测试了解了怎么用这个动态过滤器,那么如果仅仅是为了实现目的,知道怎么用就可以完成相应的功能开发,但我还想了解的问题是 作者是怎么将动态过滤器与EF结合的 有哪些设计思 ...

  8. jquery源码分析(二)——架构设计

    要学习一个库首先的理清它整体架构: 1.jQuery源码大致架构如下:(基于 jQuery 1.11 版本,共计8829行源码)(21,94)                定义了一些变量和函数jQu ...

  9. Sizzle源码分析:一 设计思路

    一.前言 DOM选择器(Sizzle)是jQuery框架中非常重要的一部分,在H5还没有流行起来的时候,jQuery为我们提供了一个简洁,方便,高效的DOM操作模式,成为那个时代的经典.虽然现在Vue ...

随机推荐

  1. python的内存分配

    一.前言 大多数编译型语言,变量在使用前必须先声明,其中C语言更加苛刻:变量声明必须位于代码块最开始,且在任何其他语句之前.其他语言,想C++和java,允许“随时随地”声明变量,比如,变量声明可以在 ...

  2. Java基础(六) static五大应用场景

    static和final是两个我们必须掌握的关键字.不同于其他关键字,他们都有多种用法,而且在一定环境下使用,可以提高程序的运行性能,优化程序的结构.上一个章节我们讲了final关键字的原理及用法,本 ...

  3. shell多线程(2)之基于管道实现并发

    在shell脚本里批量执行程序是比较常见的方式,如果程序很多,每个执行时间比较长,则顺序执行需要花费大量的时间. 此时并发就成为我们考虑的方向. 上篇<shell多线程>中我们已经简单实现 ...

  4. 02 我的第一个Javascript代码

    02-第一个JavaScript代码   在页面中,我们可以在body标签中放入<script type=”text/javascript”></script>标签对儿,< ...

  5. Dart 异步编程相关概念简述

    目录 isolate: event loop: Future: async/await: 总结 参考链接 ​ 学习 Dart 的异步编程时,需要对异步编程所涉及的相关知识体系进行梳理,我们可根据以下几 ...

  6. Storm —— 集群环境搭建

    一.集群规划 这里搭建一个3节点的Storm集群:三台主机上均部署Supervisor和LogViewer服务.同时为了保证高可用,除了在hadoop001上部署主Nimbus服务外,还在hadoop ...

  7. spring 5.x 系列第4篇 —— spring AOP (代码配置方式)

    文章目录 一.说明 1.1 项目结构说明 1.2 依赖说明 二.spring aop 2.1 创建待切入接口及其实现类 2.2 创建自定义切面类 2.3 配置切面 2.4 测试切面 2.5 切面执行顺 ...

  8. JSON对象和JavaScript对象直接量的区别--不同之处

    JSON对象和JS对象直接量 在工作当中,我们总是可以听到人说将数据转换为JSON对象,或者说把JSON对象转换为字符串之类的话,下面是关于JSON的具体说明. JSON对象并不是JavaScript ...

  9. 浅谈C#泛型

    一.为什么要提出泛型的概念 我们在声明对象或者方法中,对象中成员变量的定义或者函数参数都传递都要指定具体的对象类型,但是有的时候参数的类型是变化的,但是实现的功能却又差不多,这个时候我们就想,是否存在 ...

  10. 使用JavaScript带你体验V8引擎解析字符串过程

    AST模块其实要写的话,100篇都写不完,我将一些简单知识点翻译成JavaScript代码来进行讲解(v8内部的复杂性永远都能超出我的意料,现在看到万行的源码都已经没感觉了),如果谁想看C++源码,就 ...