Extensibility in Go
Recently I’ve come across some code that allows extensibility in some ways that are limiting.
Interfaces in Go are simply a set of methods that a type happens to implement. I went over a couple of major use cases for them before; in this post I want to clearly point out where they’ve either been perverted or underused.
🔗 spf13/pflag
The first perversion is pflag
. In general pflag
is fine; it allows the much
more common GNU style flags, like --help
instead of -help
. In theory this
allows collapsing single flag arguments together, like with ls -hal
. That has
some issues but they are inevitable, so I am not going to discuss those.
Instead, I’ll point out the Value
interface:
type Value interface {
String() string
Set(string) error
Type() string
}
It’s just like the original flag.Value
, except there’s a Type()
method that
returns a string. This method isn’t documented in the code anywhere, so I
spelunked and found that this is, as far as I can tell, only used for the Get
family of methods (such as
GetFloat32). This
adds a userspace type assertion (by returning a string from a Type method,)
which can result in panic
s based on real type assertions, for people who are
using the flagset
as a container. All of this is bad. I am almost of a mind
to fork pflag
and remove all of this stuff, but I’ll just stick with flag
and it’s oddities.
🔗 mitchellh/mapstructure
mapstructure
(used by
cobra
) “exposes functionality to
convert an arbitrary map[string]interface{}
into a native Go structure.”
Useful! So how does it work? It requires the user to implement hook functions
like this:
converter := func(from reflect.Kind, to reflect.Kind, value interface{}) (interface{}, error) {
...
}
While making reflect
part of your API is kinda gross, it’s not the end of the
world. What kills me is that, as I said before, cobra
uses this. So we
decode our config into structs, which have types on each field, and end up
having to maintain a central list of hook functions.
If this code were better factored, it would literally use the flag.Value
interface and we’d have a user extensible framework that works for CLI
arguments, env vars, and config files out of the box. But as it stands now we
have to maintain that in our core config package. Boo.
Fundamentally I feel like this is due to people writing Go as if it were a
language like Perl or Python where, due to not having types, people are used to
just returning values, rather than modifying values that are passed in. I
understand that the flag.Value.Set
interface method is weird, but it’s
flexible and works perfectly well.
If you are interested in learning Go, this is my recommendation:
(The following includes affiliate links.)
If you don’t already know Go, you should definitely check out The Go Programming Language. It’s not just a great Go book but a great programming book in general with a generous dollop of concurrency.
Another book to consider learning Go with is Go Programming Blueprints. It has a nearly interactive style where you write code, see it get syntax errors (or whatever,) fix it, and iterate. A useful book that shows that you don’t have to get all of your programs perfectly working on the first compile.
Posted Wed, Aug 14, 2019If you're interested in being notified when new posts are published, you can subscribe here; you'll get an email once a week at the most.