Object Oriented Go

Object Oriented Programming is a programming paradigm in which code is organized into objects, abstract data types which contain state and behaviors. Go is an object oriented language, but at the same time not quite, at least not in the traditional way. Go has many of the features of a traditional OOP language, including:

Methods on any defined type

Polymorphism

Namespacing

Message Passing/Delegation

The way Go defines relationships between objects, however, is a little unconventional.

Unlike a traditional OOP language, Go doesn’t support Objects and Classes. You may be asking yourself, “how can Go be object-oriented if it doesn’t support objects?” Well, Go doesn’t have an explicit Object type. Instead of objects and classes, Go has types and methods. The language allows user-created types composed of existing types and of custom data structures known as structs. We can then declare methods on those types. A method is exactly the same as a regular function in Go with the exception of having an explicit receiver, found between the “func” and the function name, which denotes what type the method acts on. This looks a lot like the OOP most developers are accustomed to. However, despite this familiarity Go doesn’t support inheritance. There is no type hierarchy in Go. Rather, Go uses a different approach known as Composition over Inheritance.

Composition over Inheritance

Many popular programming languages use inheritance, in some form or another. Inheritance is behavioral reuse through subclassing. Essentially, you have a base class with some kind of behavior, and a child class which inherently has all the behavior of the base class, plus more. In C++, that would look something like this:

class Vehicle

{

public:

void drive() {}

int getWheels() { return wheels } private:

int wheels; };



class Car: public Vehicle

{

public:

void drive() { /*Override with some behvaior */}

void honk() { /* Some honking Logic */ } };

Here, the “Vehicle” class defines Vehicles as having wheels and being able to drive. The child class, “Car”, also has wheels, but has redefined how to drive, and added honking to its behavior. However, there is a drawback with this common pattern: it is inflexible. As the system grows and we add new children and grandchildren and great-grandchildren, it becomes more and more difficult to alter the base class. Any changes in the base class could cascade through to all the descendants and have unexpected effects on their implementations.

An alternative is to use composition. Object composition allows objects to reuse behavior by combining simple objects into larger, more complex ones. In Go, you can embed types within each other thereby extending their behavior.

type Person struct {

Name string

JobTitle string

ShoeSize float32

} func (p Person) SayHello() {

fmt.Println(“Hello, I’m “, p.Name)

} func (p Person) GetJobTitle() {

fmt.Println(“I’m a “, p.JobTitle)

} type Doctor struct {

Person

Degree string

} func (d Doctor) SayHello() {

fmt.Println(“Hello, I’m Dr.”, d.Name, “, “, d.Degree)

} func (d Doctor) Diagnose() {

fmt.Println(“It’s Lupus.”)

}

In the above example, we accomplish the same things as in the C++ Car example. Here we create a “Person” base type. Then, we create a “Doctor” type and embed a “Person” in it. This gives a “Doctor” all the attributes and behaviors of a “Person”, plus more. This approach is more flexible than traditional inheritance, as each object is loosely coupled and changes to one type don’t really cause dramatic changes down the line. In essence, compositions combined with interfaces meets all the demands of an object oriented system, without the complexity and limitations of inheritance.

What are interfaces?

Interfaces in Go provide a way to specify behavior for a type. An interface is basically a set of methods. For a type to satisfy an interface, it must implement at least all the methods specified in the interface. Java and C# also employ this, but Go uses it exclusively. Also, all interfaces are satisfied implicitly (no implements declaration), so you can write interfaces for code that you don’t own. As a result all types in Go automatically satisfy the empty interface. We can take advantage of this quality by using an empty interface as an input parameter for a function which we want to accept any type, and then using Reflection to determine the type and act on the input accordingly.

Interfaces for Polymorphism

Go facilitates polymorphism through interfaces. Any type that satisfies an interface can be used interchangeably as that interface type. Here’s a quick example.

package main import “fmt” type Animal interface {

Type() string

Swim() string

} type Dog struct {

Name string

Breed string

} type Frog struct {

Name string

Color string

} func main() {

f := new(Frog)

d := new(Dog)

zoo := […]Animal{f, d}

for _, a := range zoo {

fmt.Println(a.Type(), “ can “, a.Swim())

}

} func (f *Frog) Type() string {

return “Frog”

} func (f *Frog) Swim() string {

return “Kick”

} func (d *Dog) Swim() string {

return “Paddle”

} func (d *Dog) Type() string {

return “Doggie”

}

This example is a bit contrived, but it gets the point across. This pattern is used in many places throughout the language. A more practical example is in Go’s Sort functionality. This built-in package sorts collections. The developer can define how a type should be sorted by implementing the “Interface” interface in the Sort package. The “Interface” contains just three methods: “Len”, “Less”, and “Swap”. The sort package calls these three methods in a Quicksort algorithm. A sample implementation may look like this:

type Person struct {

Name string

Age int

ShoeSize float32

} type PeopleByShoeSize []Person func (p PeopleByShoeSize) Len() int {

return len(p)

} func (p PeopleByShoeSize) Swap(i, j int) {

p[i], p[j] = p[j], p[i]

} func (p PeopleByShoeSize) Less(i, j int) bool {

return (p[i].ShoeSize < p[j].ShoeSize)

}

Here, you can see we have some data structure (“Person”) and a type representing a slice of type “Person” called “PeopleByShoeSize”. We then implement the “Interface” interface. The real magic happens in the “Less” function, where we define how to order the data structure. With the interface implemented we can then use the sort fuction as follows:

func main() {

people := []Person{

{

Name: “Person1”,

Age: 25,

ShoeSize: 8,

},

{

Name: “Person2”,

Age: 21,

ShoeSize: 4,

},

{

Name: “Person3”,

Age: 15,

ShoeSize: 9,

},

{

Name: “Person4”,

Age: 45,

ShoeSize: 15,

},

{

Name: “Person5”,

Age: 25,

ShoeSize: 8.5,

},

} fmt.Println(people)

sort.Sort(PeopleByShoeSize(people))

fmt.Println(people)

} /* OUTPUT

[{Person1 25 8} {Person2 21 4} {Person3 45 15} {Person4 25 8.5}]

[{Person2 21 4} {Person1 25 8} {Person4 25 8.5} {Person3 45 15}]

*/

Database Drivers

For Arnoldb (our custom datastore), we use these polymorphic mechanisms for the foundation of our database driver structure. We wanted to provide options for different database options, as well as provide an easy way for us to swap out database implementations while we experiment and prototype. In addition, we created a “storage” package which maintains a list of available database drivers. The package contains the “StorageDriver” interface, which defines all the behavior we wanted in a database driver. It contains logic to create new database driver instances as a “StorageManager” type. It also has a function called RegisterDriver which allows a new driver to identify and register itself as an available database driver to the storage package. Each type that implements the “StorageDriver” interface should call this function in their “init()” function to make it available to the factory. This structure allows us to use multiple databases and switch between them at runtime. It even allowed us to create a single test file which can then test all driver implementations automatically.