I have been studying the Go programming language for several weeks now and thought about writing a series of posts to share what I have learned so far. I figured that it will be an excellent way to reinforce my understanding of the language. I initially thought about writing a post that will discuss concurrency in Go but it turned out that I am not yet eloquent enough to talk about basic concurrency patterns with goroutines and channels. I decided to set the draft aside and write about something I am more comfortable with at the moment: basic object-oriented patterns and composition in Go.
One of the best things I like about Go is its terseness. It made me realize that being advanced does not necessarily need to be complex. There are only a few reserved words, and just going through some of the basic data structures will enable you to read and comprehend most Go projects at Github. In fact, Go is not an object oriented language in the purest sense. According to the Golang FAQ:
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).
If Go is not an object-oriented language and everyone is going crazy about Functional Programming in the web development world, then why bother learning OOP patterns in Go? Well, OOP is a widely taught paradigm in CS and IT curricula around the world. If used correctly, I still believe that object-oriented patterns still have its place in modern software development.
Using structs
Go does not have a class similar to a real object-oriented language. However, you can mimic a class by using a struct
and then attaching functions to it. The types defined inside the struct
will act as the member variables, and the functions will serve as the methods:
package main
import "fmt"
type person struct {
name string
age int
}
func (p person) talk() {
fmt.Printf("Hi, my name is %s and I am %d years old.\n", p.name, p.age)
}
func main() {
p1 := person{"John Crisostomo", 25}
p1.talk()
// prints: "Hi, my name is John Crisostomo and I am 25 years old."
}
On our example above, we have declared a type struct
called person with two fields: name and age. In Go, struct
s are just that, a typed collection of fields that are useful for grouping together related data.
After the struct
declaration, we declared a function called talk. The first parenthesis after the keyword func
specifies the receiver of the function. By using pof type person as our receiver, every variable of type person will now have a talkmethod attached to it.
We saw that in action on our main
function where we declared and assigned p1to be of type person and then invoking the talk method.
Overriding methods and method promotion
A struct
is a type, hence, it can be embedded inside another struct
. If the embedded struct
is a receiver of a function, this function gets promoted and can be directly accessed by the outer struct
:
package main
import (
"fmt"
)
type creature struct {}
func (c creature) walk() {
fmt.Println("The creature is walking.")
}
type human struct {
creature
}
func main() {
h := human{
creature{},
}
h.walk()
// prints: "The creature is walking."
}
We can override this function by attaching a similarly named function to our human struct
:
package main
import (
"fmt"
)
type creature struct {}
func (c creature) walk() {
fmt.Println("The creature is walking.")
}
type human struct {
creature
}
func (h human) walk() {
fmt.Println("The human is walking.")
}
func main() {
h := human{
creature{},
}
h.walk()
// prints: "The human is walking."
h.creature.walk()
// prints: "The creature is walking."
}
As we can see on our contrived example, the promoted method can easily be overridden, and the overridden function of the embedded struct
is still accessible.
Interfaces and Polymorphism
Interfaces in Go are used to define a type's behavior. It is a collection of methods that a particular type can do. Here's the simplest explanation I can muster: if a struct
has all of the methods in an interface
, then it can be said that the struct
is implementing that interface
. This is a concept that can be easily grasped through code, so let us make use of our previous example to demonstrate this:
package main
import (
"fmt"
)
type lifeForm interface {
walk()
}
type creature struct {}
func (c creature) walk() {
fmt.Println("The creature is walking.")
}
type human struct {
creature
}
func (h human) walk() {
fmt.Println("The human is walking.")
}
func performAction(lf lifeForm) {
lf.walk()
}
func main() {
c := creature{}
h := human{
creature{},
}
performAction(c)
// prints: "The creature is walking."
performAction(h)
// prints: "The human is walking."
}
In this modified example, we declared an interface
called lifeForm which has a walk method. Just like what we have discussed above, it can be said that both creature and human implements the interface
lifeForm because they both have a walk method attached to them.
We also declared a new function called performAction, which takes a parameter of type lifeForm. Since both c and h implements lifeForm, they can both be passed as an argument to performAction. The correct walk function will invoked accordingly.
Wrap up
There is so much more to object-oriented programming than what we have covered here but I hope it is enough to get you started in implementing class-like behavior with Golang's struct
s and interface
s. On my next post, I will talk about goroutines, channels and some basic concurrency patterns in Go. If there's something you would like to add up to what I have covered here, please feel free to leave a comment.