Object-Oriented Programming in Go

Object-Oriented Programming (OOP) is a programming paradigm based on the concept of objects which can contain properties and methods. Properties are variables that contain data. Methods are functions to manipulate data.

There are four pillars of OOP; Abstraction, Encapsulation, Inheritance, and Polymorphism. A programming language that can be said to supports OOP must achieve these pillars. It looks like Go is a procedural programming language. But it supports the OOP paradigm, even if it looks a little tricky.

Abstraction

Abstract is about hiding implementation details. We can understand the objects that contain properties and methods without having to understand the details.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import "fmt"

type animal struct {
    name    string
    feet    int
    hasPaws bool
}

func main() {
    cat := animal{"Cat", 4, false}

    fmt.Println(cat.name, "has", cat.feet, "feet.")

    if cat.hasPaws {
        fmt.Println(cat.name, "has paws.")
    }

    var dog = new(animal)

    dog.name = "Dog"
    dog.feet = 4
    dog.hasPaws = true

    fmt.Println(dog.name, "has", dog.feet, "feet.")

    if dog.hasPaws {
        fmt.Println(dog.name, "has paws.")
    }
}

Class in Go could be achieved with struct. That animal object contains three properties; name, feet, and hasPaws.

The code above shows how we defined two objects; cat and dog. We set the values of the properties then print them.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import "fmt"

type animal struct {
    name    string
    feet    int
    hasPaws bool
}

func (animal animal) print() {
    fmt.Println(animal.name, "has", animal.feet, "feet.")

    if animal.hasPaws {
        fmt.Println(animal.name, "has paws.")
    }
}

func main() {
    cat := animal{"Cat", 4, false}

    cat.print()

    var dog = new(animal)

    dog.name = "Dog"
    dog.feet = 4
    dog.hasPaws = true

    dog.print()
}    

The code above is how we refactor the way we print the properties in the print method for animal objects.

Inheritance

Inheritance lets one object acquire the properties and methods of parent objects.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import "fmt"

type animal struct {
    name    string
    feet    int
    hasPaws bool
}

type monster struct {
    animal
    abilities []string
}

func (animal animal) print() {
    fmt.Println(animal.name, "has", animal.feet, "feet.")

    if animal.hasPaws {
        fmt.Println(animal.name, "has paws.")
    }
}

func main() {
    dragon := monster{
        animal{"Dragon", 4, true},
        []string{"Nuclear blast", "Ice smoke"},
    }

    dragon.print()
}

There is a new struct called monster. We created a monster which is a dragon. The dragon has a print method from the parent.

Polymorphism

Polymorphism means the condition of occurring in several different forms.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import "fmt"

type animal struct {
    name    string
    feet    int
    hasPaws bool
}

type monster struct {
    animal
    abilities []string
}

func (animal animal) print() {
    fmt.Println(animal.name, "has", animal.feet, "feet.")

    if animal.hasPaws {
        fmt.Println(animal.name, "has paws.")
    }
}

func (monster monster) print() {
    fmt.Println(monster.name, "is a monster!!!")
    fmt.Println(monster.name, "has:")

    for _, ability := range monster.abilities {
        fmt.Println("-", ability)
    }
}

func main() {
    dragon := monster{
        animal{"Dragon", 4, true},
        []string{"Nuclear blast", "Ice smoke"},
    }

    dragon.print()
}    

There are two print methods. One from the animal object and one from the monster. They have different results. Here is the output from the code above.

Dragon is a monster!!!
Dragon has:
- Nuclear blast
- Ice smoke

Encapsulation

Lastly, Encapsulation. I purposely explained it last. Encapsulation is the action of enclosing something in or as if in a capsule. It is about making private or public for some properties or methods.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package main

import "fmt"

type properties interface {
    setName(name string)
    getName() string
    setFeet(feet int)
    getFeet() int
    setHasPaws(hasPaws bool)
    getHasPaws() bool
}

type animal struct {
    name    string
    feet    int
    hasPaws bool
}

type monster struct {
    animal
    abilities []string
}

func (animal animal) setName(name string) {
    animal.name = name
}

func (animal animal) getName() string {
    return animal.name
}

func (animal animal) setFeet(feet int) {
    animal.feet = feet
}

func (animal animal) getFeet() int {
    return animal.feet
}

