Go Language and Object-Oriented Programming

Go Language and Object-Oriented Programming

Is Go an Object-Oriented Language? Yes and No.

Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy. The concept of “interface” in Go provides a different approach that we believe is easy to use and in some ways more general. There are also ways to embed types in other types to provide something analogous — but not identical — to subclassing. Moreover, methods in Go are more general than in C++ or Java: they can be defined for any sort of data, even built-in types such as plain, “unboxed” integers. They are not restricted to structs (classes).

Also, the lack of a type hierarchy makes “objects” in Go feel much more lightweight than in languages such as C++ or Java.

https://golang.org/doc/faq#Is_Go_an_object-oriented_language

Classic OOP isn’t the holy grail of computer programming as some might think. Some even consider it an anti-pattern because it’s brittle and heavy. OOP is also a pretty vague term and everyone seems to interpret and use it differently.

Basically, object-oriented programming is about:

  • Encapsulation
  • Abstraction
  • Inheritance
  • and Polymorphism

and every single of these methodologies are applicable in Go without the concept of classes.

Encapsulation

Encapsulation is defined as hiding irrelevant attributes to the outside world. That is basically where we get public and private methods from.

Go structs provide encapsulation features like private (starts with a lower-case character) and public (capitalised) fields.

Here’s a simple bank account example showcasing structures, public & private methods and private fields.

package main

import (
	"errors"
	"fmt"
	"testing"

	"github.com/stretchr/testify/require"
)

// bankAccount represents a bank account instance
type bankAccount struct {
	// Private fields
	id      string
	blocked bool
	balance float64
}

// newBankAccount creates a new bank account instance
func newBankAccount(id string) (*bankAccount, error) {
	if len(id) < 1 {
		return nil, errors.New("invalid bank account id")
	}
	return &bankAccount{
		id:      id,
		blocked: false,
		balance: 0,
	}, nil
}

func (ba *bankAccount) checkBlocked() error {
	if ba.blocked {
		return fmt.Errorf("account %s is blocked", ba.id)
	}
	return nil
}

// Balance returns the current balance of the account
func (ba *bankAccount) Balance() (float64, error) {
	if err := ba.checkBlocked(); err != nil {
		return 0, err
	}
	return ba.balance, nil
}

// Deposit deposits money onto the account
func (ba *bankAccount) Deposit(value float64) error {
	if err := ba.checkBlocked(); err != nil {
		return err
	}
	if value > 0 {
		ba.balance += value
		return nil
	}
	return fmt.Errorf("invalid deposit value: %f", value)
}

// Withdraw widthdraws money from the account
func (ba *bankAccount) Withdraw(value float64) error {
	if err := ba.checkBlocked(); err != nil {
		return err
	}
	if ba.balance-value < 0 {
		return errors.New("insufficient funds")
	}
	ba.balance -= value
	return nil
}

// TestDepositAndWithdrawal tests BankAccount.Deposit and BankAccount.Withdraw
func TestWithdrawal(t *testing.T) {
	account, err := newBankAccount("ABC")
	require.NoError(t, err)
	require.NotNil(t, account)

	b1, err := account.Balance()
	require.NoError(t, err)
	require.Equal(t, 0.0, b1)

	require.NoError(t, account.Deposit(4.5))

	b2, err := account.Balance()
	require.NoError(t, err)
	require.Equal(t, 4.5, b2)

	require.NoError(t, account.Withdraw(2.5))

	b3, err := account.Balance()
	require.NoError(t, err)
	require.Equal(t, 2.0, b3)
}

Abstraction

Abstraction is defined as showing only the relevant data to the user. In a pure OOP language a Abstract Object cannot be instantiated, and it’s only purpose is to be derived from. So you basically define it’s behavior’s and let someone else’s worry about the implementation.

Abstraction is achieved with Go interfaces. Same example as above but now with an intermediate interface:

package main

import (
	"errors"
	"fmt"
	"testing"

	"github.com/stretchr/testify/require"
)

// BankAccount represents a bank account instance
type BankAccount interface {
	// Balance returns the current balance of the account
	Balance() (float64, error)

	// Deposit deposits money onto the account
	Deposit(value float64) error

	// Withdraw widthdraws money from the account
	Withdraw(value float64) error
}

// NewBankAccount creates a new bank account instance
func NewBankAccount(id string) (BankAccount, error) {
	return newBankAccount(id)
}

