前言
最近在项目开发时,常用到
Context
这个包。context.Context
是Go语言中独特的设计,在其余编程语言中咱们不多见到相似的概念。因此这一期咱们就来好好讲一讲Context
的基本概念与实际使用,麻麻不再担忧个人并发编程啦~~~。html
什么是context
在理解context
包以前,咱们应该熟悉两个概念,由于这能加深你对context
的理解。git
1. Goroutine
Goroutine
是一个轻量级的执行线程,多个Goroutine
比一个线程轻量因此管理他们消耗的资源相对更少。Goroutine
是Go中最基本的执行单元,每个Go程序至少有一个Goroutine
:主Goroutine
。程序启动时会自动建立。这里为了你们能更好的理解Goroutine
,咱们先来看一看线程与协程的概念。程序员
-
线程(Thread)
线程是一种轻量级进程,是CPU调度的最小单位。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程本身不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属于一个进程的其余线程共享进程所拥有的所有资源。线程拥有本身独立的栈和共享的堆,共享堆,不共享栈,线程的切换通常也由操做系统调度。github
-
协程(coroutine)
又称为微线程与子例程同样,协程也是一种程序组建,相对子例程而言,协程更为灵活,但在实践中使用没有子例程那样普遍。和线程相似,共享堆,不共享栈,协程的切换通常由程序员在代码中显式控制。他避免了上下文切换的额外耗费,兼顾了多线程的优势,简化了高并发程序的复杂。golang
Goroutine和其余语言的协程(coroutine)在使用方式上相似,但从字面意义上来看不一样(一个是Goroutine,一个是coroutine),再就是协程是一种协做任务控制机制,在最简单的意义上,协程不是并发的,而Goroutine支持并发的。所以Goroutine能够理解为一种Go语言的协程。同时它能够运行在一个或多个线程上。web
咱们来看一个简单示例:面试
func Hello() {
fmt.Println("hello everybody , I'm asong")
}
func main() {
go Hello()
fmt.Println("Golang梦工厂")
}
上面的程序,咱们使用go又开启了一个Goroutine
执行Hello
方法,可是咱们运行这个程序,运行结果以下:数据库
Golang梦工厂
这里出现这个问题的缘由是咱们启动的goroutine
在main
执行完就退出了,因此为了main
等待这个Goroutine
执行完,咱们就须要一些方法,让goroutine
告诉main
执行完了,这里就须要通道了。编程
2. 通道
这是 goroutine 之间的沟通渠道。当您想要将结果或错误,或任何其余类型的信息从一个 goroutine 传递到另外一个 goroutine 时就可使用通道。通道是有类型的,能够是 int 类型的通道接收整数或错误类型的接收错误等。设计模式
假设有个 int 类型的通道 ch,若是你想发一些信息到这个通道,语法是 ch <- 1,若是你想从这个通道接收一些信息,语法就是 var := <-ch。这将从这个通道接收并存储值到 var 变量。
如下程序说明了通道的使用确保了 goroutine 执行完成并将值返回给 main 。
func Hello(ch chan int) {
fmt.Println("hello everybody , I'm asong")
ch <- 1
}
func main() {
ch := make(chan int)
go Hello(ch)
<-ch
fmt.Println("Golang梦工厂")
}
这里咱们使用通道进行等待,这样main
就会等待goroutine
执行完。如今咱们知道了goroutine
、channel
的概念了,下面咱们就来介绍一下context
。
3. 场景
有了上面的概念,咱们在来看一个例子:
以下代码,每次请求,Handler会建立一个goroutine来为其提供服务,并且连续请求3次,request
的地址也是不一样的:
func main() {
http.HandleFunc("/", SayHello) // 设置访问的路由
log.Fatalln(http.ListenAndServe(":8080",nil))
}
func SayHello(writer http.ResponseWriter, request *http.Request) {
fmt.Println(&request)
writer.Write([]byte("Hi"))
}
========================================================
$ curl http://localhost:8080/
0xc0000b8030
0xc000186008
0xc000186018
而每一个请求对应的Handler,常会启动额外的的goroutine进行数据查询或PRC调用等。
而当请求返回时,这些额外建立的goroutine须要及时回收。并且,一个请求对应一组请求域内的数据可能会被该请求调用链条内的各goroutine所须要。
如今咱们对上面代码在添加一点东西,当请求进来时,Handler
建立一个监控goroutine
,这样就会每隔1s打印一句Current request is in progress
func main() {
http.HandleFunc("/", SayHello) // 设置访问的路由
log.Fatalln(http.ListenAndServe(":8080",nil))
}
func SayHello(writer http.ResponseWriter, request *http.Request) {
fmt.Println(&request)
go func() {
for range time.Tick(time.Second) {
fmt.Println("Current request is in progress")
}
}()
time.Sleep(2 * time.Second)
writer.Write([]byte("Hi"))
}
这里我假定请求须要耗时2s,在请求2s后返回,咱们指望监控goroutine在打印2次Current request is in progress
后即中止。但运行发现,监控goroutine打印2次后,其仍不会结束,而会一直打印下去。
问题出在建立监控goroutine后,未对其生命周期做控制,下面咱们使用context做一下控制,即监控程序打印前需检测request.Context()
是否已经结束,若结束则退出循环,即结束生命周期。
func main() {
http.HandleFunc("/", SayHello) // 设置访问的路由
log.Fatalln(http.ListenAndServe(":8080",nil))
}
func SayHello(writer http.ResponseWriter, request *http.Request) {
fmt.Println(&request)
go func() {
for range time.Tick(time.Second) {
select {
case <- request.Context().Done():
fmt.Println("request is outgoing")
return
default:
fmt.Println("Current request is in progress")
}
}
}()
time.Sleep(2 * time.Second)
writer.Write([]byte("Hi"))
}
基于如上需求,context包应用而生。context包能够提供一个请求从API请求边界到各goroutine的请求域数据传递、取消信号及截至时间等能力。详细原理请看下文。
4. context
在 Go 语言中 context 包容许您传递一个 "context" 到您的程序。Context 如超时或截止日期(deadline)或通道,来指示中止运行和返回。例如,若是您正在执行一个 web 请求或运行一个系统命令,定义一个超时对生产级系统一般是个好主意。由于,若是您依赖的API运行缓慢,你不但愿在系统上备份(back up)请求,由于它可能最终会增长负载并下降全部请求的执行效率。致使级联效应。这是超时或截止日期 context 派上用场的地方。
4.1 设计原理
Go 语言中的每个请求的都是经过一个单独的 Goroutine 进行处理的,HTTP/RPC 请求的处理器每每都会启动新的 Goroutine 访问数据库和 RPC 服务,咱们可能会建立多个 Goroutine 来处理一次请求,而 Context
的主要做用就是在不一样的 Goroutine 之间同步请求特定的数据、取消信号以及处理请求的截止日期。
每个 Context
都会从最顶层的 Goroutine 一层一层传递到最下层,这也是 Golang 中上下文最多见的使用方式,若是没有 Context
,当上层执行的操做出现错误时,下层其实不会收到错误而是会继续执行下去。
当最上层的 Goroutine 由于某些缘由执行失败时,下两层的 Goroutine 因为没有接收到这个信号因此会继续工做;可是当咱们正确地使用 Context
时,就能够在下层及时停掉无用的工做减小额外资源的消耗:
这其实就是 Golang 中上下文的最大做用,在不一样 Goroutine 之间对信号进行同步避免对计算资源的浪费,与此同时 Context
还能携带以请求为做用域的键值对信息。
这里光说,其实也不能彻底理解其中的做用,因此咱们来看一个例子:
func main() {
ctx,cancel := context.WithTimeout(context.Background(),1 * time.Second)
defer cancel()
go HelloHandle(ctx,500*time.Millisecond)
select {
case <- ctx.Done():
fmt.Println("Hello Handle ",ctx.Err())
}
}
func HelloHandle(ctx context.Context,duration time.Duration) {
select {
case <-ctx.Done():
fmt.Println(ctx.Err())
case <-time.After(duration):
fmt.Println("process request with", duration)
}
}
上面的代码,由于过时时间大于处理时间,因此咱们有足够的时间处理改请求,因此运行代码以下图所示:
process request with 500ms
Hello Handle context deadline exceeded
HelloHandle
函数并无进入超时的select
分支,可是main
函数的select
却会等待context.Context
的超时并打印出Hello Handle context deadline exceeded
。若是咱们将处理请求的时间增长至2000
ms,程序就会由于上下文过时而被终止。
context deadline exceeded
Hello Handle context deadline exceeded
4.2 接口
context.Context
是 Go 语言在 1.7 版本中引入标准库的接口1,该接口定义了四个须要实现的方法,其中包括:
-
Deadline
— 返回context.Context
被取消的时间,也就是完成工做的截止日期; -
Done
— 返回一个 Channel,这个 Channel 会在当前工做完成或者上下文被取消以后关闭,屡次调用Done
方法会返回同一个 Channel; -
Err
— 返回context.Context
结束的缘由,它只会在Done
返回的 Channel 被关闭时才会返回非空的值; -
若是 context.Context
被取消,会返回Canceled
错误; -
若是 context.Context
超时,会返回DeadlineExceeded
错误; -
Value
— 从context.Context
中获取键对应的值,对于同一个上下文来讲,屡次调用Value
并传入相同的Key
会返回相同的结果,该方法能够用来传递请求特定的数据;
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
context 使用详解
建立context
context
包容许如下方式建立和得到context
:
-
context.Background()
:这个函数返回一个空context
。这只能用于高等级(在 main 或顶级请求处理中)。 -
context.TODO()
:这个函数也是建立一个空context
。也只能用于高等级或当您不肯定使用什么 context,或函数之后会更新以便接收一个 context 。这意味您(或维护者)计划未来要添加 context 到函数。
其实咱们查看源代码。发现他俩都是经过 new(emptyCtx)
语句初始化的,它们是指向私有结构体 context.emptyCtx
的指针,这是最简单、最经常使用的上下文类型:
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
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"
}
从上述代码,咱们不难发现 context.emptyCtx
经过返回 nil
实现了 context.Context
接口,它没有任何特殊的功能。
从源代码来看,context.Background
和 context.TODO
函数其实也只是互为别名,没有太大的差异。它们只是在使用和语义上稍有不一样:
-
context.Background
是上下文的默认值,全部其余的上下文都应该从它衍生(Derived)出来。 -
context.TODO
应该只在不肯定应该使用哪一种上下文时使用;
在多数状况下,若是当前函数没有上下文做为入参,咱们都会使用 context.Background
做为起始的上下文向下传递。
context的继承衍生
有了如上的根Context,那么是如何衍生更多的子Context的呢?这就要靠context包为咱们提供的With
系列的函数了。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context
这四个With
函数,接收的都有一个partent参数,就是父Context,咱们要基于这个父Context建立出子Context的意思,这种方式能够理解为子Context对父Context的继承,也能够理解为基于父Context的衍生。
经过这些函数,就建立了一颗Context树,树的每一个节点均可以有任意多个子节点,节点层级能够有任意多个。
WithCancel
函数,传递一个父Context做为参数,返回子Context,以及一个取消函数用来取消Context。WithDeadline
函数,和WithCancel
差很少,它会多传递一个截止时间参数,意味着到了这个时间点,会自动取消Context,固然咱们也能够不等到这个时候,能够提早经过取消函数进行取消。
WithTimeout
和WithDeadline
基本上同样,这个表示是超时自动取消,是多少时间后自动取消Context的意思。
WithValue
函数和取消Context无关,它是为了生成一个绑定了一个键值对数据的Context,这个绑定的数据能够经过Context.Value
方法访问到,后面咱们会专门讲。
你们可能留意到,前三个函数都返回一个取消函数CancelFunc
,这是一个函数类型,它的定义很是简单。
type CancelFunc func()
这就是取消函数的类型,该函数能够取消一个Context,以及这个节点Context下全部的全部的Context,无论有多少层级。
下面我就展开来介绍一个每个方法的使用。
WithValue
context
包中的 context.WithValue
函数能从父上下文中建立一个子上下文,传值的子上下文使用 context.valueCtx
类型,咱们看一下源码:
// WithValue returns a copy of parent in which the value associated with key is
// val.
//
// Use context Values only for request-scoped data that transits processes and
// APIs, not for passing optional parameters to functions.
//
// The provided key must be comparable and should not be of type
// string or any other built-in type to avoid collisions between
// packages using context. Users of WithValue should define their own
// types for keys. To avoid allocating when assigning to an
// interface{}, context keys often have concrete type
// struct{}. Alternatively, exported context key variables' static
// type should be a pointer or interface.
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
Context
key, val interface{}
}
// stringify tries a bit to stringify v, without using fmt, since we don't
// want context depending on the unicode tables. This is only used by
// *valueCtx.String().
func stringify(v interface{}) string {
switch s := v.(type) {
case stringer:
return s.String()
case string:
return s
}
return "<not Stringer>"
}
func (c *valueCtx) String() string {
return contextName(c.Context) + ".WithValue(type " +
reflectlite.TypeOf(c.key).String() +
", val " + stringify(c.val) + ")"
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
此函数接收 context 并返回派生 context,其中值 val 与 key 关联,并经过 context 树与 context 一块儿传递。这意味着一旦得到带有值的 context,从中派生的任何 context 都会得到此值。不建议使用 context 值传递关键参数,而是函数应接收签名中的那些值,使其显式化。
context.valueCtx
结构体会将除了 Value
以外的 Err
、Deadline
等方法代理到父上下文中,它只会响应 context.valueCtx.Value
方法。若是 context.valueCtx
中存储的键值对与 context.valueCtx.Value
方法中传入的参数不匹配,就会从父上下文中查找该键对应的值直到在某个父上下文中返回 nil
或者查找到对应的值。
说了这么多,比较枯燥,咱们来看一下怎么使用:
type key string
func main() {
ctx := context.WithValue(context.Background(),key("asong"),"Golang梦工厂")
Get(ctx,key("asong"))
Get(ctx,key("song"))
}
func Get(ctx context.Context,k key) {
if v, ok := ctx.Value(k).(string); ok {
fmt.Println(v)
}
}
上面代码咱们基于context.Background
建立一个带值的ctx,而后能够根据key来取值。这里为了不多个包同时使用context
而带来冲突,key
不建议使用string
或其余内置类型,因此建议自定义key
类型.
WithCancel
此函数建立从传入的父 context 派生的新 context。父 context 能够是后台 context 或传递给函数的 context。返回派生 context 和取消函数。只有建立它的函数才能调用取消函数来取消此 context。若是您愿意,能够传递取消函数,可是,强烈建议不要这样作。这可能致使取消函数的调用者没有意识到取消 context 的下游影响。可能存在源自此的其余 context,这可能致使程序以意外的方式运行。简而言之,永远不要传递取消函数。
咱们直接从 context.WithCancel
函数的实现来看它到底作了什么:
// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
-
context.newCancelCtx
将传入的上下文包装成私有结构体context.cancelCtx
; -
context.propagateCancel
会构建父子上下文之间的关联,当父上下文被取消时,子上下文也会被取消:
func propagateCancel(parent Context, child canceler) {
done := parent.Done()
if done == nil {
return // 父上下文不会触发取消信号
}
select {
case <-done:
child.cancel(false, parent.Err()) // 父上下文已经被取消
return
default:
}
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
child.cancel(false, p.err)
} else {
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
上述函数总共与父上下文相关的三种不一样的状况:
-
当 parent.Done() == nil
,也就是parent
不会触发取消事件时,当前函数会直接返回; -
当 child
的继承链包含能够取消的上下文时,会判断parent
是否已经触发了取消信号; -
若是已经被取消, child
会马上被取消; -
若是没有被取消, child
会被加入parent
的children
列表中,等待parent
释放取消信号; -
在默认状况下 -
运行一个新的 Goroutine 同时监听 parent.Done()
和child.Done()
两个 Channel -
在 parent.Done()
关闭时调用child.cancel
取消子上下文;
context.propagateCancel
的做用是在 parent
和 child
之间同步取消和结束的信号,保证在 parent
被取消时,child
也会收到对应的信号,不会发生状态不一致的问题。
context.cancelCtx
实现的几个接口方法也没有太多值得分析的地方,该结构体最重要的方法是 cancel
,这个方法会关闭上下文中的 Channel 并向全部的子上下文同步取消信号:
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return
}
c.err = err
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}
for child := range c.children {
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c)
}
}
说了这么,看一例子,带你感觉一下使用方法:
func main() {
ctx,cancel := context.WithCancel(context.Background())
defer cancel()
go Speak(ctx)
time.Sleep(10*time.Second)
}
func Speak(ctx context.Context) {
for range time.Tick(time.Second){
select {
case <- ctx.Done():
return
default:
fmt.Println("balabalabalabala")
}
}
}
咱们使用withCancel
建立一个基于Background
的ctx,而后启动一个讲话程序,每隔1s说一话,main
函数在10s后执行cancel
,那么speak
检测到取消信号就会退出。
WithDeadline
此函数返回其父项的派生 context,当截止日期超过或取消函数被调用时,该 context 将被取消。例如,您能够建立一个将在之后的某个时间自动取消的 context,并在子函数中传递它。当由于截止日期耗尽而取消该 context 时,获此 context 的全部函数都会收到通知去中止运行并返回。
咱们来看一下源码:
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
propagateCancel(parent, c)
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded) // 已通过了截止日期
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) }
}
context.WithDeadline
也都能建立能够被取消的计时器上下文 context.timerCtx
:
context.WithDeadline
方法在建立 context.timerCtx
的过程当中,判断了父上下文的截止日期与当前日期,并经过 time.AfterFunc
建立定时器,当时间超过了截止日期后会调用 context.timerCtx.cancel
方法同步取消信号。
context.timerCtx
结构体内部不只经过嵌入了context.cancelCtx
结构体继承了相关的变量和方法,还经过持有的定时器 timer
和截止时间 deadline
实现了定时取消这一功能:
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
func (c *timerCtx) cancel(removeFromParent bool, err error) {
c.cancelCtx.cancel(false, err)
if removeFromParent {
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
context.timerCtx.cancel
方法不只调用了 context.cancelCtx.cancel
,还会中止持有的定时器减小没必要要的资源浪费。
接下来咱们来看一个例子:
func main() {
now := time.Now()
later,_:=time.ParseDuration("10s")
ctx,cancel := context.WithDeadline(context.Background(),now.Add(later))
defer cancel()
go Monitor(ctx)
time.Sleep(20 * time.Second)
}
func Monitor(ctx context.Context) {
select {
case <- ctx.Done():
fmt.Println(ctx.Err())
case <-time.After(20*time.Second):
fmt.Println("stop monitor")
}
}
设置一个监控goroutine
,使用WithTimeout建立一个基于Background的ctx,其会当前时间的10s后取消。验证结果以下:
context deadline exceeded
10s,监控goroutine
被取消了。
WithTimeout
此函数相似于 context.WithDeadline。不一样之处在于它将持续时间做为参数输入而不是时间对象。此函数返回派生 context,若是调用取消函数或超出超时持续时间,则会取消该派生 context。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
观看源码咱们能够看出WithTimeout
内部调用的就是WithDeadline
,其原理都是同样的,上面已经介绍过了,来看一个例子吧:
func main() {
ctx,cancel := context.WithTimeout(context.Background(),10 * time.Second)
defer cancel()
go Monitor(ctx)
time.Sleep(20 * time.Second)
}
func Monitor(ctx context.Context) {
select {
case <- ctx.Done():
fmt.Println(ctx.Err())
case <-time.After(20*time.Second):
fmt.Println("stop monitor")
}
}
Context使用原则
-
context.Background 只应用在最高等级,做为全部派生 context 的根。
-
context 取消是建议性的,这些函数可能须要一些时间来清理和退出。
-
不要把
Context
放在结构体中,要以参数的方式传递。 -
以
Context
做为参数的函数方法,应该把Context
做为第一个参数,放在第一位。 -
给一个函数方法传递Context的时候,不要传递nil,若是不知道传递什么,就使用context.TODO
-
Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递。context.Value 应该不多使用,它不该该被用来传递可选参数。这使得 API 隐式的而且能够引发错误。取而代之的是,这些值应该做为参数传递。
-
Context是线程安全的,能够放心的在多个goroutine中传递。同一个Context能够传给使用其的多个goroutine,且Context可被多个goroutine同时安全访问。
-
Context 结构没有取消方法,由于只有派生 context 的函数才应该取消 context。
Go 语言中的 context.Context
的主要做用仍是在多个 Goroutine 组成的树中同步取消信号以减小对资源的消耗和占用,虽然它也有传值的功能,可是这个功能咱们仍是不多用到。在真正使用传值的功能时咱们也应该很是谨慎,使用 context.Context
进行传递参数请求的全部参数一种很是差的设计,比较常见的使用场景是传递请求对应用户的认证令牌以及用于进行分布式追踪的请求 ID。
总结
好啦,这一期文章到这里就结束啦。为了弄懂这里,参考不少文章,会在结尾贴出来,供你们学习参考。由于这个包真的很重要,在日常项目开发中咱们也是常用到,因此你们弄懂context
的原理仍是颇有必要的。
文章的示例代码已上传github:https://github.com/asong2020/Golang_Dream/tree/master/code_demo/context_example
有须要的小伙伴能够下在观看学习,若是再能给个小星星就很是感谢了。
结尾给你们发一个小福利吧,最近我在看[微服务架构设计模式]这一本书,讲的很好,本身也收集了一本PDF,有须要的小伙能够到自行下载。获取方式:关注公众号:[Golang梦工厂],后台回复:[微服务],便可获取。
我翻译了一份GIN中文文档,会按期进行维护,有须要的小伙伴后台回复[gin]便可下载。
我是asong,一名普普统统的程序猿,让我一块儿慢慢变强吧。我本身建了一个golang
交流群,有须要的小伙伴加我vx
,我拉你入群。欢迎各位的关注,咱们下期见~~~

推荐往期文章:
参考文章:
-
https://leileiluoluo.com/posts/golang-context.html -
https://studygolang.com/articles/13866 -
https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-context/
本文分享自微信公众号 - Golang梦工厂(AsongDream)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。