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