OOP in Go: Interfaces

Table of contents

No heading

No headings in the article.

In the last article, we discussed Go structs, and how they can be used to mimic key OOP concepts like Encapsulation, Abstraction and Inheritance.

A Go interface is a collection of method signatures that define behaviors. An interface is defined using the type keyword followed by the name of the new type and the keyword interface.

type Ethnicity interface {
    IsAsian() bool
    IsIndian() bool
}

In the above example, we define an interface type Ethnicity with method signature IsAsian() bool & IsIndian() bool. Any type that has a method with the same signatures is said to implement the interface.

Let us declare a Person struct, and implement IsAsian() and IsIndian() methods with the same arguments and return types.

type Person struct {
    name      string
    country   string
    continent string
}

type Employee struct {
    Person
    salary int
}

func (p Person) IsAsian() bool {
    return p.continent == "Asia"
}

func (p Person) IsIndian() bool {
    return p.country == "India"
}

func main(){
    p := Person{name: "John", country: "India", continent: "Asia"}
    c.IsAsian()
    c.IsIndian()
}

Since type Person has methods(receiver functions) with the same name and similar return types as defined in the Ethnicity interface, it implements the Ethnicity interface.

We could also implement the interface by simply declaring a Person variable and assigning it to an Ethnicity variable, like so:

func main() {
    p := Person{name: "John", country: "India", continent: "Asia"}
    var e Ethnicity = p
    e.IsAsian()
    e.IsIndian()
}

Go also allows the embedding of interfaces in structs, which can be helpful in inheriting behaviors and defining new structs with additional behaviors.

type Person struct {
    name      string
    age       int
    netIncome int
    country   string
    continent string
    Ethnicity
}

type WealthManager interface{
    NetWorth() int
    IsWealthy() bool
}

type SuperRich struct{
    Person
    WealthManager
}

func (p Person) NetWorth() int {
    return p.netIncome - p.debt
}

func (p Person) IsWealthy() bool {
    return p.NetWorth() > 1
}

func (s SuperRich) IsSuperRich() {
    fmt.Printf("%v is from %v and, is super rich with a networth of %v", s.Person.name, s.Person.Country, s.Person.NetWorth())
}

func main() {
    fmt.Println("Hello World!")
    p := Person{name: "John", age: 30, netIncome: 50000, debt: 10000, country: "India", continent: "Asia"}
    var isSuperRich SuperRich = SuperRich{Person: p, WealthManager: p}
    isSuperRich.IsSuperRich()
}

Here, we have Person implementing the WealthManger interface by having two receiver functions IsWealthy() and NetWorth().

The SuperRich struct combines the properties and behaviors of Person and the WealthManager, and has additional behavior in the form of IsSuperRich() method.

In the main function, we have the isSuperRich variable, where we assign new Person p twice. We can assign p to WealthManager as the Person struct implements the WealthManager interface.

Interfaces with no method signatures are not bound to any behavior and hence can hold values of any type. This property allows us to achieve Polymorphism.

type x interface{}
x = "hello"
fmt.Println(x)

x = 42
fmt.Println(x)

x = true
fmt.Println(x)

Hence, we can see that interfaces in Go can allow us to mimic OOP properties like Inheritance using nesting and Polymorphism using empty interfaces. Interfaces are used to define behaviors that a type must implement. This allows functions to accept any type that implements the interface, making the code more flexible and easier to maintain.

In the upcoming article, we shall discuss receiver functions in Go.