(Not) Object Oriented Go

#programming #go #golang #composition #oop

Now that I've got a solid foundation in Python, plus 5-10 years of thinking I knew it already, I've jumped back over to Golang. Despite how popular Python is, I still can't help but want to spend time with Go. It's mainly just a feeling, but I love this language. I'll try and outline a few reasons why I always go back to Go.

It's Compiled

Golang is the first compiled language I've used to any real extent. Having spent so much time with interpreted languages like Python (and PHP / Ruby years ago), using a language that needs to be compiled brings freedom. With Go, I find that I spend way less time trying to manage environments and packages during development, and none at all if I ever need to distribute the compiled program. Maybe that's inexperience talking, but for now it's totally true.

Uniform Style + gofmt

package main
import ("fmt")

func main() 
{ 
    fmt.Println("Hello World!") 
} 

The official formatting style is an intentional no-brainer — the built ingo fmt (gofmt) tool implements a uniform style across all your code. “Formatting issues are the most contentious but the least consequential” says (Effective Go) and I can't agree more. The less time I need to spend worrying about formatting and style conventions, the better.

I remember submitting my first PR with tabs instead of spaces at an old job (in Perl or Coldfusion or something). It was kicked back to me almost immediately and it, still to this day, annoys the hell out of me. I get the whole debate and understand that uniformity is important — but come on. Python may have PEP8 formatters, but the fact that go has one built into the SDK is a real advantage on the style front.

It's Mostly Simple

Go was designed to solve problems pragmatically. You can pick up Go and implement a workable solution to a simple problem in very little time. I did it when I was first learning Go by making a wrapper around the shutdown command, so I could watch Youtube and not worry about my laptop if I fell asleep.

That's not to say that there's nothing difficult to grasp about Go — quite the opposite, especially for those of us without any real C/C++ experience. One of the biggest challenges I had was managing types — especially slices, arrays, and maps.

I've been practicing Go and Python with CodeWars and I'm noticing a glaring difference between the two. When I solve problems with Go, my solutions are typically compact and compare similarly to the ones submitted by others. When I solve problems in Python, more often than not, my solutions are longer than others — there's a heap of built-in functionality in Python that I just don't know about. There are list comprehensions all over the place, which... I'm pretty sure are just for show — most of the time they're just plain unreadable. I guess it could be the sheer number of Python programmers out there and that it's more mature... but I think it speaks to Go's nature.

Objected-Oriented with out being Object-Oriented

Go had been described as a post-OOP programing language. Despite having no official class support, you can still implement object oriented patterns into your project. Go takes a different approach by using interfaces, structs, methods, pointers, etc. Admittedly, these are things I struggle with the most — especially pointers.

Even though the learning curve is steep for me, I see the decision to not implement classes in Go results in a less constrained and less limited environment. Though, I can't argue with the ease and simplicity that comes from using classes, especially in Python.

___

Go Interfaces, Methods, Types, and Composition

I'll try to outline the basic idea behind composition and how Go lets you to solve problems with object-oriented thinking, without being conventionally object-oriented.

So, say an insurance company has a yard full of cars and their adjusters needs to generate a list of vehicles that need repair. At it's most basic, we need a Car type that will store information about each car, and then a function that will tell us whether each car needs to be repaired.

package main

import "fmt"

type InsuranceAdjuster interface {
	needsRepair() string
}

type Car struct {
	brand, model string
	runs         bool
}

func (c Car) needsRepair() string {
	if c.runs == false {
		return "needs repair"
	}
	return "runs fine"
}

func main() {

	car1 := Car{"Toyota", "Camry", true}
	car2 := Car{"Chevy", "Impala", false}

	fmt.Println(car1.brand, car1.model, car1.needsRepair())
	fmt.Println(car2.brand, car2.model, car2.needsRepair())
}

Run in Go Playground

Here, I create a new type named Car and gave it a few definable fields: like Brand, Model, and a boolean for whether it runs. Below it, I create a method needsRepair() that takes in the new Car type and returns a text string that says whether it runs or not.

I also created an interface called InsuranceAdjuster with a single requirement: to implement the interface, the type must have a method called needsRepair(). And since Car has a needsRepair() method, it implements the InsuranceAdjuster interface. What's the point of the interface? In this example, there's really no point. Everything would still work correctly with or without the interface.

But, say the insurance adjusters need to track trucks separately — maybe because they need to know the truck bed length. With Go, it's fairly easy — we can expand the program by implementing a Truck type.

type Truck struct {
	brand, model string
	bed          float32
	runs         bool
}

We've got our Truck type now, but now our text string doesn't say anything about the bed length. This is where the interface comes in handy.

