Go Object Oriented Approach

An object oriented system integrates the data & it’s behavior using the concept of an object. An object is an abstract data type that has state & behavior.

The fundamental concepts of object orientation are alive in Go. Go utilizes structs as the union of data & behavior. Through composition, has-a relationships can be established between Structs to minimize code repetition while staying clear of the brittle mess that is inheritance. Go uses interfaces to establish is-a relationships between types without unnecessary and counteractive declarations.

Objects In Go

While Go doesn’t have something called object, it does have a type that matches the same definition of a data structure that integrates both data & behavior. In Go, this is called as “struct”.

type rect struct {
    width int
    height int
}

func (r *rect) area() int {
    return r.width * r.height
}

func main() {
    r := rect{width: 10, height: 20
    fmt.Println("Area: " + r.area())
}

Inheritance in Go

Go is intentionally designed without any inheritance at all. This doesn’t mean that objects viz. struct values do not have relationships. Go authors have chosen to use an alternative mechanism to convey relationships which resolves decade old issues and arguments around inheritance.

So, what really are the arguments around inheritance? To understand this may be its worth to quote James Gosling’s (Java’s inventor) quote in one of the Q&A –

During the memorable Q&A session, someone asked him: “If you could do Java over again, what would you change?” “I’d leave out classes,” he replied. After the laughter died down, he explained that the real problem wasn’t classes per se, but rather implementation inheritance (the extends relationship). Interface inheritance (the implements relationship) is preferable. You should avoid implementation inheritance whenever possible.

Loosing Flexibility

Explicit use of concrete class names locks you into specific implementations making down the line changes unnecessarily difficult. This hinders the possibility to write code in such a way that you can incorporate newly discovered requirements into the existing code as painlessly possible.

Coupling

Inheritance introduces coupling. As a designer, you should strive to minimize such coupling altogether. In an implementation-inheritance system that uses extends, the derived classes are very tightly coupled to base classes, and this close connection is undesirable. Base classes are considered to fragile because you can modify a base class in a seemingly safe way, but this new behavior, when inherited by derived classes, might cause the derived classes to malfunction. You simply can’t tell whether a base class change is safe by examining the base’s class methods in isolation; you must check all code that uses both base class & derived class objects too!

In short, Inheritance Is Best Left Out!

Object composition in Go

Go uses embedded types to implement the principle of object composition. Go permits you to embed a struct within a struct giving them a has-a relationship.

type Person struct {
   Name string
   Address Address
}

type Address struct {
   Number string
   Street string
   City   string
   State  string
   Zip    string
}

func (p *Person) Talk() {
    fmt.Println("Hi, my name is", p.Name)
}

func (p *Person) Location() {
    fmt.Println("I’m at", p.Address.Number, p.Address.Street, p.Address.City, p.Address.State, p.Address.Zip)
}

func main() {
    p := Person{
        Name: "Steve",
        Address: Address{
            Number: "13",
            Street: "Main",
            City:   "Gotham",
            State:  "NY",
            Zip:    "01313",
        },
    }

    p.Talk()
    p.Location()
}

Subtyping in Go

Pseudo subtyping

The pseudo is-a relationship works via anonymous fields.

Let’s use the following statement to depict this. A person can talk. A citizen is a person therefore a citizen can talk.

type Citizen struct {
   Country string
   Person
}

func (c *Citizen) Nationality() {
    fmt.Println(c.Name, "is a citizen of", c.Country)
}

func main() {
    c := Citizen{}
    c.Name = "Steve"
    c.Country = "America"
    c.Talk()
    c.Nationality()
}

However, anonymous fields are not true subtyping -Anonymous fields are still accessible as if they were embedded.

  • Anonymous fields are still accessible as if they were embedded.
func main() {
    c := Citizen{}
    c.Name = "Steve"
    c.Country = "America"
    c.Talk()         // <- Notice both are accessible
    c.Person.Talk()  // <- Notice both are accessible
    c.Nationality()
}
  • True subtyping becomes the parent
package main

type A struct{
}

type B struct {
    A  //B is-a A
}

func save(A) {
    //do something
}

func main() {
    b := B
    save(&b);  //OOOPS! b IS NOT A
}

True subtyping

Subtyping is the is-a relationship. In Go each type is distinct and nothing can act as another type, but both can adhere to the same interface. Interfaces can be used as input and output of functions & methods and consequently establish an is-a relationship between types.

func SpeakTo(p *Person) {
    p.Talk()
}

func main() {
    p := Person{Name: "Dave"}
    c := Citizen{Person: Person{Name: "Steve"}, Country: "America"}

    SpeakTo(&p)
    SpeakTo(&c) //cannot use c (type *Citizen) as type *Person in function argument
}

Now adding an interface called “Human” and using that as the input for our speakTo function will work as intended.

type Human interface {
    Talk()
}

func SpeakTo(h Human) {
    h.Talk()
}

func main() {
    p := Person{Name: "Dave"}
    c := Citizen{Person: Person{Name: "Steve"}, Country: "America"}

    SpeakTo(&p)
    SpeakTo(&c)
}