func (animal animal) setHasPaws(hasPaws bool) {
    animal.hasPaws = hasPaws
}

func (animal animal) getHasPaws() bool {
    return animal.hasPaws
}

func (animal animal) print() {
    fmt.Println(animal.getName(), "has", animal.getFeet(), "feet.")

    if animal.hasPaws {
        fmt.Println(animal.getName(), "has paws.")
    }
}

func (monster monster) print() {
    fmt.Println(monster.getName(), "is a monster!!!")
    fmt.Println(monster.getName(), "has:")

    for _, ability := range monster.abilities {
        fmt.Println("-", ability)
    }
}

func main() {
    var dragon properties = monster{
        animal{"Dragon", 4, true},
        []string{"Nuclear blast", "Ice smoke"},
    }

    fmt.Println(dragon.getName())
}

I refactored the code first. I created an interface that contains getter and setter for the properties which is best practices I think. So, we don't need to access the properties directly to set and get the value.

The dragon now has an interface of properties. Try to remove a method that contained in properties, we will get an error. Those methods now are a must.

...

// Monster is a special form of animals.
type Monster struct {
    animal
    abilities []string
}

...

func (monster Monster) print() {
    fmt.Println(monster.getName(), "is a monster!!!")
    fmt.Println(monster.getName(), "has:")

    for _, ability := range monster.abilities {
        fmt.Println("-", ability)
    }
}

// Init initializes a monster.
func Init(name string, feet int, hasPaws bool, abilities []string) *Monster {
    return &Monster{
        animal{name, feet, hasPaws},
        abilities,
    }
}

func main() {
    var dragon properties = Init("Dragon", 4, true, []string{"Nuclear blast", "Ice smoke"})

    fmt.Println(dragon.getName())
}

I changed the monster to Monster (with uppercase at the first letter) and created the Init function as a constructor.

It looks like we missed something if we don't cover about constructor in OOP. Constructor is a function or method to set some values for the object. In other programming languages, mostly the constructor has the same name as the class or object. That was how we implemented the constructor in Go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package main

import "fmt"

type properties interface {
    setName(name string)
    getName() string
    setFeet(feet int)
    getFeet() int
    setHasPaws(hasPaws bool)
    getHasPaws() bool
    Print()
}

// Animal has a name and feet. Maybe has paws too.
type Animal struct {
    name    string
    feet    int
    hasPaws bool
}

// Monster is a special form of animals.
type Monster struct {
    Animal
    abilities []string
}

func (animal Animal) setName(name string) {
    animal.name = name
}

func (animal Animal) getName() string {
    return animal.name
}

func (animal Animal) setFeet(feet int) {
    animal.feet = feet
}

func (animal Animal) getFeet() int {
    return animal.feet
}

func (animal Animal) setHasPaws(hasPaws bool) {
    animal.hasPaws = hasPaws
}

func (animal Animal) getHasPaws() bool {
    return animal.hasPaws
}

// Print all properties of an animal.
func (animal Animal) Print() {
    fmt.Println(animal.getName(), "has", animal.getFeet(), "feet.")

    if animal.hasPaws {
        fmt.Println(animal.getName(), "has paws.")
    }
}

// Print all abilities of a monster.
func (monster Monster) Print() {
    fmt.Println(monster.getName(), "is a monster!!!")
    fmt.Println(monster.getName(), "has:")

    for _, ability := range monster.abilities {
        fmt.Println("-", ability)
    }
}

// Init initializes a monster.
func Init(name string, feet int, hasPaws bool, abilities []string) *Monster {
    return &Monster{
        Animal{name, feet, hasPaws},
        abilities,
    }
}

func main() {
    var dragon properties = Init("Dragon", 4, true, []string{"Nuclear blast", "Ice smoke"})

    dragon.Print()
}

That was the final code. There are access modifiers such as default, private, protected, and public in other programming languages such as C/C++, Java, PHP, etc. There are only two in Go; private and public. They are contained in the name. The private starts with lowercase and the public starts with uppercase. It is cool to put comments on the public methods.

Closing

The structs or objects are public. The properties and methods are private. The only public methods are Print and Init which is a constructor. It is cool I think. Maybe we will learn about exporting objects or modules in my next article later. You can find the source code at https://github.com/aristorinjuang/go-oop.

References