Let's Go: Object-Oriented Programming in Golang

Datetime:2016-08-23 05:14:26          Topic: Golang           Share

Go is a strange mix of old and new ideas. It has a very refreshing approach where it isn't afraid to throw away established notions of "how to do things". Many people are not even sure if Go is an object-oriented language. Let me put that to rest right now. It is!

In this tutorial you'll learn about all the intricacies of object-oriented design in Go, how the pillars of object-oriented programming like encapsulation, inheritance, and polymorphism are expressed in Go, and how Go compares to other languages.

The Go Design Philosophy

Go's roots are based on C and more broadly on the Algol family . Ken Thompson half-jokingly said that Rob Pike, Robert Granger and himself  got together and decided they hate C++. Whether it's a joke or not, Go is very different from C++. More on that later. Go is about ultimate simplicity. This is explained in detail by Rob Pike in Less is exponentially more .

Go vs. Other Languages

Go has no classes, no objects, no exceptions, and no templates. It has garbage collection and built-in concurrency. The most striking omission as far as object-oriented is concerned is that there is no type hierarchy in Go. This is in contrast to most object-oriented languages like C++, Java, C#, Scala, and even dynamic languages like Python and Ruby.

Go Object-Oriented Language Features

Go has no classes, but it has types. In particular, it has structs. Structs are user-defined types. Struct types (with methods) serve similar purposes to classes in other languages.

Structs

A struct defines state. Here is a Creature struct. It has a Name field and a boolean flag called Real, which tells us if it's a real creature or an imaginary creature. Structs hold only state and no behavior.

type Creature struct {

  Name string

  Real bool

}

Methods

Methods are functions that operate on particular types. They have a receiver clause that mandates what type they operate on. Here is a Dump() method that operates on Creature structs and prints their state:

func (c Creature) Dump() {
  fmt.Printf("Name: '%s', Real: %t\n", c.Name, c.Real)
}

