[HN Gopher] Spectral Contexts in Go
___________________________________________________________________
 
Spectral Contexts in Go
 
Author : todsacerdoti
Score  : 27 points
Date   : 2023-06-18 19:32 UTC (3 hours ago)
 
web link (hypirion.com)
w3m dump (hypirion.com)
 
| daniel-thompson wrote:
| The author demonstrates attaching services to a context, which
| afaict goes against the Go authors' usage recommendations in
| https://pkg.go.dev/context:
| 
| > Use context Values only for request-scoped data that transits
| processes and APIs, not for passing optional parameters to
| functions.
 
| [deleted]
 
| morelisp wrote:
| Don't do it. Don't past non-request-scoped data through contexts,
| and none of these are request-scoped. You can trivially pass such
| values through middlewares using methods or closures.
| 
| Also, while I think I know what "unlike Rust, these phantom types
| appear at runtime in Go" is saying, it's saying it in a really
| confusing way. These types do not "appear at runtime"; they are
| exactly like all other types, constructed at compile time and
| _visible_ when interface-boxed at runtime. They 're in no way
| "spectral".
 
| vore wrote:
| I'm not a big fan of context variables: they're basically a super
| clunky kind of goroutine-local global variable, with all the
| problems you might get with these kinds of mutable global
| variables. I would argue that if you're having to pass
| dependencies down, you should be explicitly dependency injecting
| them instead rather than implicitly via context.
 
  | cyberax wrote:
  | How would you do that with standalone functions? You'll need at
  | least one parameter for cancelation, and one parameter for the
  | logging context, and perhaps one more parameter for tracing.
  | 
  | Even passing the single context variable increases clutter a
  | lot.
 
    | [deleted]
 
  | ilyt wrote:
  | They are not "goroutine local", they are essentially (at least
  | the way they are used) a _request-local_ variable. Which in
  | itself is useful tool.
  | 
  | My only real issue with them is lack of type safety so you have
  | to remember which contest var is what.
  | 
  | For example I'd like to have User context variable that is set
  | somewhere early if user is authenticated but now at every
  | subsequent call there needs to be a bit of fluff to check
  | whether it is right type, else you risk runtime safety.
 
    | vore wrote:
    | I think it always a mistake to start a goroutine without
    | passing a context to it, since you are no longer able to
    | cancel the goroutine unless you build your own mechanism to
    | do so. Given that, I think you can handwave it a bit and
    | regard context variables as goroutine local.
 
    | tommiegannert wrote:
    | Not sure I understand the typing issue. The standard pattern
    | is to provide an x.Context(ctx, ...) and an
    | x.FromContext(ctx), and those are the only two functions that
    | know about the context key, and they do the type-casting.
    | Since they are the only functions knowing the context key
    | (the key is often a pointer to an unexported variable), you
    | are guaranteed type-safety as long as there is no unsafe code
    | that can pretend-inject the key.
    | 
    | https://cs.opensource.google/go/go/+/refs/tags/go1.20.5:src/.
    | ..
 
      | collinvandyck76 wrote:
      | When people mention type safety in this context, they are
      | talking about compiler guarantees. Application code that
      | uses type assertions will never be "type safe" in the way
      | that a compiler can ensure. So while you're correct that if
      | the application code that does the type checking is bug
      | free then the type safety is implied, but is not certain in
      | all cases.
 
  | [deleted]
 
| atombender wrote:
| There's no need to put any dependencies in the context, ever. For
| HTTP servers, the easiest way is just to use a struct to hold the
| dependencies:                   type Server struct {
| logger *Logger           users *UserService           // etc.
| }
| 
| Assuming we use a router like Chi or similar:
| router.Get("/users", srv.HandleUsers)
| 
| ...where HandleUsers is a method on Server.
| 
| Some people like to spread handlers in different packages. You
| can of course declare handlers as functions with no receiver
| struct, and pass the data as an argument:
| router.Get("/users", func(w io.ResponseWriter, r *http.Request) {
| userroutes.HandleUsers(srv, w, r)         })
| 
| For middlewares, e.g. something that parses a session cookie and
| fetches the current user, do exactly the same thing.
 
  | Cyph0n wrote:
  | Exactly this. Your dependencies are not request scoped, so
  | there is no reason for them to be included in a per-request
  | context. Instead, dependencies should be passed in to and
  | stored in the struct that uses them.
  | 
  | The data you store in the context should be specific to the
  | current request. Think: request ID, user ID, auth info, etc.
 
| et1337 wrote:
| We use strongly typed contexts for:
| 
| - Request-scoped parameters like user ID, request ID, etc.
| 
| - Dependency injection (database connection and other IO)
| 
| - Cancellation
| 
| It works great. The one hairy part is when you need to change the
| context, for example when logging in you take an anonymous
| context and end up with a logged-in context. We use linters to
| ensure that stuff is right, but it still takes some thought in
| the edge cases.
| 
| Still, I think those issues are exposing problems in our codebase
| rather than problems with typed contexts.
 
  | candiddevmike wrote:
  | Do you use a single context key with a struct or iota keys for
  | each value?
 
  | rcme wrote:
  | Why do you use it for dependency injection? Why can you not
  | pass those parameters directly when instantiating your service?
 
___________________________________________________________________
(page generated 2023-06-18 23:01 UTC)