Hopefully you’ve taken some time to go over our previous post, where we talked about data types, variables, and structs. These are important aspects of programming that have connections to many other programming languages (although the syntax can some times be slightly different). Once you’ve understood this aspect of development, it is important to move on and understand what control flow components of your language. This is how you do different things for different situations and separate your code for easier maintenance.
Hopefully you’ve taken some time to go over our previous post, where we talked about data types, variables, and structs. These are important aspects of programming that have connections to many other programming languages (although the syntax can some times be slightly different).
Once you’ve understood this aspect of development, it is important to move on and understand what control flow components of your language. This is how you do different things for different situations and separate your code for easier maintenance.
So, follow along as we explore the basics of control flow in the go programming language.
The first control flow statement we’ll look at is something you should be familiar with from regular life. These are if statements. There are many times that you’ll want to do something different, depending upon the values of your variables.
For example, something that I frequently encounter in my life, is trying to decide what I want to eat at the restaurant. It always feels like a battle between my wallet and my stomach/taste buds. If I have enough money, I will order something a bit more expensive. But if not, then I’ll buy something less.
Lets look at a simple go program that could encapsulate this experience.
package main
type Person struct {
Name string
Money int // It is good to represent money as whole numbers in this case number of cents, as rounding errors can cause problems.
}
func main() {
person := Person{Name: "me", Money: 3000} // I have $30
primeRib := 3200 // Prime rib costs $32
justTheSalad := 1400 // Just the salad costs $14
if person.Money >= primeRib {
fmt.Println("I'll have the prime rib.")
} else {
fmt.Println("I'll have just the salad.")
}
}
This is your first if-else statement (in go any way, but it may be your first overall). This is using the example from above. We’re comparing the person’s money with the cost of a prime rib meal. If they have enough money, they will buy the prime rib but if not, they’ll buy just the salad.
This is using a couple things, first the if keyword. The if keyword is telling the compiler that we want to run a different section of code depending upon the value that we get, we’ll be able to run any branch that meets the requirements of the guard statement. This so called guard statement can be anything that evaluates to true/false. In this case we’re just using a basic mathematical expression that you should be used to from all of that fun math you’ve done.
But, what if we have enough money for something else, that is not just the salad? We can add another clause to our if else statement tree.
package main
type Person struct {
Name string
Money int // It is good to represent money as whole numbers in this case number of cents, as rounding errors can cause problems.
}
func main() {
person := Person{Name: "me", Money: 3000} // I have $30
primeRib := 3200 // Prime rib costs $32
theHamburger := 2100 // The hamburger $21
justTheSalad := 1400 // Just the salad costs $14
if person.Money >= primeRib {
fmt.Println("I'll have the prime rib.")
} else if person.Money >= theHamburger {
fmt.Println("I'll have the hamber.")
} else {
fmt.Println("I'll have just the salad.")
}
}
Boom, we’ve just added another item to the menu, and we can afford the hamburger.
This extra clause adds an else if, then another mathematical expression (that turns into a Boolean true/false result). In this case, whatever section of the if else equates to true, that is what we’ll run.
But, you may notice something the else if and the last else could technically both be true. We only get the result of “I’ll have the hamburger.” This is called short circuit evaluation. We’ll get the first true, and in this case, most specific result.
There is another thing that I would like you to be aware of, and that is multiple tests can be added to the if clause. These can be strung together using && (and) as well as || (or) operators. We’ll go into those later when we have a more specific example to go over.
Switch statements are a lot like if-else statements, but with a twist. But not just a basic if-else statement. If you find yourself writing multiple if-else if-else if-else blocks, now is a good time to look towards the switch. Which, in go specifically, switches have a couple different usecases (we’ll not dig into the difference too much at this point, if at all, because there is some more experience that needs to be gained with programming in general and go specifically before digging in to those).
I think of all of the topics to be covered in this post at the previous post, this is one of the more challenging topics to discuss. Not because in and of itself switches are difficult. But it is hard to come up with a good example of why they are important, and how they will connect to your general experiences.
Let’s look at some basic syntax for the switch case. Before we do though, let’s set the stage for our example. You’re building a cross platform app, and inside of a function you want to do something slightly different (again, in go, there are many ways to accomplish this, but for now this is how we’ll discuss it).
package main
import (
"fmt"
"runtime"
)
func main() {
currentOS := runtime.GOOS
switch currentOS {
case "darwin":
fmt.Println("You're using MacOS")
case "linux":
fmt.Println("You're using linux")
default:
fmt.Println("It looks like you're not using MacOS or Linux")
}
}
Again, this is a very simple example. But the intent, is that you can see we’re able to check many different cases using a slight more compact method than multiple if-else if blocks.
Something to keep in mind, is this is a general introduction to these concepts. It might not make sense now, but as we go forward, we’ll encounter cases where we’ll want to use this.
So far, we’ve really covered just the basics. Going forward we’re going to talk about the two things that will help you make your code more re-usable and maintainable. It will come at a bit of a learning curve (especially really understanding why interfaces are important), but once you’ve got a grasp of it, you’ll become much more proficient.
So far, all of the code samples that we’ve looked at have just been in the main function (see, we’ve already been looking at a basic function and you didn’t even notice it).
The basics of a function is the keyword, the name, parens (and parameters if needed), and a return type. For a function, the parameters and return types are optional. (Other languages require a void return type if no type is returned, but the go compiler is more sophisticated and can handle an omitted return).
Let’s look at an example of a function, that works with one of our previous examples. Our person struct, has a name and some money. Let’s create a special kind of function that is known as a constructor function.
package main
import "fmt"
type Person struct {
Name string
Money int // It is good to represent money as whole numbers in this case number of cents, as rounding errors can cause problems.
}
func main() {
person := NewPerson() //We're calling our function here
fmt.Println(person.Name)
}
// This is our function (which is a constructor function).
func NewPerson() Person {
return Person{Name: "Me", Money: 100}
}
Let’s look at the same example, but with a parameter to the NewPerson function.
package main
import "fmt"
type Person struct {
Name string
Money int // It is good to represent money as whole numbers in this case number of cents, as rounding errors can cause problems.
}
func main() {
person := NewPerson("My Name") //We're calling our function here, with the required name parameter.
fmt.Println(person.Name)
}
// This is our function (which is a constructor function).
func NewPerson(name string) Person {
return Person{Name: name, Money: 100}
}
Here we’re requiring a parameter to the function, you see that in what is called the function signature as well as there is a value added to the function call with the parameter.
Before we jump to interfaces, we need to understand why that is important. We can add functions to our struct types, and then we can work with those struct types functions in other parts of our code. These functions connected to a struct are called methods.
package main
import "fmt"
type Person struct {
Name string
Money int // It is good to represent money as whole numbers in this case number of cents, as rounding errors can cause problems.
}
// This is the String() string method for our person
// Coincidentally, this satisfies the Stringer interface, but we'll get to that later
func (p Person) String() string {
return p.Name
}
func main() {
person := NewPerson("My Name") //We're calling our function here, with the required name parameter.
fmt.Println(person.String()) // Calling the String() method on our person struct
}
// This is our function (which is a constructor function).
func NewPerson(name string) Person {
return Person{Name: name, Money: 100}
}
If you remember our discussion on structs, we talked about them being a collection of data that works together. Interfaces are a collection of methods (or functionality) that work together.
Just like with the switch, this is one of the other challenging topics to discuss. Not because the syntax of the interface is hard to learn. It has more to do with explaining why it is important. There are many people that take years in their Computer Science programs, before they understand why they’re important. However, as we go through the development of our game, we’ll encounter a few places where interfaces are important.
For now, we’ll keep it simple and look at the syntax for an interface. (And we’ll look at an interface that we’ll use for game development).
package main
type Animateable interface {
Location() (float64, float64)
DrawOffset() (float64, float64) // Look, two values to return, go allows for multiple return values
}
func main() {
}
Now anything that has these methods attached to it, can be passed into functions or maps/arrays that require this interface be implemented.
Let’s add a player class, and an animation struct. It will be a simple example of our interface, without going into too much detail about using it yet at this point.
package main
type Animateable interface {
Location() (float64, float64)
DrawOffset() (float64, float64) // Look, two values to return, go allows for multiple return values
}
type Player struct {
X float64
Y float64
}
func (p Player) Location() (float64, float64) {
return X, Y
}
func (p Player) DrawOffset() (float64, float64) {
return 0, 0
}
type Animation struct {
Target Animateable
}
func main() {
p := Player{}
animation := Animation{Target: p}
}
We’ll go down this rabbit hole later on. But you’ll notice we have a struct (the player), another struct (the animation), and an interface that lets the animation with a specific type.
We’ve implemented this interface on our player struct. But, the reason why this is important, we could add an enemy, projectile, or prop struct that could implement this interface and then they could be animateable.
If you’ve made it to the end of this and the previous post, congratulations. We’ve just covered the 30,000 foot view of programming. I’m sure that it feels a lot like drinking from a fire hose. Don’t worry, this is only meant to be a 30,000 foot view. It is an introduction to concepts.
I look at it like this, it will take a handful of interactions with these topics before you understand them. Then a few more before you’ll know why it is important. And a life time of using them to become a master. But they are like a light switch, every time you need them, you’ll flip them on and see more and more about around them.
But, bonus points if you start playing around with the concepts that we’ve covered here.