Structs, Methods, and Receivers in Go
Tags: go, beginners, tutorial, computerscience
Published: 2023-01-30 14:03:11

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.

```go
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:

```go
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` 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:

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

```go
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.

```go
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:

```go
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](https://medium.com/@jpoly1219/structs-methods-and-receivers-in-go-d3d18b764a19).