10

If I understand Go practices correctly, callers (aka consumers) are supposed to define interfaces of what they want to use from their dependencies (aka producers).

However, if the producer has a function that accepts a custom type, then it's better to make it accept an interface, right? This way a consumer could just pass some value that complies with producer's interface, without knowing the exact type. Because an input value into a producer's function makes the producer become the "consumer" of that input value.

Okay, fair enough.

The question is, how can consumer define an interface, which contains a function, whose parameter is an interface defined in the producer?

Trying to make the question clearer

Let's say I have a package called chef which has a struct Chef. It has a method Cut(fruit) error and fruit is an interface defined in my chef package.

Now let's say I am in the calling code, and I import package chef. I want to give it a fruit to cut, but in my case, I implemented a specific fruit called Apple. Naturally, I will try to build this interface for myself:

type myRequirements interface {
  Cut(Apple) error
}

Because I have the specific implementation of fruit interface called Apple, I want to indicate that my interface just works with apple.

However, if I try to use Chef{} against my interface, Go will throw a compile error, because my interface wants to Cut(Apple) and the Chef{} wants to Cut(Fruit). This is despite the fact that Apple implements fruit.

The only way to avoid this, it seems, is to make chef.Fruit a public interface, and use that in my own interface.

type myRequirements interface {
  Cut(chef.Fruit) error
}

But this completely ruins my ability to plug a different implementation (instead of chef) under my interface, because now I'm tightly coupled to chef.

So Chef has an internal interface fruit, but caller only knows about Apple. How can I indicate in the caller's interface what input should go into Cut without referencing chef?

Answering a comment "Why do you need myRequirements?"

I was surprised that this isn't a more agreed upon concept in the Go community.

The reason I need a myRequirements interface is because I’m a consumer of chef package. Besides Cut, chef may have 100 more methods. But I only use Cut. I want to indicate to other developers, that in my situation I’m only using Cut. I also want to allow tests to only mock Cut for my code to work. Additionally, I need to be able to plug a different implementation of Cut (from a different chef). This is a golang best practice as alluded to in the beginning of my post.

Some quotes as evidence:

Golang Wiki says: "Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values."

Dave Cheney's blog explains: "Interfaces declare the behaviour the caller requires not the behaviour the type will provide. Let callers define an interface that describes the behaviour they expect. The interface belongs to them, the consumer, not you."

Jason Moiron's tweet points out a common misunderstanding: "people have it backwards: #golang interfaces exist for the functions that use them, not to describe the types that implement them"

Update

The best advice I got so far is to move the interface into a 3rd package, independent of caller and producer. For example, make a kitchen package, define Fruit interface in it, and use it in both chefs and callers. Kind of like everyone uses time.Time. Perhaps that's the best advice. That said, I would still like to get an authoritative perspective from someone who tried to deal with this problem in their real work.

Max Chernyak
  • 37,015
  • 6
  • 38
  • 43

4 Answers4

3

I would say that it comes down to what you have control over. In your example, it appears that you've described two separate packages. There are a number of ways to handle this issue:

Accept a Function

You could modify ApiFunction to accept a function that handles the cases you want:

type consumerDeps interface {
    ApiFunction(func() string) string
}

This would allow you to inject the exact functionality you desire into the consumer. However, the downside here is that this can quickly become messy and it can obfuscate the intent of the defined function and lead to unintended consequences when the interface is implemented.

Accept an interface{}

You could modify ApiFunction to accept an interface{} object that is handled by whoever implements the interface:

type consumerDeps interface {
    ApiFunction(interface{}) string
}

type producer struct{}

type apiFunctionInput interface {
    hello() string
}

func (producer) ApiFunction(i interface{}) string {
    return i.(apiFunctionInput).hello()
}

This is a little better but now you're depending on the producer-side to interpret the data correctly, and if it doesn't have all the context necessary to do that, you might wind up with unexpected behavior or panics if it casts to the wrong type.

Accept a Third-Party Interface

You could also create a third-party interface, call it Adapter here, that will define functions both the producer-side and consumer-side can agree to:

type Adapter interface {
    hello() string
}

type consumerDeps interface {
    ApiFunction(Adapter) string
}

Now, you have a data contract that can be used to send by the consumer and to receive by the producer. This may be as simple as defining a separate package, or as complex as an entire repository.

Redesign

Finally, you could redesign your codebase so the producer and consumer are not coupled together like this. Although I don't know your specific usecase, the fact that you're having this particular problem implies that your code is coupled too tightly, and should probably be redesigned. There's probably an element split between both the consumer-side and producer-side package that could be extracted to a third package.

Max Chernyak
  • 37,015
  • 6
  • 38
  • 43
