Check out a free preview of the full Build Go Apps That Scale on AWS course
The "Structs & Receivers" Lesson is part of the full, Build Go Apps That Scale on AWS course featured in this preview video. Here's what you'd learn in this lesson:
Melkey introduces structs, which are class-like data types in Go. Functions can be added to structs by providing a receiver data type, allowing developers to create more complicated data types through composition. Passing structs as copies versus references is also demonstrated in this lesson.
Transcript from the "Structs & Receivers" Lesson
[00:00:00]
>> I'm gonna go ahead and remove the delete, just for now, because now I wanna go over looping. And in Go, prior to Go 1.22, there's almost two ways, there's still two ways to loop something, but there's always this question like, which way am I gonna use to run my loops now?
[00:00:16]
The first way is more of the traditional sense of a loop, so you can declare i:=0. Let's say the conditional is, while i is less than the length of our animal slice, and then we increment i, okay? And here we wanna just go over all the elements in our slice.
[00:00:34]
We can do fmt.Printf. I'm gonna say, this is my animal, cool. And then we do animals at the ith index, all right? I'm gonna go ahead and move this print line animals at the end. To run this, No one called me out again. We need like a counter, every time I forget this break line.
[00:00:56]
We can go again and you can see this is my animal dog, this is my animal, cat, this is my animal moose, okay? So this is like a traditional sense, if you come from like JavaScript or whatever, you probably recognize the syntax to a degree. You can do the same thing with an end, right?
[00:01:11]
So for i:=0, let's say, while i < 10, or something, and we increment i, we'll go over we could do format just print line, cuz we don't need to do any kind of formatting or concatenation here. And we run it. You can see here 0 to 9. Cool, simple, easy stuff.
[00:01:33]
But then, what am I even talking about? What is this other way that somehow was introduced recently in Go 1.22. And that's the built-in range function. So, I'm gonna go ahead and delete these four loops, okay? And in Go, you could also iterate through a slice using the built-in range key.
[00:01:50]
So you can do for index, value, range, animals, and you can just do fmt.Printf. This is my index, And this is my animal. All right, and then we should put the index value that we get as a return value and end value here. And if you run this, caught it, I was about to do it again.
[00:02:21]
Here you go. If you run it, you can see here, this is my index 0, 1, 2.cat, moose, okay? So what's the contention? The contention is, prior to Go 1.22, you could not use range with an int, okay? So you can not iterate through an integer with the range function, you'd have to do it with the previous method.
[00:02:38]
But now we can do for value, range 10, and you can just do fmt.Printline value, right? And if you run this, boom, 0 tonight. Super easy stuff, I personally like the range function, I think it's like very like go ask. It's something about like, I'm only gonna see that in Go, which I kind of adopt to the more things that Go offers.
[00:03:04]
Again, there's some gotchas here and there, but they're not too bad. And the last thing about loops is a while loop. How do we make while loops? There's no while keyword, the only looping keyword in Go is for. So if you really wanna break your computer right now, run this.
[00:03:20]
It's gonna be a loop, right? You can print something it doesn't you're not gonna break out of it, right? But what you can do instead is, like a traditional for loop, you can do something like declare i:=0, and in for, this is the condition while i < 5, we can do a fmt.Println(i), and then we can increment i, okay?
[00:03:46]
Okay, and let's go ahead and remove this portion here, cuz we're gonna get confused, 1, 2, 3, 4. So that's while loops in Go, okay? It's just using the for keyword as a conditional, but essentially, it's the same thing, yes.
>> I had a question about the append.
[00:04:01]
>> Yes.
>> So, it returns a new array that you reassign to animals, so it's a pure function.
>> Depends, it actually conditionally does, either returns a new array or it just appends to it. So you can see here, append elements to the end of a slice, if it has sufficient capacity, it doesn't, but it will re-accommodate and return new slice.
[00:04:27]
So it's very conditional on how Go does it. So sometimes it is like a brand, you do have to redeclare the slice with the append method, but sometimes you get returned a new slice all together.
>> Okay, so you can't just say append slice?.
>> No, and it won't change in place, you have to re-declare the slice that you'd statuated before.
[00:04:46]
>> Cool.
>> What happens if you use an out of range index, as in the previous traditional loop?.
>> If you use an out of range index.
>> Like, if you went to five, but your array is only three.
>> You'll have a runtime panic, because that's out of range.
[00:05:08]
You won't even be able to compile it, actually. If you go back to, like the delete function, for example, Go knows what you can iterate over in the capacity of things. So let's say we go back to our animal slice, and we do a delete, or sorry, slices.delete, and we put in the animals array, this has how many values?
[00:05:34]
3, and we go 3, 4. This was a, let's see, no m was never used. Sorry, let's go here, it should give us an error. Yeah, runtime error, slice bounds out of range. So we'll get these out-of-range panics and these nils depending on kinda how you write the code.
[00:06:01]
So that's something to look out for. Great question.
>> If you append animals and you created a new variable, would animals remain unchanged?
>> Yeah, because you're passing a copy of the original animal slice. We're gonna get into that more in detail right now. Great question. Okay, so we did loops, variables, I wanna go into, yeah, I'm gonna do this, I'm gonna go into structs.
[00:06:39]
This is where, I think, it gets a little bit more exciting. So we're gonna do package main, func.main, okay, here. So structs, what are they? The bare bone definition of a struct is that it's a type that holds data and passes that data along around your application. That is kind of the textbook definition of a struct in a purist form.
[00:07:04]
But the way you can use structs with other concepts in Golang at interface, it is kind of Go's answer to object-oriented programming, okay? So, Go does not have the contents of classes, and Go choose a composition over inheritance. And I'm not saying this is innately better, there's pros and cons, right?
[00:07:28]
If you come from a class-based language like Java, Python, PHP, you write great code and it has inherited classes, right? But there is pros and cons, and the Go development team chose composition versus inheritance, right? And structs play a vital role of how they achieved that. But I wanna showcase first handling data field portion before we move on to that composition example.
[00:07:57]
So, to declare a struct in Go, use the type keyword, and instead we're gonna create a person struct. So person struct, all right? And then, every struct can contain fields. And these fields are like properties, but they hold data essentially, right? Let's say we want a person to have a name, string age int, okay?
[00:08:20]
And this is fine, Go will not yell at you if you have an unused drug type. This is just a type definition, all right? So the compiler is with this. And let's say we wanna create a new person, we'll just say, myPerson is going to be, type Person, and then we're gonna populate the field.
[00:08:37]
So name is gonna be Melky, string Melky, right? And you can notice one thing, is the compiler is yelling at us for person is declared not used, which is fine. We'll just print it. But it's not yelling at us for the age. And why is that? Because it goes back to the zero value, right?
[00:08:58]
So if you don't populate the field, they go back to the zero values. Here's a key example of why they have, one of the reasons why they had that type zero declaration fallback. All right, but let's say for our case, I put in my age. Okay, 26, and then here.
[00:09:13]
So now I'll do a fmt.Println, and it'll put myPerson. And we can run this, Go run structs/main.go. Boom, Melkey, 26. I find this to be a decent print statement, but if I look at this, I'm like, what is this? So, personally, what you can use is the format package.
[00:09:40]
Fmt.Printf, and you can use this is myPerson, and you can do %+v, and put myPerson, okay? And now if you run it, you see this is my person {Name:melkey, Age:26}. The v format specifier is basically for interfaces, it can accept any type. I'm gonna go into interface in a lot more detail, but it basically can accept any type that you want.
[00:10:07]
And then the plus in front of v shows the actual name of the field. So it's a little bit more explicit into what a struct is, okay? Cool, and this is some pretty powerful stuff, right? We have a person we can do, we can print our person, we can change the name of the person, so we have access to the fields.
[00:10:24]
So myPerson.Name, let's change that to Marc, right? And now if we reprint our person, we have {Name:Marc, Age:26}. All right, so we can change these values in case you have access to these properties. Cool, there's a bunch of different use cases of why this would be necessary. But if you wanna change the name, it's a little cumbersome to just do it in place.
[00:10:46]
So let's create a function for it. So the first function that we do that's action be not just main, so func, we're gonna do a change name. That's the name of the function, is gonna two arguments. One is gonna be the name, for actually, you know what? Sorry, before that, I'm gonna create a new function, and it should be newPerson.
[00:11:02]
Because what I like to do, if I know if I'm gonna have a struct, I can initialize the court all the time. I want a function that does the work, rather than writing this my person, nothing innately wrong with it, I just liked the structure of function, okay?
[00:11:14]
So we're gonna do newPerson, and this is gonna take two arguments. The first one, let's say is the name, which is string, and then age, which is int, and this is gonna return a person type. Cuz remember, a struct is just a data type in Go. So when we return variables and values in Go, we have to specify the type.
[00:11:35]
here we're gonna return the person type. And in here we're gonna do return person, all right, and we're gonna populate the name field, where the name parameter get passed into our function and an age as age. And here we can just delete this. And can do myPerson is just NewPerson, NewPerson and person, those arguments, melkey and then 26, and everything works as is.
[00:12:00]
So if we run this again, exact same print statement, there's a little helper function for us, okay? Next, like I said, I wanna keep doing person.name to change the name, let's create a little function to make it a little easier, okay? Func.changeName, this is going to take two things.
[00:12:19]
One is the actual person type, cuz we want to modify our person. So we're gonna pass in our person. So person is gonna be person type, and then new name is gonna be of string type. This isn't gonna return anything, cuz we've changed the property. We don't need a return value here, we're not reassigning anything just yet.
[00:12:36]
And we're just gonna say, person.Name is newName, cool. The compiler is not happy because it's unused. So let's go ahead and use this. Let's change myperson.Name. We're gona do changeName, I'm gonna pass in myPerson and the new name of Marc. So now, if you run this, now we should build a helper function, and so we'll see the name is, it still says, melkey, okay, my bad.
[00:13:05]
Let's just check this out. If I debug this, fmt.Println, person.Name, and we'll do a nameBefore, and I'm passing in something weird. Let's do this, we have after, let's go here and just do something like that. So let's just see what's going on behind the scenes. Name before milking after Marc, but it's not persistent thing.
[00:13:36]
This is, I'm playing a bit, I'm a really bad actor, I'm sorry. This is on purpose, okay? Because Go has this concept of passing in values as copies or references, okay? And I thought the best way to show this is with an example rather than me try to explain it, and we're gonna go into how this all works under the hood.
[00:13:56]
But essentially, we're passing a copy of our person type here, okay? So myPerson, we declared it here, and initialize it here, when we call this changeName, and we expect this person argument for passing a copy of our person, okay? And so when we change the name, we're changing only the name of the copy, but the original reference, the original allocated memory is still unchanged.
[00:14:25]
It's actually hasn't even been touched since we declared it. Okay, so this is a core concept in Go when it comes to passing values as copies and passing values by reference. So, how can we change this? How can we actually change the property of name? I wanna be Marc, or I wanna change from Melkey to Mark, so what do I do?
[00:14:46]
Well, let's first clean a few things up here. Let's instead of passing in a person, that's a little cumbersome, right? What if our struct's really big and we keep passing it in? That's just, to me, it doesn't make sense. So instead, let's do what's called a receiver in Go.
[00:15:03]
And this is gonna be kind of what I mentioned about that more object-oriented solution to Go. This is called a receiver. And the textbook definition, is a method implemented on a type. That is, I believe, the direct quote for a receiver. And what this does is now changeName can only be called on persontype, because you can see here on line eight, it's yelling at me.
[00:15:34]
Undefined changeName, doesn't know what it is anymore. So how do we call it? MyPerson.changeName. Nice, this is really powerful stuff, right? This is how we have that ability of composition versus inheritance. Cuz you can compose your structs with different structs as well, you can build off of these structs with different methods, different properties, and have more granularity to how you define your code.
[00:16:02]
So here, let's go ahead and just remove this and skip Marc. We have to fix a few things up here. So instead of person will do p, and now finally, because we have this receiver, it's going to work. So if you run this code, it's still Melkey. All right, at this point, it's not about how you write your function, it's not about how you choose to pass in, it's still a copy.
[00:16:28]
ChangeName is still only changing the copy of our person's struct. We have to actually pass the real reference where the memory is actually allocated, okay? And how do we do this? Well, we can just change one thing. Let's put an asterix here. Now asterix has dual meaning in Go.
[00:16:50]
The one that we're focused on right now, is this defines that this is an actual reference to the allocated memory of our person struct, our personType, okay? So here, if I now run this code, Name before Melkey, after Marc, it's Marc. It's now persisted, because we've changed the actual allocated memory, right?
[00:17:17]
And if you want to find out in a more systematic way, let's say, you forget the asterisks or you compose your code in a different way, there's actually a really cool feature in Go. So let's remove the asterisks, let's go back a step. I wanna do something. I'm gonna do fmt.Println, what I'm gonna say is, address of copy, and there's going to be &p.Name.
[00:17:42]
Bear with me, okay? I'm gonna remove these print statements here, so we're not gonna see them before an astersk cuz I wanna clean up what we show. And then here, I'm going to call the same thing. So let's grab this print statement over here. Let's jump down, paste it.
[00:17:58]
I'm gonna do address of allocated memory. Instead of p.Name, we'll do myPerson.Name. All right, and I'm gonna just print this and we'll see that address allocated is this address, and the address of the copy is different. So this is the fundamental method of seeing that you are either passing a copy or an actual reference to where that memory exists, okay?
[00:18:31]
So you can see here, this is kind of the proof in the pudding that we're changing the type in our copy, but it doesn't persist to the actual allocated memory.
Learn Straight from the Experts Who Shape the Modern Web
- In-depth Courses
- Industry Leading Experts
- Learning Paths
- Live Interactive Workshops