package main

import (
	"fmt"
)

type InsuranceAdjuster interface {
	needsRepair() string
}

type Car struct {
	brand, model string
	runs         bool
}

type Truck struct {
	brand, model string
	bed          float32
	runs         bool
}

func (c Car) needsRepair() string {
	if c.runs == false {
		return fmt.Sprintf("%s %s needs repair", c.brand, c.model)
	}
	return fmt.Sprintf("%s %s runs fine", c.brand, c.model)
}

func (t Truck) needsRepair() string {
	if t.runs == false {
		return fmt.Sprintf("%s %s with a %.1f foot bed needs repair", t.brand, t.model, t.bed)
	}
	return fmt.Sprintf("%s %s with a %.1f foot bed runs fine", t.brand, t.model, t.bed)
}

func main() {

	car1 := Car{"Toyota", "Camry", true}
	car2 := Car{"Chevy", "Impala", false}
	truck1 := Truck{
		brand: "Toyota",
		model: "Tacoma",
		bed:   5.5,
		runs:  true,
	}
	truck2 := Truck{
		brand: "Nissan",
		model: "Frontier",
		bed:   4.9,
		runs:  false,
	}

	claims := []InsuranceAdjuster{car1, car2, truck1, truck2}
	for _, claims := range claims {
		fmt.Println(claims.needsRepair())
	}
}

Run in Go Playground

Now we've got cars and trucks tracked separately and the interface has a purpose now (kind of). At the bottom, we create a new claims variable and make it an instance of the InsuranceAdjuster interface, and we throw all the vehicles into it. Now we can treat them all the same and when we run needsRepair(), we get the correct output depending on whether it's a truck or a car.

Now, say they want to track the type of damage that each vehicle has — body or engine. We can create a new type called Damage, and then require that each vehicle type uses it.

type Damage struct {
	engine bool
	body   bool
}

type Car struct {
	brand, model string
	damage       Damage
	runs         bool
}

type Truck struct {
	brand, model string
	bed          float32
	damage       Damage
	runs         bool
}

Now, we'll adjust needsRepair() to describe the damage. We end up with this:

package main

import (
	"fmt"
)

type InsuranceAdjuster interface {
	needsRepair() string
}

type Damage struct {
	engine bool
	body   bool
}

type Car struct {
	brand, model string
	damage       Damage
	runs         bool
}

type Truck struct {
	brand, model string
	bed          float32
	damage       Damage
	runs         bool
}

func (c Car) needsRepair() string {
	if c.runs == false {
		return fmt.Sprintf("%s %s needs repair. \n\tBody damage: %t, \n\tEngine damage: %t.", c.brand, c.model, c.damage.body, c.damage.engine)
	}
	return fmt.Sprintf("%s %s runs fine. \n\tBody damage: %t, \n\tEngine damage: %t.", c.brand, c.model, c.damage.body, c.damage.engine)
}

func (t Truck) needsRepair() string {
	if t.runs == false {
		return fmt.Sprintf("%s %s with a %.1f foot bed needs repair. \n\tBody damage: %t, \n\tEngine damage: %t.", t.brand, t.model, t.bed, t.damage.body, t.damage.engine)
	}
	return fmt.Sprintf("%s %s with a %.1f foot bed runs fine. \n\tBody damage: %t, \n\tEngine damage: %t.", t.brand, t.model, t.bed, t.damage.body, t.damage.engine)
}

func main() {

	car1 := Car{
		brand: "Toyota",
		model: "Camry",
		damage: Damage{
			body:   true,
			engine: false,
		},
		runs: true,
	}
	car2 := Car{
		brand: "Chevy",
		model: "Impala",
		damage: Damage{
			body:   false,
			engine: true,
		},
		runs: false,
	}
	truck1 := Truck{
		brand: "Toyota",
		model: "Tacoma",
		bed:   5.5,
		damage: Damage{
			body:   true,
			engine: false,
		},
		runs: true,
	}
	truck2 := Truck{
		brand: "Nissan",
		model: "Frontier",
		bed:   4.9,
		damage: Damage{
			body:   false,
			engine: true,
		},
		runs: false,
	}

	claims := []InsuranceAdjuster{car1, car2, truck1, truck2}
	for _, claims := range claims {
		fmt.Println(claims.needsRepair())
	}
}

Run in Go Playground

Sure, this isn't the best example, and my outline kind of assumes you already know a bit about Go, but it still shows how interfaces and composition can work in Go. And, in defense of the sheer number of lines this small program took — keep in mind that this was all formatted for readability.


See something wrong? Email [email protected].