Woody1193
  • 7,252
  • 5
  • 40
  • 90
  • Apologies, was editing my question to make it clearer, and changed my examples a bit. It seems like your 3rd solution (Third Party), is the most feasible. Seems that every example I find, people sidestep this problem by only having primitive types like `time.Time` `string`, `int` inputed in functions of their interface, which is essentially what this is. Your 4th solution (Redesign) seems almost the same as 3rd. Strange that we have to build a separate package to extract a common interface, will have to think about it some more. It'd be ideal if my (updated) example just worked. – Max Chernyak Oct 05 '22 at 03:12
  • @MaxChernyak It's not just a Go solution, either. That's a common solution to import-loops in most languages. The 3rd and 4th solutions differ in that extracting the shared component is much more difficult than creating an Adapter interface, but will generally offer the better long-term solution, IMHO – Woody1193 Oct 05 '22 at 03:35
  • @MaxChernyak My other question, however, would be why you need the `myRequirements` interface at all? Why not just use `Fruit` instead? – Woody1193 Oct 05 '22 at 03:46
  • It's because I'm supposed to define the interface in the caller code. The quote from Go wiki goes: "Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values." Here's one of many articles on the topic: https://hyeomans.com/golang-and-interfaces-misuse/ – Max Chernyak Oct 05 '22 at 04:01
  • @MaxChernyak Exactly, so, in your example, the `Fruit` interface would belong in the `chef` package. But, your `myRequirements` interface appears to do nothing more than redefine the functions defined by the `Chef` struct. In any case, your `Fruit` interface should be declared in the `chef` package with the code that uses it. – Woody1193 Oct 05 '22 at 05:00
  • `myRequirements` does a lot more, I explained a little bit clearer in this comment: https://stackoverflow.com/questions/73954228/in-golang-how-can-a-consumer-define-an-interface-for-a-function-that-accepts-an#comment130590032_73957065. As per your suggestion, `chef.Fruit` is coupled to `chef` package, so I shouldn't put it in `myRequirements`. It will defeat the purpose of my interface trying to decouple from chef package implementation. What if I want to use a different chef implementation? I'll import `chef2` package, but my code still uses `chef.Fruit`. – Max Chernyak Oct 05 '22 at 13:56
  • Updated the OP with explanation of this approach. – Max Chernyak Oct 05 '22 at 14:17
  • Awarding/picking this answer for "Accept a Third-Party Interface". – Max Chernyak Oct 13 '22 at 23:54
2

After discussing a parallel case in "Problem with "define interface where it is used" principle in Golang", I believe that, in your case, the essence of the problem seems to be this: when a function/method in a producer package expects an interface as an argument, that creates a conundrum when we try to create an interface in the consumer package to abstract away the producer.

That is because we either have to reference the producer's interface in our consumer interface (violating the principle of defining interfaces where they are used), or we have to create a new interface in the consumer package that might not be fully compatible with the producer's interface (resulting in compile errors).

That is a situation where the "define interfaces where they are used" guideline (as discussed in "Effective Go / Interfaces and other types / Generality") comes into tension with the goal of reducing coupling.

In such cases, it might be useful to think about why we are trying to reduce coupling. In general, we want to reduce coupling to increase flexibility: the less our consumer package knows about the producer package, the easier it is to swap out the producer package for a different implementation.
But at the same time, our consumer package needs to know enough about the producer package to use it correctly.

See also, as illustration (in different languages, but with a similar idea) "Why coupling is always bad / Cohesion vs. coupling" from Vidar Hokstad.


One possible solution, as you have noted, is to define a shared interface in a third package. That reduces the coupling between the consumer and producer packages, since they both depend on the third package, but not on each other. However, this solution can feel like overkill, especially for small interfaces. Plus, it introduces a new dependency, which can increase complexity.

Given the chef and fruit package structure, one approach could involve creating a common interface in a shared package. Let's name it kitchen.

package kitchen

// Define the Fruit interface in a shared package.
type Fruit interface {
    Color() string
    Taste() string
}

Then in your chef package, you reference this Fruit interface:

package chef

import "yourproject/kitchen"

type Chef struct{}

func (c Chef) Cut(f kitchen.Fruit) error {
    // Do the cutting...
    return nil
}

Now, in your consumer package:

package consumer

import "yourproject/kitchen"

// Define an interface specific to the needs of your package.
type myRequirements interface {
  Cut(kitchen.Fruit) error
}

That approach allows the consumer package to define an interface to match its needs without directly depending on the chef package. However, it does rely on a common understanding of what a Fruit is, which is defined in the shared kitchen package.

Keep in mind, this approach introduces a dependency on the kitchen package, but if Fruit is a fundamental concept to your application that many packages will need to understand, it might be reasonable to define it in a common place.


