12 ms·
How to Use Go Interfaces
- deleted 9y ago[deleted]
- tepidandroid 9y agoGreat article that corroborates what I've also personally seen in many Go applications. IMHO, if you're coding Java style interfaces up-front ("Repository", "Service", etc) you're already doing it wrong. It's likely that you're either over-engineering your project or prematurely optimizing for flexibility you will not need. You'll likely feel as though you're fighting the language to build these abstractions and layered architectures. Go is all about minimalism. As the article says, you should be writing the concrete implementations first, then defining interfaces in the client code. The calling code should pick and choose the behaviour it needs from the implementation. As hard as it is for most seasoned Java developers, you have to silence that anxious voice inside that keeps asking "what if I need to swap this out for something else?". Go is about getting things done, not beautiful abstractions.
- dionian 9y agoAs someone who works in a lot of Java codebases, I would say that good Java code is more similar to how you describe Go and less similar to the bad, over-abstracted Java code.
- seanmcdirmid 9y agoSometimes you don’t know what the implementation will be yet, and writing a bunch of interfaces out can help you discover what it should be, then you start working it out with interfaces gradually giving way to implementation. Interfaces are rarely rarely about interoperability even in Java. Most interfaces have one implementation. Instead, they are about abstraction, design, and so on. This article assumes that the end product is what was intended all along.
- xienze 9y ago> Most interfaces have one implementation. Close, with the exception of mocking interfaces for unit testing, which is very common in Java.
- stubish 9y agoThis. So many packages are a pain to test, because only a concrete implementation is provided. If an interface was used, consumers can swap it out for a mock implementation. It is an almost endemic problem, but the article is criticizing the only available solution (apart from forking). I am utterly sick of writing fragile scaffolding to deal with these dependencies, to the point I wish that Go provided an implicit interface for every declared type. Testing would be so much easier if I could pass in a MockFoo instead of a Foo, provided MockFoo satisfied the implicit interface.
- sythe2o0 9y agoThe article, from how I read it, is not suggesting single-implementation interfaces are the problem but instead where those interfaces are stored. The package accepting the interface should define the interface it needs (and perhaps that package defines the mock implementation), and the implementation should exist somewhere else.
- ghthor 9y agoI've programmed in both ways and they each have there place. But in general this is the right way, you shouldn't export interfaces, you should export concrete types and then use the documentation and tests to example what types of interfaces the types can cover.
- seanmcdirmid 9y agoTypeScript, having a purely structural type system, means any class has an interface. Unfortunately, if the class has any private or protected members, its interface becomes unimplementable.
- peterevans 9y agoAbstractions are not a bad thing, but early abstraction often lacks understanding of the problem to solve. Abstraction is complexity. We often need complexity, but we should not add complexity without cause. And I don't think this is a Java vs. Go thing. Let the program, or the problem if you will, prove to you where you need abstraction.
- frou_dh 9y agoAn interface whose purpose is to reduce known coupling is not really "abstraction". It's a mechanism for indirection. IIRC Zed Shaw had an essay on this.
- chewxy 9y agohttps://zedshaw.com/archive/indirection-is-not-abstraction/ https://zedshaw.com/archive/indirection-is-not-abstraction/
- balefrost 9y agoTo be fair, that Zed Shaw essay isn't particularly good. 1. He uses lay (i.e. non-technical) definitions of "indirect" and "abstract" to argue that the technical definitions are somehow flawed. Words have different meanings in different contexts, but he rejects those different contexts. 2. He argues that abstract classes in Java are actually a form of indirection because the abstract modifier "creates an indirect path to the real implementation". This is after he covers that "abstract" can mean "a concept or idea not associated with any specific instance". 3. He claims that no lay definition of abstraction permits the idea of "swapping out the implementation". He fails to understand that, from the client code's point of view, "swapping out the implementation" is a direct consequence of coding to the abstraction. 4. In the words of Rich Hickey, I think Zed is conflating Simple and Easy. I don't doubt that Zed saw something legitimately objectionable and that drove him to write the essay. But in trying to formulate his point, I think he ended up conflating ideas, relying on false premises, and built an argument that didn't have much of a logical progression. He's essentially saying "I'm right and everybody else is wrong because I looked it up in a dictionary."
- Pxtl 9y agoDirty secret: do that in Java too.
- ploxiln 9y agoThere's a difference though: in Java, the class declaration must mention the interface, but in Go it does not. In Go, you can declare an interface for a subset of a type's methods, in some other package, and use it there, without any change to the original type. You can create a new interface which a standard-library type satisfies!
- pjmlp 9y agoYes, and then be amazed that certain types match by accident interfaces with completely different semantics, just because the methods happen to be called the same by accident. Then Start() will throw a rocket instead of starting the coffee machine. just because RocketLauncher and CoffeeMachine happen to have the same set of methods.
- frou_dh 9y agoSo not only does a type have to "accidentally" implement an interface, but also a programmer has to explicitly write code that has a value of that type in scope, and pass it to a function in a wildly different domain. Sounds very much like only a theoretical gotcha.
- pjmlp 9y agoIt is a theoretical gotcha like writing unsafe code in C is. Sure it is easy to avoid such gotchas in small code bases, handled by two or three programmers. Scale it up to the typical sizes of enterprise projects, maintained by generation of programmers in distributed teams, with a goal of at least 10 years deployed in production and those gotchas happen all the time.
- frou_dh 9y ago
- eadmund 9y agoPremature abstraction is a smell in every language, but it's particularly bad in Go. If you're building out a giant hierarchy of directories & packages at the beginning of your project — you're probably wrong. If you're building out a giant hierarchy at the middle of your project — you're probably wrong. If you're building it out at the end of your project — your project is over now, why are you spending time abstracting stuff when you can deliver already? It's not that Go is against abstractions, really; it's just that we programmers love, absolutely love to build a system with operators and binary operators and summing binary operators and addition and numbers and real numbers and integers and complex numbers and imaginary numbers — when all we really need to do is add 2 & 2 to get 4. Go says, 'hey there, slow down, hang on a sec.' You're free to abstract if you need to — but there's a good chance you're wrong to do so. Write the simplest thing that could possibly work. Maybe, maybe once you've written it three or thirty times, you'll have a good sense of the problem and be able to write a sane abstraction for it. But if you're spending time fleshing out interface or package hierarchies before you even write any code: you're probably wrong.
- googlemike 9y agoSpoken like someone who has never worked on a long-lived project. There is no beginning, middle or end. You joined when it has been around for 7 years. It needs to be around 10 more years. Early and careful abstraction of absolutely every layer of your program is something you will always thank yourself for. If your classes only talk to interfaces at every single layer (no concrete classes talking to concrete classes, except for maybe a factory here or there to vend out said interfaces), your project will be much easier to test, refactor, and dynamically change.
- dnomad 9y agoYes I find these sorts of comments as GP to be hopelessly naieve. It speaks to somebody who has never worked on a large project (think 4 teams of 10-20 developers spread across NYC, London, Tokyo, and Hong Kong) or a long-lived project (think 10 years). That sort of minimial, incremental approach does not scale at all. Software engineering in the large requires, as you say, interfaces at every boundary and a near-religious understanding of how all the different components and boundaries fit together.
- hota_mazi 9y agoEvery single thing you say apply to Java. And C#. And C++. And pretty much any language in existence. It's just healthy engineering practices. Whether developers apply them or not has nothing to do with the language.
- tepidandroid 9y agoThe difference is that other languages (like the ones you mentioned) are actually quite accommodating when it comes to indulging developer abstraction fetishes, whereas you will be punished for trying to do so in Go.
- closeparen 9y ago... until you write unit tests, at which point you need either interfaces or global mutable function pointers to swap out underlying implementations with mocks.
- tepidandroid 9y agoI don't think we're in disagreement here. I have no qualms with abstractions/interfaces. It's simply a matter of putting the horse before the cart. The implementations should not have to tip-toe around pre-conceived abstractions.
- flukus 9y agoWe really need some better solutions to this. Aside from abstraction it also comes with the performance penalty of virtual functions everywhere. I think we had some good tools to solve this but either threw them away in newer languages or just forgot they were possible. Working with c recently I wanted to mock something and the language obviously couldn't help me so I came up with something along the lines of: #ifdef TEST #define foo mock_foo #endif This worked for mocking single functions but would need to evolve for more complex code. There are a number of ways to do this from optional includes to compiling test fixtures to their own binaries with the mock implementation being the actual implementation. Both options should work with just about any language to varying degrees of effort. The biggest barrier to approaches like this is the reliance on IDE's and what they can do. I've never seen an IDE that can handle rules like this, they want to compile the project and control it's structure. In c# for instance, it would be pretty easy to have a mock library with concrete alternative implementations and compile the tests individually, something like: csc.exe MyTest.cs MyClassUnderTest.cs /reference MyMockAssembly.dll This is perhaps even easier in c/c++ where the includes are more explicit. Once you take back control of the compilation process a lot more options open up though.
- guelo 9y agoI don't know much about Go but in Java and C# mocking frameworks use runtime bytecode manipulation to make it really easy to swap out concrete implementations without needing to create separate interfaces.
- nostalgeek 9y ago> IMHO, if you're coding Java style interfaces up-front ("Repository", "Service", etc) you're already doing it wrong. Then how do you unit test your controllers in a web app? or anything that depends on a database connection without decoupling service and repository? > Go is all about minimalism. No, it isn't. It's your opinion about Go. Unit testing doesn't become irrelevant because you are using Go. > you should be writing the concrete implementations first, then defining interfaces in the client code That's not how you do test driven development via unit testing. Just because Go is relatively new it doesn't mean software engineering best practices don't apply anymore.
- tsavola 9y agoThe distinction is that you don't first declare a few all-encompassing interfaces and then their concrete implementations, but piecemeal interfaces for each API that takes those implementations - only the bits of the interface that the API in question needs. That makes it actually even easier to do unit testing, because for a narrow test you don't need to implement irrelevant methods.
- DougBTX 9y ago> That's not how you do test driven development via unit testing Unit tests test concrete implementation "units", so this is completely compatible with writing unit tests.
- reificator 9y ago> Then how do you unit test your controllers in a web app? or anything that depends on a database connection without decoupling service and repository? From the post you responded to: > you should be writing the concrete implementations first, then defining interfaces in the client code. When you need to take an argument that you can swap out for another type, that's when you define your interface. Not earlier. > Unit testing doesn't become irrelevant because you are using Go. Unit testing doesn't become irrelevant because you don't architecture astronaut from the start. When you need to test, define an interface. But only the interface that you need when you need it. > Just because Go is relatively new it doesn't mean software engineering best practices don't apply anymore. By providing the client the ability to define the interface it removes the need to pretend you can plan for all circumstances upfront. Instead, at the moment you need to be able to mock things, you make an interface and mock them. That's not to say that you should never provide interfaces in a library, just that it's no longer the only option available. I'm not sure why you think that unit testing becomes harder with this approach, because to me it seems far easier.
- notheguyouthink 9y agoThat's a nice perspective. With that said though, what do you think about centralized data types? I admit it, I make Repository and Service quite frequently. With that said, I also have data types shared among implementations. Normally the data types are defined within the parent packages to the implementations, the same packages that hold the Service definition. I actually agree with your assertion that Service is bad form, and think I'll change it. I still have the issue though where a shared data type is commonly used among many implementations. Where do you think that will live? Perhaps in the same location, just without a formal Service interface?
- JepZ 9y agoBtw. Interfaces can also be used to resolve circular dependencies: Just define compatible interfaces in both packages et voilà, no circular dependencies anymore. I am not sure if I agree with > Defining an interface upfront is usually a code smell for overengineering. So far I had less problems with overusing interfaces than with not using them enough. So when you write tests for a package, you define the general interface to the package in terms of types and methods. The remaining challenge is to break that general definition into smaller pieces as one big interface for a package is almost certainly a bad idea. However, in general I follow the authors verdict to not just build 'Java-style' interfaces, as that completely misses the point of interfaces in Go.
- fiatjaf 9y agoIf you have circular dependencies you're doing something wrong. By disagreeing with that and accepting circular dependencies as a normal state of the Go code, you put yourself in the position of doing another wrongness: defining a lot of interfaces.
- sanderjd 9y agoThere is some stuff I really love about Go, but the seemingly pervasive "you're doing that wrong" culture typified by your comment is a really bad look for a language community. It is much better to show people why things are better than to make proclamations. Neither of the things you proclaim to be "wrong" are wrong a priori; show us why your way is better.
- lsaferite 9y agoHaving recently moved into go at work and interfacing with its community, I 100% agree with this. The absolute dogma drives me crazy and makes me not want to deal with the go community at all.
- JepZ 9y agoWell, it might be that I was doing something wrong, but in the end I didn't have circular dependencies anymore, so I was doing right again? (just kidding) ;-) So in fact, I don't like to have circular dependencies in Go either and try to avoid them, so I agree with you on that. But at the same time I like very small and simple packages and sometimes you have to choose: 1. do you build a larger package with everything inside 2. do you build multiple smaller packages with dependencies on each other As I said, in general, I favor the smaller packages and that can lead to circular dependencies, if you are not using interfaces properly.
- tptacek 9y agoGo's crypto interfaces, in the standard library, don't hew to this "accept interfaces return structs" rule. NewAES, for instance, returns a cipher.Block.
- peterevans 9y agoA lot of advice I've seen (such as this) postdates much of the standard library implementation by quite a bit. I could not, however, suggest that the current design for crypt is good or bad, for I have not used it often enough to offer a worthy opinion. People seem to really click with the net/http design, and that does seem well done, but I imagine the standard library itself is a bit unevenly architected.
- tptacek 9y agoThe general advice I've always gotten is that if you're looking for well-executed Go idiom, the standard library is your first stop. And while NewAES might predate this advice, NewGCM (which returns an opaque crypto.AEAD) does not.
- dsymonds 9y agoThat's true, but the standard library also has strictures about backward compatibility that have ossified some bad choices. The crypto examples you give are arguably exactly this; even the later ones are probably a mistake, but are at least consistent with what can't be changed.
- tptacek 9y agocontext also returns context.Context (an interface), and sql returns driver.Value, an interface.
- dsymonds 9y agocontext.Context is an interface because it is known there are multiple implementers (several in the context package itself). The sql driver.Value is actually the other way around: the sql package is the consuming package (for the driver).
- kasey_junk 9y agoIf your package itself defines multiple implementations (or you know of multiple packages that do) feel free to preemptively define/return interfaces. Other than that interface definition should be done at the call site, not the implementation site. This is true in Java also. Golangs structural typing doesn’t change that, it just removes boilerplate in the most trivial cases.
- zzbzq 9y agoThis was so absurd to me at first I had to think about it for a while. I guess you wrote it as a continuation of the opinion from the article that interfaces should only be used as input. From my perspective, I was still considering all other cases also. For other cases, it's precisely because of Go's structural typing that such a thing can even be considered. Java can't implement an interface for a type it doesn't control. You could create a wrapper class, but not exhaustively wrap all present and future implementations.
- kasey_junk 9y agoThe only reason to have an interface is to decouple the use from the implementation (other than some fairly specific reasons that the article mentions like sealed interfaces). To decouple the use from the implementation we generally are either adapting the implementation or narrowing it. In the latter case, structural typing saves you boilerplate, in the former it likely doesn't. The OOP consultant set calls the the concept we are dancing around Dependency Inversion. In Java it very routinely results in wrapper classes. In the most basic cases those classes are pass through boilerplate, but those are fairly rare in practice. Golang dismisses with those. In more cases in practice, you are actually defining the interface that matches the call site demands and modeling different implementations to that. Golang's structural typing doesn't help much with that. I tried (not terribly successfully) to write up some of this in the DIP section of this http://kc.my-junk.info/di-ioc-dip http://kc.my-junk.info/di-ioc-dip it uses scala for the code samples, but it should be fairly easy to see that golang doesn't remove much of the wrapper code if I'd used that.
- ramenmeal 9y agoWould this lead to the same or very similar interfaces being defined multiple times? Maybe that's not a problem?
- cdoxsey 9y agoOne of the more surprising things about Go is that you don't need to name interfaces at all. This: func f1(r io.Reader) {} Is the same as: func f2(r interface{ Read([]byte) (int, error) }) {} In fact this is super common, with the empty interface: func f3(r interface{}) But somehow the ramifications of that don't sink in and we fail to generalize the principle. One practical usage: li, _ := net.Listen("tcp", ":9000") for { conn, err := li.Accept() if err != nil { if err, ok := err.(interface { Temporary() bool }); ok && err.Temporary() { time.Sleep(time.Second) continue } else { panic(err) } } } This takes a bare interface and checks to see if it supports the "Temporary() bool" interface (which all net errors do). When paired with a type switch this makes for very interesting behavior: switch err := err.(type) { case nil: case interface{ Temporary() bool }: if err.Temporary() { time.Sleep(time.Second) continue } panic(err) default: panic(err) } https://play.golang.org/p/J66vaIBGrs2 https://play.golang.org/p/J66vaIBGrs2 All that to say, creating duplicate interfaces is perfectly fine.
- chewxy 9y agoShould probably name them - makes it readable. But you are correct - duplicate interfaces are fine
- juststeve 9y agoyeah i dont like it
- nothrabannosir 9y agoI’m not a big fan of contrived examples with imaginary use cases, because they tend to fall short in actual use. > “Accept interfaces, return structs” Real example from personal (professional) life against that: Amazon AWS SDK defined S3 as a struct. That makes it bloody hard to mock during unit testing. I eventually ended up defining my own “s3iface” interface with corresponding mock to combat this, but now I have to repeat this for every program that uses it. Ok, I’ll separate it into its own package and include that everywhere. Wait, what was the point of the AWS SDK not doing this directly, again? Oh, look, they eventually ended up doing just that: providing an interface version of their S3 struct: https://godoc.org/github.com/aws/aws-sdk-go/service/s3/s3iface https://godoc.org/github.com/aws/aws-sdk-go/service/s3/s3ifa... > Package s3iface provides an interface to enable mocking the Amazon Simple Storage Service service client for testing your code. With the exact same name, to boot. Please consider providing and returning interfaces. Even if your struct is the only type implementing it.
- fwip 9y agoYour code should accept interfaces, not the S3 struct. Interfaces are light-weight, and it's okay to define an interface that is just the subset of functionality your code needs.
- burntsushi 9y agoBut this advice is kind of ridiculous in its unqualified form as you have presented it. If folks followed your advice, there wouldn't be an "io" package in the standard library. So why does it exist? Because it's damn useful to not define interfaces every single time you need them. It gets old real fast. This is exactly what the GP is saying.
- sethammons 9y agoMy real world AWS sdk s3 experience: I have an uploader interface (I only needed to upload). The concrete uploader has the s3 code. The fake uploader has any custom result/err I need to test against.
- 9y ago
- naikrovek 9y ago> I tend to read a lot of other peoples’ codes "codes?" Plural form of this type of code is "code." Stopped reading there. This is one thing I just can't get past when I see it. Yes, I realize that I am not a superhero-level altruist.
- Scramblejams 9y ago“Codes” is idiomatic in some disciplines. For example in aerospace engineering when discussing various analysis implementations, everyone calls them codes. “Which vortex lattice method codes do you like?” “Oh I just use panel codes.” And it’s not slang, you see that usage in textbooks and academic papers as well.
- walesey 9y agoGreat article! Makes a lot of sense.
- holografix 9y agoGolang newbie here. Using a slice to hold different struct types of the same interface, finding it very annoying that every time I want to access a common attribute of the struct types I need to define a getter/setter.
- flashmob 9y agoPerhaps you could try creating a struct with just the common properties, then embed the common struct in all your 'extended' structs. Eg. http://www.hydrogen18.com/blog/golang-embedding.html http://www.hydrogen18.com/blog/golang-embedding.html
- holografix 9y agoThat sounds like a bit of a hack. I like it.
- fiatjaf 9y agoI wished godoc.org (or similar service) somehow listed which types in a package implemented which interfaces. They could match with all the interfaces defined at the standard library (io.Writer etc.) plus the interfaces defined in that same package.
- keypusher 9y agoI don't see what the benefit is of switching it around like this. The author says the problems with the original implementation are: > The most obvious of which is that it has only one type that fulfills the interface with no obvious means of extension. (sic) Seems obvious to me. type Cat struct{} func (a Cat) Speaks() string { return "meow" } Does this not work? > Functions typically take concrete types instead of interface types. Ok...? This seems like some arbitrary rule that the creators of Go claimed was a good idea, I guess. The author has offered no justification for it. In general I guess I fail to see most of the proclaimed benefits of Go, every time I read articles like this I come away favoring something like Java for anything in the application layer. I can see preferring Go to C for low-level and systems programming, but I shudder to think of what a mess the large Go codebases must look like.
- chewxy 9y agoAuthor here. > Does this not work? It works However, this is what I have recently found quite a lot. An interface is defined: type Foo interface { Method1() unexportedMethod() } And in the same package, only one type (usually unexported) implements the interface. And the functions declared in the package typically take the concrete type instead of the interface type. This is typical when developers come from Java and they do `abstract class Fooer` and immediately do `class Foo implements Fooer` within the same package. This is an anti pattern in Go. > This seems like some arbitrary rule that the creators of Go claimed was a good idea, I guess No. I think I was unclear. I specifically mention that it's not a great idea. It's a code smell. Instead, interfaces should be defined at point-of-use.
- freyir 9y agoWhat's the point of exporting the Foo interface if other packages cannot create or use Foo values?
- keypusher 9y ago> I specifically mention that it's not a great idea. It's a code smell. Instead, interfaces should be defined at point-of-use. You still have not said WHY it's not a great idea, or what the benefits of defining the interfaces at point-of-use are.
- hota_mazi 9y ago> Accept interfaces, not structs Besides the fact that there is zero justification from this claim, the simple fact that Go think it's important to distinguish the two is another illustration of late 90s programming language design. There is really no good reason to separate these concepts today, it only causes confusion and fragments the code base in arbitrary chunks that accumulate smelly code over the years. But I'm happy to hear a good justification of why it's important to have both structs and interfaces, if there is one.
- chewxy 9y ago> But I'm happy to hear a good justification of why it's important to have both structs and interfaces, if there is one. Performance is a good reason. If you are familiar with Java or C#, Go interfaces can be seen from one point of view as boxing (i.e. making a primitive value an object). Think what that would require: vtables for function dispatch. A bunch of inlining opportunities cannot happen with that (well, it can, if you allow for name mangling during your compile phase, but that's very gross). JIT is the second fix to that, but with JIT, you're bringing in a lot of unpredictability - useful in some contexts, not so in others. Perhaps you are fine with the "Everything Is An Object/Function" paradigm, but I'm not entirely fine with that.
- yegle 9y agoWhy would you think struct and interface should be treated the same? To me the fact a struct can implement multiple interfaces means they are not on the same "layer" of abstraction.
- hota_mazi 9y agoImagine a struct with functions that don't have an implementation: that's an interface. If it has some implementations and some without, it's an interface with default implementations. See how Kotlin does it, it's really nice and consistent, and you never need to agonize over the decision of picking a class or a struct.
- tepidandroid 9y agoI don't think interface segregation can be simply dismissed as just 'another illustration of late 90s programming language design' (implying there's something wrong with late 90s language design...). The clearest benefit is the decoupling of dependencies which leads to more testable, extensible and maintainable code. That any struct can simply implement the desired behaviour to implicitly satisfy externally defined interfaces is an extremely powerful notion. Of all the things to complain about in Go, interfaces seem like a strange bone to pick.
- ericnyamu 9y agothanks for making it simple to real with real life objects
- klmr 9y agoI don’t think the reference to Postel’s law is correct here: Postel’s law is about robustness, not about abstractness. Furthermore, taken at face value, Postel’s law was a phenomenal failure in principle and in practice (see e.g. [1]). In hindsight it’s probably a bad design principle. [1] https://tools.ietf.org/html/draft-thomson-postel-was-wrong-02 https://tools.ietf.org/html/draft-thomson-postel-was-wrong-0...
- brightball 9y agoThis is really excellent advice. It was the first thing that jumped out at me when I read the language spec. It's one of the reason why I think Go seems to draw more people from dynamic languages than static because the static folks have a tendency to shoehorn some old habits into it.