OOP and Go… Sorta

So I started Go July 18th, 2016. Three weeks later, I gave my first tech talk ever at the NYC Women Who Go meetup. This is a write up of that talk. The intended audience is folks coming from class-based languages with keywords like “class”, “implements”, “abstract”. I specifically programmed PHP prior to starting Go.

All inline code snippets will be italicized in this blog post.
Hyperlinks of referenced articles are also included throughout this post. 

0. Terminology

“Go has types and values rather than classes and objects.”

It is first important to understand that you can use object-oriented programming principles without having objects. The term “object” comes with a lot of implied meaning that is not possible with Go.

In Go, we have values, whereas traditional OOP languages have objects. Let’s differentiate the two.

Object vs. Value

An object is an instance of a class. The object is accessed through a named reference.

<?php
class SomeObject {
    public $classVar;
    public function __construct( $classVar ) {
$this->classVar = $classVar;
}
}


$object = new SomeObject( "Hello, world." );
$reference = $object;
$reference->classVar = "Look! I can access object!";
echo $object->classVar;    // "Look! I can access object!"
echo $reference->classVar; // "Look! I can access object!"

This PHP code illustrates that both $object and $reference point to the same instantiation of SomeObject.

Outside of being able to be accessed by various named references, objects also include key concepts like inheritance & subclasses that is not possible with Go. Thus, when learning Go, it is best to sever our ties from objects, and focus on using the correct terminology.

A value is exactly just that, a value. For example, a struct is a value. Each instance of a struct is copied when passed around.

package main
import ("fmt")
type SomeStruct struct{
Field string
}
func main() {
value := SomeStruct{Field: "Structs are values"}
copy := value
copy.Field = "This is a Copy & doesn't change the variable
value"
fmt.Println(value.Field) // "Structs are values"
fmt.Println(copy.Field) // "This is a Copy & doesn't change
value"
}

In the example above, you can see that when we assigned copy.Field, value.Field never changes its value. When we want to reference the same instance, similar to C, we have pointers to explicitly do so.

Types & Method Set

Now that we know why Go doesn’t have objects, let’s figure out how to operate on an instance of a type, specifically a struct type.

Types are associated with a method set. Each method in the method set can operate on the given receiver.

type SomeStruct struct{
Field string
}
// foo is in the method set of SomeStruct
// (s *SomeStruct) is a receiver for SomeStruct pointers
func (s *SomeStruct) foo(field string) {
s.Field = field
}
func main() {
    someStruct := new(SomeStruct)

someStruct.foo("a")
fmt.Println(someStruct.Field) // "a"
    someStruct.foo("b")
fmt.Println(someStruct.Field) // "b"
}

Here, we see that method foo operates on the same instance of someStruct and changes its Field value.

Again. Go does not have objects, but we can see similarities between method receivers and class methods.


Now that we got down terminology, we can finally dive into applicable OOP patterns we can use to break up our Go code.

1. enCAPSulation

Encapsulation is the mechanism of hiding of data implementation by restricting access to public methods.

Traditionally, encapsulation in class-based OOP is achieved through private and public class variables / methods. In Go, encapsulation is achieved on a package level.

“Public” elements can be exported out of the package and indicated by capitalizing the first letter. Here, public is in quotes because the more accurate terminology is exported vs. unexported elements. Unexported elements are indicated with a lowercase first letter, and can only be accessed within its respective package.

Public/protected/private are keywords relative to classes, while exporting/importing are relative to packages.
package encapsulation
import "fmt"
// Encapsulation struct can be exported outside of this package
type Encapsulation struct{}
// Expose method can be exported outside of this package
func (e *Encapsulation) Expose() {
fmt.Println("AHHHH! I'm exposed!")
}
// hide method can only be used within this package
func (e *Encapsulation) hide() {
fmt.Println("Shhhh... this is super secret")
}
// Unhide uses the unexported hide function
func (e *Encapsulation) Unhide() {
e.hide()
fmt.Println("...jk")
}

In the package encapsulation, Encapsulation (struct), Expose (method), and Unhide (method) are all exported and can be used from other packages.

package main
import "github.com/amy/tech-talk/encapsulation"
func main() {
e := encapsulation.Encapsulation{}

e.Expose() // "AHHHH! I'm exposed!"

// e.hide() // if uncommented, causes the following error
// ./main.go:10: e.hide undefined (cannot refer
// to unexported field or method encapsulation.
// (*Encapsulation)."".hide)
    
e.Unhide() // "Shhhh... this is super secret"
// "...jk"
}

Here, we imported the package encapsulation as well as its exportable elements into the main package. Note how if we attempted to export the hide method, our compiler would produce an error.

2. Polymorphism

Polymorphism describes a pattern in object oriented programming in which classes have different functionality while sharing a common interface.

In Go, interfaces are implicitly satisfied. Interfaces are also types. These two sentences pack a lot of meaning, so lets break it down.

Interfaces are implicitly satisfied→ A type satisfies an interface when all the methods in the interface are included in the type’s method set. In Go, there is no implements keyword.

