Table of Contents:
-
Give your commands plenty of room:
cmd/{name}/main.go-- put themain()method here! But make it a tiny call to...cmd/{name}/app/*.go-- put theMain(args, stdin, stdout, stderr) intmethod here!cmd/{name}/app/*_test.go-- wrap tests aroundMain! Test your arg parser, outputs, etc.
-
Separate code by its active lifespan:
gadgets/**-- anything that you call, it blocks, and it returns: it's a gadget.actors/**-- anything that has a lifecycle or uses channels or concurrency: turn it into an actor. (Actors tend to call gadgets; but never the other way around, because if a gadget called an actor, it'd be subject to the actor's lifespan, and then it's not a gadget, is it?)app/**-- business logic goes here. It's the companion ofcmd/{name}/app, but all the args parsing should be done before this. (The top level App tends to call gadgets and actors.)
All the same rules as for applications, except you probably won't have cmd/*,
and most libraries are all "gadget" so it's reasonable to skip that directory.
- Step 1: Don't have plugins. Add complexity when you need it; not before.
- Step 2: If you do need plugins: read Wise Choices for Plugin Patterns in Golang.
- Key takeway: it's important to choose package arrangements that avoid foisting an import cycle problem on your users!
- You don't have to follow all of that advice, but definitely heed the distinction between the interface package and the mux package. You'll regret it if you try to skip this one.
-
NEVER FORGET TO PASS
context.Contextif an operation can be blocking!- A
context.Contextarg is your documentation than an operation can block. - Per the "by lifespan" guidelines in Package Layout:
- Gadgets can have a
context.Contextparameter as long as they're expected to return even if it's never cancelled. - Actors should always have a
context.Contextparameter! It's possible they won't return unless it's cancelled.
- Gadgets can have a
- A
-
Whenever designing an API with channels, read Inconshreveable's Designing With Channels.
- And then read it again.
- Especially Principle #4.
- Read it a third time when you're done designing your API and before you've pushed it. Make sure you're not violating any of the principles -- you'll almost certainly be in for an eventual refactor if you do, and refactors of concurrent APIs are painful.
- Use go-wish for testing. It composes well with
t.Run(and anything else that composes by passingtesting.T), and it offers diff output both for large strings and for complex objects. - Use go-sup if you have concurrent code. It provides supervision and error handling defaults that are otherwise either a large amount of boilerplate, or more importantly, likely to be forgotten. It also makes actor patterns easy to implement (but doesn't get in your way; the
selectlogic is still in your hands). - Use refmt for serialization. It offers a lot of customization options beyond anything in the standard library. If you're reaching for a hand-rolled
MarhsalJSONfunction, reach forrefmtinstead. - Try go-errcat for error handling. It offers composable systems for categorizing and handling errors, without conflicting with any of the standard golang pragmatism about
errorreturn values. - Use go-wish/wishfix for storing test fixture data that's too big for storing in heredoc strings. It provides mechanisms for automatically updating the content (useful so you can generate new data, and simply review it when committing rather than manually updating the fixture) and stores content in simple human-readable files that resemble markdown.