Go Context解读与实践

简介: Go Context解读与实践Go Context解读与实践Go Context解读与实践Go Context解读与实践

[TOC]

1 Context的初衷

In Go servers, each incoming request is handled in its own goroutine. Request handlers often start additional goroutines to access backends such as databases and RPC services. The set of goroutines working on a request typically needs access to request-specific values such as the identity of the end user, authorization tokens, and the request's deadline. When a request is canceled or times out, all the goroutines working on that request should exit quickly so the system can reclaim any resources they are using.

如上图,很多时候,尤其是分布式架构环境中,一个请求到达服务端后,会被拆分为若干个请求转发至相关的服务单元处理,如果一个服务单元返回结束信息(通常是错误造成的),其他服务单元都应该及时结束该请求的处理,以避免资源浪费在无意义的请求处理上。

正是因于此,Google开发了context包,提供对使用一组相同上下文(context)的goroutine的管理,及时结束无意义的请求处理goroutine。

1.1 如何下发取消(结束)命令?

这就成为一个亟待解决的问题。我们都知道在Go语言中,提倡“通过通信共享内存资源”,那么下发取消命令最简单直接的办法就是创建一个结束通道(done channel),各个服务单元(goroutine)根据channel来获取结束命令。

1.2 如何根据channel来获取结束命令呢?

So easy,读值呗!有值就表示结束啊!

哈哈,事实并非如此,通道有非缓冲通道和缓冲通道,应该选择哪一种?通道中写什么值呢?是有值即结束还是根据值判断呢?

1.2.1 使用非缓冲通道

type Result struct {
    status bool
    value int
}

func thirtyAPI(done chan struct{}, num int, dst chan Result){
    fmt.Printf("我正在调用第三方API:%d\n", num)
    tc := make(chan int, 1)
    i := 0
    for {
        // 业务逻辑代码
        select {
        case <-done:
            fmt.Printf("%d: 我要结束了,Bye ThirtyAPI\n", num)
            dst <- Result{status:false, value:num}
            return
        case tc <- i:
            if num == 3 {
                time.Sleep(time.Second)
                dst <- Result{status: false, value:num}
                return
            }
            i = <-tc
            i++
        }
    }
}

func eg3() {
    dst := make(chan Result, 5)
    done := make(chan struct{})
    for i:=0; i<5; i++{
        go thirtyAPI(done, i, dst)
    }

    for result := range dst {
        if result.status == false {
            // 第一个false到来时,必须发布取消命令
            fmt.Printf("%d: I met error\n", result.value)
            done <- struct{}{}
            break
        }
    }
}

func main() {
    eg3()
    time.Sleep(time.Second*5)
    fmt.Printf("Current Active Goroutine Num: %d\n",runtime.NumGoroutine())
}

分析一下运行结果,我们发现只有一个goroutine接收到结束命令,其他的goroutine都未结束运行。这是因为代码中使用非缓冲通道造成的。

1.2.2 使用缓冲通道

type Result struct {
    status bool
    value int
}

func thirtyAPI(done chan struct{}, num int, dst chan Result){
    fmt.Printf("我正在调用第三方API:%d\n", num)
    tc := make(chan int, 1)
    i := 0
    for {
        // 业务逻辑代码
        select {
        case <-done:
            fmt.Printf("%d: 我要结束了,Bye ThirtyAPI\n", num)
            dst <- Result{status:false, value:num}
            return
        case tc <- i:
            if num == 3 {
                time.Sleep(time.Second)
                dst <- Result{status: false, value:num}
                return
            }
            if num == 4 {
                dst <- Result{status: true, value: num}
                return
            }
            i = <-tc
            i++
        }
    }
}

func eg4() {
    dst := make(chan Result, 5)
    done := make(chan struct{}, 5)
    for i:=0; i<5; i++{
        go thirtyAPI(done, i, dst)
    }

    for result := range dst {
        if result.status == false {
            // 第一个false到来时,必须发布取消命令
            fmt.Printf("%d: I met error\n", result.value)
            done <- struct{}{}
            done <- struct{}{}
            done <- struct{}{}
            done <- struct{}{}
            done <- struct{}{}
            break
        } else {
            fmt.Printf("%d: I have success\n", result.value)
        }
    }
}

func main() {
    eg4()

    time.Sleep(time.Second*5)
    fmt.Printf("Current Active Goroutine Num: %d\n",runtime.NumGoroutine())
}

分析一下结果,令人欣慰的是所有的goroutine都结束了,但是有两点缺陷,第一,写了五行done <- struct{}{}是不是很垃圾?第二,在代码中实际受done通道指示结束运行的goroutine只有三条,是不是资源浪费?

