loading...

The Problem with Interfaces, and how Go Fixed it

dean profile image Dean Bassett Originally published at blog.deanveloper.com ・2 min read

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();
}

Now, a Burger class to implement Edible.

package mypackage;

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

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!")
  }
}

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()
}

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!")
}

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!")
}

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.

Discussion

pic
Editor guide
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
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 Bassett Author

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 Bassett Author

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 Bassett Author

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
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 Bassett Author

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
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 Bassett Author

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 Bassett Author

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 Bassett Author

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
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 Bassett Author

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
legolord208 profile image
jD91mZM2

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

Collapse
dean profile image
Dean Bassett Author

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 Bassett Author

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 Bassett Author

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 Bassett Author

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
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
oscherler profile image
Olivier “Ölbaum” Scherler

Reading this, I’m thinking it could be called “The Problem with Duck Typing, and how Go Fixed it.” I don’t know how good code that was written without the knowledge of the existence of an interface will be at implementing it. But if you write code that expects an object that you can order, mix, and eat, then instead of enduring three “has such method” tests in every method that uses such an object (like you spend your time doing in if you do duck typing in Ruby), you can define an interface that turns those tests into a type declaration, and be done with it. I my opinion, that’s a fix for the ordeal that duck typing it in practice.

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 Bassett Author

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
ddekany profile image
ddekany

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 Bassett Author

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

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.

Collapse
iglosiggio profile image
Ignacio Losiggio

I added these links on the twitter thread but i think that i would be useful to leave them here to!

The first comment is that the go interfaces remind me a lot on haskell's type classes which on quick search are compared a lot with classic interfaces

And upon reading on type classes i stumbled with a C++ proposed feature that the call concepts and that page gives a bit of insight into how's all this compiled statically.

Collapse
coreload profile image
📶

Haskell type classes are syntactical. There is no runtime dynamic dispatch. Go interfaces are structural types, their implementations are always inferred, and their purpose is solely for runtime dynamic dispatch.

Collapse
in_harmonia profile image
Bustanil Arifin

I think this is called duck typing.

Collapse
dean profile image
Dean Bassett Author

It's similar, but not quite! Usually when people refer to duck typing, it's runtime (ie checking if something is a type by checking if it has a certain method at runtime). Go's interfaces are determined at compile-time rather than runtime.

Collapse
roddi profile image
Ruotger Deecke

True but the problem is the implicit part. Implicit is bad because.