Go中的HTTP请求处理概述

在刚刚过去的 2019 gopher china 大会上 context
概念被多次提起,包括很多框架的源码也大量运用了。看得出来 context 在
golang 的世界中是一个非常重要的知识点,所以有必要对 context
有一个基本的使用和认知。官方文档解释和示例都比较详细正规,本着学习的态度翻译一遍加深理解。

概览

context 包定义了 Context 类型,它在 API
边界和进程之间传递截止时间,取消信号和其他请求作用域的值。

服务收到请求应该要创建一个 Context,对服务的响应应该要接受一个
Context。它们之间的函数调用链必须传递 Context,也可以使用
WithCancel,WithDeadline,WithTimeout 或 WithValue 等方法创建派生
Context 替换它。取消 Context 后,也会取消从中派生的所有 Context。

WithCancel,WithDeadline 和 WithTimeout 函数接受
Context(父)并返回派生的 Context(子)和一个 CancelFunc 函数。调用
CancelFunc 函数会取消该派生的子 Context 及其孙子
Context,删除父项对子项的引用,并停止任何关联的计时器。如果没有调用
CancelFunc 会泄漏子项和孙子项,直到父项被取消或计时器触发。 go vet
工具检查是否在所有控制流路径上使用了 CancelFuncs。

使用 Contexts
的程序应遵循这些规则,以保持各个包的接口一致,并启用静态分析工具来检查上下文的传递:

不要将 Contexts 存储在结构类型中;相反,要将 Context
明确地传递给需要它的每个函数。 Context 应该是第一个参数,通常命名为
ctx:

func DoSomething(ctx context.Context, arg Arg) error {
    // ... use ctx ...
}

即使函数允许,也不要传递 nil Context。如果你不确定要使用哪个
Context,请传递 context.TODO。

仅将上下文的值用于 API
边界和进程之间的请求作用域数据,而不是将可选参数传递给函数。

可以将相同的 Context 传递给在不同 goroutine 中运行的函数;Contexts
对于同时使用多个 goroutine 是安全的。

有关服务中使用 Contexts
的示例代码,请参考https://blog.golang.org/context

变量

Canceled 是上下文取消时,通过 Context.Err 返回的错误。

var Canceled = errors.New("context canceled")

DeadlineExceeded 是在上下文超过截止时间时,通过 Context.Err 返回的错误。

var DeadlineExceeded error = deadlineExceededError{}

函数 WithCancel

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

WithCancel 返回带有新 Done channel 的父副本。返回的上下文的 Done channel
在调用返回的取消函数或父上下文的 Done channel
关闭时关闭,取决于谁先发生。

取消此上下文会释放与其相关的资源,因此代码应在此上下文中的操作完成后立即调用
cancel。

示例

此示例演示了使用可取消的上下文来防止 goroutine
泄漏。在示例函数的最后,gen 启动的 goroutine 将返回,并且不会造成
goroutine 泄漏。

package main

import (
    "context"
    "fmt"
)

func main() {
    // gen 在单独的 goroutine 中生成整数并将它们发送到返回的 channel。
    // 一旦消费了生成的整数,gen 的调用者需要取消上下文,从而不会泄漏 gen 启动的内部 goroutine。
    gen := func(ctx context.Context) <-chan int {
        dst := make(chan int)
        n := 1
        go func() {
            for {
                select {
                case <-ctx.Done():
                    return // 返回以致不泄露 goroutine
                case dst <- n:
                    n++
                }
            }
        }()
        return dst
    }

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // 当我们消费完整数后调用取消函数

    for n := range gen(ctx) {
        fmt.Println(n)
        if n == 5 {
            break
        }
    }
}

Run in playground

函数 WithDeadline

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

WithDeadline 返回父上下文的副本,其截止日期调整为不迟于
d。如果父级的截止日期早于 d,则 WithDeadline(parent, d)在语义上等同于
parent。返回的上下文的 Done channel
在超过截止时间后,调用返回的取消函数时或父上下文的 Done channel
关闭时关闭,三者取决于谁先发生。

取消此上下文会释放与其关联的资源,因此代码应在此上下文中的操作完成后立即调用
cancel。

示例

这个例子传递一个带有任意截止时间的上下文来告诉一个阻塞的函数它应该在超时的时候丢弃它的任务。

package main

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

func main() {
    d := time.Now().Add(50 * time.Millisecond)
    ctx, cancel := context.WithDeadline(context.Background(), d)

    // 即使 ctx 将要过期,在任何情况下最好也要调用它的取消函数。
    // 如果不这样做,可能会使上下文及其父级的活动时间超过必要时间。
    defer cancel()

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

}

Run in playground

函数 WithTimeout

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

WithTimeout 返回 WithDeadline(parent, time.Now().Add(timeout))。

取消此上下文会释放与其关联的资源,因此代码应在此上下文中运行的操作完成后立即调用
cancel:

func slowOperationWithTimeout(ctx context.Context) (Result, error) {
    ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
    defer cancel()  // 如果 slowOperation 在超时之前完成,则释放资源
completes before timeout elapses
    return slowOperation(ctx)
}
示例

此示例传递具有超时的上下文,以告知一个阻塞的函数在超时后它应该丢弃它的任务。

package main

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

