Composition Over Inheritance in Go
Go does not use classical object-oriented inheritance and instead favors composition. Complex types are built by combining smaller structs rather than deriving from base classes.
Defining and Using Structs
Structs are defined with the type and struct keywords to model structured data. Instances are created by assigning values to fields, representing real-world entities such as users in a banking system.
Composing Nested Structures
Larger models are created by embedding structs within other structs, such as accounts within users and transactions within accounts. This demonstrates how composition builds layered data relationships.
Adding Methods to Structs
Methods can be attached to structs using receiver functions, including pointer receivers for state modification. Business logic, such as withdrawing funds and handling errors, is implemented through these methods.
Interfaces and Duck Typing
Go uses implicit interface implementation, meaning a type satisfies an interface by implementing its methods. This enables flexible, behavior-based design without explicit declarations.
Practical Use of Interfaces
Interfaces allow functions to operate on any type that implements required behavior, such as a withdraw operation. They should be introduced when shared behavior emerges, not preemptively, and are widely used in Go’s standard library.
If you have ever programmed in other languages like Python, Ruby, PHP,
you'll be familiar with the concept of inheritance and object-oriented programming.
Go is not an object-oriented language,
as the creators have chosen to favor what is known as composition instead.
A classical way of explaining object-oriented languages and inheritance
is to think of a blueprint for, say, a car.
You have a base model that all other cars will inherit from.
But doing it this way can and often will lead to some cars having attributes
and methods that does not really belong on that specific car.
What Go does instead is to define structs that will represent elements of a car
and when combined together becomes a car or represents a car.
We don't really need to discuss,
the finer details of object-oriented versus composition.
Both of them have their place.
Just remember that in Go, you need to visualize structures.
But how do we create a structure?
Well, let's begin by creating a user struct here.
So we have the type keyword, then we have the name of the struct,
and then we have the struct keyword.
And let's say we are creating a bank and we need to represent a user in the bank.
This way, so we'll have an ID, that's an int64.
Then we have a name, that's a string.
And then finally, we'll have the address of the user.
And the way we can create a user from this struct is we go into our function here.
It's a user and it's a user struct.
So the ID is just going to be one.
Let's say the name is going to be Alice.
And then finally, the address.
It's going to be 123 Main Street.
Our users would also have an account in this bank, right?
So to illustrate that, we could create another struct here.
Let's just call it account.
That has an ID.
And let's just say a balance.
And then we could go ahead and add this struct to our user struct.
So now our users also have...
an account associated with them.
An account will also typically hold transactions, right?
So we can create another struct here.
Let's say type transactions.
And the transaction will again have an ID of int64.
And let's just say we have a field here called created at.
That's a time dot time.
And then finally, let's just say amount.
Again, an int64.
And then we could update our account struct to have all the transactions made on that account.
So let's say transactions, which is a slice of transaction.
There we go.
So this is composition in play, right?
We create structures and then we combine them together to create larger structures.
But we can also add methods to these structs.
Say we wanted to withdraw money from our account.
We will go down here and we will say func and then a pointer to the struct.
And then we will say, let's just call this withdraw amount int64.
And then let's just return an error.
And then we could see if a balance minus amount is less than zero.
We would say return errors new insufficient funds.
And if not, then we would just say a balance.
And then we subtract the amount and then finally we just return.
Now, we would also need to record the transaction and we could do so by adding another method to the transaction struct and then update.
So we can see that those transactions associated with the account.
Now, the method we just added, withdraw, can be used to describe a struct.
And what I mean by this is that in Go we have what is known as duck typing.
And that's this little quote that says, if it walks like a duck and quacks like a duck, then it must be a duck.
So we could define this in an interface.
So say we have an interface.
Here we have a duck.
Here we have a duck.
here called "Withdrawer Interface" and follows a similar pattern to structs.
We have the type keyword and the name and then the interface keyword.
And in here we would say "Withdraw Amount 64" and then "Error".
Now, anything that implements that interface can be thought of as a "Withdrawer".
And this becomes very powerful when we have, say, different accounts that can send, let's say, money.
So if we were to go down here, we could have a function that's called "SendMoney".
That would have an amount argument and then, let's say,
receiving and then a receiving account.
And then finally, we would have a sending account that implements
with
"Withdrawer". There we go.
And then let's just return an "Error".
Return nil.
So I'm not going to fill out the logic here, but
this simply means that this function accepts anything that implements this
"Withdrawer Interface" right here.
This is very powerful.
And when you're just learning about interfaces, it can be very tempting to use them all the time.
But as a rule of thumb, these interfaces should be discovered, not created.
So once you start seeing the same type
of behavior being used across different structs,
it can be beneficial to abstract those into an interface.
Go comes with a comprehensive standard
library, which we will also touch upon soon.
But if you want a good example of how we can utilize interfaces
to accept
more generic behavior, you could look up the I/O Writer interface,
which does a really good job at describing a simple interface that is then