Context本质

golang标准库里Context实际上是一个接口(即一种编程规范、 一种约定)。

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

通过查看源码里的注释,我们得到如下约定:

  1. Done()函数返回一个只读管道,且管道里不存放任何元素(struct{}),所以用这个管道就是为了实现阻塞
  2. Deadline()用来记录到期时间,以及是否到期。
  3. Err()用来记录Done()管道关闭的原因,比如可能是因为超时,也可能是因为被强行Cancel了。
  4. Value()用来返回key对应的value,你可以想像成Context内部维护了一个map。

Context实现

go源码里提供了Context接口的一个具体实现,遗憾的是它只是一个空的Context,什么也没做。

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 any) any {
return nil
}

emptyCtx以小写开头,包外不可见,所以golang又提供了Background和TODO这2个函数让我们能获取到emptyCtx。

var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}

backgroud和todo明明是一模一样的东西,就是emptyCtx,为什么要搞2个呢?真心求教,知道的同学请在评论区告诉我。

emptyCtx有什么用?创建Context时通常需要传递一个父Context,emptyCtx用来充当最初的那个Root Context。

With Value

当业务逻辑比较复杂,函数调用链很长时,参数传递会很复杂,如下图:

f1产生的参数b要传给f2,虽然f2并不需要参数b,但f3需要,所以b还是得往后传。

如果把每一步产生的新变量都放到Context这个大容器里,函数之间只传递Context,需要什么变量时直接从Context里取,如下图:

f2能从context里取到a和b,f4能从context里取到a、b、c、d。

package main

import (
"context"
"fmt"
) func step1(ctx context.Context) context.Context {
//根据父context创建子context,创建context时允许设置一个<key,value>对,key和value可以是任意数据类型
child := context.WithValue(ctx, "name", "大脸猫")
return child
} func step2(ctx context.Context) context.Context {
fmt.Printf("name %s\n", ctx.Value("name"))
//子context继承了父context里的所有key value
child := context.WithValue(ctx, "age", 18)
return child
} func step3(ctx context.Context) {
fmt.Printf("name %s\n", ctx.Value("name")) //取出key对应的value
fmt.Printf("age %d\n", ctx.Value("age"))
} func main1() {
grandpa := context.Background() //空context
father := step1(grandpa) //father里有一对<key,value>
grandson := step2(father) //grandson里有两对<key,value>
step3(grandson)
}

Timeout

在视频 https://www.bilibili.com/video/BV1C14y127sv/ 里介绍了超时实现的核心原理,视频中演示的done管道可以用Context的Done()来替代,Context的Done()管道什么时候会被关系呢?2种情况:

1. 通过context.WithCancel创建一个context,调用cancel()时会关闭context.Done()管道。

func f1() {
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(100 * time.Millisecond)
cancel() //调用cancel,触发Done
}()
select {
case <-time.After(300 * time.Millisecond):
fmt.Println("未超时")
case <-ctx.Done(): //ctx.Done()是一个管道,调用了cancel()都会关闭这个管道,然后读操作就会立即返回
err := ctx.Err() //如果发生Done(管道被关闭),Err返回Done的原因,可能是被Cancel了,也可能是超时了
fmt.Println("超时:", err) //context canceled
}
}

2. 通过context.WithTimeout创建一个context,当超过指定的时间或者调用cancel()时会关闭context.Done()管道。

func f2() {
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100) //超时后会自动调用context的Deadline,Deadline会,触发Done
defer cancel()
select {
case <-time.After(300 * time.Millisecond):
fmt.Println("未超时")
case <-ctx.Done(): //ctx.Done()是一个管道,context超时或者调用了cancel()都会关闭这个管道,然后读操作就会立即返回
err := ctx.Err() //如果发生Done(管道被关闭),Err返回Done的原因,可能是被Cancel了,也可能是超时了
fmt.Println("超时:", err) //context deadline exceeded
}
}

Timeout的继承问题

