Go语言规范context 类型的key用法示例解析

 更新时间:2023年08月17日 08:47:22   作者:amc  
这篇文章主要为大家介绍了Go语言规范context 类型的key用法示例解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

引言

现在团队里几乎所有的代码都需要经过 Code Review(代码审查)之后,才允许合入主分支。作为 CR 负责人之一,在 CR 中看到了不少不适合的问题,也看到了不少值得学习的点。笔者决定从今天开始,一点一滴地记录这些做法、经验、教训,以飨读者。

这系列文章,我都会先抛出一句话规范说明,然后解释问题的背景,最后再给出正确的规范。

一句话规范

  • 当使用 context.Context 类型保存 KV 对时, key 不能使用原生类型,而应该使用派生类型

问题背景

我们知道,可以利用 context.Context 类型来存一些自定义的键值对——当然了,需要保存新的 context 对象:

ctx = context.WithValue(ctx, someKey, someValue)

很快就可以注意到 key 的参数类型是 any,也就是 Go 1.17 之前的 interface{}。“可能是为了能够使用 int 吧?” ——初学者很可能会这么想。

在实际的 CR 中,可以看到很多人都使用 string 类型作为 key,比如这是一个非常典型的例子:

ctx = context.WithValue(ctx, "openid", userOpenID)

存在问题

如果你使用了 VSCode,并且一键安装了 Go 开发工具包,那么 VSCode 大概率也安装了 golangci-lint 工具。此时,上面的这段代码会喜提一个 warning:

SA1029: should not use built-in type string as key for value; define your own type to avoid collisions (staticcheck)

告警信息虽然是英文,但很容易理解:

  • 不应该使用内建类型作为 KV 中 key 的类型,而应该使用自定义的类型来避免冲突。

为什么要这么做呢?很简单,现代软件都是团队开发的,多模块相互耦合,互相协作。在一个 ctx 对象的整个生命周期中,它需要经过多个逻辑 / 模块的洗礼,每一个模块都可能使用 ctx 来存储相应信息。

假设 user 模块,它使用 ctx 类型缓存了用户的 openid 字段。这个逻辑没什么问题。然后这个 ctx(和代码逻辑)继续往后走,大家约定,就使用这个 "openid" 来存储。

有一天,来了一个紧急需求,比如说要做一个群聊功能,尽可能复用老代码减少开发。或许 group 模块就利用了 user 模块的代码。好巧不巧,从其他团队过来支援的开发同学,也使用了 "openid" 这个 key,来存储群主的 openid。结果绕了一圈,这位同学发现:咦怎么这群主的 openid 老变成别人的 openid?搁这群主轮流做是吧?

解决方法

可能有人觉得:那我把 key 的定义统一收集起来规定不就行了?解决一个问题总有上中下策,这是个办法,但是一个下下下策。软件工程主打一个分而治之,在没有必要的情况下,尽可能避免集中式的管理。

最上策的解决方法简单而言,就是使用自定义的类型作为 key 的类型。我们看看下面的代码:

type myString string
func main() {
    ctx := context.Background()
    ctx = context.WithValue(ctx, "openid", "不是群主")
    ctx = context.WithValue(ctx, myString("openid"), "群主")
    fmt.Println(ctx.Value("openid"))
    fmt.Println(ctx.Value(myString("openid")))
}

两行输出:

不是群主
群主

这就非常清晰了,尽管底层类型相同(都是 string 类型),但是经过 type 定义之后,Go 是作为完全不同的 key 来处理的。针对具体类型自定义 key 类型之后,很好地解决了同名 key 冲突的问题。

不过呢,如果你并不需要使用同一个 key 类型,存储多个不同 value,那么上面的模式还只能说是中策,真正的上策是这么做的:

type myString struct{}

func main() {
    ctx := context.Background()
    ctx = context.WithValue(ctx, "openid", "不是群主")
    ctx = context.WithValue(ctx, myString{}, "群主")

    fmt.Println(ctx.Value("openid"))
    fmt.Println(ctx.Value(myString{}))
}

我还记得我当年第一次看到这个模式的时候,我就是上图的这个表情。是的,struct{} 类型也可以作为 KV 的 key 类型——当然了,也应该定义为自定义类型。