In practice, the decision might come down to practical considerations. If the producer's interface is stable and unlikely to change, and if the consumer package is already closely tied to the producer package in other ways, then it might make sense to simply reference the producer's interface in the consumer package, even though this increases coupling.

On the other hand, if the producer's interface is likely to change, or if you want to preserve the flexibility to swap out the producer package for a different implementation, then defining a shared interface in a third package might be the better choice, despite the added complexity.

In that last case, you would still have the chef package, but you would also have to deal with another package, butler, which does similar tasks but might not comply with the same interface as chef.

First, you have the Fruit interface in the kitchen package like before:

package kitchen

// Define the Fruit interface in a shared package.
type Fruit interface {
    Color() string
    Taste() string
}

And in the chef package:

package chef

import "yourproject/kitchen"

type Chef struct{}

func (c Chef) Cut(f kitchen.Fruit) error {
    // Do the cutting...
    return nil
}

Now, let's imagine a new package butler which has a similar Prepare method:

package butler

import "yourproject/kitchen"

type Butler struct{}

func (b Butler) Prepare(f kitchen.Fruit) error {
    // Do some preparing...
    return nil
}

In your consumer code, you can now define two separate interfaces, one for each package's requirements:

package consumer

import "yourproject/kitchen"

// Define an interface for the chef package
type chefRequirements interface {
  Cut(kitchen.Fruit) error
}

// And another for the butler package
type butlerRequirements interface {
  Prepare(kitchen.Fruit) error
}

That means you have defined separate interfaces for the chef and butler, each tailored to the consumer's use case for each package.
By doing this, you retain the flexibility to use either the chef or butler packages (or even both) without tightly coupling your code to either one. You have moved the shared interface (Fruit) to a third package, kitchen, and you are using that in your consumer's interfaces.

That example demonstrates the flexibility of Go's interface system and how it can be used to decouple packages and support multiple implementations of a concept. But that does add some complexity, in exchange for a good deal of flexibility in return.


In other words, there is no hard and fast resolution to this dilemma: the best solution depends on the specific context and requirements of your project. Both approaches have trade-offs, and the right choice depends on which trade-offs are more acceptable in your situation.

It is also worth noting that this is a fairly niche problem that you are unlikely to encounter often in day-to-day Go programming. In many cases, you can structure your code in such a way that this issue does not arise. But when it does, it is a reminder that guidelines are just that—guides, not rigid rules, and it is up to us as developers to make the final judgment based on our understanding of the specific problem and the broader context in which it exists.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • Great insights! In a way, it seems that to follow this interface practice, you're encouraged to design "fundamental" interfaces that will serve multiple packages around a similar topic. You create these "kitchen"-like packages at the core of your mini-ecosystem, and surround them with functional packages. The more people depend on your common interfaces, the more authoritative your interfaces get in the wider ecosystem. Eventually they might reach the ubiquity of something like `time.Time`, and people will start pulling them in just to contribute new functionality to a given topic. – Max Chernyak Jul 30 '23 at 00:25
  • @MaxChernyak True, centralizing common or fundamental interfaces in a shared package, like the `kitchen` example, can lead to better structure and maintainability of the codebase. But it does come with its own challenges, such as the need for careful design and maintenance of these core interfaces to avoid breaking changes. – VonC Jul 30 '23 at 04:37
1

I'm not quite sure why you would introduce the myRequirements interface. If Chef requires a Fruit to Cut and you want to define a specific fruit called Apple - all you need to do is to define an Apple struct which implements the Fruit inteface.

type Chef struct {
}

type fruit interface {
    Cut() error
}

func (c Chef) Cut(fruit fruit) error {
    return fruit.Cut()
}

All you need to do is then to define Apple which implements the Fruit interface based on your requirements:

package kitchen

import chef "goplayground/interfaces/fruits/chef"

type Apple struct {
}

func (a Apple) Cut() error {
    // lets cut
    return nil
}

type myRequirements interface {
    Cut(Apple) error
}

type myChef struct {
    chef chef.Chef
}

func (m myChef) Cut(apple Apple) error {
    // since Apple implements the chef`s fruit interface this is possible
    return m.chef.Cut(apple)
}

func cook() {
    remy := myChef{}
    apple := Apple{}
    _ = remy.Cut(apple)
}


