DEV Community

Jacob Kim
Jacob Kim

Posted on

Structs, Methods, and Receivers in Go

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
}
Enter fullscreen mode Exit fullscreen mode

Let's say that we want to represent a MacBook Air. We can do this:

mba := laptop{"M2", 16, 256, "Apple"}
Enter fullscreen mode Exit fullscreen mode

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 and public 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 of laptop and Cpu instead of cpu.

  • 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
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode
256
256
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 (2)

Collapse
 
kiniconnet profile image
kiniconnet

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??

Collapse
 
vishalshinde profile image
Vishal Shinde

Great article. I was not understanding receivers and methods from tour of go and go by example.
Thanks