 The way we define structs and go is just like how we define them in GoPigeon, except with slightly different syntax. We start with the reserved word type, then the name of the struct we're defining, then you put the word struct after, and then curly braces, and then each line is the field. This first field is called name, and it's a string. The second is called age, and it's an int, and the third is called weight, and it's a float32. And so here's how we work with structs down here in main. We're creating a variable x of type cat, and we want to assign to x a cat value, and the way you create cat values is with the syntax. We have the struct name followed by curly braces, and then you put the values for that struct in the curly braces, separated by commas, and you do so in the order as they're defined in the struct. So we have the string for name first, int age second, and then float32 weight. So we have Oscar, 14, that's the age, and 15.2, that's the weight. We can leave the curly braces empty, in which case the fields all default to their default value, what Go calls the zero value for a variable, its default value is called the zero value. And for a string that's going to be an empty string, for an int that's going to be zero, and for float32 that's going to be zero. And also what we can do in the curly braces is we can specify the fields and their values by name, in which case we don't have to write them in the same order. So here we're specifying the age field has the value 14 and the name field is the value Oscar. And we didn't specify a weight field in which case it'll default to zero. So when you use this syntax with the named fields, you don't have to provide every value, whereas in this form where you don't name the fields, you have to provide every value in order. And lastly in this example, same deal except we're specifying the weight explicitly and saying it's 15.2. And here we're creating another cat variable we call y and we're signing the value of x to y. And when you assign one struct to another, what you're really doing is you're copying all the respective fields from one to the other. So now x and y all of the fields now match. That's really what an assignment always does you have some value and you're copying that value to the target variable. And also we can use an equality test on two structs. So this is testing if x and y are equal. So it's going through all the respective fields and comparing them. So tests if name of x is equal to name of y, if age of x is equal to age of y, if weight of x is equal to weight of y. And if all of those are equal and only if all of them are equal, is this condition true? Otherwise, if any one of them is different, then it's false. In this case, we know they're true because we just assigned x to y. So this is going to be printed out. It's going to say that x and y are equal. And the way we access the fields of our structs is with the dot operator, which is confusing because as we saw earlier, dot is also used to access the elements of imported packages and also to access methods when we call them. This is a third use of dot. And so here we're taking the struct x and we're accessing its name. And that is then being assigned to this new variable s of type string. We're then printing it out. And so we're going to see Oscar print it out. And to set the field of a struct, we use the same dot syntax as the target of assignment. So this is assigning the string snowball six to the name field of x. And this is assigning 4.3 to the weight field of x. So we don't have getting set instead we just have dot and we use for setting, we use the dot expression as a target of an assignment. Here in this main function, we're creating at the top a variable a, which is an array of four integers. And you denote arrays by preceding the base type. The thing which is this is an array of, it's an array of ints in this case. You proceed it with square brackets and you put the size of the array inside. So this is how you do an array type. Whereas to create a slice down here, we're creating a slice of ints and you don't put any number inside. You put nothing inside and that makes it a slice. And understand when you create arrays, when you specify array types, you have to have a constant expression in here, meaning it has to be a number constant or some expression like you can actually have like four plus two, something like that. This is an expression which the compiler can figure out at compile time. It doesn't have to use any of your variables or your functions. It can be fully figured out at compile time. Because the compiler needs to know at compile time how big every array is. Anyway, on the next line, we're signing to a an array of four ints with these values, seven, two, four, five. And when you sign to an array, you're, it's expecting you to provide an array. And so it's like we're copying this array to a. And now a will have all of these same values that will have the value seven, two, four, and five in that order. If you leave the curly braces empty, then all four values default to their zero value, which for an int would be the value zero, of course. And if you specify a number of values, which is less than the size of the array, then the remaining values default to their zero value. So here this would be an array of the value seven, two, and then zero and zero. For a slice, the notation is almost exactly the same, except we don't have a number inside the square brackets. I'd be very clear about what exactly this is really doing. This expression, the compiler considers this to be an in slice expression. It has an in slice value, but it also is implicitly creating an array with these four values in this order. And the slice value here is referencing that array. And that slice will have a length of four and a capacity of four, or actually might have a capacity of something larger, but it'll be at least four. And also be clear, you can put as many values as you want in here, whatever you want, right? Or you can leave it empty, but what that means is that the array being created will then be that size. So like here now with the curly braces empty, the array referenced by the slice will have no values, it'll be empty. On the next line, a common trick is that we want to get a slice referencing the entirety of some existing array. So we have our int array of size four called a and we want to get a slice of it ranging from index zero up to the whole length of the array. And so we use the slice operator on a and we specify that we're starting at index zero and we were going up to but not including index four. And this effectively is giving us back a slice representing that whole array and it'll have a length of four and a capacity of four or greater. But as shorthand, when we use the slice operator before the colon, if you leave that blank, it defaults to zero. And after the colon, if you leave that blank, it defaults to the length of the slice or array which we're using the slice operator on. So in this case, we're using it on this array a and so implicitly after the colon, it's Len a. Lastly here, we're using the make operator or they don't call them operators. They call them built in functions because when you call them, they look like function calls, but they're not really functions being called. They're really just built in operations. Anyway, so this make operation, you specify the type of thing you want to create. In this case, we want to create a slice events. And so this make operation will return the slice events. And when you create a slice events, the second argument is specifying what the length is. And the third argument is specifying what the capacity of that slice is going to be. So this is creating a new array of ints of size 40 and giving us back a slice events referencing the start of that array with a length of 20. So it's giving us back a slice, which is referencing only the first half of this newly created array of 40 ints. Lastly down here, we're creating a variable C, which is an array of five ints. And if we then try and assign C to a, we get a compile error because they're different sizes. The really central thing to keep in mind about arrays is that the size of the arrays considered integral to the type, such that say an array of four ints is not the same thing as an array of five ints. You can't substitute one for the other. You can't assign an array of five ints to an array of four ints or vice versa. They're just different things as far as the compiler is concerned. So we can't use assignment to copy values from one array to another if they're different sizes. We do, though, have actually the copy operator or built-in function as they call it. And it's actually defined to copy values from one slice to another. So if I have two slices, well, we do have slices here. So we've got a slice of everything in A and B. And the first one is the destination, and the second argument is the source. So this is going to copy everything from B to A. And right now, B is defined to be a slice of 20 values, 20 ints. And A, if we got a slice of the whole of A, well, A is only four ints in size. And the number of elements copied is the length of the smaller slice. So in this case, A is smaller. It's four. The first four elements of B are going to be copied to the four elements of A, in this case. So using this operator, if I wanted to copy elements from one array to another if they're different sizes, instead of using assignment, I would use copy. And let's see, I want to copy, let's say, everything from A to C. So C is our destination. And let's just get a slice of it, and we'll get a slice of A as well. We can't use C and A directly. Copy is expecting slices. So we have to get slices of our arrays, but that's easily done enough. And here, because A is smaller, A is a length of four, only four elements are going to be copied instead of five. So we're effectively copying all four elements of A to the first four elements of C, leaving the fifth element of C, the last element of C untouched by this copy operation. Something we can do and go, which we couldn't do and go pigeon, is create what are called named types. Like here, if I create a type foo, which is defined to be an array of three ints. Well, when I create a variable of type foo, what it really is is just an array of three ints. Likewise, here I'm creating type bar, which is really just a boolean. And here I'm creating a type act, which is a slice of slice of booleans. There would be the same thing in terms of what the data are. A foo would be the same thing as an array of three ints. But they're not really aliases because the compiler would not consider them to be the same type. If you recall back up here, the built-in base types of the language, we have these integer types. And we have byte, which is just an alias of uint8. And we have rune, which is just an alias of int32. And those are entirely equivalent. If you use the name rune in your code instead of int32, it's just a stylistic thing the compiler considers them to be exactly the same. Whereas when you create named types, like foo, which is an array of three ints. Yes, every foo you create is an array of three ints. But the compiler doesn't consider them to be the same thing. It wouldn't accept values of type foo to be arrays of three ints or vice versa. So if I have a variable of type foo, I couldn't assign to an array of three ints or vice versa. If I had a variable which is an array of three ints, I couldn't assign to it a value of type foo. Even though in terms of what they are in terms of data are exactly the same thing. Having to find this type bar, which is a boolean, what can we do with it? Well, looking here, I'm creating variables of type bar x, y, and z. And I'm assigning, these are boolean values, yes, but they're considered constants. These things actually don't really have type. They're not specifically the booleans, they're just values which are valid boolean values. I can equally assign them to a named type which is defined to be a boolean, because given this type bar which is defined to be a boolean, I can do all the same operations and things with this type that I can do with a boolean. It's just not considered to be the same thing as a boolean. So down here, if I create a variable b, first off, very simply, now I have x, y, and z which are bars and b which is a bool, I can't do this. This is a compiler because b and x are not considered to be the same thing. If I want to assign b to x, I would have to cast it, which is what we call a conversion operation. So you just use the name of the type you want to cast to, and then inside you specify the value you're casting. And this will take the boolean value b, and you get back a bar value which is, let's see, the boolean is currently false, and it's going to get us a bar value which is false. So it's the same value. The data is not changing in any way with this kind of conversion, but we're making the compiler happy because the compiler is expecting, for values assigned to x, it's expecting a bar value. b itself is not a bar value. We would have to get the bar equivalent of b using the syntax. And be clear, the compiler allows this kind of conversion because it knows that bars and booleans are really the same thing. You can't just cast values of one type into any other type necessarily. It depends on what type you're casting from and what you're casting to. Like say, the compiler, if I had an infable here, I couldn't just take my bar value and arbitrarily cast it to an end because what even would that be? The compiler doesn't know what to do there, so it just allows that. So that's an error. Anyway, so let me get rid of that. And so here on the next line, we have our bar variable z, and we're assigning to it the end of x and y. This is an end operation on x and y. And because x and y, they're bar values, but a bar is known to be a boolean by the compiler. So it knows you can perform the end operation on these things just like you can on booleans. But what we get back from this operation, because both operands are bar values, we get back a bar value, not a boolean. And so this is an acceptable assignment because z is expecting a boolean. Whereas here, this is a compile error because b was a boolean variable. This expression is giving us back a bar. So we're getting back the wrong type. If we want to take the result of this operation and assign it to b, then we need to cast. We need to take the result of this operation and cast it into boolean and now it's an acceptable assignment. And then down here, we're trying to end b and x, but this is a compile error because and is expecting operands of the same type. Yes, they're both ultimately booleans in terms of data, but the compiler considers them to be different types. Because we want to assign to z here, we want the operation to return a bar, so we're going to take b and make it a bar. And now the operation, the sand operation, both of its inputs are bar values and it's going to return a bar value, which we assigned to z. So you're surely wondering what's the point of having these name types. They seem a little silly. They're not even really aliases of existing types. They're not just syntactical conveniences. They're creating types that are considered distinct by the compiler. What's the point of that? Well, here's an example of where that conceivable might be useful. We're creating a type weight, which is really just a float 64. And this can be useful because in our code, you know, float 64 is they're just numbers. They could be used to represent all kinds of things, units of measurement, mass, distance, money, all sorts of things, right? And generally, you wouldn't want to accidentally mix those things together. So having created this name type weight, we could have all of our weight values kept distinct from other float 64 values in our code, such that here, if we create a variable f of type float 64 and a variable w of type weight, well, first off, notice that number constants, again, have no particular type. And so the compiler here accepts this assignment because it says, OK, a weight is really just a float 64. And the value 64.72, that number constant is a valid value for float 64. So it accepts this assignment. But here, when we're trying to assign w to f or f to w, those are compile errors because f and w are different types. They're really float 64s behind the scenes, but the compiler considers them to be different things. And so if I really want to assign w to f or f to w, I would need to convert. And so this is taking my w value, getting that same value as a float 64, and this is taking my float value f and getting its equivalent as weight. And there's no distortion in the value. It's still the same exact value because they're both really just float 64s. But this is satisfying the compiler and now it accepts this assignment because w here needs to have a weight value as the value assigned to it. And then say we have this function foo, which is expecting a weight as input. Well, you can call foo and pass in w, and that's good. But if we try and pass in f or any other float 64 or anything that's not a weight, then we get an error. And this conceivably will help us catch our own mistakes where we have a bunch of different float 64 values and some of them are weights, some of them are different kinds of things. And we don't want to accidentally use just any old number when we expect specifically a weight. And so yeah, we can trivially get around this by instead of passing f itself, we convert it to a weight and then the compiler is okay with this call. But at least now we have to explicitly acknowledge that we really know what we're doing. It's much less likely that you're going to accidentally take your random float value and pass it as a weight if you have to explicitly say that you're passing a weight. So it's really just sort of a guards rail mechanism to make sure you're using values as you intend to. And lastly down here, if we take w and add 4 to and assign the result to w, again, this is a shorthand for w equals w plus 4. Well, the rule as I said with arithmetic is that if we do an arithmetic operation with a typed value and a constant, the result is going to have that same type. So because w here is a weight value, the result of the addition is also a weight value which is valid to assign to w. So that's all good. And again, I'll just put it back to the more convenient syntax. Same thing, just different syntax. And here say if we had some float value that we wanted to add to a number and then assign to w, you could either just convert f to weight first and then add that to 4, or you can add f and 4 together first and then convert the result of that to weight. I'd probably do this first form here, but it's just a stylistic thing. You get exactly the same result in the end here. These types weight and float 64 again are in terms of data exactly the same thing. And we have to use these conversions to tell the compiler that, yeah, yeah, I know what I'm doing here.