Enums
Enums are a way of defining a fixed list of values that are all related. Go doesn’t have a built-in type for enums, but it does provide tools such as iota
to let you define your own using constants. We’ll explore this now.
For example, in the following code, we have the days of the week defined as constants. This code is a good candidate for Go’s iota
feature:
… const ( Sunday = 0 Monday = 1 Tuesday = 2 Wednesday = 3 Thursday = 4 Friday = 5 Saturday = 6 ) …
With iota
, Go helps us manage lists just like this. Using iota
, the following code is equal to the preceding code:
… const ( Sunday = iota Monday Tuesday Wednesday Thursday Friday Saturday ) …
Now, we have iota
assigning the numbers for us. Using iota
makes enums easier to create and maintain, especially if you need to add a new value to the middle of the code later. Order matters when using iota
as it is an identifier that tells the Go compiler to start the first value at 0 and increment by 1 for each subsequent value in the case of this example. With iota
, you can skip values using _
, start with a different offset, and even use more complicated calculations.
Next, we’ll take a detailed look at Go’s variable scoping rules and how they affect how you write code.
Scope
All the variables in Go live in a scope. The top-level scope is the package scope. A scope can have child scopes within it. There are a few ways a child scope gets defined; the easiest way to think about this is that when you see {
, you are starting a new child scope, and that child scope ends when you get to a matching }
. The parent-child relationship is defined when the code compiles, not when the code runs. When accessing a variable, Go looks at the scope the code was defined in. If it can’t find a variable with that name, it looks in the parent scope, then the grandparent scope, all the way until it gets to the package scope. It stops looking once it finds a variable with a matching name or raises an error if it can’t find a match.
To put it another way, when your code uses a variable, Go needs to work out where that variable was defined. It starts its search in the scope of the code using the variable it’s currently running in. If a variable definition using that name is in that scope, then it stops looking and uses the variable definition to complete its work. If it can’t find a variable definition, then it starts walking up the stack of scopes, stopping as soon as it finds a variable with that name. This searching is all done based on a variable name. If a variable with that name is found but is of the wrong type, Go raises an error.
In this example, we have four different scopes, but we define the level
variable once. This means that no matter where you use level
, the same variable is used:
package main import "fmt" var level = "pkg" func main() { fmt.Println("Main start :", level) if true { fmt.Println("Block start :", level) funcA() } } func funcA() { fmt.Println("funcA start :", level) }
The following is the output displaying variables when using level
:
Main start : pkg Block start : pkg funcA start : pkg
In this example, we’ve shadowed the level
variable. This new level
variable is not related to the level
variable in the package scope. When we print level
in the block, the Go runtime stops looking for variables called level
as soon as it finds the one defined in main
. This logic results in a different value getting printed out once that new variable shadows the package variable. You can also see that it’s a different variable because it’s a different type, and a variable can’t have its type changed in Go:
package main import "fmt" var level = "pkg" func main() { fmt.Println("Main start :", level) // Create a shadow variable level := 42 if true { fmt.Println("Block start :", level) funcA() } fmt.Println("Main end :", level) } func funcA() { fmt.Println("funcA start :", level) }
The following is the output:
Main start : pkg Block start : 42 funcA start : pkg Main end : 42
Go’s static scope resolution comes into play when we call funcA
. That’s why, when funcA
runs, it still sees the package scope’s level
variable. The scope resolution doesn’t pay attention to where funcA
gets called.
You can’t access variables defined in a child scope:
package main import "fmt" func main() { { level := "Nest 1" fmt.Println("Block end :", level) } // Error: undefined: level //fmt.Println("Main end :", level) }
The following is the output:
Figure 1.23: Output displaying an error
Activity 1.03 – message bug
The following code doesn’t work. The person who wrote it can’t fix it, and they’ve asked you to help them. Can you get it to work?
package main import "fmt" func main() { count := 5 if count > 5 { message := "Greater than 5" } else { message := "Not greater than 5" } fmt.Println(message) }
Follow these steps:
- Run the code and see what the output is.
- The problem is with
message
; make a change to the code. - Rerun the code and see what difference it makes.
- Repeat this process until you see the expected output.
The following is the expected output:
Not greater than 5
In this activity, we saw that where you define your variables has a big impact on the code. Always think about the scope you need your variables to be in when defining them.
In the next activity, we are going to look at a similar problem that is a bit trickier.
Activity 1.04 – bad count bug
Your friend is back, and they have another bug in their code. This code should print true
, but it’s printing false
. Can you help them fix the bug?
package main import "fmt" func main() { count := 0 if count < 5 { count := 10 count++ } fmt.Println(count == 11) }
Follow these steps:
- Run the code and see what the output is.
- The problem is with
count
; make a change to the code. - Rerun the code and see what difference it makes.
- Repeat this process until you see the expected output.
The following is the expected output:
True