其实,最致命的问题是采用缓存通道并不能真正的结束所有该退出的goroutine,想一想,如果在thirtyAPI中继续调用其他API怎么办?我们并不能在预知有多少个goroutine在运行!!!

1.2.3 借助closed channel特性

在1.2.2中,我们知道我们无法预知实际有多少goroutine该执行结束,因而无法确定done channel的长度。

问题似乎不可解,我们不妨换个思路,既然写这条路走不通,那么可否不写呢?

A receive operation on a closed channel can always proceed immediately, yielding the element type's zero value.

当需要下发取消命令时,下发端只需要关闭done channel即可,这样所有需要退出的goroutine都能从done channel读取零值,也就都退出啦!

type Result struct {
    status bool
    value int
}

func thirtyAPI(done chan struct{}, num int, dst chan Result){
    fmt.Printf("我正在调用第三方API:%d\n", num)
    tc := make(chan int, 1)
    i := 0
    for {
        // 业务逻辑代码
        select {
        case <-done:
            fmt.Printf("%d: 我要结束了,Bye ThirtyAPI\n", num)
            dst <- Result{status:false, value:num}
            return
        case tc <- i:
            if num == 3 {
                time.Sleep(time.Second)
                dst <- Result{status: false, value:num}
                return
            }
            if num == 4 {
                dst <- Result{status: true, value: num}
                return
            }
            i = <-tc
            i++
        }
    }
}

func eg4() {
    dst := make(chan Result, 5)
    done := make(chan struct{}, 5)
    defer close(done)
    for i:=0; i<5; i++{
        go thirtyAPI(done, i, dst)
    }

    for result := range dst {
        if result.status == false {
            // 第一个false到来时,必须发布取消命令
            fmt.Printf("%d: I met error\n", result.value)
            break
        } else {
            fmt.Printf("%d: I have success\n", result.value)
        }
    }
}

func main() {
    eg4()

    time.Sleep(time.Second*5)
    fmt.Printf("Current Active Goroutine Num: %d\n",runtime.NumGoroutine())
}

其实,Context也正是基于closed channel这个特性实现的。

2 解读Context

2.1 Context接口

type Context interface {
    // Done returns a channel that is closed when this Context is canceled
    // or times out.
    Done() <-chan struct{}

    // Err indicates why this context was canceled, after the Done channel
    // is closed.
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value associated with key or nil if none.
    Value(key interface{}) interface{}
}
  • Done():该方法返回一个channel,该channel扮演取消信号角色,当该channel被关闭时,所有应该退出的goroutine均可从Done()读值,故而结束执行。
  • Err():打印错误信息,解释为什么context被取消。
  • Deadline(): 返回该context的截止时间,依赖函数可以根据该时间节点为IO操作设定超时时间。
  • Value(key): 该方法根据key返回context对应的属性值,这些值在goroutine之间共享。

2.1.1 基于Context改写1.2.3代码

type Result struct {
    status bool
    value int
}

func thirtyAPI(ctx context.Context, num int, dst chan Result){
    fmt.Printf("我正在调用第三方API:%d\n", num)
    tc := make(chan int, 1)
    i := 0
    for {
        // 业务逻辑代码
        select {
        case <-ctx.Done():
            fmt.Printf("%d: 我要结束了,Error信息: %s\n", num, ctx.Err())
            dst <- Result{status:false, value:num}
            return
        case tc <- i:
            if num == 3 {
                time.Sleep(time.Second)
                dst <- Result{status: false, value:num}
                return
            }
            if num == 4 {
                dst <- Result{status: true, value: num}
                return
            }
            i = <-tc
            i++
        }
    }
}

func eg4() {
    dst := make(chan Result, 5)

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    for i:=0; i<5; i++{
        go thirtyAPI(ctx, i, dst)
    }

    for result := range dst {
        if result.status == false {
            // 第一个false到来时,必须发布取消命令
            fmt.Printf("%d: I met error\n", result.value)
            break
        } else {
            fmt.Printf("%d: I have success\n", result.value)
        }
    }
}

func main() {
    eg4()

    time.Sleep(time.Second*5)
    fmt.Printf("Current Active Goroutine Num: %d\n",runtime.NumGoroutine())
}

2.1.2 Deadline Demo

func gofunc(ctx context.Context) {
    d, _ := ctx.Deadline()

    for {
        select {
        case <-time.After(1 * time.Second):
            fmt.Printf("Deadline:%v, Now:%v\n",d, time.Now())
        case <-ctx.Done():
            fmt.Println(ctx.Err())
            return
        }
    }
}

func main() {
    d := time.Now().Add(5 * time.Second)
    ctx, cancel := context.WithDeadline(context.Background(), d)
    fmt.Printf("Deadline:%v\n", d)
    defer cancel()
    go gofunc(ctx)

    time.Sleep(time.Second*10)
    fmt.Printf("Current Active Goroutine Num: %d\n",runtime.NumGoroutine())
}

