4 ms·
Out of curiosity, how would you avoid `Context.Value(interface{}) interface{}` with generics? The net/context package was designed to pass request lifetime con
by randomdata 10y ago
Out of curiosity, how would you avoid `Context.Value(interface{}) interface{}` with generics?
The net/context package was designed to pass request lifetime context throughout your service infrastructure (think microservices), where the values provide metadata from your transport protocol. HTTP headers, if we want to think in terms of HTTP, although obviously there is no specific protocol dependency at this layer. The value may not even be assigned by a Go program, so it seems that runtime validation would be a necessity even with generics. A broken or malicious program upstream could easily inject something into the context that you weren't expecting, or simply not supply the value at all.
- comex 10y agoTo clarify, Contexts don't automatically get HTTP headers stuffed into them, right? Instead you mean that some layer of the Go program would insert data based on headers, and a later layer would take it back out? I'm not a Go user but that's what I gathered from some Googling. So, if you want to use types to make Context safer to use - and I'm not saying this is necessarily desirable, but assuming it is - you could start with something that works in the existing Go language: explicitly marking what kinds of objects can be keys rather than using interface{}. type Key interface { _dummy_key_method() } type Context interface { ... func Value(key Key) interface{} } func WithValue(parent Context, key Key, val interface{}) Context (Incidentally, I find it odd that WithValue is a freestanding function while Value is a method; I suppose this is to reduce the burden of reimplementing the Context interface for a new type, but is there any actual reason to want to do that?) This helps prevent you from accidentally using the wrong object as a key, a bug that might be tricky to catch if the key you meant to use is legitimately sometimes not present in the Context (in which case your code wouldn't error out due to nonpresence of the wrong key). It also prevents hacks like using strings as keys in lieu of declaring dedicated constants, which would create the risk of silent failure due to typos. (It seems this is already considered a bad practice, but such a signature would enforce it.) Once you do that, it's apparent how generics could further improve safety. Using imaginary Go syntax: type Key<T> interface { _dummy_key_method() } type Context interface { ... Value<T>(key Key<T>) *T } func WithValue<T>(parent Context, key Key<T>, val *T) Context Now you're safe from type mismatches. Actually, it seems that best practice is already for packages to provide type safety by defining wrapper functions, such as this from userip: func NewContext(ctx context.Context, userIP net.IP) context.Context func FromContext(ctx context.Context) (net.IP, bool) That works well enough, but generics save you the boilerplate; packages could just expose the keys as public constants while retaining type safety. Admittedly, those two functions are only a few lines to define, so there's not that much boilerplate to save, but it's something. Generics would also make it possible to write type-safe generic helper functions involving contexts, though tbh I can't think of many obvious ones you'd want. Maybe a function that panics if it can't find the key: func MustHaveValue<T>(ctx Context, key Key<T>) T ...though that would be better handled by a proper Option<T> type anyway. But that too would be enabled by generics! Okay, but there's still more that the compiler could guarantee. The value is now guaranteed to have the right type if it's there, but for all we know it could be missing. Indeed, this is probably a bigger footgun than types are in practice: it's easy to accidentally have some code path that bypasses the code responsible for setting a particular key. Unfortunately, this is a harder problem to solve even with generics, and in fact multiple Rust web frameworks have run into very similar issues: https://github.com/nickel-org/nickel.rs/issues/79 https://github.com/nickel-org/nickel.rs/issues/79 https://www.reddit.com/r/rust/comments/2cl7bw/web_frameworks_why_not_static_typing/ https://www.reddit.com/r/rust/comments/2cl7bw/web_frameworks... Arguably what you really want here is extensible records with a structural type system. For example, in Elm, you can have the expression { foo = "1", bar = "2" } and it'll be of type { foo : String, bar : String } But then, given any record type, you can add more stuff onto it (I think this works but haven't tested it): type alias HasFoo a = { a | foo : String } addFoo : a -> HasFoo a addFoo rec = { rec | foo = "1" } With a feature like that, your functions' context parameters, instead of having the plain Context type, could explicitly specify that they need a Context with a given key. Thanks to generics, you could call functions that add new keys without losing track of the keys already in the map; for example, with the above Elm code, if you called addFoo { bar = "2" } the result type would be { bar : String, foo : String } In fact, it is possible to simulate this in languages that lack native support for extensible records but do have sophisticated enough type systems - anything from C++ to Rust to Haskell. I think the Iron web framework for Rust does that now, though I only took a quick glance at the docs. I'm not sure exactly what this would look like in Go, both because the implementations tend to get pretty ugly/hacky, and because a hypothetical generics feature for Go might not add enough power to make this expressible. (Which, to be clear, isn't necessarily a bad thing; every bit of complexity has a price, and C++ templates in particular are notoriously awful to read and debug.)