Interfaces are types → If a type implements an interface, that type will also satisfy anything type restricted by the interface it satisfies.

package polymorphism 
import "fmt"
type SloganSayer interface {
Slogan()
}
// SaySlogan takes in a parameter of type SloganSayer
func SaySlogan(sloganSayer SloganSayer) {
sloganSayer.Slogan()
}
// Hillary implicitly satisfies the SloganSayer interface
// by implementing the function Slogan.
// Thus, Hillary is also of type SloganSayer.

type Hillary struct{}
func (h *Hillary) Slogan() {
fmt.Println("Stronger together.")
}
// same idea for Trump
type Trump struct{}
func (t *Trump) Slogan() {
fmt.Println("Make America great again.")
}

The main takeaway here is that the function SaySlogan can restrict its parameter to a SloganSayer type. Thus, any type that satisfies that interface will be accepted for that parameter.

package main 
import "github.com/amy/tech-talk/polymorphism"
func main() {
    hillary := new(polymorphism.Hillary)
hillary.Slogan() // "Stronger together."
polymorphism.SaySlogan(hillary) // "Stronger together."
    trump := new(polymorphism.Trump)
polymorphism.SaySlogan(trump) // "Make America great again."
}

We don’t need to worry about which presidential candidate is saying a slogan. As long as the type implements the SloganSayer interface, we can pass it into SaySlogan.

3. Composition

In Go, inheritance is not possible. Instead, we build our structs with composable and reusable elements through embedding.

Go allows us to embed types within interfaces or structs. Through embedding, we are able to forward the methods included from the inner type, to the outer type.

When we embed a type, the methods of that type become methods of the outer type, but when they are invoked the receiver of the method is the inner type, not the outer one.
package composition 
import "fmt"
type Human struct {
FirstName string
LastName string
CanSwim bool
}
// Amy is embedded with the Human type
// and can thus invoke methods in Human's method set
type Amy struct {
Human
}
// Alan is also embedded with the Human type
type Alan struct {
Human
}
func (h *Human) Name() {

fmt.Printf("Hello! My name is %v %v", h.FirstName, h.LastName)
}
func (h *Human) Swim() {

if h.CanSwim == true {
fmt.Println("I can swim!")
} else {
fmt.Println("I can not swim.")
}
}

You’ll often hear the phrase “composition over inheritance”.

package main
import "github.com/amy/tech-talk/composition"
func main() {
    // amy is composed of type Human
amy := composition.Amy{
Human: composition.Human{
FirstName: "Amy",
LastName: "Chen",
CanSwim: true,
},
}
    // alan is composed of type Human
alan:= composition.Alan{
Human: composition.Human{
FirstName: "Alan",
LastName: "Chen",
CanSwim: false,
},
}
    // Human's method set are forwarded to type Amy
amy.Name() // "Hello! My name is Amy Chen"
amy.CanSwim() // "I can swim!"
    alan.Name()    // "Hello! My name is Alan Chen"
alan.CanSwim() // "I can't swim"
}

4. Abstraction

Abstraction means working with something we know how to use without knowing how it works internally.

Similar to embedding structs within a struct, we can also embed interfaces within structs. Remember that any type that satisfied an interface also adopts that interface type.

package abstraction
import "fmt"
type SloganSayer interface {
Slogan()
}
// Campaign can accept a SloganSayer in its instantiation
// Campaign also is a SloganSayer because it also implements
// the SloganSayer interface. This is useful for chaining.

type Campaign struct{
SloganSayer
}
// SaySlogan can also accept Campaign as one of its parameters!
func SaySlogan(s SloganSayer) {
s.Slogan()
}
// Hillary implements the SloganSayer interface
// Hillary is a SloganSayer

type Hillary struct{}
func (h *Hillary) Slogan() {
fmt.Println("Stronger together.")
}
// Trump implements the SloganSayer interface
// Trump is a SloganSayer

type Trump struct{}
func (t *Trump) Slogan() {
fmt.Println("Make American great again.")
}

The usefulness of interface embedding will be more evident when we see this being used.

package main
import "github.com/amy/tech-talk/abstraction"
func main() {
    hillary := new(abstraction.Hillary)
trump := new(abstraction.Trump)
    h := abstraction.Campaign{hillary}
t := abstraction.Campaign{trump}
    // Hillary and Trump implementations of Slogan are abstracted
// away. Instead, Campaign just knows that it is a SloganSayer
// and can thus call Slogan.

h.Slogan() // "Stronger together."
t.Slogan() // "Make America great again."
    // We can inject a SloganSayer into the SaySlogan parameter
abstraction.SaySlogan(hillary) // "Stronger together."
abstraction.SaySlogan(trump) // "Make America great again."
    // Remember that h and t are also type Campaign
abstraction.SaySlogan(h) // "Stronger together."
abstraction.SaySlogan(t) // "Make America great again."
}

And that’s it! Now that you know how to borrow OOP principles you were originally familiar with, start writing some modular code. Below is a summary of how our OOP principles translate in Go.


If you found this helpful, please recommend / share! If you have questions / suggestions, feel free to reach out to my twitter @theamydance.