Readit News logoReadit News
spenczar5 · a month ago
"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.

skybrian · a month ago
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.
lenkite · a month ago
Not to mention, introducing all the permutations of methods as separate interfaces on the "consumer side" means extreme combinatorial explosion of interfaces. It is far better to judge the most common patterns and make single-method interfaces for these on the provider side.

Lots of such frequently-quoted Go "principles" are invalid and are regularly broken within the standard library and many popular Go projects. And if you point them out, you will be snootily advised by the Go gurus on /r/golang or even here on HN that every principle has exceptions. (Even if there are tens of thousands of such exceptions).

the_gipsy · a month ago
Is this pattern commonly used? Any drawbacks?

Sounds much better than the interface boilerplate if it's just for the sake of testing.

jgdxno · a month ago
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 · a month ago
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.

MarkMarine · a month ago
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 · a month ago
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.

MarkMarine · a month ago
Your two paragraphs give example to the exact problem. “It doesn’t matter what the name is, interfaces aren’t precious”, and “you don’t need to see the implementation to know what it does, just read the method name and type signature” right?

Not sure how you hold those two things in your head at the same time, but they are anathema to each other. Different implementations of the same function name and type signature can have drastically different effects in go because side effects are everywhere in go, so you must read the implementation to understand what it does.

If this was Haskell and I could read the type signature and trust that I know what the system did with that (ignoring Rich Hickey’s point about the type signature not describing what “reverse” does) then fine, but in every function call there are unconstrained numbers of side effects, go functions which persist after the function goes out of scope and can modify any pointed to memory at any arbitrary time later… go is the Wild West in this regard. The interface method name + weak go type system function definition is not enough to tell a developer what the implementation of that interface actually does. Finally: Java’s “implements” plus excellent IDE support for Java DI allows a developer to jump to the implementation in one keyboard press, this does not exist in go. You’ll probably never know what method is actually called unless it’s runtime with the actual config on the server.

I’m not going to explain the whole reasoned argument about why it’s important for a programmer to understand program execution flow in their head clearly, Dijkstra did a much better job than I ever could with GOTO considered harmful, but check out a modern descendant of this article specifically talking about go functions, and try to internalize the point about being able to understand program execution flow:

https://vorpus.org/blog/notes-on-structured-concurrency-or-g...

retrodaredevil · a month ago
I have written production Java code, but no production Go code. I think you skipped over the commenter's main point while replying to them: you need to be able to have a good mental model of the code.

A few well defined interfaces have the advantage of being easy to understand and see usages around the codebase without the overhead of many different variants of an interface. This is extremely important if you are not familiar with a given codebase.

I'm not against segregated interfaces, but I feel like over abstracting can result in code that's harder to understand. There's a balance to be had and thought should go into introducing new interfaces, especially when working on a project with many other devs contributing.

I'm a Java dev, so I'm biased. I love being to easily understand and reason about the type system. I understand that an interface is about a set of behaviors, but when I've worked with Go code I've found it much more difficult to get my IDE to point out all the different ways some interface could be implemented. I see the advantages that Go style interfaces bring, but I personally find it harder to keep a mental model when working with Go.

piazz · a month ago
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 · a month ago
"The Tyranny of nouns" when everything has a subtly different name in every context
B-Con · a month ago
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 · a month ago
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 · a month ago
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.

_256 · a month ago
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.
Joker_vD · a month ago
> 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 · a month ago
>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 · a month ago
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.

et1337 · a month ago
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 · a month ago
> 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 · a month ago
> 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".

eximius · a month ago
> 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 · a month ago
> 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.

the_gipsy · a month ago
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 · a month ago
> 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.

Groxx · a month ago
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.

suralind · a month ago
Xeoncross · a month ago
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 · a month ago
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.

wizhi · a month ago
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.
jbreckmckye · a month ago
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

lenkite · a month ago
With Go had something similar to Java's `@FunctionalInterface` annotation, where a functions signature (parameters and return type) is implicitly matched against the single abstract method of a functional interface and where existing matching methods of any object can also be used as implementations of functional interfaces.
saclark11 · a month ago
You can do this in Go by making a type declaration defining a function and then adding a method with the same signature on that type, which calls the function. The Go standard library does exactly this with the `HandlerFunc` type [1].

  // The HandlerFunc type is an adapter to allow the use of
  // ordinary functions as HTTP handlers. If f is a function
  // with the appropriate signature, HandlerFunc(f) is a
  // [Handler] that calls f.
  type HandlerFunc func(ResponseWriter, *Request)
  
  // ServeHTTP calls f(w, r).
  func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
      f(w, r)
  }
[1]: https://cs.opensource.google/go/go/+/refs/tags/go1.25.4:src/...