fabs
  • 77
  • 1
  • 1
  • 13
  • The reason I need a myRequirements interface is because I’m a consumer of chef package. Besides Cut, chef has 100 more methods. But I only use Cut. I want to indicate to other developers, that in my situation I’m only using Cut. I also want to allow tests to only mock Cut for my code to work. Additionally, I need to be able to plug a different implementation of Cut (from a different chef). This is a golang best practice as alluded to in the beginning of my post. – Max Chernyak Oct 05 '22 at 12:33
  • I`m not aware that the callers should define what they want to use from their dependencies given you are using a pre-existing package. It sounds like the Chef package is poorly designed and the interfaces are just too big if there are really more than 100 methods that need to be implemented to satisfy the interface. Do you have any control over the chef package? – fabs Oct 05 '22 at 13:58
  • From here: https://github.com/golang/go/wiki/CodeReviewComments#interfaces "Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values." From here: https://dave.cheney.net/practical-go/presentations/gophercon-israel.html#_let_callers_define_the_interface_they_require "Interfaces declare the behaviour the caller requires not the behaviour the type will provide. Let callers define an interface that describes the behaviour they expect. The interface belongs to them, the consumer, not you." – Max Chernyak Oct 05 '22 at 14:08
  • From here: https://twitter.com/jmoiron/status/532314843689132032. "people have it backwards: #golang interfaces exist for the functions that use them, *not* to describe the types that implement them". This is a different approach in Go compared to Java, .NET, etc. You shouldn't expose interfaces from your packages, you should return concrete types, and let consumers define interface for what they rely on. – Max Chernyak Oct 05 '22 at 14:11
  • Updated the OP with this info. – Max Chernyak Oct 05 '22 at 14:17
  • I get your point but the problem lies within the chef package. – fabs Oct 05 '22 at 14:21
  • I don't think we have to bite this bullet. Too many functions doesn't have to indicate that a package is badly designed. There are many legitimate reasons to have many methods that correctly reflect responsibility of a package. Arguably that's why Go interfaces are powerful: indicate exactly what you consume. Quite a few stdlibs out there have many methods (e.g. https://pkg.go.dev/strings@go1.19.2) – Max Chernyak Oct 05 '22 at 17:08
  • I think we are misunderstanding each other. The package can have a lot of functions. What I mean is an interface should be small. – fabs Oct 06 '22 at 13:38
  • Hm, in my example the interface only has 1 method (`Cut`) regardless of how many other methods `Chef{}` has. You said "the problem lies within the chef package", but chef package doesn't offer any interfaces. It only defines the interface for an input value it expects from the caller. In that situation roles are reversed, because chef is the consumer of the input value. So chef has a small interface for an input value, and caller has a small interface for `Cut`. Which interface is not small? – Max Chernyak Oct 06 '22 at 16:17
  • If you want to use the Cut method from Chef your apple needs to be compatible to the expected interface - thats what I mean with the problem lies within the chef package. You cannot define an interface outside of chef which is not compatible with your fruit interface. I have updated my code example with a workaround to easily replace chef which hopefully makes it a bit clearer. – fabs Oct 07 '22 at 09:35
  • 1
    The workaround is well-illustrated, thank you, but the cost of it is too high. If I must wrap all the needed types and methods, I'd rather just not write caller interfaces. – Max Chernyak Oct 07 '22 at 16:15
1

There are nuances to the correct use of duck-typing, which is what Go type system is when it comes to interfaces. It is usually a good practice to define the interfaces where you use them, but io.Reader interface is defined in the standard library. So there are limits to the applicability of that advice.

In your case, the package chef has two interfaces, Chef and Fruit. These two interfaces are closely coupled, because Chef has a method that uses Fruit. With the current Go type system, you cannot use Chef without exporting Fruit from that package. So:

type myRequirements interface {
  Cut(chef.Fruit) error
}

is the only way you can use the chef.Chef with an Apple implementation from your package.

But what you want to do is:

type myRequirements interface {
  Cut(Apple) error
}

and that you want to be able to convey that this is a subset of Chef, that is, the semantics of Cut is the same as the semantics of Chef. Well, the semantics are different. It is unsafe otherwise.

Say, you implemented Apple as:

type Apple struct {}

func (a Apple) SomeFunc()
func (a Apple) FruitFunc()

whereas chef.Fruit is:

type Fruit interface {
   FruitFunc()
}

Clearly, Apple implements chef.Fruit, so you can pass Apple to whereever a chef.Fruit is required. But you cannot pass chef.Fruit to myRequirements.Cut(Apple) func. Because in myRequirements.Cut you also implied that you may use Apple.SomeFunc, which is not defined in chef.Fruit.

So, if you really would like to define an interface like myRequirements, then you have to define it using chef.Fruit. If you define is using Apple, the myRequirements.Cut method is different from chef.Cut.

Burak Serdar
  • 46,455
  • 3
  • 40
  • 59
  • This is the clearest explanation on why it doesn't work. The only missing part is: an effective alternative solution. Right now the only one is given by Woody1193 in the section "Accept a Third-Party Interface". I'm waiting to see if there's a better solution out there based on real experience. If not, then that's likely the best one. – Max Chernyak Oct 07 '22 at 19:56