Jauhar's Blog


Golang Implicit Interface Implementation

2022/11/29

In Golang, we can’t explicitly says that type A implements interface B. Implementing an interface is done implicitly. It’s kind of like Python’s duck typing, if it looks like a duck and quacks like a duck, it’s a duck. In Golang, If a type A has all the methods defined by interface B, it means A implements B. Consider the code below:

 1// package cache
 2type KeyValueStore interface {
 3  Get(ctx context.Context, key string) (string, error)
 4  Set(ctx context.Context, key string, value string) error
 5}
 6
 7// package config
 8type KeyValueStore interface {
 9  Get(ctx context.Context, key string) (string, error)
10  Set(ctx context.Context, key string, value string) error
11}
12
13// package memcached
14type client struct {}
15
16func (*client) Get(ctx context.Context, key string) (string, error) { ... }
17
18func (*client) Set(ctx context.Context, key string, value string) error { ... }

In the example above, there is an interface called cache.KeyValueStore and config.KeyValueStore which has identical contract. In this example, *client implements both cache.KeyValueStore and config.KeyValueStore. We never explicitly says that *client implements those two interfaces, but since it has all the methods required for those two interfaces, it is considered as the implementation of cache.KeyValueStore and config.KeyValueStore.

One good thing about implicit interface implementation is that you don’t have to depend on the thing you implement just to implement it. By doing this, you can write the interface later without updating the implementation. Patterns in software development are hard to see in the beginning, thus it’s hard to define the “correct” interface in the early stage. It will be easier to just write the implementation and see how the patterns show up. When we see the pattern, we can decide to harden the pattern by creating the interface without changing the implementation.

But, the implicit interface implementation is actually not good in a lot of aspects. For starter, the advantage is not that big. You can create interface without changing implementation, so what? Changing the implementation is not hard. Adding a simple implements KeyValueStore is not a big job. Considering the issue it causes, the advantage is not that useful.

The fact that you don’t need to import the interface is a lie in most cases. Usually, the interfaces that we have are not only contain methods, but also some structs. The KeyValueStore above is actually very generic, thus this problem is not visible. But, consider how most interfaces are written. Here is an example of more realistic interface:

 1package auth
 2
 3type User struct {
 4  ID   int64
 5  Name string
 6}
 7
 8type UserRepository interface {
 9  InsertUser(ctx context.Context, user User) error
10}

In the example above, I defined interface auth.UserRepository that can be used to insert a user to a persistent storage like MySQL. To implement the auth.UserRepository, you need to import the auth package because you need the auth.User. This happens very often in actual software. So, most of the time, implementing an interface means you need to import the interface package and have dependency to the interface’s package. Only on a very generic interface like io.Reader and io.Writer should you able to gain advantage of implicit interface implementation.

Now, implicitly implementing interface can cause a confusion, which is dangerous. Consider the first example where we have KeyValueStore interface. Based on the definition of the interface, *memcached.Client should be considered as the implementation of both cache.KeyValueStore and config.KeyValueStore. But, if you think about it, it really shouldn’t. *memcached.Client is an implementation of key-value storage for caching purpose. Whereas the config.KeyValueStore probably needs a more persistent property. You don’t want your config gone when the memcached storing it get destroyed or the eviction policy of memcached causing it to be evicted, right? But, Golang doesn’t forbid you to use *memcached.Client as the implementation of config.KeyValueStore interface. Identical interface can have different semantic meaning depending on their context. A programming languages with explicit interface implementation such as Java and Rust don’t allow you to do this. You need to explicitly say that *memcached.Client implements config.KeyValueStore if you want to do it. But, of course the implementor of *memcached.Client will never do this because it’s not meant to be used as the implementation of config.KeyValueStore.

Having an implicit interface implementation causes a bad error reporting. Most of the time, you never want to accidentally implements an interface. If you want to create a type that implements cache.KeyValueStore, you must first know what are the method required by cache.KeyValueStore before implementing it. Otherwise, you don’t know what methods you need to write. So, your type solely purpose is actually just to implement cache.KeyValueStore interface, nothing more. Now, let’s say the interface of cache.KeyValueStore changes, maybe now it doesn’t accept string anymore. Instead, now it accepts []byte like this:

1package cache
2
3type KeyValueStore interface {
4  Get(ctx context.Context, key []byte) ([]byte, error)
5  Set(ctx context.Context, key []byte, value []byte) error
6}

Now, your type is not considered as the implementation of cache.KeyValueStore anymore since the methods don’t match anymore. This is a legit error. In any language, if you change the interface, you need to change the implementations, there is nothing wrong with this. So what’s the problem? The problem is, now the error doesn’t show up in your type implementation, but somewhere else. It shows up in the place where you are trying to pass your type as the implementation of cache.KeyValueStore. That is not a good place to report the error. A good place to report the error is as close as possible to its root cause. The root cause of the error is because you haven’t change the implementation when you change the interface, but the error show up somewhere else. And not only that, the error will show up multiple times in all places where you pass your type as cache.KeyValueStore, where it should only be reported once, in your implementation code.

The most annoying problem with implicit interface implementation is that it is hard to tell if a type is actually implementing an interface intendedly, or it just happen to have the same methods accidentally. Let me give you an example. Let’s consider the KeyValueStore example above. Say you have this code:

 1// package cache
 2type KeyValueStore interface {
 3  Get(ctx context.Context, key string) (string, error)
 4  Set(ctx context.Context, key string, value string) error
 5}
 6
 7// package config
 8type KeyValueStore interface {
 9  Get(ctx context.Context, key string) (string, error)
10  Set(ctx context.Context, key string, value string) error
11}
12
13// package memcached
14type client struct {}
15
16func (*client) Get(ctx context.Context, key string) (string, error) { ... }
17
18func (*client) Set(ctx context.Context, key string, value string) error { ... }

Now, you change the interface of config.KeyValueStore into:

 1// package cache
 2type KeyValueStore interface {
 3  Get(ctx context.Context, key string) (string, error)
 4  Set(ctx context.Context, key string, value string) error
 5}
 6
 7// package config
 8type Value struct {
 9  LastChanged time.Time
10  Author      int64
11  RawValue    []byte
12}
13
14type KeyValueStore interface {
15  Get(ctx context.Context, key string) (Value, error)
16  Set(ctx context.Context, key string, value Value) error
17}
18
19// package memcached
20type client struct {}
21
22func (*client) Get(ctx context.Context, key string) (string, error) { ... }
23
24func (*client) Set(ctx context.Context, key string, value string) error { ... }

Now, let me ask you a question: “should you update the memcached.client struct?”. It’s hard to tell, right? Previously, it implements config.KeyValueStore, then we change the interface of config.KeyValueStore, so logically, we should change the memcached.client implementation as well, right? But, no. It never meant to implement config.KeyValueStore from the beginning. The intention of memcached.client is to be the implementation of cache.KeyValueStore, and it just happen to have the same method as config.KeyValueStore. But, there is no way to deterministically tell whether you should change the memcached.client implementation or not. You need to understand the semantic meaning of the code to be able to answer those question. This problem makes code refactoring tool become harder. If you have explicit interface implementation, it will be easier for computer to understand your intention and automate all the necessary fix in your code. In this case, the refactoring tool can help you change the method definition automatically.