• go 语言的 Context



    title: go语言的context
    date: 2021-10-16
    updated: 2021-10-16
    comments: true
    toc: true
    excerpt: go语言的context听说是必考题...
    tags:

    • Golang
      categories:
    • 编程

    前言

    听说是面试必问, 之前只有浅显的认知, 用的时候我一般传一个空的进去 , 今天难得有休息时间, 来学习一下

    本文总结自七米老师的日志搜集项目视频

    为什么需要context

    go的代码通常使用 goroutine 来提高代码速度, 这样的话怎么控制 goroutine 生成的协程就成了问题, 如果不小心出现了代码问题导致 goroutine 陷入死循环, 或者夯死, 就会导致意想不到的问题发生. 所以, context就出生了, 他类似一个信号, 通过传入的方式来让多个逻辑块进行联系, 以便做出操作

    最基本的例子如下

    package main
    
    import (
    	"context"
    	"fmt"
    	"sync"
    
    	"time"
    )
    
    var wg sync.WaitGroup // wg等待
    
    func worker(ctx context.Context) { // context.Context一般都叫ctx
    LOOP:
    	for {
    		fmt.Println("worker")
    		time.Sleep(time.Second) // 等待1s
    		select {
    		case <-ctx.Done(): // 如果接收到了ctx的Done信号, 就退出循环
    			break LOOP
    		default:
    		}
    	}
    	wg.Done() // wg完成
    }
    
    func main() {
    	ctx, cancel := context.WithCancel(context.Background()) // 生成ctx和cancel
    	wg.Add(1)
    	go worker(ctx)
    	time.Sleep(time.Second * 3)
    	cancel() // 调用cancel即可通知ctx需要Done掉
    	wg.Wait()
    	fmt.Println("over")
    }
    
    

    goroutine嵌套时, 如果需要一起监听同一个ctx, 则可以使用

    package main
    
    import (
    	"context"
    	"fmt"
    	"sync"
    
    	"time"
    )
    
    var wg sync.WaitGroup // wg等待
    
    func worker(ctx context.Context) { // context.Context一般都叫ctx
    	go worker1(ctx)
    LOOP:
    	for {
    		fmt.Println("worker")
    		time.Sleep(time.Second) // 等待1s
    		select {
    		case <-ctx.Done(): // 如果接收到了ctx的Done信号, 就退出循环
    			break LOOP
    		default:
    		}
    	}
    }
    
    func worker1(ctx context.Context) { // context.Context一般都叫ctx
    LOOP:
    	for {
    		fmt.Println("worker1")
    		time.Sleep(time.Second) // 等待1s
    		select {
    		case <-ctx.Done(): // 如果接收到了ctx的Done信号, 就退出循环
    			break LOOP
    		default:
    		}
    	}
    	wg.Done() // wg完成
    }
    
    func main() {
    	ctx, cancel := context.WithCancel(context.Background()) // 生成ctx和cancel
    	wg.Add(1)
    	go worker(ctx)
    	time.Sleep(time.Second * 3)
    	cancel() // 调用cancel即可通知ctx需要Done掉
    	wg.Wait()
    	fmt.Println("over")
    }
    
    

    context派生

    我们看之前的代码

    context.WithCancel(context.Background())
    

    仔细看, 这里分成了两步, 一个是通过 context.Background() 生成了一个 emptyCtx 最上层的ctx

    然后通过context.WithCancel来从这个最上层的ctx派生一个新的子ctx, ctx就像树一样, 从一个根一直发散

    四种context

    context分为四种上下文可以派生, 分别为 WithCancel, WithDeadline, WithTimeout, WithValue, 这四种有不同的作用. 需要注意的是, 派生这件事, ctx 可以不停的派生子的context, 当一个被取消时, 他派生的上下文也会被取消

    • WithCancel 需要手动的出发Done才会取消
    • WithDeadline 指定一个终止时间(明确的时间), 当时间到就自动取消
    • WithTimeout 指定一个终止时间间隔, 当时间间隔到时自动取消
    • WithValue 这个目的不是取消, 而是上下文之间的数据传输

    context结构

    context其实是接口, 我们查看其结构

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

    其中:

    • Deadline方法返回当前的这个 ctx` 被取消的时间
    • Done返回的是一个channel
    • Err 返回ctx结束的原因, 只有确实结束了才会返回非空的值
      • ctx被取消就返回canceled错误
      • ctx超时结束则会返回DeadlineExceeded错误
    • value方法则根据key返回value, 这个是用来传递数据使用的

    background()和TODO()

    之前提到过, ctx实际上是可以派生的, 那么, 作为最顶层的ctx, 我们只能选择两种, backgroundTODO, 由这两个生成的ctx来派生更多的ctx

    • background用于 初始化/main/测试 中, 也就是第一个, 最顶层的ctx
    • TODO目前还没有具体的使用场景, 我们知道, go语言的传参是必须的, 如果你并不想使用ctx, 但是第三方的又需要你传, 或者你还没想好它的作用, 你可以使用TODO

    这两个生成的ctx都是不可取消, 没有截止时间, 没有携带任何值的emptyCtx

    四种with函数派生

    之前简单的介绍了 WithCancel, WithDeadline, WithTimeout, WithValue, 这里分别举出例子

    WithCancel

    WithCancel 的函数定义

    func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
    

    该函数返回一个派生的新ctx和一个done的函数, 当调用这个函数, 就会关闭上下文的Done通道

    package main
    
    import (
    	"context"
    	"fmt"
    )
    
    func gen(ctx context.Context) <-chan int {
    	dst := make(chan int)
    	n := 1
    	go func() {
    		for {
    			select {
    			case <-ctx.Done():
    				return // 监听到结束后, 退出函数, 进行垃圾回收
    			case dst <- n:
    				n++
    			}
    		}
    	}()
    	return dst
    }
    func main() {
    	ctx, cancel := context.WithCancel(context.Background())
    	defer cancel() // 关闭管道
    
    	for n := range gen(ctx) {
    		fmt.Println(n)
    		if n == 5 {
    			break
    		}
    	}
    }
    
    

    WithDeadline

    WithDeadline的函数定义

    func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
    

    传参不止一个ctx, 还有一个time.Time, 标识这个ctx的超时时间, 依旧返回了done函数, 派生的ctx在任务到期后会自动关闭, 而在到期之前可以通过手动调用cancel函数来关闭

    package main
    
    import (
    	"context"
    	"fmt"
    	"time"
    )
    
    func main() {
    	d := time.Now().Add(50 * time.Millisecond)  // 获取当前时间50ms后的时间
    	ctx, cancel := context.WithDeadline(context.Background(), d)  // 把过期时间传入
    
    	// 尽管ctx会过期,但在任何情况下调用它的cancel函数都是很好的实践。
    	// 如果不这样做,可能会使上下文及其父类存活的时间超过必要的时间。
    	defer cancel()
    
    	select {
    	case <-time.After(1 * time.Second):
    		fmt.Println("overslept")
    	case <-ctx.Done():
    		fmt.Println(ctx.Err())
    	}
    }
    
    

    需要注意的是, 推荐依旧注册一个cancel的执行, 这是为了保险起见

    WithTimeout

    WithTimeout的函数定义

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

    Deadline不同的是, 这里传的是一个时间间隔, 等待时间间隔完成后发起关闭

    这种方式同常用于超时控制

    package main
    
    import (
    	"context"
    	"fmt"
    	"sync"
    
    	"time"
    )
    
    var wg sync.WaitGroup
    
    func worker(ctx context.Context) {
    LOOP:
    	for {
    		fmt.Println("db connecting ...")
    		time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
    		select {
    		case <-ctx.Done(): // 50毫秒后自动调用
    			break LOOP
    		default:
    		}
    	}
    	fmt.Println("worker done!")
    	wg.Done()
    }
    
    func main() {
    	// 设置一个50毫秒的超时
    	ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
    	wg.Add(1)
    	go worker(ctx)
    	time.Sleep(time.Second * 5)
    	cancel() // 通知子goroutine结束
    	wg.Wait()
    	fmt.Println("over")
    }
    
    

    WithValue

    WithValue可以在ctx中写入数据库, 也可以读取数据

    func WithValue(parent Context, key, val interface{}) Context
    

    WithValue返回派生的ctx, 使用方法如下

    package main
    
    import (
    	"context"
    	"fmt"
    	"sync"
    
    	"time"
    )
    
    type TraceCode string
    
    var wg sync.WaitGroup
    
    func worker(ctx context.Context) {
    	key := TraceCode("TRACE_CODE")
    	traceCode, ok := ctx.Value(key).(string) // 获取ctx中存储的"TRACE_CODE"的值
    	if !ok {
    		fmt.Println("invalid trace code")
    	}
    LOOP:
    	for {
    		fmt.Printf("worker, trace code:%s\n", traceCode)
    		time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
    		select {
    		case <-ctx.Done(): // 50毫秒后自动调用
    			break LOOP
    		default:
    		}
    	}
    	fmt.Println("worker done!")
    	wg.Done()
    }
    
    func main() {
    	// 设置一个50毫秒的超时
    	ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
    	// 向ctx添加值 "TRACE_CODE": "12512312234"
    	ctx = context.WithValue(ctx, TraceCode("TRACE_CODE"), "12512312234")
    	wg.Add(1)
    	go worker(ctx) // 传入
    	time.Sleep(time.Second * 5)
    	cancel() // 通知子goroutine结束
    	wg.Wait()
    	fmt.Println("over")
    }
    
    

    注意事项

    • context 以参数的方式传递
    • 如果需要context, 应该吧context当做第一个参数, 且别名为 ctx
    • 如果一个函数需要context, 而你又没有, 又不想使用, 可以传入context.TODO()
    • contextWithValue应当传递必要的数据, 不要什么数据都放里面. 切记不要用他来替代传参的方式
    • context是线程安全的, 可以放心的在多个goroutine中传递
    • 派生函数返回值第二个都是done, 执行可以关闭这个context

        作者:ChnMig

        出处:http://www.cnblogs.com/chnmig/

        本文版权归作者和博客园所有,欢迎转载。转载请在留言板处留言给我,且在文章标明原文链接,谢谢!

        如果您觉得本篇博文对您有所收获,觉得我还算用心,请点击左下角的 [推荐],谢谢!

  • 相关阅读:
    mysql 语句case when
    Hibernate应用SQL查询返回实体类型
    JavaBean 和 Map 之间互相转换
    基于注解风格的Spring-MVC的拦截器
    Spring MVC与表单日期提交的问题
    自适应网页设计(Responsive Web Design)
    JSP页面用EL表达式 输出date格式
    EL表达式中如何截取字符串
    DOCTYPE html PUBLIC 指定了 HTML 文档遵循的文档类型定义
    javascript对table的添加,删除行的操作
  • 原文地址:https://www.cnblogs.com/chnmig/p/15607623.html
Copyright © 2020-2023  润新知