5

Context-less Go

 2 years ago
source link: https://medium.com/@michael_epps/context-less-go-854db3e5510
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Context-less Go

How to stop using context as an IoC and love writing http services.

One of the things that many Go developers, especially new ones, find non-obvious is how the heck to I pass all the things I need into my handlers?

We don’t have fancy Inversion of Control systems like in Java or C#. http.Handlers are static signatures so I cant just pass in what I really want. It seems we only have 3 options: use globals, wrap handlers in a function, or pass things down in context.Context!

Lets look at all three of these options.

A Little Context | Pun Definitely Intended

The handler we are writing is something you would see at your everyday, normal e-commerce shop.

Our task is to write a endpoint that given some category ID, returns a list of items in that category.

This endpoint will need to access our items.Serviceto do the actual lookup, logging.Service in case things go wrong, and metrics.Service to get them sweet marketing metrics!

Globals | Call Me Mr. Worldwide

Our first attempt at getting things into our handlers is with globals. This is a fairly natural way of doing this that many beginners tend to do.

We all have wrote code like this at one point or another. Then we all quickly learned that globals are bad since they make inflexible, untestable code. This naturally leads to a rabbit hole of books, articles, videos, and more that tells you “inject your dependencies!”. This code just wont do, lets try a bit of injection.

Dependency Injection | Getting your Booster

Well, we don’t want to use globals, but http.Handler has a fixed signature. So how do we do this? We wrap it of course!

This is better since we are no longer relying on global state, but it still leaves a lot to be desired. Mainly, it makes every handler into a big, long, annoying mess to write. Its easy to imagine this signature getting 2–3 times longer if we add another few services.

So we read around and find out our *http.Request has a context.Context inside it! We can create a middleware, inject all of our dependencies, then the handler can just pull what it needs! This surely will solve all of our problems!

Context | Now in Color!

First lets make that middleware we were talking about. We are just going to do this inline after we setup all our dependencies.

Now that we have all that added we can redo our handler once again.

Nice and easy! Nothing can go wrong here. Well, except when 3 months down the line someone moves a line of code elsewhere in the app and one of these services no longer exists in the context.

“Well Mr. Smart Medium Writer I’ll just check to make sure!” you may say.

As you can see even in small, simple, contrived examples it becomes a bit mess to do this in a safe manner.

One of the reasons we use Go is to avoid all these type checking messes! By using context as some sort of grab bag for our dependencies we effectively give up on type safety. In addition, we don’t know if things exist until runtime. To fix that we end up with long strings of error checking everywhere.

This is no good. All three of our options suck in their own way. It seems we can’t escape the verbosity hell if we want to inject our dependencies. Sure you can hack around these problems and create fancy functions that wrap a lot of this, but its not actually solving the problem.

Unless there is a fourth way ….?

Structs | Adding structure to your handlers…

Some of you may have been screaming at me to get to this already, but having taught this subject a good few times it really helps to see the other solutions first.

The “fourth way” we did not consider is actually quite simple.

  1. We create a struct that holds the dependencies we need.
  2. We add our handlers as methods on that struct

Lets take a look at an example.

This solution has a few great benefits:

  1. Everything we depend on is known at build time and is type safe (unlike contexts)
  2. We have minimal extra boilerplate (unlike wrapper functions)
  3. Maintains testability (unlike globals)
  4. Allows us to “group” related handlers into units

4 is one we haven’t touched on yet, but now that we have this struct we can create groupings of handlers that share common dependencies or just go together logically.

For instance, we may add a handler to add a new category on here. We may create a MetricsHandler to group all of the endpoints related to metrics together. You can be as granular or broad with this as you wish (granular, is probably, better in most cases).

Conclusion | Who even reads this far?

We finally have our goal. Our boss at the totally-contrived e-commerce shop will be happy that the endpoint is ready (baring some testing). We can move onto the next task.

This is not a new or even novel idea. If you look at classic go example posts such as things such as https://pace.dev/blog/2018/05/09/how-I-write-http-services-after-eight-years.html and https://github.com/benbjohnson/wtf/ you will see these ideas in action.

It is an great and, I think, non-obvious solution though.

Of course we can use interfaces and the like to make this even more testable, but that’s another article!

If you enjoy this article follow me on medium for more content! Also come join the Go Discord server at https://discord.gg/golang!

Thanks for Reading!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK