DEV Community

dean
dean

Posted on • Originally published at blog.deanveloper.com

The Problem with Interfaces, and how Go Fixed it

Interfaces are great

They're honestly brilliant. Interfaces allow for simple, elegant programming. Let's use Java to get a quick idea of how to use interfaces.

package mypackage;

public interface Edible {
  public void eat();
}
Enter fullscreen mode Exit fullscreen mode

Now, a Burger class to implement Edible.

package mypackage;

public class Burger implements Edible {
  public void eat() {
    System.out.println("Burgers, yum!");
  }
}
Enter fullscreen mode Exit fullscreen mode

This means that we can use our Burger class wherever we see Edible, such as in method arguments!

The problem with interfaces

But let's say that we are using a library, and that library has a class called Salad. Here's what Salad looks like:

package theirpackage;

public class Salad {
  public void eat() {
    System.out.println("I'm being healthy!")
  }
}
Enter fullscreen mode Exit fullscreen mode

So Salad has an eat() method, but because the other library doesn't even know about Edible interface exists, it obviously can't implement it. This problem has definitely been encountered before, so let's talk about how Go has fixed it.

How Go fixes interfaces

With how Go interfaces work, you don't need to declare an interface implementation. If you implement the proper methods, you implement the interface. This is very prominent in Go, and is a key feature. Let's use our previous example.

First, let's remake our Edible interface.

package mypackage

type Edible interface {
  Eat()
}
Enter fullscreen mode Exit fullscreen mode

Now, let's make a Burger struct that implements Edible.

package mypackage

type Burger struct {}

// How to define a method in Go
func (b Burger) Eat() {
  fmt.Println("Burgers, yum!")
}
Enter fullscreen mode Exit fullscreen mode

Notice that we never acknowledged Edible anywhere while we made the Burger struct. This is because any type that has an Eat() method associated with it is considered Edible. This means that even types from other libraries can be Edible, as long as they have an Eat() method!

This means that even this Salad struct from a different package is Edible.

package theirpackage

type Salad struct {}

func (s Salad) Eat() {
  fmt.Println("I'm being healthy!")
}
Enter fullscreen mode Exit fullscreen mode

This shows just how awesome Golang is. I highly recommend watching their video, A Tour of Go, which shows all of the brilliant features that Go has to offer.

EDIT: I really like the discussion on this post! One of the key things that I didn't mention here is that interfaces in Go should be used to describe simple patterns. Here is a clip from A Tour of Go that should hopefully describe how interfaces in Go should be used compared to how they're used in OOP.

Top comments (46)

Collapse
 
danielhoffmann_ profile image
Daniel HB

What about name collisions? This seems so wrong to me, how is type-safety enforced?

Collapse
 
thatjoemoore profile image
Joseph Moore

The Go developers believe that, with interfaces, you mostly care about what something can do, not what something is. So, instead of having a rigid type system with inheritance and all that mess, they allow functions and methods to just declare "I need something that can do these things." If you're coming from a more complex type system, like Java (which is where I came from), it can take some getting used to, but it's really one of my favorite parts of Go. It lets me concentrate on what things need to be doing, not what crazy class tree they're part of.

Collapse
 
danielhoffmann_ profile image
Daniel HB

I also come from Java world. I can see what you mean, but still what is the problem of just saying that this class implements this interface? This is completely unrelated to inheritance.

Thread Thread
 
dean profile image
dean

Because declaring what an object is requires that you know that something exists.
Take that salad example from earlier: even though salads can be eaten, because it doesn't know the Edible interface exists, it isn't "Edible"
In Go, because Salad has an "Eat()" method, it can do everything that an Edible can do, so it is considered Edible.

Thread Thread
 
danielhoffmann_ profile image
Daniel HB

Well, I stopped to think about this a little more and I don't think I ever used 2 interfaces with the same method names in any of my code ever (although I have not been coding type-safe languages for a while now). I can see why go went this route, but it is hard to measure how many problems this might cause without making a large-scale project.

I still like the idea of specifying in the class what interfaces it implements, for example if a class implements Serializable I know ALL methods of Serializable are implemented there. Instead of having to check all the methods in a class, I can just check the class stub. This very useful when browsing the javadocs, I can just check the class implemented interfaces and see if it implements Serializable and just throw it at a JSON encoder or some other IO operation.

Thread Thread
 
dean profile image
dean

It doesn't cause problems from my experience. Also interfaces in Go still require a full method signature. But in Go, interfaces shouldn't be used to define what a type is, but rather what it does. To tell what a type is, you can use GoDoc.

Collapse
 
dean profile image
dean • Edited

I re-read your question and realized that no one really answered it. I didn't want to get into the whole concept of packages in Go in the blog post so I didn't go into it. Basically, when referring to a type, variable, or anything, you refer to it through packagename.whatever. Our Edible interface could be referred to as mypackage.Edible, the Burger struct could be referred to as mypackage.Burger, although we can just refer to them as Edible and Burger because they are in our package.

Meanwhile, the Salad struct must be referred to as theirpackage.Salad because it is in a different package.

Also, in terms of type collisions with method names, all arguments and the return type must match the interface's in order for it to be considered "implemented".

Collapse
 
coreload profile image
📶

A type that implements an interface does so solely by using a value of that type in a position for that interfaces. So a use is a kind of implicit definition.

If a type is used in the position of two interfaces with some or all method signatures in common this should be 1. suspect or 2. based on informal contracts that those common methods are intended to conform to each other. Type safety will be enforced. Intent should be suspect.

Collapse
 
danielhoffmann_ profile image
Daniel HB

Interesting, so there is some type inference going on when you throw an object at a method that expects an Interface as an argument. Thanks for the explanation!

Thread Thread
 
kunde21 profile image
Chad Kunde

In the case of passing a struct into a function that takes an interface, the compiler enforces interface requirements. It will even tell you what methods are missing from the type which cause it not to fulfill the interface.

Collapse
 
yelluw profile image
Pablo Rivera

This is Go's core value: It's dead simple. Glad you write this. People need to learn about simpler OOP approaches even if they don't plan to implement them. Knowing about how other languages do things ultimately helps you become better.

Collapse
 
mrlarson2007 profile image
Michael Larson

What happens if the interface changes? How would you even know that happen until runtime? If the interface is an internal one to your application no problem, just add tests to cover that code. But if you are dealing with a thrid party library and trying to conform to an internal interface, this could become a real pain point.

Collapse
 
tyrion85 profile image
Ivan Ivic

I think you would catch such changes at compile time?
Also, what about dependency management? I think managing dependencies is one of the main reasons for why interfaces exist in the first place (dependency inversion principle). Without explicit declaration of interfaces, how do you know upon which modules or libraries you depend? Do you have an easy way of figuring out on which interface(s) you depend, when you look at one struct/class/whatever-it-is in Go?

Collapse
 
dean profile image
dean

Interfaces should not be used to handle dependencies in Go. Go has features specifically for managing dependencies in the CLI with go get and such. Go is built around open source, so it's able to use things like git to manage dependencies (import paths for third party libraries are actually the URL to get to their library from). There are also third party managers for people who don't like the way that go get works, by putting dependencies in a vendor folder.

There is a bit of responsibility on the library maker and user though. A library maker shouldn't change an interface without documenting it, just as the user shouldn't update a dependency without reading the changes. A change in an interface will still most likely result in compile-time errors, as Go does use a strong typing system.

Thread Thread
 
tyrion85 profile image
Ivan Ivic

Good post. Thanks.

Collapse
 
dean profile image
dean

If the interface changes, you no longer implement the interface if your struct does not have the matching methods. This will most likely cause issues at compile-time, so you are able to catch it very early.

Collapse
 
roddi profile image
Ruotger Deecke

Wow! Implicitly making a type conform an interface sounds like a really bad idea to me. I mean, what could possibly go wrong? Right?

I like the Swift approach way more where you can explicitly declare that an existing type conforms to an interface (called protocol in Swift) like this:

class Salad: Edible {}

No accidental conformance.

Or as the joke goes: if it walks like a duck and quacks like a duck it probably throws exceptions at runtime.

Collapse
 
emasoft profile image
Emasoft

@Ruotger Deecke: Well said. The author of the article doesn't seem to realize the trouble Go programmers are gonna get in with this.

Let me make an example.

If I have an "Audio" object (parsed and instanced from a random file in a folder) and a "AnimatedGif" object (from the same folder), and both have a "Play" method, I can easily happen to pass the "Audio" object to the "AnimatedGifVisualizer" by error, because both classes have a "Play()" method.

This will work fine until the "Play()" method would be actually called, suddenly crashing everything. And you will NEVER found why. Replace this for any similar scenario using any other data types, locally or server side, and you get the same silent, untraceable crash. This implicit interface conformance thing is a debugging hell.

This is why Go will never compete with better designed langages like Swift.
Because while who designed Swift really thought about this when created Swift protocols, Go designers just throwed in features because those sounded nice at first sight, with no careful assessment of consequences.

Collapse
 
dean profile image
dean

If an error happens on your part, it isn't Go's fault for being designed badly, it's most likely yours. In order to crash the system in your example, you need to put your Audio struct in a place called AnimatedGifVisualizer...

In Go, methods have very clear meaning about what they do. If you see a method that's called CloseThenExit(io.Closer) in an io library, would you really pass in your Door struct which has a Close() method?

Collapse
 
thomaslevesque profile image
Thomas Levesque

Sounds like a really bad idea to me. What if a class has a method with the same name as an interface method, but with completely different semantics? If you want to use Salad as an Edible, just make an EdibleSaladAdapter, and you're done... Sure, that's more work; but at least it's safe.

Collapse
 
dean profile image
dean

If you see that a method takes a Closer ("Closable" in other languages), sure a struct for a door might have a Close() method, but is it really a good idea to pass it to a method like io.CloseAndQuit(Closer)?

In Go, it's usually quite clear what interfaces are intended to be used for, so while it's possible to pass something like a door or a cabinet into an io function, it's pretty clear that you'll get unintended consequences. It might not be as safe in a few select contexts, but you get the huge benefit of convenience that I've described above.

Collapse
 
solitarycipher profile image
Nick

Haskell's approach is interesting as well, and not dissimilar from Go. In Haskell you can declare an interface (but really a typeclass), and then explicitly make a type "part" of that interface. It would be like declaring a class in Java, then declare interface, then you can define an implementation of the interface's method separate from the original class.

This has the benefit of not having to know which interfaces a type follows when you declare the type, as well as having to explicitly make a type to be a part of the interface.

Collapse
 
legolord208 profile image
jD91mZM2

This is really cool. Would be awesome to see in more languages to be honest

Collapse
 
dean profile image
dean

Someone sent me a language on twitter that has a similar concept. They had both "traits" and "interfaces", where traits had to be explicitly implemented and interfaces would be more like Go.

Collapse
 
n3llyb0y profile image
Neil Chambers

That sounds like Scala. It has a type safe concept of structural typing (a lot safer than Go)

Thread Thread
 
dean profile image
dean • Edited

While structural typing is safer, the post pretty clearly explains the problems with it. The safety brought by structural typing isn't worth the benefits that it restricts.

Thread Thread
 
n3llyb0y profile image
Neil Chambers

That's a pretty broad statement. The best you can argue is a tradeoff for the language features that are right for the product/team/roadmap. I've work for years with static and dynamic languages. They both have strengths and weaknesses. That Go is duck typed is just not a selling point. Many languages do it. I would avoid comparing Go with statically typed, OOP, languages to be honest (unless you fancy a flame war). Go doesn't solve interface problems, it just has a different idea of what an interface is, and it's not that novel. Go should be considered when runtime performance is critical and a simple and effective concurrency paradigm is needed. Go is many things, but it will always have safety issues (maybe that is why the error handling seems to be, err, well).

Thread Thread
 
dean profile image
dean

