Skip to content

Instantly share code, notes, and snippets.

@warpfork
Last active November 13, 2018 15:38
Show Gist options
  • Select an option

  • Save warpfork/f82cd52f4b0d17dd557dc42a98b7fcbe to your computer and use it in GitHub Desktop.

Select an option

Save warpfork/f82cd52f4b0d17dd557dc42a98b7fcbe to your computer and use it in GitHub Desktop.
Golang Practices: How Warpfork Structures Go Programs

Warpfork's Guide to Structuring Large Go Programs

Table of Contents:


Package Layout

For Applications

  • Give your commands plenty of room:

    • cmd/{name}/main.go -- put the main() method here! But make it a tiny call to...
    • cmd/{name}/app/*.go -- put the Main(args, stdin, stdout, stderr) int method here!
    • cmd/{name}/app/*_test.go -- wrap tests around Main! 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 of cmd/{name}/app, but all the args parsing should be done before this. (The top level App tends to call gadgets and actors.)

For Libraries

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.

Plugins

  • 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.

Coding Styles

  • NEVER FORGET TO PASS context.Context if an operation can be blocking!

    • A context.Context arg is your documentation than an operation can block.
    • Per the "by lifespan" guidelines in Package Layout:
      • Gadgets can have a context.Context parameter as long as they're expected to return even if it's never cancelled.
      • Actors should always have a context.Context parameter! It's possible they won't return unless it's cancelled.
  • 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.

Helpful Libraries

  • Use go-wish for testing. It composes well with t.Run (and anything else that composes by passing testing.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 select logic 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 MarhsalJSON function, reach for refmt instead.
  • 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 error return 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment