[HN Gopher] Interface Upgrades in Go (2014)
       ___________________________________________________________________
        
       Interface Upgrades in Go (2014)
        
       Author : panic
       Score  : 79 points
       Date   : 2024-06-23 03:23 UTC (19 hours ago)
        
 (HTM) web link (avtok.com)
 (TXT) w3m dump (avtok.com)
        
       | McMini wrote:
       | While the article highlights interface upgrades, it overlooks
       | potential downsides like increased complexity in debugging.
       | Anyone experienced issues with this?
        
       | PUSH_AX wrote:
       | I remember working in a team that used interfaces for a ton of
       | things. Every single one of those things only ever had one
       | concrete implementation, still to this day. A lot of those things
       | were also purely for mocking in tests too.
       | 
       | 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..
        
         | radicalbyte wrote:
         | So you don't test the parts of your system which need mocks to
         | be effectively tested? How do you avoid the maintenance
         | nightmare that causes?
         | 
         | 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.
        
           | PUSH_AX wrote:
           | I'm saying we did test parts that required mocks, and we used
           | interfaces as a vehicle to streamline that. Rightly or
           | wrongly.
        
             | PUSH_AX wrote:
             | I've just realised you understood and we're asking present
             | tense I think. The truth is I also do less unit testing,
             | identifying instead where I know confidence is needed and
             | where it's not.
        
           | the_gipsy wrote:
           | The question is, are those interfaces used for anything but
           | mocking tests? If not, could there be a better solution?
        
             | valzam wrote:
             | I keep hearing this but I don't quite understand what the
             | alternative is? I get that maybe introducing interfaces
             | within your own business domain CAN be overused but
             | certainly any external depedency should be fronted by an
             | interface so the rest of the code can be tested without
             | spinning up test http servers, database containers etc. all
             | over the place. It just feels like a really lightweight,
             | simnple thing to have an interface for some API that you
             | are calling. 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
        
               | afavour wrote:
               | This is a situation where I enjoy coding in TypeScript.
               | You don't need to front with interfaces, you just mock
               | objects that map 1:1 with the expected API. So you write
               | your code targeting the standard Fetch API then just pass
               | in a mock when testing.
        
               | ashishmax31 wrote:
               | +1 to this. Especially with go, most of the (non-
               | integration) tests I end up writing are behaviour driven
               | tests. I'm not really sure how one can write these sort
               | of tests without interfaces and mocks.
        
               | randomdata wrote:
               | One could use build tags to provide alternative
               | implementations over a concrete type, I suppose.
               | 
               | 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.
        
               | dgb23 wrote:
               | Instead of pushing the external, side-effecting thing
               | down the stack, I put it up the stack and call the logic
               | that generates the data for it from there. Now I don't
               | need to mock an interface, I just need to test data
               | in/out.
               | 
               | 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.
        
               | randomdata wrote:
               | _> Instead of pushing the external, side-effecting thing
               | down the stack, I put it up the stack and call the logic
               | that generates the data for it from there._
               | 
               | 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.
        
               | wavemode wrote:
               | To be honest, if I were writing software in Go I would
               | just test it end-to-end.
               | 
               | 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.
        
       | nickcw wrote:
       | I used to use interface upgrades in rclone (a program which syncs
       | your data to many different cloud providers) for all the optional
       | features a backend (cloud provider) might have, for example
       | renaming a file.
       | 
       | 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.
        
         | lazypenguin wrote:
         | Thanks for rclone, great tool!
        
         | memset wrote:
         | Could you share a very small code snippet describing what you
         | mean by bound method pointers?
         | 
         | (Huge fan of rclone!)
        
           | jerf wrote:
           | Something like this: https://go.dev/play/p/AVVz0qs2i1e
           | 
           | 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.
        
         | 38 wrote:
         | http://avtok.com/2014/11/05/interface-upgrades.html#the-prox...
        
         | chrsig wrote:
         | I face this a lot, and I think you have one very valid approach
         | to it that I often employ. Especially if you have some
         | centralized point to make a list of structs, or walk a tree to
         | collect them.
         | 
         | 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.
        
           | nickcw wrote:
           | The main problem the method pointers solves for me is
           | answering the question "can this object do X". The mere
           | existence of a method you can type assert isn't enough.
           | 
           | 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.
        
             | chrsig wrote:
             | > 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.
             | 
             | 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)         }
        
       | rollulus wrote:
       | Note that this is not necessarily a great thing to overuse. Also
       | note that the article is 10 years old.
       | 
       | 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.
        
         | never_inline wrote:
         | I encountered this and went through some corners of the
         | standard library.
         | 
         | shameless plug to my own blog post: https://mahesh-
         | hegde.github.io/posts/go-interface-smuggling/
        
           | rollulus wrote:
           | If you never encountered this you never created your own
           | ResponseWriter, or you did it and inadvertently broke its
           | Flushing, Pushing and Hijacking capabilities. It's not more
           | complicated than this.
        
       | hmage wrote:
       | November 5, 2014
        
         | dang wrote:
         | Year added above. Thanks!
        
       | MrBuddyCasino wrote:
       | Am I missing something or is this a lot of words to describe the
       | equivalent of the JVMs _instanceof_ / type casting operator?
        
         | neonsunset wrote:
         | It is. What's with the languages and their communities trying
         | to reinvent worse wheel and label it with a new word all the
         | time?                   if (thing is IFasterThing ft)         {
         | ft.FastPath(arg1, arg2);         }
        
           | blev wrote:
           | The author's mistake was assuming that interfaces are unique
           | to Go. I think it's still a helpful article as someone who
           | works frequently with interfaces because, while I
           | "discovered" the interface upgrade idea on my own, I had
           | never considered the proxy problem.
        
             | philosopher1234 wrote:
             | Go's interfaces are unique in that they are structural,
             | which makes this problem more difficult to solve.
        
       | karmakaze wrote:
       | This is basically a hack to get pragmatic performance at a cost
       | of comprehensibility. It would have been better if Go allowed
       | union types then a func could be declared to use interface A|B
       | efficiently. Passing a narrow interface when a wider implemented
       | one could get used is lying about the actual "interface" in the
       | signature. Added to that is that Go's interfaces are structural
       | _(which I think are great)_ , but could lead to accidental misuse
       | by passing in interface A for an object that also has method X
       | for unrelated purposes that co-incidentally satisfies interface B
       | which the called func on A magically uses.
       | 
       | > 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._
        
         | 38 wrote:
         | > 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"
        
           | karmakaze wrote:
           | They are all documented in the io package. There's no point
           | in telling you just one of them. But you have to RTM which
           | was my point.
        
       | yawz wrote:
       | The title needs "2014" to be added.
        
         | dang wrote:
         | Added. Thanks!
        
       | dang wrote:
       | Discussed at the time:
       | 
       |  _Interface Upgrades in Go_ -
       | https://news.ycombinator.com/item?id=8714051 - Dec 2014 (40
       | comments)
        
       | geoka9 wrote:
       | > While it just so happens that all the ResponseWriters that
       | net/http give you implement (e.g.) CloseNotifier, there's not
       | really any way for you to know that without reading the source.
       | 
       | 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.
        
       ___________________________________________________________________
       (page generated 2024-06-23 23:01 UTC)