3 ms·
I tend to use structs like mini "dynamically" scoped namespaces for breaking up hairy tasks. Pattern looks like: struct fooer { x int blah stri
by brandonbloom 11y ago
I tend to use structs like mini "dynamically" scoped namespaces for breaking up hairy tasks. Pattern looks like:
struct fooer {
x int
blah string
// ... more state
err error
}
func Foo(x int) (string, error) {
f := &fooer{x: x};
defer f.cleanup()
f.stepOne()
f.stepTwo()
f.stepThree()
return f.blah, f.err
}
func (f *fooer) stepOne() {
// ... do stuff and maybe set f.err
}
func (f *fooer) stepTwo() {
if f.err != nil {
return
}
// ... do stuff and maybe set f.err
}
func (f *fooer) stepThree() {
if f.err != nil {
return
}
// ... do stuff and maybe set f.err; set f.blah
}
func (f *fooer) cleanup() {
// ...
}
While this is initially a tad more verbose, it has some nice benefits. For one thing, you can easily fmt.Printf("%#v", f) to dump all the relevant state. However, it also tends to be more resilient to refactoring:
- No method signatures to patch up.
- Easier to non-local jump via return if you have some unusual control flow, etc.
- You can easily find all references to `f.err` to verify your error handling.
- The method names and step list take on a self-documenting quality.
Another key insight I had when learning to tolerate Go: It's ok to do throw away work. You don't need to abort instantly on failure, you can just make sure code paths handle zero values or other invalid states more robustly. For example, I sometimes add a function like:
func (f *fooer) fail(err error) {
if f.err == nil {
f.err = err
}
}
And then just add sprinkle some f.fail() calls, make code handle nils, invalid states, etc, and make cleanup code idempotent. Seems to work out much more nicely than if err, return.