[HN Gopher] Revisiting Interface Segregation in Go
___________________________________________________________________
Revisiting Interface Segregation in Go
Author : ingve
Score : 54 points
Date : 2025-11-02 10:17 UTC (5 days ago)
(HTM) web link (rednafi.com)
(TXT) w3m dump (rednafi.com)
| et1337 wrote:
| At $WORK we have taken interface segregation to the extreme. For
| example, say we have a data access object that gets consumed by
| many different packages. Rather than defining a single interface
| and mock on the producer side that can be reused by all these
| packages, each package defines its own minimal interface
| containing only the methods it needs, and a corresponding mock.
| This makes it extremely difficult to trace the execution flow,
| and turns a simple function signature change into an hour-long
| ordeal of regenerating mocks.
| leetrout wrote:
| > a single interface and mock on the producer side
|
| I still believe in Go it is better to _start_ with interfaces
| on the consumer and focus on "what you need" with interfaces
| instead of "what you provide" since there's no "implements"
| concept.
|
| I get the mock argument all the time for having producer
| interfaces and I don't deny at a certain scale it makes sense
| but I don't understand why so many people reach for it out of
| the gate.
|
| I'm genuinely curious if you have felt the pain from interfaces
| on the producer that would go away if there were just
| (multiple?) concrete types in use or if you happen to have a
| notion of OO in Go that is hard to let go of?
| mekoka wrote:
| > or if you happen to have a notion of OO in Go that is hard
| to let go of?
|
| So much this. I think Go's interfaces are widely
| misunderstood. Often times when they're complained about, it
| boils down to "<old OO language> did interface this way. Why
| Go won't abide?" There's insistence in turning them into
| cherished pets. Vastly more treasured than they ought to be
| in Go, a meaningless thin paper wrapper that says "I require
| these behaviors".
| wizhi wrote:
| Maybe your actual issue is needing to mock stuff for tests to
| begin with. Break them down further so they can actually be
| tested in isolation instead.
| the_gipsy wrote:
| Yes, this is exactly the problem with go's recipe.
|
| Either you copypaste the same interface over and over and over,
| with the maintenance nightmare that is, or you always have
| these struct-and-interface pairs, where it's unclear why there
| is an interface to begin with. If the answer is testing, maybe
| that's the wrong question ti begin with.
|
| So, I would rather have duck typing (the structural kind, not
| just interfaces) for easy testing. I wonder if it would
| technically be possible to only compile with duck typing in
| test, in a hypothetical language.
| 9rx wrote:
| _> I wonder if it would technically be possible to only
| compile with duck typing in test_
|
| Not exactly the same thing, but you can use build tags to
| compile with a different implementation for a concrete type
| while under test.
|
| Sounds like a serious case of overthinking it, though. The
| places where you will justifiably swap implementations during
| testing are also places where you will justifiably want to be
| able to swap implementations in general. That's what
| interfaces are there for.
|
| If you cannot find any reason why you'd benefit from a second
| implementation outside of the testing scenario, you won't
| need it while under test either. In that case, learn how to
| test properly and use the single implementation you already
| have under all scenarios.
| the_gipsy wrote:
| > The places where you will justifiably swap
| implementations during testing are also places where you
| will justifiably want to be able to swap implementations in
| general.
|
| I don't get this. Just because I want to mock something
| doesn't mean I really need different implementations. That
| was my point: if I could just duck-type-swap it in a test,
| it would be so much easier than 1. create an interface that
| just repeats all methods, and then 2. need to use some mock
| generation tool.
|
| If I don't mock it, then my tests become integration test
| behemoths. Which have their use too, but it's bad if you
| can't write simple unit tests anymore.
| 9rx wrote:
| _> then my tests become integration test behemoths._
|
| There are no consistent definitions found in the world of
| testing, but I assume integration here means entry into
| some kind of third-party system that you don't have
| immediate control over? That seems to be how it is most
| commonly used. And that's exactly one of the places you'd
| benefit from enabling multiple implementations, even if
| testing wasn't in the picture. There are many reasons why
| you don't want to couple your application to these
| integrations. The benefits found under test are a
| manifestation of the very same, not some unique
| situation.
| Xeoncross wrote:
| What is the alternative though? In strongly typed languages
| like Go, Rust, etc.. you must define the contract. So you
| either focus on what you need, or you just make a kitchen-sink
| interface.
|
| I don't even want to think about the global or runtime
| rewriting that is possible (common) in Java and JavaScript as a
| reasonable solution to this DI problem.
| jerf wrote:
| I'm still fiddling with this so I haven't seen it at scale
| yet, but in some code I'm writing now, I have a centralized
| repository for services that register themselves. There is a
| struct that will provide the union of all possible
| subservices that they may require (logging, caching, db,
| etc.). The service registers a function with the central
| repository that can take that object, but can also take an
| interface that it defines with just a subset of the values.
|
| This uses reflect and is nominally checked at run time, but
| over time more and more I am distinguishing between a runtime
| check that runs arbitrarily often over the execution of a
| program, and one that runs in an init phase. I have a
| command-line option on the main executable that runs the
| initialization without actually starting any services up, so
| even though it's a run-time panic if a service misregisters
| itself, it's caught at commit time in my pre-commit hook. (I
| am also moving towards worrying less about what is
| necessarily caught at "compile time" and what is caught at
| _commit_ time, which opens up some possibilities in any
| language.)
|
| The central service module also defines some convenient one-
| method interfaces that the services can use, so one service
| may look like: type myDependencies
| interface { services.UsesDB
| services.UsesLogging } func init() {
| services.Register(func(in myDependencies) error {
| // init here } }
|
| and another may have type myDependencies
| interface { services.UsesLogging
| services.UsesCaching services.UsesWebCrawler
| } // func init() { etc. }
|
| and in this way, each services declaring its own dependencies
| means each service's test cases only need to worry about what
| it actually uses, and the interfaces don't pollute anything
| else. This fully decouples "the set of services I'm providing
| from my modules" from "the services each module requires",
| and while I don't get compile-time checking that a module's
| service requirements are satisfied, I can easily get commit-
| time checking.
|
| I also have some default fakes that things can use, but
| they're not necessary. They're just one convenient
| implementation for testing if you need them.
| Groxx wrote:
| tbh this sounds pretty similar to go.uber.org/fx (or dig).
| or really almost any dependency injection framework, though
| e.g. wire is compile-time validated rather than run-time
| (and thus much harder for some kinds of runtime flexibility
| - I make no claim to one being better than the other).
|
| DI frameworks, when they're not gigantic monstrosities like
| in Java, are pretty great.
| jerf wrote:
| Yes. The nice thing about this is that it's one function,
| about 20-30 lines, rather than a "framework".
|
| I've been operating up to this point without this
| structure in a fairly similar manner, and it has worked
| fine in the tens-of-thousands-of-lines range. I can see
| maybe another order or two up I'd need more structure,
| but people really badly underestimate the costs of these
| massive frameworks, IMHO, and also often fail to
| understand that the value proposition of these frameworks
| often just boils down to something that could fit
| comfortably in the aforementioned 20-30 lines.
| Groxx wrote:
| I 100% agree with what you've written, but if you haven't
| checked it out, I'll highly suggest trying mockery v3 for
| mocks: https://vektra.github.io/mockery
|
| It's generally faster than a build (no linking steps),
| regardless of the number of things to generate, because it
| loads types just once and generates everything needed from
| that. _Wildly_ better than the go:generate based ones.
| eximius wrote:
| > Rather than defining a single interface and mock on the
| producer side that can be reused by all these packages
|
| This is the answer. The domain that exports the API should also
| provide a high fidelity test double that is a fake/in memory
| implementation (not a mock!) that all internal downstream
| consumers can use.
|
| New method on the interface (or behavioral change to existing
| methods)? Update the fake in the same change (you have to,
| otherwise the fake won't meet the interface and uses won't
| compile!), and your build system can run all tests that use it.
| 9rx wrote:
| _> The domain that exports the API should also provide a high
| fidelity test double that is a fake /in memory implementation
| (not a mock!) _
|
| Not a mock? But that's exactly what a mock is: An
| implementation that isn't authentic, but that doesn't try to
| deceive. In other words, something that behaves just like the
| "real thing" (to the extent that matters), but is not
| authentically the "real thing". Hence the name.
| B-Con wrote:
| There are different definitions of the term "mock". You
| described the generic usage where "mock" is a catch-all for
| "not the real thing", but there are several terms in this
| space to refer to more precise concepts.
|
| What I've seen:
|
| * "test double" - a catch-all term for "not the real
| thing". What you called a "mock". But this phrasing is more
| general so the term "mock" can be used elsewhere.
|
| * "fake" - a simplified implementation, complex enough to
| mimic real behavior. It probably uses a lot of the real
| thing under the hood, but with unnecessary testing-related
| features removed. ie: a real database that only runs in
| memory.
|
| * "stub" - a very thin shim that only provides look-up
| style responses. Basically a map of which inputs produce
| which outputs.
|
| * "mock" - an object that has _expectations_ about how it
| is to be used. It encodes some test logic itself.
|
| The Go ecosystem seems to prefer avoiding test objects that
| encode expectations about how they are used and the
| community uses the term "mock" specifically to refer to
| that. This is why you hear "don't use mocks in Go". It
| refers to a specific type of test double.
|
| By these definitions, OP was referring to a "fake". And I
| agree with OP that there is much benefit to providing
| canonical test fakes, so long as you don't lock users into
| only using your test fake because it will fall short of
| someone's needs at some point.
|
| Unfortunately there's no authoritative source for these
| terms (that I'm aware of), so there's always arguing about
| what exactly words mean.
|
| Martin Fowler's definitions are closely aligned with the Go
| community I'm familiar with:
| https://martinfowler.com/articles/mocksArentStubs.html
|
| Wikipedia has chosen to cite him as well:
| https://en.wikipedia.org/wiki/Test_double#General .
|
| My best guess is that software development co-opted the
| term "mock" from the vocabulary of other fields, and the
| folks who were into formalities used the term for a more
| specific definition, but the software dev discipline
| doesn't follow much formal vocabulary and a healthy portion
| of devs intuitively use the term "mock" generically. (I
| myself was in the field for years before I encountered any
| formal vocabulary on the topic.)
| sirsinsalot wrote:
| Follow the trail of the blog post and you end up with Python and
| duck typing, and all the foot guns there too.
| zbentley wrote:
| How so? Genuine question. Duck typing is "try it and see if it
| supports an action", where interface declaration is the
| opposite: declare what methods must be supported by what you
| interact with.
|
| In Python, that would be a Protocol
| (https://typing.python.org/en/latest/spec/protocol.html), which
| is a newer and leas commonly used feature than full, un-
| annotated duck typing.
|
| Sure, type _checking_ in Python (Protocols or not) is done very
| differently and less strongly than in Go, but the semantic
| pattern of interface segregation seems to be equivalently
| possible in both languages--and very different from duck
| typing.
| cube2222 wrote:
| Duck typing is often equated with structural typing. You're
| right that officially (at least according to Wikipedia) duck
| typing is dynamic, while structural is the same idea, but
| static.
|
| Either way, the thing folks are contrasting with here is
| nominal typing of interfaces, where a type explicitly
| declares which interfaces it implements. In Go it's "if it
| quacks like a duck, it's a duck", just statically checked.
| sirsinsalot wrote:
| I'm saying that at some point declaring the minimal interface
| a caller uses, for example Reader and Writer instead of a
| concrete FS type, starts to look like duck typing. In python
| a functions use of v.read() or v.write() defines what v
| should provide.
|
| In Go it is compile time and Python it is runtime, but it is
| similar.
|
| In Python (often) you don't care about the type of v just
| that it implements v.write() and in an interface based
| separation of API concerns you declare that v.write() is
| provided by the interface.
|
| The aim is the same, duck typing or interfaces. And the
| outcome benefits are the same, at runtime or compile time.
| sirsinsalot wrote:
| Also yes Protocols can be used to type check quacks,
| bringing it more inline with the Go examples in the blog.
|
| However my point is more from a SOLID perspective duck
| typing and minimal dependency interfaces sort of achieve
| similar ends... Minimal dependency and assumption by
| calling code.
| themafia wrote:
| > starts to look like duck typing.
|
| Except you need a typed variable that implements the
| interface or you need to cast an any into an interface
| type. If the "any" type implemented all interfaces then it
| would be duck typing, but since the language enforces types
| at the call level, it is not.
| mayoff wrote:
| See also https://news.ycombinator.com/item?id=36908369 ("The
| bigger the interface, the weaker the abstraction")
| Joker_vD wrote:
| > However, there's still one issue: Backup only calls Save, yet
| the Storage interface includes both Save and Load. If Storage
| later gains more methods, every fake must grow too, even if those
| methods aren't used.
|
| First, why would you ever add methods to a public interface?
| Second, the next version of the Backup's implementation might
| very well want to call Load as well (e.g. for deduplication
| purposes) and then you suddenly need to add more methods to your
| fakes anyhow.
|
| In the end, it really depends on who owns FileStorage and Backup:
| if it's the same team/person, the ISP is immaterial. If they are
| different, then yes, the owner of Backup() would be better served
| by declaring a Storage interface of their own and delegate the
| job of writing adapters that make e.g. FileStorage to conform to
| it to the users of Backup() method.
| brodouevencode wrote:
| >First, why would you ever add methods to a public interface?
|
| In the go world, it's a little more acceptable to do that
| versus something like Java because you're really not going to
| break anything
| B-Con wrote:
| If you add a method to an interface, you break every source
| file that uses a concrete type in place of the interface (ie,
| passes a struct to a function that takes an interface) unless
| you also update all the concrete types to implement the new
| method ( _or_ you update them embed the interface, which is
| yucky).
|
| For a public interface, you have to track down all the
| clients, which may be infeasible, especially in an open
| ecosystem.
| jimbobimbo wrote:
| "But accepting the full S3Client here ties UploadReport to an
| interface that's too broad. A fake must implement all the methods
| just to satisfy it."
|
| In NET, one would simply mock one or two methods required by the
| implementation under the test. If I'm using Moq, then one would
| set it up in strict mode, to avoid surprises if unit under test
| starts calling something it didn't before.
| spenczar5 wrote:
| "But accepting the full S3Client here ties UploadReport to an
| interface that's too broad. A fake must implement all the methods
| just to satisfy it."
|
| This isn't really true. Your mock inplementation can embed the
| interface, but only implement the one required method. Calling
| the unimplemented methods will panic, but that's not unreasonable
| for mocks.
|
| That is: type mockS3 struct {
| S3Client } func (m mockS3) PutObject(...) {
| ... }
|
| You don't have to implement all the other methods.
|
| Defining a zillion interfaces, all the permutations of methods in
| use, makes it hard to cone up with good names, and thus hard to
| read.
| the_gipsy wrote:
| Is this pattern commonly used? Any drawbacks?
|
| Sounds much better than the interface boilerplate if it's just
| for the sake of testing.
| jgdxno wrote:
| At work we use it heavily. You don't really see "a zillion
| interfaces" after a while, only set of dependencies of a
| package which is easy to read, and easy to understand.
|
| "makes it hard to cone up with good names" is not really a
| problem, if you have a `CreateRequest` method you name the
| interface `RequestCreator`. If you have a request CRUD
| interface, it's probably a `RequestRepository`.
|
| The benefits outweigh the drawbacks 10 to one. The most
| rewarding thing about this pattern is how easy it is to split
| up large implementations, and _keep_ them small.
| durbatuluk wrote:
| Any method you forget to overwrite from the embed struct
| gives a false "impression" you can call any method from
| mockS3. Most of time code inside test will be:
| // embedded S3Client not properly initialized mock :=
| mockS3{} // somewhere inside the business logic
| s3.UploadReport(...) // surprise
|
| Go is flexible, you can define a complete interface at
| producer and consumers still can use their own interface only
| with required methods if they want.
| skybrian wrote:
| While you can do that, having unused methods that don't work is
| a footgun. It's cleaner if they don't exist at all.
| piazz wrote:
| You're decreasing coupling at the cost of introducing more
| entities, and a different sort of complexity, into your system.
|
| Sometimes it's absolutely worth it. Sometimes not.
| klooney wrote:
| "The Tyranny of nouns" when everything has a subtly different
| name in every context
| hyperpape wrote:
| > Object-oriented (OO) patterns get a lot of flak in the Go
| community, and often for good reason.
|
| This isn't really an OO pattern, as the rest of the post
| demonstrates. It's just a pattern that applies across most any
| language where you can make a distinction between an
| interface/typeclass or whatever, and a concrete type.
| discreteevent wrote:
| > distinction between an interface/typeclass or whatever, and a
| concrete type.
|
| This is the essence of OOP.
|
| "The notion of an interface is what truly characterizes objects
| - not classes, not inheritance, not mutable state. Read William
| Cook's classic essay for a deep discussion on this." - Gilad
| Bracha
|
| https://blog.bracha.org/primordialsoup.html?snapshot=Amplefo...
|
| http://www.cs.utexas.edu/~wcook/Drafts/2009/essay.pdf
| 9rx wrote:
| _> The notion of an interface is what truly characterizes
| objects_
|
| Objects, but not OO. OO takes the concept further -- what it
| calls message passing -- which allows an object to
| dynamically respond to messages at runtime, even where the
| message does not conform to any known interface.
| discreteevent wrote:
| The object has a known interface in this case. It's just
| not statically defined. Its interface is the set of
| messages that it responds to.
| 9rx wrote:
| Not quite. With OO, there is no set. An object always
| responds to all messages, even when the message contains
| arbitrary garbage. An object can respond with "I don't
| understand" when faced with garbage, which is a common
| pattern in OO languages, but it doesn't have to. An
| object could equally respond with a value of 1 if it
| wants.
|
| Dynamic typing is a necessary precondition for OO[1], but
| that is not what defines it. Javascript, for example, has
| objects and is dynamically typed, but is not OO. If I
| call object.random_gibberish in Javascript, the object
| will never know. The runtime will blow up before it ever
| finds out. Whereas in an OO language the object will
| receive a message containing "random_gibberish" and it
| can decide what it do with it.
|
| [1] Objective-C demonstrated that you can include static-
| typing in a partial, somewhat hacky way, but there is no
| way to avoid dynamic-typing completely.
| za3faran wrote:
| golang allows for the same
| za3faran wrote:
| The ironic thing is that golang itself is OO.
| 9rx wrote:
| It is not. The only languages (that people have actually
| heard of, at least) that are OO are Smalltalk, Ruby, and
| Objective-C. Swift also includes OO features, enabled with
| the @objc directive, for the sake of backwards compatibility
| with Objective-C, but "Swift proper" has tried to distance
| itself from the concept.
|
| Go channels share some basic conceptual ideas with message
| passing, but they don't go far enough to bear any direct
| resemblance to OO; most notably they are not tied to objects
| in any way.
| MarkMarine wrote:
| I revile this pattern. Look at the examples and imagine these are
| real and everything in the system is abstracted like this, and
| your coworkers ran out of concise names for their interfaces. Now
| you have to hop to 7 other files, through abstractions (and then
| read the DI code to understand which code actually implements
| this and what it specifically does) and keep all that context in
| your head... all in service of the I in some stupid acronym, just
| to build a mental model of what a piece of code does.
|
| Go used to specifically warn against the overuse of this pattern
| in its teaching documentation, but let me offer an alternative so
| I'm not just complaining: Just write functions where the logic is
| clear to the reader. You'll thank yourself in 6 months when
| you're chasing down a bug
| mekoka wrote:
| This is a common gripe among former Java programmers who still
| believe that the point of interfaces is the type hierarchy (and
| as a result misunderstand Interface Segregation). They hang on
| to interfaces like they're these precious things that must be
| given precious names.
|
| Interfaces are not _precious_. Why would anyone care what their
| name is? Their actual purpose is to wrap a set of behaviors
| under a single umbrella. Who cares what the color of the
| umbrella is? It 's locally defined (near the function where the
| behaviors are used). Before passing an object, just make sure
| that it has the required methods and you're done. You don't
| have to be creative about what you name an interface. It does a
| thing? Call it "ThingDoer".
|
| Also, why would you care to know which code implements a
| particular interface? It's equivalent to asking _give me a list
| of all types that have this exact set of behavior?_ I 'm
| possibly being myopic, but I've never considered this of
| particular importance, at least not as important as being
| conservative about the behavior you _require_ from
| dependencies. Having types enumerate all the interfaces they
| implement is the old school approach (e.g. Java). Go 's
| approach is closer to true Interface Segration. It's done
| downstream. Just patch the dependency with missing methods. No
| need to patch the type signature up with needless "implements
| this, that, other" declarations, which can only create the
| side-effect that to patch a type from some distant library,
| you'd have to _inherit_ just so that you can locally declare
| that you also _implement_ an additional interface. I don 't
| know about you, but to the idea of never having to deal with
| inheritance in my code ever again I say "good riddance".
|
| Again, interface segregation is about the behavior, not the
| name. The exact same combination of methods could be defined
| under a hundred different umbrellas, it would still not matter.
| If a dependency has the methods, it's good to go.
| _256 wrote:
| I don't care that much about defining a minimal interface or
| whether the producer or consumer defines it. the pain point for
| me is when you start passing interfaces up and down the stack and
| they become impossible to trace back to the concrete type. If you
| take an interface you should use it directly and avoid passing it
| down to another one of your dependencies. This keeps the layers
| you need to jump through to find the concrete type to a minimum.
| jfadfwddas wrote:
| Scheme taught me that OOP is the poor man's closure.
| func Backup(saver func(data []byte) error, data []byte) error {
| return saver(data) }
| B-Con wrote:
| I generally advise to avoid introducing interfaces strictly for
| testing. Instead, design the data types themselves to be testable
| and only use interfaces when you _expect_ to need differing
| implementations. ie, avoid premature abstraction and you get rid
| of a whole class of problems.
|
| For example, if you only use S3, it is premature abstraction to
| accept an interface for something that may not be S3. Just accept
| the S3 client itself as input.
|
| Then the S3 client can be designed to be testable by itself by
| having the lowest-level dependencies (ie, network calls) stubbed
| out. For example, it can take a fake implementation that has
| hard-coded S3 URLs mapped to blobs. Everything that tests code
| with S3 simply has to pre-populate a list of URLs and blobs they
| need for the test, which itself can be centralized or distributed
| as necessary depending on the way the code is organized.
|
| Generally, I/O is great level to use an interface and to stub
| out. Network, disk, etc. Then if you have good dependency
| injection practicies, it becomes fairly easy to use real structs
| in testing and to avoid interfaces purely for testing.
|
| Related reading from the Google style guide, but focused
| specifically on the transport layer:
| https://google.github.io/styleguide/go/best-practices.html#u...
| imiric wrote:
| I agree with not introducing abstractions prematurely, but your
| suggestion hinges on the design of the S3 client. In practice,
| if your code is depending on a library you have no control
| over, you'll have to work with interfaces if you want to
| prevent your tests from doing I/O. So in unit tests you can
| pass an in-memory mock/stub, and in integration tests, you can
| pass a real S3 client, and connect to a real S3 server running
| locally.
|
| So I don't see dependency injection with interfaces as being
| premature abstractions. You're simply explicitly specifying the
| API your code depends on, instead of depending on a concrete
| type of which you might only use one or two methods. I think
| this is a good pattern to follow in general, with no practical
| drawbacks.
| B-Con wrote:
| Yes, this is absolutely dependent on the design S3 client.
|
| The reality of development is we have to merge different
| design philosophies into one code base. Things can get messy.
| 100% agreed.
|
| The approach I advocate for is more for a) organizing the
| code you do own, and b) designing in a way that you play nice
| with others who may import your code.
| jbreckmckye wrote:
| Rather than defining all these one-method interfaces, why not
| specify a function type?
|
| Instead of type Saver interface {
| Save(data []byte) error }
|
| You could have type saves func([]byte) error
|
| Seems less bulky than an interface, more concise to mock too.
|
| It's more effort when you need to "promote" the port / input type
| to a full interface, but I think that's a reasonable tradeoff to
| avoid callers of your function constantly creating structs just
| to hang methods off
___________________________________________________________________
(page generated 2025-11-07 23:02 UTC)