golang context 操作

context 描述
go 1.7 版本之后加入一个新的标准库context 它定义了Context 类型 ,专门用来简化对于处理单个请求的多个goroutine之间与请求域的数据、
取消信号、截止时间等相关操作,这些操作可能涉及到多个API调用。
 
对服务器传入的请求应该创建上下文,而对服务器的传出调用的应该接收上下文,它们之间的函数调用链必须传递上下文,或者可以使用
WithCancel、WithDeadline 、WithTimeout、WithValue 创建的派生上下文,当一个上下文被取消时,它派生的所有上下文也被取消。
 

background 和 todo 本质上都是emptyCtx 结构类型,是一个不可取消,没有设置截止时间, 没有携带任何值的Context
 
context 使用场景
在go http 包的server中 每一个请求在都有一个对应的goroutine去处理,请求处理函数通常会启动额外的goroutine用来访问后端服务例如数据库和RPC服务
用来处理一个请求的goroutine通常需要访问一些请求特定的数据,例如终端用户的身份认证信息,验证相关的token、请求的截止时间
当一个请求取消或者超时,所有用来处理该请求的goroutine 都应该迅速退出,然后系统才会释放这些goroutine占用资源
 
Background()TODO()
 
go 内置两个函数: Background() 和 TODO()这两个函数分别返回一个实现了 Context 接口 的background 和todo 我们代码中最开始都是以两个内置的上下文
对象作为最顶层的partent context 衍生出更多的子上下文对象

Background 主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context 也就是根Context

TODO() 它目前还不知道具体使用场景,如果我们不知道该使用什么context 的时候,可以使用这个
 
没有context 功能时实现goroutine 通知退出操作 
1. 通过变量方式通知的
package main
var wg sync.WaitGroup

var notify bool

func demo(){
    var i int
    defer wg.Done()
    
    for {
        i++
        fmt.Printf("判断的方式 执行%v次\n",i)
        time.Sleep(time.Millisecond * 500)
        if notify{
            break
        }
    }
}
// 使用判断参数退出子goroutine

func main(){
    wg.Add(1)
    go demo()
    time.Sleep(time.Second * 3 )
    notify = true
    fmt.Println("判断方式退出子goroutine")
wg.wait() }

2. 通过channel 方式通知子goroutine 退出

package main
var exitChan chan bool 
var wg sync.WaitGroup
func demo1(){
    defer wg.Done()
    var i int
    // FORLOOP 跳出指定循环体
    EXITFOR:
    for {
        i++
        fmt.Printf("channel方法, 执行%v次\n",i)
        time.Sleep(time.Millisecond * 500 )
        select {
        case <- exitChan:
            break EXITFOR
        default:
        }
    }
}

func main(){
    exitChan = make(chan bool,1)
    wg.Add(1)
    go demo1()
    time.Sleep(time.Second * 3 )
    exitChan <- true
    fmt.Println("channel方法退出子goroutine")
    wg.Wait()
}

以上的使用方法比较单一,如果涉及到嵌套的子goroutine 时就无法通知嵌套里面的子goroutine 这时就可以利用context 方法了

context.WithCancel

package main
import (
    "context"
    "fmt"
    "sync"
    "time"
)


var wg sync.WaitGroup

// context 方法退出子goroutine
func woker01(ctx context.Context){
    defer wg.Done()
    go woker02(ctx)
    var i int
    // FORLOOP 跳出指定循环体
    EXITFOR:
    for {
        i++
        fmt.Printf("woker02 context方法, 执行%v次\n",i)
        time.Sleep(time.Millisecond * 500 )
        select {
        case <- ctx.Done():
            break EXITFOR
        default:
        }
    }
}

func woker01(ctx context.Context){
    defer wg.Done()
    var i int
    // FORLOOP 跳出指定循环体
    EXITFOR:
    for {
        i++
        fmt.Printf("woker01 context方法, 执行%v次\n",i)
        time.Sleep(time.Millisecond * 500 )
        select {
        case <- ctx.Done():
            break EXITFOR
        default:
        }
    }
}