通过context.WithTimeout创建的Context,其寿命不会超过父Context的寿命。比如:

  1. 父Context设置了10号到期,5号诞生了子Context,子Context设置了100天后到期,则实际上10号的时候子Context也会到期。
  2. 父Context设置了10号到期,5号诞生了子Context,子Context设置了1天后到期,则实际上6号的时候子Context就会到期。
func inherit_timeout() {
parent, cancel1 := context.WithTimeout(context.Background(), time.Millisecond*1000) //parent设置100ms超时
t0 := time.Now()
defer cancel1() time.Sleep(500 * time.Millisecond) //消耗掉500ms // child, cancel2 := context.WithTimeout(parent, time.Millisecond*1000) //parent还剩500ms,child设置了1000ms之后到期,child.Done()管道的关闭时刻以较早的为准,即500ms后到期
child, cancel2 := context.WithTimeout(parent, time.Millisecond*100) //parent还剩500ms,child设置了100ms之后到期,child.Done()管道的关闭时刻以较早的为准,即100ms后到期
t1 := time.Now()
defer cancel2() select {
case <-child.Done():
t2 := time.Now()
fmt.Println(t2.Sub(t0).Milliseconds(), t2.Sub(t1).Milliseconds())
fmt.Println(child.Err()) //context deadline exceeded
}
}

context超时在http请求中的实际应用

定心丸来了,最后说一遍:”context在实践中真的很有用“

客户端发起http请求时设置了一个2秒的超时时间:

package main
import (
"fmt"
"io/ioutil"
"net/http"
"time"
) func main() {
client := http.Client{
Timeout: 2 * time.Second, //小于10秒,导致请求超时,会触发Server端的http.Request.Context的Done
}
if resp, err := client.Get("http://127.0.0.1:5678/"); err == nil {
defer resp.Body.Close()
fmt.Println(resp.StatusCode)
if bs, err := ioutil.ReadAll(resp.Body); err == nil {
fmt.Println(string(bs))
}
} else {
fmt.Println(err) //Get "http://127.0.0.1:5678/": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
}
}

服务端从Request里取提context,故意休息10秒钟,同时监听context.Done()管道有没有关闭。由于Request的context是2秒超时,所以服务端还没休息够context.Done()管道就关闭了。

package main
import (
"fmt"
"net/http"
"time"
) func welcome(w http.ResponseWriter, req *http.Request) {
ctx := req.Context() //取得request的context
select {
case <-time.After(10 * time.Second): //故意慢一点,10秒后才返回结果
fmt.Fprintf(w, "welcome")
case <-ctx.Done(): //超时后client会撤销请求,触发ctx.cancel(),从而关闭Done()管道
err := ctx.Err() //如果发生Done(管道被关闭),Err返回Done的原因,可能是被Cancel了,也可能是超时了
fmt.Println("server:", err) //context canceled
}
} func main() {
http.HandleFunc("/", welcome)
http.ListenAndServe(":5678", nil)
}

