Learning Go - Interfaces

Intro

If you have written code in Java, then you must have come across the concept of interfaces. The object-oriented meaning of an interface is a contract. Though Go is not an object-oriented programming language, we still have interfaces in Go.

In Go, an interface is a type that specifies a set of methods. Any type that implements those methods satisfies the interface, without explicitly declaring it.

At first, the way Go handles interfaces can feel confusing, but interfaces are a fundamental building block for writing flexible, decoupled, and testable code. Go follows a simple idiom:
“If it can walk like a duck and quack like a duck, then it is a duck.”

Declaring Interfaces

We can use the type keyword followed by the name of the interface and then the keyword interface to create an interface in Go.

1
2
3
type ShoppingMall interface {
Pay(amount float64)
}

Suppose that we have an interface called ShoppingMall. It simply has a method called Pay(). We can think of it like a place where, if we visit and buy anything, then we need to make some payment.

Implementing Interfaces

Now I am planning to visit the shopping mall with my credit card. But will I be allowed to pay using my credit card? Let’s see the structure of my credit card:

1
2
3
4
5
6
7
8
9
type CreditCard struct {
Name string
Number string
balance float64
}

func (cc CreditCard) Balance() float64 {
return cc.balance
}

My credit card has a name, a card number, and some balance. Also, there is a method that I can use to check my card balance after its usage. Coming back to the purpose, I want to visit the shopping mall, and I have to pay if I buy something. Currently, I will not be able to visit the shopping mall because my credit card does not comply with the shopping mall interface.

Note that the balance is kept private to the package and cannot be accessed from outside.

My credit card provider does not implement the Pay() method for my card, and hence I cannot visit the shopping mall with my credit card. To make it possible, my credit card must implement the Pay() method.

1
2
3
4
5
func (cc *CreditCard) Pay(amount float64) {
if (amount > 0) {
cc.balance -= amount
}
}

Since my credit card has now implemented the Pay() method, I can visit the shopping mall and make the payment. This is how an interface is satisfied in Go. If our struct has implementations of the interface methods, we are automatically implementing the interface.

Different Structs but Same Interface

After shopping, I showed the cool stuff I bought to my friends. My friend Tom is not very eager to visit the mall, but he uses a digital wallet instead of a credit card. The structure of Tom’s wallet looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Wallet struct {
Name string
Id int64
balance float64
}

func (w Wallet) Details() string {
return "Owner: " + w.Name + " ID: " + fmt.Sprint(w.Id)
}

func (w *Wallet) Pay(amount float64) {
if w.balance <= 100 {
return
}
if amount > 0 {
w.balance -= amount
}
}

As we can see, the digital wallet Tom owns already has a Pay() method. Although its implementation is a bit different, the presence of the Pay() method satisfies the ShoppingMall interface, and it can be used for shopping payments. This also shows how, in Go, multiple structs (which can be thought of as classes) implement the same interface. This example shows an important concept in Go: polymorphism.

Polymorphism

Both CreditCard and Wallet are different structs with different internal logic, but they both satisfy the same interface. This allows the shopping mall to accept any payment method that implements Pay() without caring about the concrete type. We can use values of both the wallet and credit card types for shopping, as long as they satisfy the ShoppingMall interface. This is how Go achieves polymorphism—through interfaces and behavior, not inheritance.

Embedding Interfaces

Interface composition is a way to build bigger interfaces from smaller ones. Instead of defining one large interface with many methods, Go encourages creating small, focused interfaces and then combining them when needed.

This matches Go’s philosophy: simple pieces, loosely connected.

1
2
3
4
5
6
7
8
9
10
11
12
type Reader interface {
Read() string
}

type Writer interface {
Write(data string)
}

type ReadWriter interface {
Reader
Writer
}

The above is one of the most common example for interface embedding (or composition). ReadWriter is composed of Reader and Writer. Any type that implements both Read() and Write() satisfies ReadWriter.

Interfaces for Testing and Mocking

Important Notes

Role of Receivers in Struct Methods

The use of the receiver types for the struct methods play important role in satisfaction of the interface. We have used the pointer receivers for both the CreditCard and the Wallet structs.

1
2
func (cc *CreditCard) Pay(amount float64)
func (w *Wallet) Pay(amount float64)

Due to the above case of pointer receivers, CreditCard and Wallet doe not satisfy ShoppingMall interface but *CreditCard and *Wallet does. This is something I baffled with for sometime while testing the example codes and though to add this as a note.

Nil Interfaces vs Interfaces Holding Nil Values

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

type Speaker interface {
Speak()
}

type Person struct {
Name string
Age uint8
}

func (p Person) Speak() {
fmt.Println("Hello World")
}

var p *Person = nil
var s Speaker = p

fmt.Println(s == nil) // false

In the above code, the variable of person p is nil but the interface holding a person value with nil is not actually nil.

The Empty Interface

The empty interface which is denoted by interface{} has no methods, so every type satisfies it. It does gives us flexibilities but at the same time it has some severe caveats:

  • It loses compile-time type safety
  • It requires type assertions or switches
  • It makes code harder to understand

Method Name Conflicts

If two embedded interfaces have methods with the same name but different signatures, the code will not compile.

1
2
3
4
5
6
7
8
9
10
11
12
type A interface {
Do(int)
}

type B interface {
Do(string)
}

type C interface {
A
B // conflict and compilation error
}

Outro

That’s all. Understanding why interfaces matter helps us write Go code that is flexible, testable, and easy to evolve over time. See you in another topic. Stay tuned.