2.1.3 Value Demo

func main() {
    type favContextKey string

    f := func(ctx context.Context, k favContextKey) {
        if v := ctx.Value(k); v != nil {
            fmt.Println("found value:", v)
            return
        }
        fmt.Println("key not found:", k)
    }

    k := favContextKey("language")
    ctx := context.WithValue(context.Background(), k, "Go")

    f(ctx, k)
    f(ctx, favContextKey("color"))

}

2.2 context的函数与Context接口关系

2.2.1 Background vs TODO

3 答疑与最佳实践

3.1 答疑

3.1.1 Context衍生树

The context package provides functions to derive new Context values from existing ones. These values form a tree: when a Context is canceled, all Contexts derived from it are also canceled.

WithCancel and WithTimeout return derived Context values that can be canceled sooner than the parent Context.

对子context的cancel操作,只会影响该子context及其子孙,并不影响其父辈及兄弟context。

package main

import (
    "context"
    "fmt"
    "runtime"
    "time"
)

func child(ctx context.Context, p, c int) {
    fmt.Printf("Child Goroutine:%d-%d\n", p, c)
    select {
    case <-ctx.Done():
        fmt.Printf("Child %d-%d: exited reason: %s\n", p, c, ctx.Err())
    }
}

func parent(ctx context.Context, p int) {
    fmt.Printf("Parent Goroutine:%d\n", p)
    cctx, cancel := context.WithCancel(ctx)
    defer cancel()
    for i:=0; i<3; i++ {
        go child(cctx, p, i)
    }

    if p==3 {
        return
    }

    select {
    case <- ctx.Done():
        fmt.Printf("Parent %d: exited reason: %s\n", p, ctx.Err())
        return
    }
}

func main() {

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    for i:=0; i<5; i++ {
        go parent(ctx, i)
    }

    time.Sleep(time.Second*3)
    fmt.Printf("Current Active Goroutine Num: %d\n",runtime.NumGoroutine())
}

3.1.2 上下层Goroutine

A Context does not have a Cancel method for the same reason the Done channel is receive-only: the function receiving a cancelation signal is usually not the one that sends the signal. In particular, when a parent operation starts goroutines for sub-operations, those sub-operations should not be able to cancel the parent. Instead, the WithCancel function (described below) provides a way to cancel a new Context value.

Context自身是没有cancel方法的,主要原因是Done channel是只读通道。一般而言,接收取消信号的方法不应该是下发取消信号的。故而,父Goroutine不应该被其创建的子Goroutine取消。

但是,如果在子Goroutine中调用cancel函数,是不是也能取消父Goroutine呢?

package main

import (
    "context"
    "fmt"
    "runtime"
    "time"
)

func SubGor(ctx context.Context, p, c int, cancel context.CancelFunc) {
    fmt.Printf("Child Goroutine:%d-%d\n", p, c)
    if p==2 && c==2 {
        cancel()
    }

    select {
    case <-ctx.Done():
        fmt.Printf("Child %d-%d: exited reason: %s\n", p, c, ctx.Err())
    }
}

func Gor(ctx context.Context, p int,cancel context.CancelFunc) {
    fmt.Printf("Goroutine:%d\n", p)
    for i:=0; i<3; i++ {
        go SubGor(ctx, p, i, cancel)
    }


    select {
    case <- ctx.Done():
        fmt.Printf("Parent %d: exited reason: %s\n", p, ctx.Err())
        return
    }
}


func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    for i:=0; i<3; i++ {
        go Gor(ctx, i, cancel)
    }

    time.Sleep(time.Second*3)
    fmt.Printf("Current Active Goroutine Num: %d\n",runtime.NumGoroutine())
}

由示例代码可知,如果在子Goroutine调用cancel函数时,一样可以关闭父类Goroutine。但是,不建议这么做,因为它不符合逻辑,cancel应该交给具有cancel权限的人去做,千万不要越俎代庖。

Question:有没有想过context cancel的执行逻辑是什么样子的?

3.1.3 如果goroutine func中不做ctx.Done处理,是不是不会被取消呢?

package main

import (
    "context"
    "fmt"
    "runtime"
    "time"
)

func dealDone(ctx context.Context, i int){
    fmt.Printf("%d: deal done chan\n", i)
    select{
    case <-ctx.Done():
        fmt.Printf("%d: exited, reason: %s\n", i, ctx.Err())
        return
    }
}

