我在围棋的上下文包中犯了一个有趣的错误

2020-08-31 00:03:11

今天,戴夫·切尼在推特上又做了一个围棋突击测试,他问下面的代码是否打印了-6,0,<;nil>;;';或者:

Import(";context";";fmt";)func f(CTX context.Context){context.WithValue(ctx,";foo";,-6)}func main(){ctx:=context.TODO()f(Ctx)fmt.Println(ctx.Value(";foo";))}。

我关注的是将字符串用作上下文键,部分原因是我使用Python等语言的经验。首先,contextpackage的文档中写道:

提供的键必须是可比较的,并且不应该是字符串类型或任何其他内置类型,以避免使用上下文的包之间的冲突。WithValue的用户应该为键定义他们自己的类型。[...]。

像Python这样的语言中的一个传统问题是,两个字符串可能会比较相同,但实际上并不是相同的东西,而且一些代码确实希望您用完全相同的东西来表示它。但是,上下文包并不要求您使用完全相同的键来表示它,只需要使用键的接口值比较相同的键即可。

(因为上下文比较接口值,所以值和类型都必须匹配;仅让两个值具有相同的底层具体类型(例如字符串)并进行相同的比较是不够的。这就是为什么定义自己的字符串类型是避免包之间冲突的可靠方法。)。

所以在我看完所有这些之后,我自信地回答说,这个代码打印了-6。设置该值时使用的字符串不一定与检索该值时使用的字符串相同,但这无关紧要。(#34;foo&34;string=#34;foo&34;string),但这并不重要。然而,这不是代码的问题所在。实际问题是context.WithValue()返回一个设置了值的新上下文,它不会更改它调用的上下文。Dave Cheney的代码就好像.WithValue()改变了当前上下文一样,因为f()忽略了.WithValue()提供的新上下文,并且不向main()返回任何内容。因为原来的context_in main()是调用.value()的,所以它没有";foo&34;键,结果实际上是';<;nil>;';。

代码的这个问题实际上是一个相当有趣的错误,因为就我目前所知,通常的Go样式检查器都没有检测到它。这段代码通过了审核,它不会产生来自errcheck的抱怨,因为我们没有忽略错误返回值,而golangci-lint这样的工具只会抱怨使用内置类型字符串作为.WithValue()中的键。似乎没有注意到我们忽略了来自.WithValue()的关键返回值,这使得它或多或少变成了一个不可操作的东西。

(现在戴夫·切尼已经把这个问题公诸于众了,我怀疑会有人给Staticcheck提供一张支票,因为Staticcheck已经使用内置类型作为关键问题检测到了这个问题。)