Go is statically typed. If you were to pass a Dog struct (which can't Quack()) into a function that takes a Duck interface, you will hit a compile-time error.

Thread Thread
 
danielw profile image
Daniel Waller (he/him)

If I understand correctly Go isn't even strictly duck typed.

The type inference doesn't happen at runtime but at compile time, giving very clear compiler errors.
So I don't really see how this kind of typing can lead to runtime errors in Go's case

Thread Thread
 
dean profile image
dean

The only time it would result in a runtime error is when casting, as per every staticly typed language.

Thread Thread
 
danielw profile image
Daniel Waller (he/him)

That's what I thought! So I really don't get all this duck typing trash talk some people are throwing around in regards to Go

Collapse
 
dfacastro profile image
Diogo Castro

Structural subtyping is a terrible approach. It means Go Interfaces can only abstract over a type's structure, but not its laws/semantics/algebra.

For example, not every type with a combine function is a monoid (combine must be associative). In Go, every type with combine is automatically a monoid.

Collapse
 
dean profile image
dean • Edited

It only conforms to the interface if it has the correct parameters as well. The examples listed don’t include type information, which was my oversight when trying to make it simple.

Also, interfaces in Go describe what a structure does rather than what it is, and they should be used as such. So something with a combine method is usually a Combiner rather than something with a fancier name.

Collapse
 
dfacastro profile image
Diogo Castro

Also, interfaces in Go describe what a structure does rather than what it is, and they should be used as such.

Yes, that was precisely my point. They let you abstract over the signature of a function, but not over its laws. Therefore, Go's expressiveness is limited.

Collapse
 
jervine791 profile image
john ervine

The word awesome is over used. Your main selling point is a developer doesnt need an in depth knowledge of the code base there working on. If developers are forced to work with an interface they will simply implement it. I would not want implementation of my interfaces spread throughout my code base. I think its a code smell. I want to know what my code implements.

Collapse
 
dean profile image
dean • Edited

Really the main point I was trying to make is that code from other libraries can be easily used in code you make. If you bring in a library which supplies an HttpRequest, and you have an interface called Requester which has a Request() method, as long as HttpRequest has the Request() method, it conforms to the Requester interface and can be used where you'd want it to be used.

Collapse
 
cmelthratter profile image
Cody Melthratter

Interesting approach. Java people (myself included) would have a hard time with this one. It sounds like it would work well for standard/widely used operations (i.e. GET, POST, DELETE), but also the name collision could be an issue.

HOWEVER, to avoid name collision, one would just have to "know something exists" (so that you dont reuse a function name) in the same way you would in typing systems similar to java. So to me, it could be a worthwhile tradeoff.

Collapse
 
ddekany profile image
ddekany • Edited

Not sure if the Go designers are just trolling or what... You're suggesting that the idea is that a class can implement an interface purely out of coincidence, and that's somehow useful in reality. But, if the interface was implemented accidentally, that means that the author of the implementing class didn't know what behavior the interface specifies for those methods (he/she haven't seen the API docs). So that's exactly the unfortunate case where you don't want to utilize implicit interfaces. If, however, the author of the class have implemented the interface on purpose, it's a big loss that this intent can't be expressed in the language (doing that in comments is obviously inferior; they aren't enforced or checked), as now the code is less self-documenting.

OK, there's a thin chance that both the method signatures and the specified behavior matches, purely out of blind luck, and not because the author deliberately implemented the interface. Given the certainly low chance of that happening, is it worth it to give up said self documenting capability? I mean that's quite a big sacrifice.

Collapse
 
dean profile image
dean • Edited

First of all - Go isn't object oriented. It doesn't have classes, it has structs. It doesn't have methods, it has functions that operate on structs. OOP interfaces describe what a class is, but Go interfaces describe how a struct behaves.

It's okay to accidentally implement an interface, there's no sacrifice. Sure, you might make a door struct that might have a close() function, but in order to mess something up, you'd need to pass it into a method that clearly says it takes an io.Closer as an argument.

Also, these exceptions are still caught at compile time. If you pass a struct that doesnt match a specific interface to a function that requires that interface, it will throw a compile-time error that specifies which functions are missing from the struct.

Collapse
 
ddekany profile image
ddekany • Edited

My point didn't go though. I wasn't talking about uncaught mistakes. My concern is about how self documenting the code will be, and for what was that sacrificed. Let me try again. If a type T has a set of functions associated that satisfy the I interface, and also they behave as described in the documentation of I, then most certainly the author of T (or of the functions) has deliberately "implemented" the I interface there. Right? I mean, just how often does such thing happen by chance? If almost never, then why do the Go designers want us to not state that intent (I'm implementing I) in the language? What's the actual, practical, every day use case for "implementing without stating it"? Surely it's not that Go developers often get extremely lucky and so can avoid adapters.