func notDealDone(ctx context.Context, i int) {
    fmt.Printf("%d: not deal done chan\n",i)
    for{
        i++
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    for i:=0; i<5; i++ {
        if i==4 {
            go notDealDone(ctx, i)
        } else {
            go dealDone(ctx, i)
        }
    }
    time.Sleep(time.Second*3)
    fmt.Println("Execute Cancel Func")
    cancel()

    time.Sleep(time.Second*3)
    fmt.Printf("Current Active Goroutine Num: %d\n",runtime.NumGoroutine())
}

3.2 最佳实践

Programs that use Contexts should follow these rules to keep interfaces consistent across packages and enable static analysis tools to check context propagation:

  • Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx:
func DoSomething(ctx context.Context, arg Arg) error {
    // ... use ctx ...
}
  • Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use.
  • Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.
  • The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines.

4 参考链接

相关文章
|
1月前
|
负载均衡 算法 数据库连接
Go语言性能优化实践:案例分析与解决方案
【2月更文挑战第18天】本文将通过具体的案例分析,探讨Go语言性能优化的实践方法和解决方案。我们将分析几个典型的性能瓶颈问题,并详细介绍如何通过优化代码、调整并发模型、改进内存管理等方式来提升程序的性能。通过本文的学习,读者将能够掌握一些实用的Go语言性能优化技巧,为实际项目开发中的性能优化工作提供指导。
|
1月前
|
运维 网络协议 安全
长连接网关技术专题(十):百度基于Go的千万级统一长连接服务架构实践
本文将介绍百度基于golang实现的统一长连接服务,从统一长连接功能实现和性能优化等角度,描述了其在设计、开发和维护过程中面临的问题和挑战,并重点介绍了解决相关问题和挑战的方案和实践经验。
78 1
|
1月前
|
Java Go C++
Go语言中的面向对象编程实践
【2月更文挑战第10天】本文将深入探讨在Go语言中如何进行面向对象编程实践。我们将了解如何在Go中实现封装、继承和多态,以及如何利用结构体、接口和方法来构建健壮和可维护的对象导向程序。通过实际代码示例,我们将更好地理解Go的OOP特性,并学习如何有效地运用它们。
|
2月前
|
Go 开发者
Go语言中的错误处理与异常机制:实践与最佳策略
【2月更文挑战第7天】Go语言以其独特的错误处理机制而闻名,它鼓励显式错误检查而不是依赖于异常。本文将探讨错误处理与异常机制在Go语言中的实际应用,并分享一些最佳实践,帮助开发者编写更加健壮和易于维护的Go代码。
|
1月前
|
Kubernetes Go 开发者
Go语言与Docker容器结合的实践应用与案例分析
【2月更文挑战第23天】本文通过分析实际案例,探讨了Go语言与Docker容器技术结合的实践应用。通过详细阐述Go语言在容器化环境中的开发优势,以及Docker容器技术在Go应用部署中的重要作用,本文旨在为读者提供Go语言与Docker容器结合的具体实现方法和实际应用场景。
|
1月前
|
安全 中间件 Go
Go语言Web服务性能优化与安全实践
【2月更文挑战第21天】本文将深入探讨Go语言在Web服务性能优化与安全实践方面的应用。通过介绍性能优化策略、并发编程模型以及安全加固措施,帮助读者理解并提升Go语言Web服务的性能表现与安全防护能力。
|
1月前
|
存储 算法 Go
泛型在Go语言中的引入与实践
【2月更文挑战第19天】Go语言1.18版本引入了对泛型的原生支持,这一特性使得开发者能够编写更加通用和灵活的代码。本文将深入探讨Go语言中泛型的引入背景、使用方法和实践案例,帮助读者了解并应用这一强大的编程工具。
|
1月前
|
Kubernetes Go 开发者
Go语言在容器化环境中的实践
【2月更文挑战第15天】随着容器技术的兴起,Go语言在容器化环境中的实践逐渐受到关注。本文探讨了Go语言如何与容器技术相结合,发挥其在容器化环境中的优势,包括轻量级部署、高并发处理、快速构建和部署等方面的特点,并通过实例展示了Go语言在容器化环境中的实践应用。
|
1月前
|
Java 编译器 Go
Go语言内存管理优化实践
【2月更文挑战第11天】随着Go语言在各个领域的应用日益广泛,对其性能的要求也越来越高。内存管理作为影响程序性能的关键因素之一,对Go语言的优化显得尤为重要。本文将深入探讨Go语言的内存管理机制,并提供一系列实用的内存优化策略,帮助开发者更加高效地利用内存资源,提升Go程序的整体性能。
|
2月前
|
编译器 Go 持续交付
Go语言模块导入的实践与技巧:提升代码重用与模块化开发效率
【2月更文挑战第9天】在Go语言中,模块导入是实现代码重用和模块化开发的关键环节。本文将介绍Go语言中模块导入的实践与技巧,包括本地模块的导入、远程模块的导入、导入路径的解析与重定向、导入别名与包的重命名等,旨在帮助读者更加高效地进行Go语言的项目开发。