When I was first learning how to use Go, the idea of Go being a non-OOP language was interesting. I was used to using Python, which was an OOP language, and thus had a class. I thought it was an odd design choice because I couldn't really imagine a world where I couldn't use a class. But after extensive programming in Go, I really enjoyed the Go way of writing code. Go does not have a class, but has structs. Structs can have methods attached to them, just like classes.
What are structs?
Structs are complex data types that hold other types. There are some things that we would like to represent as a group of attributes. For example, say that we have a laptop. A laptop isn't just one thing. Rather, it's a combination of many parts. So we could represent a laptop as a struct, like so.
type laptop struct {
cpu string
ram int
storage int
manufacturer string
}
Let's say that we want to represent a MacBook Air. We can do this:
mba := laptop{"M2", 16, 256, "Apple"}
This is cool because we can group multiple types into logically coherent objects.
How are structs different from classes?
People who have used another programming language, most likely Python or Javascript, may notice that Go does not have a class, only structs. Some might wonder how structs differ from classes. It turns out that they are actually pretty similar! For our purposes, at least.
Both are used to group together different data into a single logically coherent object.
Both have fields that can be private and public. Do keep in mind that there are no
private
andpublic
keywords in Go. To make a field (or a struct itself) public, you just need to capitalize the first letter of its name. For example,Laptop
instead oflaptop
andCpu
instead ofcpu
.Both have access to a set of defined methods. Methods can be private and public as well, using the method (haha) above.
And honestly, that's all we need to know about. We could go into much more depth regarding the similarities and differences, but these three points drive the point home: for our intents and purposes, they basically accomplish the same thing.
What are methods?
Methods are like functions, but they are bound to a certain type or object, which are called receivers. I honestly don't know why they are called methods, and it's probably because engineers are bad at naming things.
Receivers do NOT have to be structs. They could be other types such as int
, string
, or even interface
.
Let's write a method for our laptop
struct defined above. Say that we want to upgrade our storage. We can write a method like this:
func (l laptop) upgradeStorage(size int) {
l.storage += size
}
Seems pretty reasonable, right? It looks exactly the same as a normal function, but it has one key difference. Look at the part between func
and upgradeStorage
. The part (l laptop)
is where we define this method's receiver. An object l
that is of the type laptop
can call upgradeStorage
. We can access l
's fields by using a dot. Here, we are editing its storage by accessing it through l.storage
.
So what happens when we run this?
func main() {
thinkpad := laptop{"i5-1240p", 16, 256, "Lenovo"}
fmt.Println(thinkpad.storage)
thinkpad.upgradeStorage(100)
fmt.Println(thinkpag.storage)
}
256
256
Interesting. The storage didn't change at all! Why would this be the case?
The astute readers may have already noticed, but this has to do with the concept of pass-by-value and pass-by-reference. Pass-by-value means that the method is working on a separate copy of the receiver. Pass-by-reference means that the method is working on a reference to the receiver.
In this context, this means that when we call thinkpad.upgradeStorage()
, Go is actually creating an identical copy of thinkpad
, then applying changes to that copy instead of the original. Why does it do it? It's for safety reasons, mostly. If you didn't want to change a field value in a struct, but have the power to do so, there is a chance that you'd be overwriting an important piece of data. Go only supports pass-by-value by design. When you know you want to change the field value, you must explicitly state it. How? By using pointer receivers.
func (l *laptop) upgradeStorage(size int) {
l.storage += size
}
Do you notice the change? The receiver is now of type *laptop
, which is a pointer to the struct laptop
. This way, the method can access the memory location of l
and actually change the original object. We aren't playing around with copies anymore. Doing it this way has the added benefit of making the code run faster because Go doesn't have to spend time making an identical copy of l
.
Now let's try running the code again.
256
356
Nice.
What about getters and setters?
Short answer: if you need it, do it.
For those of you who may be unfamiliar with the concept, it's common in many other languages to define a getter and a setter method.
A getter method will get the value of a field.
A setter method will set the value of a field.
For instance, if we were to write this in Go, we would do something like this:
func (l *laptop) getCpu() string {
return l.cpu
}
func (l *laptop) setCpu(newCpu string) {
l.cpu = newCpu
}
getCpu
returns the cpu
value of l
, and setCpu
updates l.cpu
to whatever CPU we pass as newCpu
.
Why do this? Well, people wanted to limit access to some fields, and encapsulate them so that people can only access the fields via getters and setters.
Go doesn't provide this out of the box, and if you wish to use one, you must write one yourself. There are many reasons why you would want an accessor like this, but always make sure that you aren't overengineering anything. Sometimes, just accessing the struct field is ok. There's a whole debate on this topic in the Go community, as well, which you can check out if you are curious.
One more thing: if you do choose to write a getter, don't include the word get. For example, instead of getCpu()
, use Cpu()
.
Conclusion
Thank you for tuning in again this week! This is a simple topic, but I realized that I haven't touched on it specifically, and because Go is a rather nuanced language, some people may not be used to the Go way of doing things. Hopefully, this post cleared up some questions you had in mind.
You can also read this post on Medium.
Top comments (7)
Thanks so much. This really make me understand this concept very well and i also learnt the concept of pass-by-value and pass-by-reference. From the article you said the Go make use of pass-by-value, is there a way we can use pass-by-reference to achieve the same result??
func main() {
thinkpad := laptop{"i5-1240p", 16, 256, "Lenovo"}
fmt.Println(thinkpad.storage)
thinkpad.upgradeStorage(100)
fmt.Println(thinkpag.storage)
}
This code wouldn't even compile. "thinkpag" would be undefined. Haha, know it's just a typo but might wanna fix it to avoid confusion. My first thought was pass by value but then I saw that and thought it was an undefined trick and turns out, pass by value. Besides that great article!
Great article. I was not understanding receivers and methods from tour of go and go by example.
Thanks
Thanks. very useful and clear.
wowww fantastic tutorial <3 cleared my doubts
Awesome! very useful.
thank you!