Interface Upgrades in Go (2014)
avtok.com
avtok.com
However the proxy problem became unmanageable. That's when I had a backend (eg the crypt backend which encrypts any other backend) which wraps another backend. Can the crypt backend rename things? That depends on what it is wrapping and you'd have to upgrade the interface call it and then get a special error to find out.
Eventually I switched to a table of bound method pointers which were easy to test against nil to see whether they were implemented and could be filled with a very small amount of reflection.
In my experience interface upgrades are useful but the proxy problem is very real so use sparingly only!
I did suggest at one point (to rsc) an addition to Go which would allow interface proxies to take methods away from their method set at run time to fix this problem, so the proxy could only have the methods it could implement. I've no idea how difficult this would be to implement though.
(Huge fan of rclone!)
This is much less convenient than using interfaces directly when that is possible, but comes with a lot more power. You have more insight into the "interface" value, you can use this to manually implement prototype-like behavior as seen in Javascript, you can deliberately make an object where one "method" is invoked on one object and one "method" is invoked on another, which is hard to read but may be just what you need in some situation, etc.
There are a lot of ways to improvise on this tune.
Also note most OO or OO-flavored languages can do this, Go has no special claim to it. But the tradeoffs are pretty similar in all those languages... you want to be sure this is what you need before you use it, because the design costs are significant.
I think another other route is to provide lots of tooling to inspect the object graph -- include a pretty print function in the interface or something to that effect.
There's equivalency between the two, the struct with pointers to implementation functions can implement an interface. Likewise, the implementation functions may call other implementations, so you haven't exactly solved the proxy problem, the state controlling which implementations becomes function local instead of on a struct. Unless you start to provide a struct with all of the context, at which point you've actually re-invented interfaces. I've grown wary of having too much of the details as function local like this because it hinders making more advanced tooling by hiding intermediate state from reflection.
The interface provides the means to give other representations, and allows for embedding one within another, and gives runtime tooling to inspect underlying types. It gives more elegance and flexibility to the problem at language and standard library scale.
One of the nice things is that it gives a sort of bidirectional refactoring path. If someone chose struct-of-pointers and it's at it's breaking point, those structs can stay as is and start to implement the interface with minimal effort. Likewise, if I need to boil down a hodgepodge of implementations into a really simple control flow, struct methods capture the struct and can be passed around as ordinary function pointers, so I can built a struct out of existing interface implementations.
There's no real way around the complexity -- leaky abstractions are a thing. I think the author got it right: use interface upgrades and proxies sparingly. If they are the right tool for the job, then it's probably a good idea to go the extra mile to provide good documentation and instrumentation with extra attention to caller/callee expectations.
Returning a NotImplemented error isn't a great solution either as you might have to do quite a bit of work to get ready to call the method only to find it isn't implemented.
Some bit of reflection magic which allowed objects to remove methods from their method sets at runtime would be perfect. You'd just use interface upgrades and you'd be sure that it would work.
Also an available flag for each method would work too and might be neater than the function pointers.
Given that methods sets are...sets. I'd be interested at a language level, what it would look like to add some notion of set difference or expressing disjointedness in some way, e.g.,
type ReadOnly retraction {
Write([]byte) (int, error)
}
type ReadOnlyFile struct {
ReadOnly
*File
}
where `ReadOnlyFile` would have all of `*File`'s methods minus the methods defined in `ReadOnly`restriction probably isn't a good term, I could see it easily being misinterpreted, but I haven't all day to ponder it :)
(edit: after publishing I realized `retract` might be more clear)
I don't know if it would ultimately simplify the problem or not, but I agree that having some way to easily mask or hide a method subset could be quite nice.
Edit:
I want to add that the idiomatic thing to do now would be
type ReadOnlyFile File
/ *proceed to implement every method you want accessible */
maybe something like the following could be possible type ReadOnlyFile retracts *File {
Write([]byte) (int, error)
} func (f *file) Rename() = nil
Then later test for nilness with: if f.Rename == nil,
or with better readability: if v, ok := f.(interface {Rename()}); ok && v == nil // -> if true, then nil method
We could define a Type Assertion to return "nil, ok" when all methods
of the interface it is being passed to are implemented nil methods. type NotOftenImplemented interface {
RareMethod()
SuperRareMethod()
}
if v, ok := data.(NotOftenImplemented); ok && v == nil {
// RareMethod() and SuperRareMethod() are nil
}
switch data.(type) {
case NotOftenImplemented: // would be supposed to match ONLY if AT LEAST >= 1 of the methods of this interface is non-nil
}
Of course it would create a phenomenon where all callers of any interface methods would now fear of calling any method without first testing for its nillness.On another hand, there is also sometimes methods implemented with a single-line of panic("Not Yet Implemented") so...
Today I don’t use them, unless I need it retrospectively. Which I find is rare.
This is not a knock on interfaces as a language feature. Just inserting a random anecdote about pragmatism..
I'm working on a Golang project where we've not yet introduced those kind of tests and, although it can be quicker to code initially, it's slower overall. The project relies heavily on other Open Source systems so the Golang code isn't even that complex.
I can't imagine doing something hard and keeping it working over time without being to isolate dependencies to enable testing and keep the structure clear overall.
But like with everything in life, there is no right solution. Just different tradeoffs. If interfaces best align with the trades you are willing to make, go for it.
If you do this consistently, then you have much of the side effecting code residing next to each other per process/input handler etc.
It's not always feasible or the right thing. But it's a good default way of structuring code and makes testing much more straight forward. And it makes more clear "at a glance" what happens because you can reason more locally about the side effecting parts.
> It also means you are less depedent on the concrete implementation of the API and don't leak types and behaviour from systems you don't control into your codebase
You are always dependent on that. Whether you hide it down the stack or lift it up, you still need to put your data into a specific shape, do the same essential checks etc.
I've never seen Go code that does anything else. That's not to say it doesn't exist, but it's seemingly not the norm amongst Go developers (probably exactly for the reasons you describe). That doesn't address what the parent is talking about, though. You have the exact same problem spoken of no matter where in the stack you put it.
If the software's logic is too complex and fiddly to lend itself to straightforward end-to-end testing (i.e. enterprise software), I just wouldn't write it in Go. I'd choose a higher level language like Python or Java where mocking (and other kinds of dynamism) are straightforward and require no boilerplate.
We've tried both approaches, declaring the interface in one package and importing it where used but also declaring it directly colocated with the consumer.
I've found the first approach worked better when we had to add or modify the interface methods
Of course this doesn’t apply when using interfaces for polymorphic return types in a library, but that is a distinct use case for interfaces to address.
Perhaps some of this is just taste, but this is the way I’ve found interfaces in Go to be most useful.
> Like all articles about Go’s interfaces, we are obligated to start with Go’s io package.
Also the io package, or stdlib in general is not a good place to look for good patterns to use in Go. Numerous antipatterns are used in the name of performance. The principles for stdlib authors and recommendation for Go developers are different. As an example io functions can return a value AND an error--and whether to continue or not depends on the specific error (as some are benign). It's better that I don't name an example as you should always be on the lookout for (until having learned) them.
"this gun will shoot you in the foot in some cases. It's better that I don't name an example as you should always be on the lookout for (until having shot yourself in the foot) them"
Relying on such upgrades sort of introduces a dark and fuzzy part of the API. Go’s http pkg is a notorious one, with how a http.ResponseWriter can also be a Flusher and Hijacker and I don’t know what else. If you in your middleware want to wrap it, you need to implement those interfaces as well, or the functionality is lost. But they’re not part of the “visible” API, they’re type assertions buried deep in the standard library, good luck with that. For this reason Go 1.20 introduced the http.ResponseController.
shameless plug to my own blog post: https://mahesh-hegde.github.io/posts/go-interface-smuggling/
For functionality like that[1] I would have expected them to fallback to `bufio.Peek` if the `fs.File` is not an `io.Seeker`. Sure, it's a bit slower, but it would just work. If someone implements custom `fs.File`, then it's up to them implementing any additional interface that allows better performance.
It's the same idea as `io.Copy` trying to use first `io.WriterTo` or `io.ReaderFrom` if implemented, and then falling back to a manual copy.
The thought process should be: First try to ask for an interface that has all methods you require (that's like the whole point of an interface). If you can't do that, then try to work with the methods you have, and then mention that performance can be improved if additional interfaces are implemented. Only if when you can't do that, you fail with a runtime error complaining about missing methods you didn't ask for in the interface signature.
I might be missing some reason that prevents them from using `bufio.Peek` though.
[1]: Link to the use of Seek, for reference: https://github.com/golang/go/lob/e8ee1dc4f9e2632ba1018610d1a...
Go editor tooling helps with this: gopls can do this nowadays (e.g. `lsp-find-implementation` in Emacs) and go oracle/guru may have already supported this back in 2014. It works both for finding implementations of an interface and finding interfaces implemented by a type.
Interface Upgrades in Go - https://news.ycombinator.com/item?id=8714051 - Dec 2014 (40 comments)
if (thing is IFasterThing ft)
{
ft.FastPath(arg1, arg2);
}