func main(){
    ctx, cancel := context.WithCancel(context.Background())
    wg.Add(1)
    go woker02(ctx)
    time.Sleep(time.Second * 3 )
    fmt.Println("context 退出子goroutine")
    cancel()
    wg.wait()
}

context WithDeadline

尽管ctx会过期,但是在任何情况下调用它的cancel 函数都是很好的实践如果不这样做,可能会使上下文及其父类存活的时间超过必要的时间
package main

import (
    "context"
    "fmt"
    "sync"
    "time"
)

/*
context WithDeadline
*/

var wg sync.WaitGroup
func contextDeadline(){
    d := time.Now().Add(50 * time.Millisecond)
    ctx, cancel := context.WithDeadline(context.Background(),d)

    defer cancel()
    select{
    case <- time.After(1 * time.Second):
        fmt.Println("zhangsan")
    case <- ctx.Done():
        fmt.Println(ctx.Err())
    
    }
}

func main(){
    contextDeadline()

}
 
context.WithTimeout   和 context.WithDeadline 类似都是超时退出
package main
import (
    "context"
    "fmt"
    "sync"
    "time"
)

var wg sync.WaitGroup
func woker(ctx context.Context){
exitfor:
    for {
        fmt.Println("connect mysql databases.....")
        time.Sleep(time.Millisecond * 10)
        select{
        case <-ctx.Done():
            break exitfor
        default:

        }
    }
    fmt.Println("woker done!")
    wg.Done()

}

func contextTimeout(){
    ctx, cancel :=context.WithTimeout(context.Background(),time.Millisecond * 50 )
    wg.Add(1)
    go woker(ctx)
    time.Sleep(time.Second * 5)
    cancel()
    wg.Wait()
    fmt.Println("runnning close")
}
func main(){
    contextTimeout()
}
 
context.WithValue
仅对API和进程之间传递请求域的数据使用上下文值,而不是使用它传递可选参数,所提供的键必须可比较的并且不应该是string 类型或任何其他类型,以免使用上下文在包之间发生冲突。
WithValue 的用户应该为键定义自己的类型,为了避免在分配给interface{} 时进行分配,上下文键通常具有具体类型struct{} 。或者导出的上下文呢关键变量的静态类型应该是指针或接口
 
package main

import (
    "context"
    "fmt"
    "sync"
    "time"
)

type TraceCode string
var wg sync.WaitGroup

/*
context.WithValue 
*/
func wokerValue(ctx context.Context){
    key := TraceCode("TRACE_CODE") 
    traceCode , ok := ctx.Value( key).(string)
    if !ok{
        fmt.Println("invalid trace code")
    }
Loop:
    for {
        fmt.Printf("woker, trace code:%s\n",traceCode)
        time.Sleep(time.Millisecond * 10)
        select{
        case <- ctx.Done():
            break Loop
        default:

        }
    }
    fmt.Println("workerValue")
    wg.Done()

}

func contextValue(){
    ctx, cancel := context.WithTimeout(context.Background(),time.Millisecond*50)
    ctx = context.WithValue(ctx,TraceCode("TRACE_CODE"),"1233221112332")
    wg.Add(1)
    go wokerValue(ctx)
    time.Sleep(time.Second *5 )
    cancel()
    wg.Wait()
    fmt.Println("context value done!")
}
func main(){

    contextValue()


}

 

使用context 的注意事项
1. 推荐以参数的方式显示传递Context
2. 以Context作为参数的函数方法,应该把Context作为第一个参数。
3. 给一个函数方法传递Context的时候,不要传递nil ,如果不知道传递什么,就使用context.TODO()
4. Context的Value相关方法应该传递请求域的必要参数,不应该用于传递可选参数
5. Context是线程安全的,可以放心的在多个goroutine中传递
 
posted @ 2023-11-04 17:29  扛把子修BUG  阅读(301)  评论(0)    收藏  举报