func main() {
    // 传递一个带超时的上下文,以告知一个阻塞的函数在超时后它应该丢弃它的任务。
    ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
    defer cancel()

    select {
    case <-time.After(1 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err()) //打印 "context deadline exceeded"
    }

}

Run in playground

类型 CancelFunc

CancelFunc 通知一个操作丢弃它的任务。 CancelFunc
不等待任务停止。第一次调用后,CancelFunc 的后续调用将失效。

type CancelFunc func()

类型 Context

一个 Context 可以跨 API 边界传递截止日期,取消信号和其他值。

Context 的方法可以由多个 goroutine 同时调用。

type Context interface {
        // Deadline 返回完成的任务的时间,即取消此上下文的时间。
        // 如果没有设置截止时间,Deadline 返回 ok == false。
        // 对截止日期的连续调用返回相同的结果。
        Deadline() (deadline time.Time, ok bool)

        // 当任务完成时,即此上下文被取消,Done 会返回一个关闭的channel。
        // 如果此上下文一直不被取消,Done 返回 nil。对 Done 的连续调用会返回相同的值。
        //
        // 当取消函数被调用时,WithCancel 使 Done 关闭; 
        // 在截止时间到期时,WithDeadline 使 Done 关闭;
        // 当超时的时候,WithTimeout使 Done 关闭。
        //
        // Done 可以使用 select 语句:
        //
        //  // Stream 使用 DoSomething 生成值并将它们发送到 out,
        //  // 直到 DoSomething 返回错误或 ctx.Done 关闭。
        //  func Stream(ctx context.Context, out chan<- Value) error {
        //      for {
        //          v, err := DoSomething(ctx)
        //          if err != nil {
        //              return err
        //          }
        //          select {
        //          case <-ctx.Done():
        //              return ctx.Err()
        //          case out <- v:
        //          }
        //      }
        //  }
        //
        // 查看 https://blog.golang.org/pipelines 获得更多关于怎么使用 Done channel 去取消的例子
        Done() <-chan struct{}

        // 如果 Done 尚未关闭,则 Err 返回 nil。
        // 如果 Done 关闭,Err 会返回一个非nil的错误,原因:
        // 如果上下文被取消,则调用 Canceled;
        // 如果上下文的截止时间已过,则调用 DeadlineExceeded。
        // 在 Err 返回非 nil 错误后,对 Err 的连续调用返回相同的错误。
        Err() error

        // Value 返回与此上下文关联的 key 的值,如果没有值与 key 关联,则返回nil。使用相同的 key 连续调用 Value 会返回相同的结果。
        //
        // 仅将上下文的值用于API边界和进程之间的请求作用域数据,而不是将可选参数传递给函数。
        //
        // key 标识上下文中的特定值。
        // 在上下文中存储值的函数通常在全局变量中分配一个 key,然后使用该 key 作为 context.WithValue 和 Context.Value 的参数。
        // key 可以是支持比较的任何类型
        // 包应该将 key 定义为非导出类型以避免冲突。
        //
        // 定义 Context key 的包应该为使用该 key 存储的值提供类型安全的访问:
        //
        //  // 包使用者定义一个存储在上下文中的 User 类型。
        //  package user
        //
        //  import "context"
        //
        //  // User 是上下文中值的类型。
        //  type User struct {...}
        //
        //  // key 是此程序包中定义的 key 的非导出类型。
        //  // 这可以防止与其他包中定义的 key 冲突。
        //  type key int
        //
        //  // userKey 是上下文中 user.User 值的 key。它是不可以被导出的。
        //  // 客户端使用 user.NewContext 和 user.FromContext 而不是直接使用 key。
        //  var userKey key
        //
        //  // NewContext 返回一个带有值为 u 的新的上下文。
        //  func NewContext(ctx context.Context, u *User) context.Context {
        //      return context.WithValue(ctx, userKey, u)
        //  }
        //
        //  // FromContext 返回存储在 ctx 中的 User 值(如果有的话)。
        //  func FromContext(ctx context.Context) (*User, bool) {
        //      u, ok := ctx.Value(userKey).(*User)
        //      return u, ok
        //  }
        Value(key interface{}) interface{}
}

函数 Background

func Background() Context

Background 返回一个非 nil 的空
Context。它永远不会被取消,没有值,也没有截止时间。它通常由 main
函数初始化和测试使用,并作为请求的顶级 Context。

函数 TODO

func TODO() Context

TODO 返回一个非 nil 的空 Context。当不清楚使用哪个 Context
或者它还不可用时(因为周围的函数尚未扩展为接受 Context
参数),代码应该使用 context.TODO。

函数 WithValue

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

WithValue 返回父级的副本,其中与 key 关联的值为 val。

仅将上下文的值用于 API
边界和进程之间的请求作用域数据,而不是将可选参数传递给函数。

提供的 key
必须是可比较的,不应该是字符串类型或任何其他内置类型,以避免使用上下文的包之间产生冲突。
WithValue 的使用者应该为 keys 定义他们自己的自定义类型。为了避免在分配
interface{}时指定,上下文的 keys 通常具有具体类型 struct
{}。或者,导出的上下文的 key 变量的静态类型应该是指针或接口。

示例

此示例展示如何将值传递给上下文以及如何检索它(如果存在)。

package main

import (
    "context"
    "fmt"
)

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"))

}

Run in playground

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容