Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using multiple interfaces in Golang

I'm learning Golang and as an exercise in using interfaces I'm building a toy program. I'm having some problem trying to use a type that "should implement" two interfaces - one way to solve that in C++ and Java would be to use inheritance(there are other techniques, but I think that is the most common). As I lack that mechanism in Golang, I'm not sure how to proceed about it. Below is the code:

var (
    faces = []string{"Ace", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Jack", "Queen", "King"}

    suits = []string{"Hearts", "Diamonds", "Spades", "Clubs"}
)

type Card interface {
    GetFace() string
    GetSuit() string
}

type card struct {
    cardNum int
    face    string
    suit    string
}

func NewCard(num int) Card {
    newCard := card{
        cardNum: num,
        face:    faces[num%len(faces)],
        suit:    suits[num/len(faces)],
    }

    return &newCard
}

func (c *card) GetFace() string {
    return c.face
}

func (c *card) GetSuit() string {
    return c.suit
}

func (c *card) String() string {
    return fmt.Sprintf("%s%s ", c.GetFace(), c.GetSuit())
}

What I'm trying to achieve:

  • I would like to hide my struct type and only export the interface so that the clients of the code use only the "Card" interface
  • I would like to have a string representation of the struct, hence the implementation of the interface with the "String()" method in order to be able to call "fmt.Println()" on instantiations of my struct

The problem comes when I'm trying to use a new card though the "Card" interface and also trying to get the string representation. I cannot pass the interface as the parameter of the implementation of the "String()" method as there is a compiler error which is related to the addressability of an interface at the core language level(still digging through that documentation). The very simple example of testing exposes the issue:

func TestString(t *testing.T) {
    card := NewCard(0)
    assert.EqualValues(t, "AceHearts ", card.String(), " newly created card's string repr should be 'AceHearts '")
}

The compiler tells me, for good reason, that "card.String undefined (type card has no field or method string)". I could just add the "String()" method to my "Card" interface, but I do not find that to be clean: I have other entities implemented with the same model and I would have to add that redundancy everywhere; there is already an interface with that method.

What would be a good solution for the above issue that I'm having?

Edit:(to address some of the very good comments)

  • I do not expect to have another implementation of the Card interface; I'm not sure I grasp why would I want to do that, that is change the interface
  • I would like to have the Card interface to hide away implementation details and for the clients to program against the interface and not against the concrete type
  • I would like to always have access to the String() interface for all clients of the "card struct" instantiations(including the ones instantiated via the Card interface). I'm not interested in having clients only with the String interface. In some other languages this can be achieved by implementing both interfaces - multiple inheritance. I'm not saying that is good or wrong, or trying to start a debate about that, I'm just stating a fact!
  • My intent is to find out if the language has any mechanism to fulfill those requirements simultaneously. If that is not possible or maybe from the point of view of the design the problem should be tackled in a different manner, then I'm ready to be educated
  • Type assertions are very verbose and explicit and would expose implementation details - they have their places but I do not think they are appropriate in the situation I have
like image 945
celavek Avatar asked Aug 28 '26 13:08

celavek


1 Answers

I should go over some prefacing points first:

  • Interfaces in Go are not the same as interfaces in other languages. You shouldn't assume that every idea from other languages should transfer over automatically. A lot of them don't.
  • Go has neither classes nor objects.
  • Go is not Java and Go is not C++. It's type system is significantly and meaningfully different than those languages.

From your question:

I would like to have the Card interface to hide away implementation details and for the clients to program against the interface and not against the concrete type

This is the root of your other problems.

As mentioned in the comments, I see this in multiple other packages and regard it as a particularly pesky anti-pattern. First, I will explain the reasons why this pattern is "anti" in nature.

  • Firstly and most pertinently, this point is proven by your very example. You employed this pattern, and it has resulted in bad effects. As pointed out by mkopriva, it has created a contradiction which you must resolve.
  • this usage of interfaces is contrary to their intended use, and you are not achieving any benefit by doing this.

Interfaces are Go's mechanism of polymorphism. The usage of interfaces in parameters makes your code more versatile. Think of the ubiquitous io.Reader and io.Writer. They are fantastic examples of interfaces. They are the reason why you can patch two seemingly unrelated libraries together, and have them just work. For example, you can log to stderr, or log to a disk file, or log to an http response. Each of these work exactly the same way, because log.New takes an io.Writer parameter, and a disk file, stderr, and http response writer all implement io.Writer. To use interfaces simply to "hide implementation details" (I explain later why this point fails), does not add any flexibility to your code. If anything, it is an abuse of interfaces by leveraging them for a task they weren't meant to fulfill.

Point / Counterpoint

  1. "Hiding my implementation provides better encapsulation and safety by making sure all the details are hidden."
  • You are not achieving any greater encapsulation or safety. By making the struct fields unexported (lowercase), you have already prevented any clients of the package from messing with the internals of your struct. Clients of the package can only access the fields or methods that you have exported. There's nothing wrong with exporting a struct and hiding every field.
  1. "Struct values are dirty and raw and I don't feel good about passing them around."
  • Then don't pass structs, pass pointers to struct. That's what you're already doing here. There's nothing inherently wrong with passing structs. If your type behaves like a mutable object, then pointer to struct is probably appropriate. If your type behaves more like an immutable data point, then struct is probably appropriate.
  1. "Isn't it confusing if my package exports package.Struct, but clients have to always use *package.Struct? What if they make a mistake? It's not safe to copy my struct value; things will break!"
  • All you realistically have to do to prevent problems is make sure that your package only returns *package.Struct values. That's what you're already doing here. A vast majority of the time, people will be using the short assignment :=, so they don't have to worry about getting the type correct. If they do set the type manually, and the choose package.Struct by accident, then they will get a compilation error when trying to assign a *package.Struct to it.
  1. "It helps to decouple the client code from the package code"
  • Maybe. But unless you have a realistic expectation that you have multiple existent implementations of this type, then this is a form of premature optimization (and yes it does have consequences). Even if you do have multiple implementations of your interface, that's still not a good reason why you should actually return values of that interface. A majority of the time it is still more appropriate to just return the concrete type. To see what I mean, take a look at the image package from the standard library.

When is it actually useful?

The main realistic case where making a premature interface AND returning it might help clients, is this:

  • Your package introduces a second implementation of the interface
    • AND clients have statically and explicitly (not :=) used this data type in their functions or types
    • AND clients want to reuse those types or functions for the new implementation also.

Note that this wouldn't be a breaking API change even if you weren't returning the premature interface, as you're only adding a new type and constructor.

If you decided to only declare this premature interface, and still return concrete types (as done in the image package), then all the client would likely need to do to remedy this is spend a couple minutes using their IDE's refactor tool to replace *package.Struct with package.Interface.

It significantly hinders the usability of package documentation

Go has been blessed with a useful tool called Godoc. Godoc automatically generates documentation for a package from source. When you export a type in your package, Godoc shows you some useful things:

  • The type, all exported methods of that type, and all functions that return that type are organized together in the doc index.
  • The type and each of its methods has a dedicated section in the page where the signature is shown, along with a comment explaining it's usage.

Once you bubble-wrap your struct into an interface, your Godoc representation is hurt. The methods of your type are no longer shown in the package index, so the package index is no longer an accurate overview of the package as it is missing a lot of key information. Also, each of the methods no longer has its own dedicated space on the page, making it's documentation harder to both find and read. Finally it also means that you no longer have the ability to click the method name on the doc page to view the source code. It's also no coincidence that in many packages that employ this pattern, these de-emphasized methods are most often left without a doc comment, even when the rest of the package is well documented.

In the wild

https://pkg.go.dev/github.com/zserge/lorca

https://pkg.go.dev/github.com/googollee/go-socket.io

In both cases we see a misleading package overview, along with a majority of interface methods being undocumented.

(Please note I have nothing against any of these developers; obviously every package has it's faults and these examples are cherry picked. I'm also not saying that they had no justification to use this pattern, just that their package doc is hindered by it)

Examples from the standard library

If you are curious about how interfaces are "intended to be used", I would suggest looking through the docs for the standard library and taking note of where interfaces are declared, taken as parameters, and returned.

https://golang.org/pkg/net/http/

https://golang.org/pkg/io/

https://golang.org/pkg/crypto/

https://golang.org/pkg/image/

Here is the only standard library example I know of that is comparable to the "interface hiding" pattern. In this case, reflect is a very complex package and there are several implementations of reflect.Type internally. Also note that in this case, even though it is necessary, no one should be happy about it because the only real effect for clients is messier documentation.

https://golang.org/pkg/reflect/#Type

tl;dr

This pattern will hurt your documentation, while accomplishing nothing in the process, except you might make it slightly quicker in very specific cases for clients to use parallel implementations of this type that you may or may not introduce in the future.

These interface design principles are meant for the benefit of the client, right? Put yourself in the shoes of the client and ask: what have I really gained?

like image 101
Hymns For Disco Avatar answered Aug 31 '26 07:08

Hymns For Disco



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!