Clarification in the description of context.WithValue()

449 views
Skip to first unread message

Amit Saha

unread,
Jan 6, 2021, 5:31:50 PM1/6/21
to golang-nuts
In the description of context.WithValue(), we have:

The provided key must be comparable and should not be of type string
or any other built-in type to avoid collisions between packages using
context. Users of WithValue should define their own types for keys. To
avoid allocating when assigning to an interface{}, context keys often
have concrete type struct{}.

I am wondering if someone can explain what exactly comparable means here?

In other languages and Go
(https://golang.org/ref/spec#Comparison_operators), comparable usually
means being able to compare two values for equality/greater/lesser.

Thanks,
Amit.

Axel Wagner

unread,
Jan 6, 2021, 5:34:30 PM1/6/21
to Amit Saha, golang-nuts
The linked spec section actually contradicts that:

The equality operators == and != apply to operands that are comparable. The ordering operators <, <=, >, and >= apply to operands that are ordered
 
(emphasis from the spec). So, the definition of "comparable" is "can be used with == and !=" and what exactly that means is listed below that quote.

Hope that helps


Thanks,
Amit.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/CANODV3kUmtJvG9Lfys_HXfedwnm54neA6uub6cY1aiz%2B9w5DUA%40mail.gmail.com.

Amit Saha

unread,
Jan 6, 2021, 5:39:56 PM1/6/21
to Axel Wagner, golang-nuts
On Thu, Jan 7, 2021 at 9:33 AM Axel Wagner
<axel.wa...@googlemail.com> wrote:
>
> On Wed, Jan 6, 2021 at 11:31 PM Amit Saha <amits...@gmail.com> wrote:
>>
>> In the description of context.WithValue(), we have:
>>
>> The provided key must be comparable and should not be of type string
>> or any other built-in type to avoid collisions between packages using
>> context. Users of WithValue should define their own types for keys. To
>> avoid allocating when assigning to an interface{}, context keys often
>> have concrete type struct{}.
>>
>> I am wondering if someone can explain what exactly comparable means here?
>>
>> In other languages and Go
>> (https://golang.org/ref/spec#Comparison_operators), comparable usually
>> means being able to compare two values for equality/greater/lesser.
>
>
> The linked spec section actually contradicts that:
>
>> The equality operators == and != apply to operands that are comparable. The ordering operators <, <=, >, and >= apply to operands that are ordered.
>
>
> (emphasis from the spec). So, the definition of "comparable" is "can be used with == and !=" and what exactly that means is listed below that quote.
>
> Hope that helps
>

Thanks for pointing it out. I missed that - we have two things here,
ordered and comparison here then.

Is it fair to say then that any user-defined type here is acceptable as a key?

Axel Wagner

unread,
Jan 6, 2021, 5:49:10 PM1/6/21
to Amit Saha, golang-nuts
On Wed, Jan 6, 2021 at 11:39 PM Amit Saha <amits...@gmail.com> wrote:
Is it fair to say then that any user-defined type here is acceptable as a key?

No. You can create a user-defined type out of *any* type T, by just writing `type X T`. In particular, you can write `type X func()` and get an incomparable user-defined type.

I highly recommend sticking to the recommendation from the docs: Use `type ctxKey struct{}` as a key type and `ctxKey{}` as the key - it's comparable and the only value equal to `ctxKey{}` is `ctxKey{}`, which gives exactly the desirable semantics. And as it's zero-sized, it also doesn't occur any allocation (at least with gc, all zero-sized objects have the same address). You could also use `[0]int` or similar, but `struct{}` is very common for use-cases like this.

Amit Saha

unread,
Jan 6, 2021, 5:53:59 PM1/6/21
to Axel Wagner, golang-nuts
On Thu, Jan 7, 2021 at 9:48 AM Axel Wagner
<axel.wa...@googlemail.com> wrote:
>
>
>
> On Wed, Jan 6, 2021 at 11:39 PM Amit Saha <amits...@gmail.com> wrote:
>>
>> Is it fair to say then that any user-defined type here is acceptable as a key?
>
>
> No. You can create a user-defined type out of *any* type T, by just writing `type X T`. In particular, you can write `type X func()` and get an incomparable user-defined type.
>
> I highly recommend sticking to the recommendation from the docs: Use `type ctxKey struct{}` as a key type and `ctxKey{}` as the key - it's comparable and the only value equal to `ctxKey{}` is `ctxKey{}`, which gives exactly the desirable semantics. And as it's zero-sized, it also doesn't occur any allocation (at least with gc, all zero-sized objects have the same address). You could also use `[0]int` or similar, but `struct{}` is very common for use-cases like this.

Thanks. If I wanted to have a struct like this

type myReqContext struct {
ID1 string
ID2 string
}

Would the correct key and value be: myReqContext{}, myReqContext{ID1:
"abcd", ID2: "defg"}?

The key isn't zero value any more, or is it? If so, should I have a
dedicated empty struct for the key?

Axel Wagner

unread,
Jan 6, 2021, 6:02:20 PM1/6/21
to Amit Saha, golang-nuts
Why do you need fields? If you need fields, you are likely putting too many things into your context (note, that the lookup is O(N), with N being the number of distinct keys - so you want the number of distinct keys to be small). The intended use of `context.WithValue` is to only have one key per "use-case" (normally, that means one key per package) and then store the actual data in the value.

So, yes, you *can* have fields and if you have fields and re-use one type for many keys that way. And if you do that, you need to specify the exact field values on the lookup as well. But if you want "many keys" from a single type, that's a code-smell and I would suggest to change your design. If you think you need it, feel free to expand on your use-case and we might come up with a better solution :)

Amit Saha

unread,
Jan 6, 2021, 6:10:31 PM1/6/21
to Axel Wagner, golang-nuts
On Thu, Jan 7, 2021 at 10:01 AM Axel Wagner
<axel.wa...@googlemail.com> wrote:
>
> Why do you need fields? If you need fields, you are likely putting too many things into your context (note, that the lookup is O(N), with N being the number of distinct keys - so you want the number of distinct keys to be small). The intended use of `context.WithValue` is to only have one key per "use-case" (normally, that means one key per package) and then store the actual data in the value.
>
> So, yes, you *can* have fields and if you have fields and re-use one type for many keys that way. And if you do that, you need to specify the exact field values on the lookup as well. But if you want "many keys" from a single type, that's a code-smell and I would suggest to change your design. If you think you need it, feel free to expand on your use-case and we might come up with a better solution :)

This is what I have now in a HTTP handler function:

Attach the data:

c := requestContext{
requestID: "request-123-abc",
clientID: "client-451",
}

currentCtx := req.Context()
newCtx := context.WithValue(currentCtx, requestContext{}, c)
req = req.WithContext(newCtx)


Retrieve the data somewhere else during processing the request:

ctx := req.Context()
v := ctx.Value(requestContext{})
if m, ok := v.(requestContext); ok {
log.Printf("Processing request: %v", m)
}

Is this inefficient?

Axel Wagner

unread,
Jan 6, 2021, 6:20:51 PM1/6/21
to Amit Saha, golang-nuts
Hi,

this works fine, but it incurs an extra allocation (AFAIK both for storing the data, and for looking it up). requestContext is not zero-sized, so to store it in an interface{}, a new value must be allocated and put into the interface. This also has to happen on lookup (I don't think the compiler optimizes that, today), because you need to convert it to interface{} when passing it to `Value`. The recommended way

type ctxKey struct{}

type requestContext struct {
    requestID string
    requestID
}

ctx := context.WithValue(req.Context(), ctxKey{}, requestContext{"request-123-abc", "client-451"})

does not have this drawback - zero-sized values are special, because there can only be one of them, so the compiler is free to re-use the same address. So, a conversion of a zero-sized value to ctxKey{} is free.
I would also argue, that the code is clearer and safer that way - because there can only be one value of type ctxKey{}, you don't have to wonder what would happen if you use different keys of the same type - and fmt.Println only prints the type-name of keys, not the actual value (https://play.golang.org/p/pAj6RclqAGw), so if you encounter a bug with unexpected context-contents, it's just one less thing to worry about.

So, yes, your way absolutely works. I would still strongly recommend going the "official" route. It's one line of additional overhead, but there are concrete technical benefits (as well as just the general benefits of uniformity).

Amit Saha

unread,
Jan 6, 2021, 6:53:29 PM1/6/21
to Axel Wagner, golang-nuts
On Thu, Jan 7, 2021 at 10:20 AM Axel Wagner
<axel.wa...@googlemail.com> wrote:
>
> Hi,
>
> this works fine, but it incurs an extra allocation (AFAIK both for storing the data, and for looking it up). requestContext is not zero-sized, so to store it in an interface{}, a new value must be allocated and put into the interface. This also has to happen on lookup (I don't think the compiler optimizes that, today), because you need to convert it to interface{} when passing it to `Value`. The recommended way
>
> type ctxKey struct{}
>
> type requestContext struct {
> requestID string
> requestID
> }
>
> ctx := context.WithValue(req.Context(), ctxKey{}, requestContext{"request-123-abc", "client-451"})
>
> does not have this drawback - zero-sized values are special, because there can only be one of them, so the compiler is free to re-use the same address. So, a conversion of a zero-sized value to ctxKey{} is free.
> I would also argue, that the code is clearer and safer that way - because there can only be one value of type ctxKey{}, you don't have to wonder what would happen if you use different keys of the same type - and fmt.Println only prints the type-name of keys, not the actual value (https://play.golang.org/p/pAj6RclqAGw), so if you encounter a bug with unexpected context-contents, it's just one less thing to worry about.
>
> So, yes, your way absolutely works. I would still strongly recommend going the "official" route. It's one line of additional overhead, but there are concrete technical benefits (as well as just the general benefits of uniformity).

Your previous reply made me think of the same approach - use a
separate type for key.

> I would also argue, that the code is clearer and safer that way - because there can only be one value of type ctxKey{}, you don't have to wonder what would happen if you use different keys of the same type

I agree, I was thinking the same.

Thank you.
Reply all
Reply to author
Forward
0 new messages