前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Golang之context

Golang之context

作者头像
LinkinStar
发布2022-09-01 13:55:54
6240
发布2022-09-01 13:55:54
举报
文章被收录于专栏:LinkinStar's Blog

当我们使用一些golang框架的时候,总能在框架中发现有个叫做context的东西。如果你之前了解过java的spring,那么你肯定也听说过其中有个牛逼的ApplicationContext。Context这个东西好像随时随地都在出现,在golang中也是非常重要的存在。今天我们就来看看这个神奇的Context。

定义

  • 首先我们要知道什么是context?

很多人把它翻译成上下文,其实这个是一个很难描述很定义的东西,对于这种东西,我习惯用功能去定义它。 我的定义是:context是用于在多个goroutines之间传递信息的媒介。 官方定义:At Google, we developed a context package that makes it easy to pass request-scoped values, cancelation signals, and deadlines across API boundaries to all the goroutines involved in handling a request.

用法

同样的我们先来看看它的一些基本用法,大致了解它的使用。

传递信息

代码语言:javascript
复制
func main() {
    ctx := context.Background()
    ctx = context.WithValue(ctx, "xxx", "123")
    value := ctx.Value("xxx")
    fmt.Println(value)
}

其实传递消息很简单,只需要通过context.WithValue方法设置,key-value然后通过ctx.Value方法取值就可以了。

暂时不用关心context.Background()只要知道context有传递值的功能就可以了。

关闭goroutine

在我们写golang的时候goroutine是一个非常常用的东西,我们经常会开一个goroutine去处理对应的任务,特别是一些循环一直处理的情况,这些goroutine需要知道自己什么时候要停止。 我们常见的解决方案是使用一个channel去接收一个关闭的信号,收到信号之后关闭,或者说,需要一个标识符,每个goroutine去判断这个标识符的变更从而得知什么时候关闭。 那么用context如何实现呢?

代码语言:javascript
复制
func main() {
    ctx, _ := context.WithTimeout(context.Background(), time.Second * 3)
    go func() {
        go1(ctx)
    }()
    go func() {
        go2(ctx)
    }()
    time.Sleep(time.Second * 5)
}

func go1(ctx context.Context) {
    for {
        fmt.Println("1 正在工作")
        select {
        case <-ctx.Done():
            fmt.Println("1 停止工作")
            return
        case <-time.After(time.Second):
        }
    }
}

func go2(ctx context.Context) {
    for {
        fmt.Println("2 正在工作")
        select {
        case <-ctx.Done():
            fmt.Println("2 停止工作")
            return
        case <-time.After(time.Second):
        }
    }
}

通过context.WithTimeout我们创建了一个3秒后自动取消的context; 所有工作goroutine监听ctx.Done()的信号; 收到信号就证明需要取消任务;

其实使用起来比较简单,让我们来看看内部的原理。

源码解析

创建

context.TODO()

这个就是创建一个占位用的context,可能在写程序的过程中还不能确定后期这个context的作用,所以暂时用这个占位

context.Background()

这个是最大的context,也就是根context,这里就有必要说一下context的整个构成了,context其实构成的是一棵树,Background为根节点,每次创建一个新的context就是创建了一个新的节点加入这棵树。

context.WithTimeout()

比如这个方法,创建一个自动过期的context

代码语言:javascript
复制
// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete:
//
// 	func slowOperationWithTimeout(ctx context.Context) (Result, error) {
// 		ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
// 		defer cancel()  // releases resources if slowOperation completes before timeout elapses
// 		return slowOperation(ctx)
// 	}
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout))
}

可以看到需要传入一个parent,和过期时间,新创建的context就是parent的子节点。

代码语言:javascript
复制
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(true, 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) }
}

注意其中cancelCtx: newCancelCtx(parent),其实是创建了一个可以取消的ctx,然后利用time.AfterFunc来实现定时自动过期。

还有一个细节 c.mu.Lock() defer c.mu.Unlock() 这个mu来自:

代码语言:javascript
复制
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
}

这个context因为有了锁,所以是并发安全的。

取消

代码语言:javascript
复制
// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
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)
	}
}

当达到过期时间或者调用cancelFunc的时候就会触发context的取消,然后看到上面的源码你就明白了,取消的时候有一个三个操作:

  1. c.mu.Lock() 加锁保证安全
  2. close(c.done) 将done信道关闭,从而所有在观察done信道的goroutine都知道要关闭了
  3. for child := range c.children 循环每个子节点,关闭每个子节点。我们知道context的结构是树状的,所以同时我们要注意父节点如果关闭会关闭子节点的context。

WithValue和Value

代码语言:javascript
复制
type valueCtx struct {
	Context
	key, val interface{}
}

首先valueCtx的结构如上所示,包含一个Context和key-val

代码语言:javascript
复制
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}
}

其实这个方法很简单,就是创建了一个parent的拷贝,并且将对应的key和val放进去。

代码语言:javascript
复制
func (c *valueCtx) Value(key interface{}) interface{} {
	if c.key == key {
		return c.val
	}
	return c.Context.Value(key)
}

Value方法就更简单了,就是判断当前key是否匹配,如果不匹配就去子节点寻找。

案例

最后我们来看看在实际的使用过程中,我们在哪里使用到了context,我举两个实际中常用的框架gin和etcd

gin

gin是一个web框架,在web开发的时候非常实用。

代码语言:javascript
复制
func main() {
	router := gin.Default()

	router.POST("/post", func(c *gin.Context) {

		id := c.Query("id")
		page := c.DefaultQuery("page", "0")
		name := c.PostForm("name")
		message := c.PostForm("message")

		fmt.Printf("id: %s; page: %s; name: %s; message: %s", id, page, name, message)
	})
	router.Run(":8080")
}

其实很多web框架都有Context,他们都自己封装了一个Context,利用这个Context可以做到一个request-scope中的参数传递和返回,还有很多操作通通都可以用Context来完成。

etcd

如果你没有了解过etcd你就可以把它想象成redis,它其实是一个分布式的k-v数据存储 我们在使用etcd进行操作(put或del等)的时候,需要传入context参数

代码语言:javascript
复制
timeoutCtx, cancel := context.WithTimeout(context.Background(), 2 * time.Second)
defer cancel()
putResponse, err := client.Put(timeoutCtx, "aaa", "xxx")
if err != nil {
	fmt.Println(err)
	return
}
fmt.Println(putResponse.Header.String())

这里传入的context是一个超时自动取消的context,也就是说,当put操作超过两秒后还没有执行成功的话,context就会自动done,同时这个操作也将被取消。

因为我们在使用etcd的时候,如果当前网络出现异常,无法连接到节点,或者是节点数量不足的时候,都会出现操作被hang住,如果没有定时取消的机制,或者手动取消,那么当前goroutine会被一直占用。所以就利用context来完成这个操作。

总结

  • context在web开发中,你可以类比java中的ThreadLocal,利用它来完成一个request-scope中参数的传递
  • context可以用于多个goroutine之间的参数传递
  • context还可以作为完成信号的通知
  • context并发安全

其实,我们不仅要学到context的使用,还可以学到这样设计一个系统的优点,如果以后自己在设计一些框架和系统的时候可以有更多的想法。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019-06-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 定义
  • 用法
    • 传递信息
      • 关闭goroutine
      • 源码解析
        • 创建
          • context.TODO()
          • context.Background()
          • context.WithTimeout()
        • 取消
          • WithValue和Value
          • 案例
            • gin
              • etcd
              • 总结
              相关产品与服务
              数据保险箱
              数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档