Lesson Complete!
Great job! What would you like to do next?
Full Course
Understanding the basic concepts of variables, values, and types in the Go programming language is essential for building a solid foundation in coding with Go. This lesson covers various primitive types supported by Go, such as booleans, numerics, and strings, as well as collection types like arrays, slices, maps, and structs. Key insights include how to define and initialize variables using both the standard and shorthand assignment operators, the importance of type inference, and the usage of constants for static values. The lesson emphasizes the difference between arrays and slices, with slices offering more flexibility for dynamic data. Additionally, it explains how maps store key-value pairs and introduces structs as composite types conducive to larger data structures. Overall, this foundational knowledge equips learners to effectively navigate the Go programming landscape, setting the stage for more advanced topics like control flow in subsequent lessons.
No links available for this lesson.
Welcome to the first lesson on the Go 101 series of this course, where we're going to briefly cover some of the basic concepts when it comes to the Go syntax and Go programming language.
If you already have some familiarity with the language, then I recommend actually skipping these lessons and moving on to module number one, which is where the course content actually begins. These lessons are mainly about going through some of the very basic understanding of Go in order to set a good baseline to be able to be successful with the rest of the course. However, if you want to stay, then feel free to do so.
In any case, in this lesson we're going to go ahead and talk about variables, values and types. What they are in Go, which ones Go supports and how we can make use of them. If we take a look at the hello world example that we started in the last lesson, you can see we're already defining a value in this code. This is the hello world string that we're presenting or passing into the fmt.Println
function, which when we go ahead and run, causes hello world to be printed to the console.
As I mentioned before, this value is of type string. A string is one of many different types when it comes to Go. If we take a look at the Go language spec, you can see the primitive types that Go supports, or the basic types, are booleans, numerics, strings, arrays, slices, structs, pointers, functions, interfaces, maps and channels. In this lesson, we're going to cover the booleans, numerics and strings, which are the scalar types, although string is sometimes considered a scalar or not. And then also take a look at arrays, slices, structs and maps. Pointers, functions, interfaces and channels we'll take a look at deeper throughout the course, but for the meantime we're going to focus on some of the more simple types.
Variables
Before we take a look at types however, let's first talk about variables. Here you can see we're creating a value of hello world, which is a string type. But we're not actually capturing this value in any way, instead we're just passing it inline to the fmt.Println
function. Many times when it comes to Go however, we want to be able to capture a value to use it at a later stage. To do so, we make use of a variable, which allows us to store values in a named way that we can use later on.
To define a variable in Go, you use the var
keyword, which is short for variable. In our case, let's say we want to create a new variable called name
. So, after the var
keyword, we pass in the name the variable that we want to use. In this case name
, but we could call it anything, such as hello
or age
. In our case, we're going to go ahead and set this to be name
, because this is what we want to say hello to. After defining the variable's name, we then need to give it a type. In this case, let's go ahead and set it to be the string type in order to use it with the hello world string that we're defining below. With that, our variable name
is now defined.
However, if we currently use the variable name
, let's say we pass it in as the second parameter to the printLine
function. When we go ahead and run this code, you can see it just prints out the word hello, and nothing else is printed. If we went and got rid of the actual hello string and just ran this, you can see we just print out an empty line. This is because, by default, when a variable is declared, it's initialized to its empty value. In the case of our name string, the empty value is an empty string. Therefore, let's go ahead and actually assign a variable to this value so that it's no longer empty, which we can do by using the following =
symbol, which represents the assignment operator in Go. Basically, it means we're assigning a value to a variable.
name = "Bob"
Now, when I go ahead and run this code as follows, you can see we print out hello Bob to the console. This is because the name
variable now has the value of Bob assigned to it. In addition to assigning values to variables when we initialize them, we can also assign them later on. So, let's say we want to go ahead and change the name
variable from Bob to be Robert, which we can do by again using the assignment operator.
Now, if we go ahead and print the value of name
after where we've assigned it to be Robert, and if we run this code you can see the first time we print out the name
variable the value is Bob, then after we've assigned it and we print it out again you can see it's now Robert. Whilst it's very useful to be able to change the value of a variable when it comes to Go, there are some situations where you would want that value to remain static or constant. In those situations, rather than using a variable, you would instead use a constant, which you can define as follows. Setting const
instead of var
.
const name = "Bob"
Now, if I go ahead and try to change the name
from Bob to be Robert, you can see that the compiler throws an error, both in my text editor and if I try to run or build the code.
As you can see,
cannot assign to name
, neither addressable nor a map index expression.
That compiler warning isn't too helpful, however, the main thing to take away is that we cannot assign to name
. This is due to the fact that it's a constant, which can only be initialized once.
Shorthand Syntax
Whilst being able to initialize variables in this way is useful, it can be quite cumbersome to write this out over and over again. Fortunately, Go provides a shorthand syntax to be able to achieve the exact same thing — the shorthand assignment operator.
If I go ahead and comment out line 6, we can use the shorthand assignment operator to do the same thing, but in a less verbose way. This is done by first defining the name of the variable that we want to initialize and assign to, and then using the :=
as the shorthand assignment operator.
name := "Bob"
This is one theory as to why Go has a gopher as a mascot, although I think the theory is more related to the fact that Go is a substring of gopher. In any case, whilst this may look like a gopher, it's actually known as the shorthand assignment operator.
Now when we run this code, you can see that it's printing out "hello Bob" as it was before.
One thing to note is you can only use the shorthand assignment operator if a value doesn't exist already. So in this case, we're assigning it to Bob, but if we try to use the shorthand assignment operator again to use Robert:
name := "Robert" // compiler error
You can see my editor throws both a warning and an error, and if I try to run this code it will actually fail. Therefore, to overwrite this value, we need to just use the assignment operator rather than the shorthand initializer.
Multiple Values
Additionally, one thing to note is you can also assign multiple values at the same time:
name1, name2 := "Bob", "Robert"
Now we can go ahead and print name1
and name2
as well.
You also saw that briefly my compiler threw a warning because we weren't using a value. This is something that Go will complain about if you declare a variable but don't use it.
Now you can see "hello Bob Robert" because we're both capturing two variables.
This syntax is only really used when a function returns multiple values, which we'll take a look at later on. Personally, I wouldn't ever really use Go this way. I would just assign both values on separate lines such as follows:
name1 := "Bob"
name2 := "Robert"
For me, it just makes things a little more readable.
Type Inference
When we use the shorthand initializer as follows, you'll notice we don't actually declare a type of this variable:
name := "Bob"
This is because the compiler assigns the variable's type through something called inference — specifically, type inference, which is where it will take the type that we're assigning the value to — a string in this case — and it will infer that the type of the variable should be a string as well.
This type inference works pretty much for most use cases when you assign a variable, such as capturing the return value of a function, which we'll take a look at more in the upcoming lesson, or just assigning a variable to a hard-coded value such as a string or one of the other types that we'll take a look at.
Numerical Types
There are some caveats when doing this, specifically around types that can't easily be inferred. One of those types is numerical types, which it can sometimes be hard to infer the exact numerical type that we're actually intending to assign to.
Because there are various different types of integers, if we had a variable, say age
, which we assign to the value of one, in this case age
would be an integer:
age := 1
But there are many situations where we want to use another numerical type rather than a standard int.
And there are a few different ones we can choose from. The most commonly used numerical type is the int
, which is short for integer. Integers are basically whole numerical values, so numbers that don't have a fractional component. This means numbers such as 1, 2, 3, 4, 5, etc, etc, all the way up to the max range that they support.
Additionally, integers also have support for negative numbers as well.
If we wanted to have numbers with fractional components, these would be the floating point numbers such as float32
or float64
, each of which both produce floating point numbers, but with different size components.
So the float32
will be a 32-bit float and float64
will be a 64-bit number. In most cases you'll probably just want to use float64
unless you have a really good reason to use float32
.
One thing to be aware of when it comes to floating point numbers is due to the way that computers work they don't have infinite precision. They actually have what's known as rounding errors, so if you add multiple floating point numbers together eventually you will have an error due to the fact that they have very slight inconsistencies when it comes to their level of precision.
So if you're doing anything related to monetary values or anything where precision with your numbers is of utmost importance, then using an integer is generally a better approach than say using a floating point number. We won't see any of those issues throughout this course but this is mostly related to things such as money.
In any case if we take a quick look at integers you can see just like floats we also have multiple size integers as well.
The base integer is going to be dependent on your system's size, so if you're running a 64-bit system this will be a 64-bit integer, if you're running a 32-bit system this will be a 32-bit integer.
Each of these sized integers provides a different number or range of values that it can support.
In the case of an int8
this will support values from minus 128 all the way up to 127.
var level int8 = 127
int16
on the other hand will support between 32,000 — I think this is 68 actually — it's one of those two numbers, but minus 32,000 to 32,000 for int16
.
int32
supports between I think minus 2 billion to 2 billion, so quite a lot of numbers.
var bigNumber int32 = 2147483647
Then int64
on the other hand supports — it's far too many numbers to even consider. It's a big number, if you need a big number make sure to use an int64
as it has a lot of numbers you can actually support.
Unsigned Integers
In addition to the standard integer types which are defined using the int
keyword and then either the size or just the standard integer, Go also provides support for unsigned integers which are similar to integers but they have the u
prefix:
uint
uint8
uint16
uint32
uint64
These values are pretty much the same as an integer or a signed integer, however the key difference is unsigned integers do not have support for negative numbers. Instead they only have support for positive numbers.
So for example a uint8
whilst it still supports 256 numbers — 256 as is supported by an int8
— its lowest value is 0 and its highest value is 255.
var power uint8 = 255
This is the same for all the other unsigned integers as well. So a uint32
supports between 0 to 4 trillion — we missed out a uint16
.
A uint16
supports between 0 and 65535.
var score uint16 = 65535
I'm not going to do the uint64
, it's huge. Whatever, 18, whatever that is.
Basically integers in their unsigned versions support the same number of values when it comes to their sizing but when it comes to unsigned integers they don't support negative numbers and only support positive ones with 0 being a positive number.
But it means they can support a higher maximum number compared to their signed counterparts.
Most of the time you'll probably want to use the standard integer as being able to go negative is a good thing at times and it can prevent some bugs from occurring. However there are situations where you will want to use an unsigned integer.
In our case for the age
we actually would want to use an unsigned integer of uint
let's say, which we can go ahead and set to be uint
of say 22, because we wouldn't ever have a negative age.
age := uint(22)
Also if you take a look at the following syntax this is how I'm telling the compiler that the integer type that I want to use is an unsigned integer, which is how you can work with type inference in order to let the compiler know the type of integer that you want to use.
In this case we're casting the number to an unsigned integer or a uint
and so the compiler knows which type to infer from the actual number and so my age
value will be a uint
rather than a standard integer.
Boolean Type
Additionally one thing to note is that integers and unsigned integers also have an empty value, whereas with the string it was an empty string. In the case of an integer:
var age uint
println(age) // Output: 0
The empty value is zero. This empty value of zero will be the same for all numerical types such as floats, integers and any of their signed, unsigned and sized counterparts.
As well as numerical types such as integers, floats and unsigned integers, the other scalar type that is provided by Go is the boolean, which allows you to store one of two values: either true
or false
.
So we could just say:
var isPerson bool
isPerson = true
println("hello", isPerson)
isPerson = false
println("hello", isPerson)
Booleans are typically used with both expressions and conditional expressions, which we'll take a look at more in the next lesson. However to quickly show what these look like, if we go ahead and set:
age := 22
isAdult := age >= 21
println("isAdult", isAdult)
Then we can go ahead and change:
age = 18
isAdult = age >= 21
println("isAdult", isAdult)
This obviously depends on which country you're in, but for the sake of this demo we're setting isAdult
to be over or greater than or equal to 21.
One thing to note is that the empty value for a boolean — so if we just go ahead and set:
var isAdult bool
println("isAdult", isAdult)
You can see isAdult
is false
. That is the empty value of a boolean — it is automatically set to false
if you do not initialize it to a value.
Arrays
In addition to the scalar types that are provided by Go — so numerics, booleans and strings — Go also provides support for collection types. These are types that can contain multiple values inside.
The simplest of these collection types is the array, which is a fixed sized list of elements.
To take a look at what this looks like, let's go ahead and create an array of names, which we can define as follows. First using the square braces and then setting the size of the array inside:
var names [5]string = [5]string{"Alice", "Bob", "Charlie", "Dean", "Elliot"}
Now we have an array of five names contained inside and we're able to access individual elements within. For example:
println("Third name is", names[2]) // Output: Charlie
You'll notice here that I'm passing in the value of 2
. This is because arrays in Go are zero-indexed, meaning the first value which is Alice is actually at index zero.
We can also set values within:
names[2] = "Claire"
println("Third name is", names[2]) // Output: Claire
Arrays themselves are actually very fundamental when it comes to Go. However when it comes to writing Go code we actually don't interface with arrays that often.
In fact, I can count the number of times on one hand that I've actually interfaced with an array when it comes to production Go code. That's because rather than using arrays we instead deal with slices.
Slices
Slices do make use of arrays under the hood, but rather than arrays that are fixed size, they're instead dynamic. This means we can both add elements to them and remove them as well.
To define a slice is actually very similar to defining an array. However, rather than setting a size in between these square braces we instead omit it as follows:
names := []string{"Alice", "Bob", "Charlie", "Dean", "Elliot"}
Now we've turned our names
array — which was a fixed size array of five elements — into a slice, which now also has five elements inside. However, the key difference here is we can now actually add values to it.
To do so we use the append
function:
names = append(names, "Fred")
We can capture the return value if needed. Now if I go ahead and print out the sixth value:
println("Sixth name is", names[5]) // Output: Fred
Before this point, Fred
doesn't exist in the slice — it only exists after we append it. For example, if we try to access:
println(names[5])
before appending, you'll see we get a panic, i.e. our application has crashed. This is because we're trying to access an index that's out of the range of the length of our slice.
To get the length of the slice, use the len
function:
println("Length:", len(names)) // Output: 5 or 6
This works for both arrays and slices.
Slices can store other types as well:
numbers := []int{1, 2, 3}
flags := []bool{true, false}
multi := [][]string{{"A", "B"}, {"C", "D"}}
You can even store slices or arrays inside other slices, creating what's known as a two-dimensional slice.
Maps
In addition to slices and arrays, Go also provides another collection type known as the map. The map is similar to a slice; however, instead of storing a single value it actually stores two that are related to each other in what's known as a key-value pair.
If you come from another language such as Python, then this is very similar to say a dictionary and it allows you to pull out values by passing in the key.
ages := map[string]int{
"Alice": 22,
"Bob": 72,
}
Access values:
println("Bob's age is", ages["Bob"])
println("Alice's age is", ages["Alice"])
Add new values:
ages["Charlie"] = 34
println("Charlie's age is", ages["Charlie"])
Maps can have any type as a value — strings, ints, booleans, slices, etc.
The key however must be comparable — which means it can't be a slice or a map. For example:
invalid := map[[]string]string{} // invalid map key type
This will fail to compile.
Most types (like strings, ints, structs, arrays) are fine as keys.
Structs
Structs are what's known as a composite type. Basically, they're a sequence of named elements called fields, each of which has a name and a type.
Example:
type Person struct {
age int
name string
}
Then we can create a new person:
alice := Person{
age: 22,
name: "Alice",
}
Access fields with the dot syntax:
println(alice.name, alice.age)
You can define multiple instances:
bob := Person{
age: 72,
name: "Bob",
}
Or create a generic person
and update fields manually.
Summary
And with that we've taken a brief overview of values, variables, and types when it comes to Go.
As I mentioned this is just a very baseline understanding and we should cement it as we go through the course. However this should give you enough knowledge to know what variables, values and types are when it comes to Go when you see them come up throughout the course.
There is a quiz in the description below and I recommend doing it to make sure that you cement your understanding and fully understand some of the key concepts that we'll need throughout this course.
Otherwise in the next lesson, we're going to take a look at control flow when it comes to Go — specifically two key concepts which are looping and conditionals.