使用 struct{} 的好处可是大大的多:首先,这个类型在 Go 中原则上是不占内存空间和 gc 开销的,可以提升性能;其次,这少了开发者额外 “写一个 key” 的时间(类型往往可以通过 IDE 快速补全),大大提高了敲代码的速度呀。

典型例子

使用 context WithValue 方法,有一个很典型的例子,就是在 ctx 中存入一个 trace ID,用于跟踪整个调用链。那么,我们可以包装一个 traceid 包,比较规范的写法是这样的:

// Package traceid 用于在 context 中维护 trace ID
package traceid
import "context"
// WithTraceID 往 context 中存入 trace ID
func WithTraceID(ctx context.Context, traceID string) context.Context {
    return context.WithValue(ctx, traceIDKey{}, traceID)
}
// TraceID 从 context 中提取 trace ID
func TraceID(ctx context.Context) string {
    v := context.Value(ctx, traceIDKey{})
    id, _ := v.(string)
    return id
}
type traceIDKey struct{}

以上就是Go语言规范context 类型的key用法示例解析的详细内容,更多关于Go context类型key的资料请关注脚本之家其它相关文章!

相关文章

  • Go语言MessageBox用法实例

    Go语言MessageBox用法实例

    这篇文章主要介绍了Go语言MessageBox用法,实例分析了MessageBox提示框的实现与使用技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-02-02
  • Golang使用ttl机制保存内存数据方法详解

    Golang使用ttl机制保存内存数据方法详解

    ttl(time-to-live) 数据存活时间,我们这里指数据在内存中保存一段时间,超过期限则不能被读取到,与Redis的ttl机制类似。本文仅实现ttl部分,不考虑序列化和反序列化
    2023-03-03
  • 基于Golang设计一套可控的定时任务系统

    基于Golang设计一套可控的定时任务系统

    这篇文章主要为大家学习介绍了如何基于Golang设计一套可控的定时任务系统,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2023-07-07
  • Go依赖注入DI工具wire使用详解(golang常用库包)

    Go依赖注入DI工具wire使用详解(golang常用库包)

    依赖注入是指程序运行过程中,如果需要调用另一个对象协助时,无须在代码中创建被调用者,而是依赖于外部的注入,本文结合示例代码给大家介绍Go依赖注入DI工具wire使用,感兴趣的朋友一起看看吧
    2022-04-04
  • go get 和 go install 对比介绍

    go get 和 go install 对比介绍

    go install和go get都是Go语言的工具命令,但它们之间有一些区别。go get:用于从远程代码存储库(如 GitHub)中下载或更新Go代码包。go install:用于编译并安装 Go 代码包,本文go get和go install对比介绍的非常详细,需要的朋友可以参考一下
    2023-04-04
  • go语言实现依赖注入的示例代码

    go语言实现依赖注入的示例代码

    依赖注入和控制反转恰恰相反,它是一种具体的编码技巧,我们不通过 new 的方式在类内部创建依赖类的对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递给类来使用,本文将给大家介绍go语言实现依赖注入,需要的朋友可以参考下
    2024-01-01
  • Go标准库net/http包的实现

    Go标准库net/http包的实现

    net/http是Go官方自带的 HTTP 框架,不用装任何第三方库,本文就来详细的介绍一下Go标准库net/http包的实现,感兴趣的可以了解一下
    2026-03-03
  • Go语言开发中有了net/http为什么还要有gin的原理及使用场景解析

    Go语言开发中有了net/http为什么还要有gin的原理及使用场景解析

    这篇文章主要为大家介绍了Go语言有了net/http标准库为什么还要有gin第三方库的原理及使用场景详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-08-08
  • sublime3+Golang+代码补全的实现

    sublime3+Golang+代码补全的实现

    本文主要介绍了sublime3+Golang+代码补全的实现,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-01-01
  • golang方法中receiver为指针与不为指针的区别详析

    golang方法中receiver为指针与不为指针的区别详析

    这篇文章主要给大家介绍了关于golang方法中receiver为指针与不为指针区别的相关资料,其实最大的区别应该是指针传递的是对像的引用,文中通过示例代码介绍的非常详细,需要的朋友可以参考借鉴,下面来一起看看吧。
    2017-10-10

最新评论