This is an unusual syntax, but it is very explicit and clear (unlike the implicit "this" or Python's confusing "self").

Embedding

You can embed anonymous types inside each other. If you embed a nameless struct then the embedded struct provides its state (and methods) to the embedding struct directly. For example, the FlyingCreature has a nameless Creature struct embedded in it, which means a FlyingCreature is a Creature .

type FlyingCreature struct {
  Creature
  WingSpan int
}

Now, if you have an instance of a FlyingCreature, you can access its Name and Real attributes directly.

dragon := &FlyingCreature{
    Creature{"Dragon", false, },
    15,
}

fmt.Println(dragon.Name)
fmt.Println(dragon.Real)
fmt.Println(dragon.WingSpan)

Interfaces

Interfaces are the hallmark of Go's object-oriented support. Interfaces are types that declare sets of methods. Similarly to interfaces in other languages, they have no implementation.

Objects that implement all the interface methods automatically implement the interface. There is no inheritance or subclassing or "implements" keyword. In the following code snippet, type Foo implements the Fooer interface (by convention, Go interface names end with "er").

type Fooer interface {
  Foo1()
  Foo2()
  Foo3()
}

type Foo struct {
}

func (f Foo) Foo1() {
    fmt.Println("Foo1() here")
}

func (f Foo) Foo2() {
    fmt.Println("Foo2() here")
}

func (f Foo) Foo3() {
    fmt.Println("Foo3() here")
}

Object-Oriented Design: The Go Way

Let's see how Go measures up against the pillars of object-oriented programming: encapsulation, inheritance, and polymorphism. Those are features of class-based programming languages, which are the most popular object-oriented programming languages.

At the core, objects are language constructs that have state and behavior that operates on the state and selectively exposes it to other parts of the program.

Encapsulation

Go encapsulates things at the package level. Names that start with a lowercase letter are only visible within that package. You can hide anything in a private package and just expose specific types, interfaces, and factory functions.

For example, here to hide the Foo type above and expose just the interface you could rename it to lower case foo and provide a NewFoo() function that returns the public Fooer interface:

type foo struct {
}

func (f foo) Foo1() {
    fmt.Println("Foo1() here")
}

func (f foo) Foo2() {
    fmt.Println("Foo2() here")
}

func (f foo) Foo3() {
    fmt.Println("Foo3() here")
}

func NewFoo() Fooer {
    return &Foo{}
}

Then code from another package can use NewFoo() and get access to a Fooer interface implemented by the internal foo type:

f := NewFoo()

f.Foo1()

f.Foo2()

f.Foo3()

Inheritance

Inheritance or subclassing was always a controversial issue. There are many problems with implementation inheritance (as opposed to interface inheritance). Multiple inheritance as implemented by C++ and Python and other languages suffers from the deadly diamond of death problem, but even single inheritance is no picnic with the fragile base-class problem. 

Modern languages and object-oriented thinking now favor composition over inheritance. Go takes it to heart and doesn't have any type hierarchy whatsoever. It allows you to share implementation details via composition. But Go, in a very strange twist (that probably originated from pragmatic concerns), allows anonymous composition via embedding.

For all intents and purposes, composition by embedding an anonymous type is equivalent to implementation inheritance. An embedded struct is just as fragile as a base class. You can also embed an interface, which is equivalent to inheriting from an interface in languages like Java or C++. It can even lead to a runtime error that is not discovered at compile time if the embedding type doesn't implement all the interface methods.

Here SuperFoo embeds the Fooer interface, but doesn't implement its methods. The Go compiler will happily let you create a new SuperFoo and call the Fooer methods, but will obviously fail at runtime. This compiles:

type SuperFooer struct {
  Fooer
}

func main() {
  s := SuperFooer{}
  s.Foo2()

Running this program results in a panic:

panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xb code=0x1 addr=0x28 pc=0x2a78]

goroutine 1 [running]:
panic(0xde180, 0xc82000a0d0)
  /usr/local/Cellar/go/1.6/libexec/src/runtime/panic.go:464 +0x3e6
main.main()
  /Users/gigi/Documents/dev/go/src/github.com/oop_test/main.go:104 +0x48
exit status 2

Process finished with exit code 1

Polymorphism

Polymorphism is the essence of object-oriented programming: the ability to treat objects of different types uniformly as long as they adhere to the same interface. Go interfaces provide this capability in a very direct and intuitive way.

Here is an elaborate example where multiple creatures (and a door!) that implement the Dumper interface are created and stored in a slice and then the Dump() method is called for each one. You'll notice different styles of instantiating the objects too.

package main

import "fmt"

type Creature struct {
  Name string
  Real bool
}

func Dump(c*Creature) {
  fmt.Printf("Name: '%s', Real: %t\n", c.Name, c.Real)
}

func (c Creature) Dump() {
  fmt.Printf("Name: '%s', Real: %t\n", c.Name, c.Real)
}

type FlyingCreature struct {
  Creature
  WingSpan int
}

func (fc FlyingCreature) Dump() {
  fmt.Printf("Name: '%s', Real: %t, WingSpan: %d\n",
    fc.Name,
    fc.Real,
    fc.WingSpan)
}

type Unicorn struct {
  Creature
}

type Dragon struct {
  FlyingCreature
}

type Pterodactyl struct {
  FlyingCreature
}

func NewPterodactyl(wingSpan int) *Pterodactyl {
  pet := &Pterodactyl{
    FlyingCreature{
      Creature{
        "Pterodactyl",
        true,
      },
      wingSpan,
    },
  }
  return pet
}

type Dumper interface {
  Dump()
}

type Door struct {
  Thickness int
  Color     string
}

func (d Door) Dump() {
  fmt.Printf("Door => Thickness: %d, Color: %s", d.Thickness, d.Color)
}

func main() {
  creature := &Creature{
    "some creature",
    false,
  }

  uni := Unicorn{
    Creature{
      "Unicorn",
      false,
    },
  }

  pet1 := &Pterodactyl{
    FlyingCreature{
      Creature{
        "Pterodactyl",
        true,
      },
      5,
    },
  }

  pet2 := NewPterodactyl(8)

  door := &Door{3, "red"}

  Dump(creature)
  creature.Dump()
  uni.Dump()
  pet1.Dump()
  pet2.Dump()

  creatures := []Creature{
    *creature,
    uni.Creature,
    pet1.Creature,
    pet2.Creature}
  fmt.Println("Dump() through Creature embedded type")
  for _, creature := range creatures {
    creature.Dump()
  }

  dumpers := []Dumper{creature, uni, pet1, pet2, door}
  fmt.Println("Dump() through Dumper interface")
  for _, dumper := range dumpers {
    dumper.Dump()
  }
}

Conclusion

Go is a bona fide object-oriented programming language. It enables object-based modeling and promotes the best practice of using interfaces instead of concrete type hierarchies. Go made some unusual syntactic choices, but overall working with types, methods, and interfaces feels simple, lightweight, and natural.

Embedding is not very pure, but apparently pragmatism was at work, and embedding was provided instead of only composition by name.





About List