func newBankAccount(id string) (*bankAccount, error) {
	if len(id) < 1 {
		return nil, errors.New("invalid bank account id")
	}
	return &bankAccount{
		id:      id,
		blocked: false,
		balance: 0,
	}, nil
}

type bankAccount struct {
	// Private fields
	id      string
	blocked bool
	balance float64
}

func (ba *bankAccount) checkBlocked() error {
	if ba.blocked {
		return fmt.Errorf("account %s is blocked", ba.id)
	}
	return nil
}

func (ba *bankAccount) Balance() (float64, error) {
	if err := ba.checkBlocked(); err != nil {
		return 0, err
	}
	return ba.balance, nil
}

func (ba *bankAccount) Deposit(value float64) error {
	if err := ba.checkBlocked(); err != nil {
		return err
	}
	if value > 0 {
		ba.balance += value
		return nil
	}
	return fmt.Errorf("invalid deposit value: %f", value)
}

func (ba *bankAccount) Withdraw(value float64) error {
	if err := ba.checkBlocked(); err != nil {
		return err
	}
	if ba.balance-value < 0 {
		return errors.New("insufficient funds")
	}
	ba.balance -= value
	return nil
}

// TestDepositAndWithdrawal tests BankAccount.Deposit and BankAccount.Withdraw
func TestWithdrawal(t *testing.T) {
	account, err := NewBankAccount("ABC")
	require.NoError(t, err)
	require.NotNil(t, account)

	b1, err := account.Balance()
	require.NoError(t, err)
	require.Equal(t, 0.0, b1)

	require.NoError(t, account.Deposit(4.5))

	b2, err := account.Balance()
	require.NoError(t, err)
	require.Equal(t, 4.5, b2)

	require.NoError(t, account.Withdraw(2.5))

	b3, err := account.Balance()
	require.NoError(t, err)
	require.Equal(t, 2.0, b3)
}

This way we decouple the interface of a bank account from it’s underlying implementation. We don’t care what’s implementing the interface as long as it behaves the way we expect it to (Duck-typing).

Inheritance

What Tom Smith inherits from his forefathers is first and foremost his family name that makes him a Smith. In addition, he inherits certain values that his parents have taught him and a skill at sculpting wood that has been the Smith family occupation for many generations. These attributes collectively identify Tom as an offspring of the Smith family tree. — Siddhartha Rao, C++ in One Hour a day.

In your programming life, you are often faced with situations where components being managed have similar attributes, differing very little in details or behavior. One way to solve this problem is to make each component a class where each class implements all attributes and re-implements the common ones. Another way to solve this is using Inheritance to allow objects that are similar to derive from a base object that contains common attributes and already implements a common functionality.

Well, There’s no inheritance in Go and for a good reason, but you can achieve very similar results with Go’s composition syntax. Let’s say we have a base-class bankAccount which is inherited by a subclass savingsAccount speaking in OOP terms. We can model this type hierarchy in Go too:

package main
import (
"errors"
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
// BankAccount represents a bank account
type BankAccount interface {
// Balance returns the current balance of the account
Balance() (float64, error)
// Deposit deposits money onto the account
Deposit(value float64) error
// Withdraw widthdraws money from the account
Withdraw(value float64) error
}
// SavingsAccount represents a savings bank account
type SavingsAccount interface {
// Embedded public interface
BankAccount
// SetWithdrawalLimit sets a new withdrawal limit
SetWithdrawalLimit(newLimit float64) error
}
/*************************************************\
BASIC BANK ACCOUNT
\*************************************************/
// NewBankAccount creates a new bank account instance
func NewBankAccount(id string) (BankAccount, error) {
return newBankAccount(id)
}
func newBankAccount(id string) (*bankAccount, error) {
if len(id) < 1 {
return nil, errors.New("invalid bank account id")
}
return &bankAccount{
id:      id,
blocked: false,
balance: 0,
}, nil
}
type bankAccount struct {
// Private fields
id      string
blocked bool
balance float64
}
func (ba *bankAccount) checkBlocked() error {
if ba.blocked {
return fmt.Errorf("account %s is blocked", ba.id)
}
return nil
}
func (ba *bankAccount) Balance() (float64, error) {
if err := ba.checkBlocked(); err != nil {
return 0, err
}
return ba.balance, nil
}
func (ba *bankAccount) Deposit(value float64) error {
if err := ba.checkBlocked(); err != nil {
return err
}
if value > 0 {
ba.balance += value
return nil
}
return fmt.Errorf("invalid deposit value: %f", value)
}
func (ba *bankAccount) Withdraw(value float64) error {
if err := ba.checkBlocked(); err != nil {
return err
}
if ba.balance-value < 0 {
return errors.New("insufficient funds")
}
ba.balance -= value
return nil
}
// TestBankAccount tests BankAccount
func TestBankAccount(t *testing.T) {
account, err := NewBankAccount("ABC")
require.NoError(t, err)
require.NotNil(t, account)
b1, err := account.Balance()
require.NoError(t, err)
require.Equal(t, 0.0, b1)
require.NoError(t, account.Deposit(4.5))
b2, err := account.Balance()
require.NoError(t, err)
require.Equal(t, 4.5, b2)
require.NoError(t, account.Withdraw(2.5))
b3, err := account.Balance()
require.NoError(t, err)
require.Equal(t, 2.0, b3)
}
/*************************************************\
SAVINGS BANK ACCOUNT
- composes a basic account
- overwrites method Withdraw
\*************************************************/
// NewSavingsAccount creates a new savings bank account instance
func NewSavingsAccount(id string) (SavingsAccount, error) {
return newSavingsAccount(id)
}
func newSavingsAccount(id string) (*savingsAccount, error) {
newBankAccount, err := newBankAccount(id)
if err != nil {
return nil, err
}
return &savingsAccount{
bankAccount:     newBankAccount,
withdrawalLimit: 0.0,
}, nil
}
type savingsAccount struct {
// Embedded private struct
*bankAccount
// Private fields
withdrawalLimit float64
}
func (ba *savingsAccount) Withdraw(value float64) error {
if value > ba.withdrawalLimit {
return fmt.Errorf("withdrawal limit (%f) exceeded", ba.withdrawalLimit)
}
return ba.bankAccount.Withdraw(value)
}
func (ba *savingsAccount) SetWithdrawalLimit(newLimit float64) error {
if newLimit < 0 {
return fmt.Errorf("invalid withdrawal limit: %f", newLimit)
}
ba.withdrawalLimit = newLimit
return nil
}
// TestSavingsAccount tests SavingsAccount
func TestSavingsAccount(t *testing.T) {
account, err := NewSavingsAccount("mysavings")
require.NoError(t, err)
require.NotNil(t, account)
b1, err := account.Balance()
require.NoError(t, err)
require.Equal(t, 0.0, b1)
require.NoError(t, account.Deposit(12.0))
b2, err := account.Balance()
require.NoError(t, err)
require.Equal(t, 12.0, b2)
account.SetWithdrawalLimit(4.0)
require.Error(t, account.Withdraw(5.0))
require.NoError(t, account.Withdraw(4.0))
b3, err := account.Balance()
require.NoError(t, err)
require.Equal(t, 8.0, b3)
}

Notice how we reuse bankAccount functionality and data embedding it in savingsAccount overwriting only the Withdraw method.

Polymorphism

Poly is Greek for many, and Morph means form. Polymorphism is the feature of Object-Oriented Programming languages that allows objects of different types to be treated similarly.

From the example above we can easily treat a savings account as a basic bank account:

package main
import (
"errors"
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
// TestPolymorphism tests treating a savings account like a basic account
func TestPolymorphism(t *testing.T) {
savingsAccount, err := NewSavingsAccount("mysavings")
require.NoError(t, err)
require.NotNil(t, savingsAccount)
savingsAccount.SetWithdrawalLimit(4.0)
// Treat the savings account as a basic bank account
basicAccount := BankAccount(savingsAccount)
b1, err := basicAccount.Balance()
require.NoError(t, err)
require.Equal(t, 0.0, b1)
require.NoError(t, basicAccount.Deposit(12.0))
b2, err := basicAccount.Balance()
require.NoError(t, err)
require.Equal(t, 12.0, b2)
require.Error(t, basicAccount.Withdraw(5.0))
require.NoError(t, basicAccount.Withdraw(4.0))
b3, err := basicAccount.Balance()
require.NoError(t, err)
require.Equal(t, 8.0, b3)
}
// BankAccount represents a bank account
type BankAccount interface {
// Balance returns the current balance of the account
Balance() (float64, error)
// Deposit deposits money onto the account
Deposit(value float64) error
// Withdraw widthdraws money from the account
Withdraw(value float64) error
}
// SavingsAccount represents a savings bank account
type SavingsAccount interface {
// Embedded public interface
BankAccount
// SetWithdrawalLimit sets a new withdrawal limit
SetWithdrawalLimit(newLimit float64) error
}
/*************************************************\
BASIC BANK ACCOUNT
\*************************************************/
// NewBankAccount creates a new bank account instance
func NewBankAccount(id string) (BankAccount, error) {
return newBankAccount(id)
}
func newBankAccount(id string) (*bankAccount, error) {
if len(id) < 1 {
return nil, errors.New("invalid bank account id")
}
return &bankAccount{
id:      id,
blocked: false,
balance: 0,
}, nil
}
type bankAccount struct {
// Private fields
id      string
blocked bool
balance float64
}
func (ba *bankAccount) checkBlocked() error {
if ba.blocked {
return fmt.Errorf("account %s is blocked", ba.id)
}
return nil
}
func (ba *bankAccount) Balance() (float64, error) {
if err := ba.checkBlocked(); err != nil {
return 0, err
}
return ba.balance, nil
}
func (ba *bankAccount) Deposit(value float64) error {
if err := ba.checkBlocked(); err != nil {
return err
}
if value > 0 {
ba.balance += value
return nil
}
return fmt.Errorf("invalid deposit value: %f", value)
}
func (ba *bankAccount) Withdraw(value float64) error {
if err := ba.checkBlocked(); err != nil {
return err
}
if ba.balance-value < 0 {
return errors.New("insufficient funds")
}
ba.balance -= value
return nil
}
// TestBankAccount tests BankAccount
func TestBankAccount(t *testing.T) {
account, err := NewBankAccount("ABC")
require.NoError(t, err)
require.NotNil(t, account)
b1, err := account.Balance()
require.NoError(t, err)
require.Equal(t, 0.0, b1)
require.NoError(t, account.Deposit(4.5))
b2, err := account.Balance()
require.NoError(t, err)
require.Equal(t, 4.5, b2)
require.NoError(t, account.Withdraw(2.5))
b3, err := account.Balance()
require.NoError(t, err)
require.Equal(t, 2.0, b3)
}
/*************************************************\
SAVINGS BANK ACCOUNT
- composes a basic account
- overwrites method Withdraw
\*************************************************/
// NewSavingsAccount creates a new savings bank account instance
func NewSavingsAccount(id string) (SavingsAccount, error) {
return newSavingsAccount(id)
}
func newSavingsAccount(id string) (*savingsAccount, error) {
newBankAccount, err := newBankAccount(id)
if err != nil {
return nil, err
}
return &savingsAccount{
bankAccount:     newBankAccount,
withdrawalLimit: 0.0,
}, nil
}
type savingsAccount struct {
// Embedded private struct
*bankAccount
// Private fields
withdrawalLimit float64
}
func (ba *savingsAccount) Withdraw(value float64) error {
if value > ba.withdrawalLimit {
return fmt.Errorf("withdrawal limit (%f) exceeded", ba.withdrawalLimit)
}
return ba.bankAccount.Withdraw(value)
}
func (ba *savingsAccount) SetWithdrawalLimit(newLimit float64) error {
if newLimit < 0 {
return fmt.Errorf("invalid withdrawal limit: %f", newLimit)
}
ba.withdrawalLimit = newLimit
return nil
}
// TestSavingsAccount tests SavingsAccount
func TestSavingsAccount(t *testing.T) {
account, err := NewSavingsAccount("mysavings")
require.NoError(t, err)
require.NotNil(t, account)
b1, err := account.Balance()
require.NoError(t, err)
require.Equal(t, 0.0, b1)
require.NoError(t, account.Deposit(12.0))
b2, err := account.Balance()
require.NoError(t, err)
require.Equal(t, 12.0, b2)
account.SetWithdrawalLimit(4.0)
require.Error(t, account.Withdraw(5.0))
require.NoError(t, account.Withdraw(4.0))
b3, err := account.Balance()
require.NoError(t, err)
require.Equal(t, 8.0, b3)
}

We could even implement different interface types that only allow reading the balance of any kind of bank account or those that allow managing it etc.

Conclusion

Go is not an OOP language, yet “OOP” is a very vague term. It really depends on how you define “Object-oriented programming”. Go supports:

  • encapsulation through structs
  • abstraction through interfaces
  • composition as a replacement for inheritance
  • and polymorphism through interfaces.

The concept of OOP classes would only make the language more complex for almost no good reason.

At the end of the day I like Go’s approach to OOP, even if you don’t want agree that it is there.

Source: medium