golang Context应用举例的更多相关文章

  1. Golang Context 详细介绍

    Golang context 本文包含对context实现上的分析和使用方式,分析部分源码讲解比价多,可能会比较枯燥,读者可以直接跳过去阅读使用部分. ps: 作者本着开源分享的精神撰写本篇文章,如果 ...

  2. Golang Context 包详解

    Golang Context 包详解 0. 引言 在 Go 语言编写的服务器程序中,服务器通常要为每个 HTTP 请求创建一个 goroutine 以并发地处理业务.同时,这个 goroutine 也 ...

  3. 带小伙伴手写 golang context

    前言 - context 源码 可以先了解官方 context.go 轮廓. 这里捎带保存一份当前 context 版本备份. // Copyright 2014 The Go Authors. Al ...

  4. golang context学习记录1

    1.前言 一个请求,可能涉及多个API调用,多个goroutine,如何在多个API 之间,以及多个goroutine之间协作和传递信息,就是一个问题. 比如一个网络请求Request,需要开启一些g ...

  5. Golang Context 的原理与实战

    本文让我们一起来学习 golang Context 的使用和标准库中的Context的实现. golang context 包 一开始只是 Google 内部使用的一个 Golang 包,在 Gola ...

  6. 【GoLang】golang context channel 详解

    代码示例: package main import ( "fmt" "time" "golang.org/x/net/context" ) ...

  7. Golang context包解读

    Context 通常被译作 上下文 ,一般理解为程序单元的一个运行状态.现场.快照,而翻译中 上下 又很好地诠释了其本质,上下上下则是存在上下层的传递, 上 会把内容传递给 下 . 在Go语言中,程序 ...

  8. golang context 剖析 1.7.4 版本

    1. 内部结构之 - timerCtx . type timerCtx struct { cancelCtx timer *time.Timer // Under cancelCtx.mu. dead ...

  9. golang Context for goroutines

    概要 goroutine 的控制 取消控制 超时控制 goroutine 之间的传值 总结 概要 golang 的提供的 channel 机制是基于 CSP(Communicating Sequenc ...

  10. golang context包

    go context标准库 context包在Go1.7版本时加入到标准库中.其设计目标是给Golang提供一个标准接口来给其他任务发送取消信号和传递数据.其具体作用为: 可以通过context发送取 ...

随机推荐

  1. Nanoframework 操作单片机蓝牙配置WIFI的案例

    Nanoframework 操作单片机蓝牙配置WIFI的案例 通过Nanoframework的蓝牙配置Wifi的名称和密码 下面是基本需要的工具 ESP32设备一个 需要支持蓝牙和wifi,一般情况的 ...

  2. JUC同步锁原理

    JUC同步锁原理 1.锁的本质 1.什么是锁? ​ 通俗来讲,锁要保证的就是原子性,就是一个代码块不允许多线程同时执行,就是锁.从生活的角度上来说,就比如你要去上厕所,当你在上厕所期间,你会把门锁上, ...

  3. Jenkins部署前后端不分离springboot项目

    背景 写这篇博客的时候我还是大学生,学校期末课程设计时要求使用Jenkins部署项目,所以使用windows,但是企业中都是使用linux,往往还会搭建一个gitlab.下面我介绍的是在window环 ...

  4. JavaCV人脸识别三部曲之三:识别和预览

    欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos <JavaCV人脸识别三部曲>链接 < ...

  5. TP5 where查询一个字段不等于多个值

    // 组装where条件$wheres = [];// 后台人员类型$people = input('people','');switch($people){ case "跟单员" ...

  6. 端口探测神器——Nmap

    Nmap 简介: 全称网络映射器,能快速扫描大型网络或单个网络上有哪些主机,这些主机提供什么服务,可以发现服务器运行在什么操作系统上,从而发现可攻击的脆弱点,扩大攻击范围 Nmap有图形化版本名叫Ze ...

  7. NodeJS使用npm安装vue脚手架

    开发环境准备:Windows10.Windows11 NodeJS,安装官网最新LTS版即可 下载地址:https://nodejs.org/安装一路下一步,默认即可 ================ ...

  8. [Java基础]String,StringBuffer,StringBuilder的区别

    String 和 StringBuffer/StringBuilder应该分开来看 java.lang.String源码中的注释说明 java.lang.StringBuffer源码中的注释说明 ja ...

  9. Java与PHP的区别

    1.PHP暂时不支持像Java那样的JIT运行时编译的热点代码,但PHP具有opcache机制,能够将脚本对应的opcode缓存在内存中. 补充:JIT与JVM的三种执行模式:解释模式.编译模式.混合 ...

  10. 【实践篇】推荐算法PaaS化探索与实践

    作者:京东零售 崔宁 1. 背景说明 目前,推荐算法部支持了主站.企业业务.全渠道等20+业务线的900+推荐场景,通过梳理大促运营.各垂直业务线推荐场景的共性需求,对现有推荐算法能力进行沉淀和积累, ...