Learning GO: Loops and Conditionals

Let’s continue our journey into Go. Last time, we got Go set up in WSL and covered some fundamentals like values, variables, and constants. This time, we’ll dive into for loops and conditional blocks, the building blocks of decision-making and iteration in Go. We’re working our way through Go by Example if you want to follow along.
At the end of this post, we’ll also take a quick detour into running Go programs in Docker using a simple Dockerfile
, making them easy to package and run anywhere.
For loops
Our first port of call is the humble for loop. The example we’re given has a few different loop types, so let’s break them down.
i := 1
for i <= 3 {
fmt.Println(i)
i = i + 1
}
Our first loop declares and assigns a variable called i
outside the loop using the :=
syntax we covered last time. It starts at 1
, prints the current value of i
in each iteration, and increments i
by 1
. The loop stops when i
reaches 4
.
While declaring i
outside the loop works, it’s more common and often clearer to declare it within the loop’s initialisation.
for j := 0; j < 3; j++ {
fmt.Println(j)
}
This second loop is much closer to what I’d expect. The variable j
is declared inside the loop, starts at 0
, increments after each iteration, and stops when j
reaches 3
. Each iteration prints the value of j
.
for i := range 3 {
fmt.Println("range", i)
}
This loop iterates 3 times, with i
taking the values 0, 1, and 2. It’s, in essence, the same as the previous loop just in a neater and more standard way. This is a range loop and we will see this syntax used with arrays and slices later on.
for {
fmt.Println("loop")
break
}
Now things are getting interesting. This loop has no stopping condition, so it would run forever, except we use break
immediately after Println
, stopping it in its tracks. This is useful when you need to conditionally exit a loop at any point.
for n := range 6 {
if n%2 == 0 {
continue
}
fmt.Println(n)
}
Sneaky example. We’re seeing multiple new things here; the modulo
operator (%
), our first if
block, and the continue
keyword.
This loop counts from 0
to 5
. It checks if n
is even using n % 2 == 0
. If so, continue
skips the rest of the iteration and moves to the next number. Otherwise, it prints n
.
In short, it prints only the odd numbers in the range.
Conditionals
Conditionals are a staple of any programming language they allow us to have branching logic based on an input variable. There are two main flavours of conditional blocks: if/else
statements and switch
statements so let’s explore them.
If/else Statements
If statements boil down to expressions that evaluate to either true
or false
; if the expression is true
do something otherwise do something else.
Again in this example we’re given several statements so let’s look at them one by one.
if 7%2 == 0 {
fmt.Println("7 is even")
} else {
fmt.Println("7 is odd")
}
Our friend, the modulo operator, is back. In this example, we’re checking if the number 7 is even (it’s not) and printing out whether it is or not (again, it’s not).
if 8%4 == 0 {
fmt.Println("8 is divisible by 4")
}
This is basically the same example but shows us that the else
section is not required.
if 8%2 == 0 || 7%2 == 0 {
fmt.Println("either 8 or 7 are even")
}
Here we have an or
condition, this will look familiar to you if you’ve used other languages, allowing the if
statement to execute if either condition is true
if num := 9; num < 0 {
fmt.Println(num, "is negative")
} else if num < 10 {
fmt.Println(num, "has 1 digit")
} else {
fmt.Println(num, "has multiple digits")
}
Finally we have an else if
example. This allows us to check multiple conditions on an input and execute the first that is true, the else
at the end acts as a catch-all for anything that makes it past all other checks. It is worth noting that the scope of the variable num
is limited to the if/else
block.
I will say I do not like how a variable can be declared in an if statement like this. To me this seems less readable but I guess we can easily rewrite their example like this to avoid that.
num := 9
if num < 0 {
fmt.Println(num, "is negative")
} else if num < 10 {
fmt.Println(num, "has 1 digit")
} else {
fmt.Println(num, "has multiple digits")
}
Go doesn’t have a ternary operator, which is a shame if you’re used to it in other languages, but let’s see if we actually miss it.
Switch Statements
A switch statement whilst similar to an if/else block is a little different. It also relies on evaluating an expression but those expressions don’t have to be just be true
or false
they can be anything and we can have a case
for each outcome and a default
case to catch anything else.
Let’s break this example down and see if they’ve snuck any new things in there.
i := 2
fmt.Print("Write ", i, " as ")
switch i {
case 1:
fmt.Println("one")
case 2:
fmt.Println("two")
case 3:
fmt.Println("three")
}
First example and they’ve already snuck in some new stuff. Print
on fmt
prints without ending with a line break and we can comma separate values to append them all together.
Other than that this example is quite straightforward, declare i
check if i
is 1, 2 or 3 and if it is print out the written word.
switch time.Now().Weekday() {
case time.Saturday, time.Sunday:
fmt.Println("It's the weekend")
default:
fmt.Println("It's a weekday")
}
Here, we see our first look at the time package. We appear to be using enum
s but as we’ve not learnt about them yet we’ll just have to wait. All we need to know is our expression gets the current day of the week and we’re using case
, with comma separation acting like the or
operator, we check to see if it’s in the weekend or a weekday.
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("It's before noon")
default:
fmt.Println("It's after noon")
}
This example is similar to the previous one only our time.Now()
call is stored as a variable and we’re not passing in an expression to our switch
. This shifts the condition checking into the case blocks, making it behave like an if/else
statement where the first true
case executes.
whatAmI := func(i interface{}) {
switch t := i.(type) {
case bool:
fmt.Println("I'm a bool")
case int:
fmt.Println("I'm an int")
default:
fmt.Printf("Don't know type %T\n", t)
}
}
whatAmI(true)
whatAmI(1)
whatAmI("hey")
Okay, I thought they might try to sneak in some extra stuff and here it is, we’ve got our first reusable function, an interface
(whatever that is) and a Printf
function.
Let’s go slowly through this and work out what’s going on. First we have a variable declaration called whatAmI
that is a func
that take i
which is an interface{}
.
I wasn’t sure on what interface{}
meant, so I did a quick search. It seems interface
is something we will learn about a little later but interface{}
is how you tell Go that we don’t know what type will be passed into the function. Think of interface{}
as a placeholder that can hold any type of value. We’re saying i
can be anything.
Next up, we have our switch expression, where we’re declaring a new variable called t
and storing the type of i
in it. This allows us to have our case
s be the types we’re looking for.
We have 2 case
s and a default
fallback. The first two case
s check if the if i
was a bool
(true
or false
) or if it was an int
(whole number) and if it’s neither the fallback prints out what the type was using Printf
. Printf
allows formatted output where %T
prints a variable’s type. We use Printf
here because %T
isn’t directly convertible to a string
. We also have to end with an \n
to put our line break back in.
Docker
A great way to run Go applications is to package them into Docker images, making them portable across different systems, including Windows, Linux, and macOS. We’ll use a multi-stage Dockerfile to build and execute our Go program efficiently.
Why Use a Multi-Stage Dockerfile?
Multi-stage builds help keep the final image small and efficient by separating the build environment from the runtime environment. This reduces unnecessary dependencies in the final image, improving security and performance.
Let’s start by writing a Dockerfile
.
# Build the GO binary
FROM golang:1.24.1-alpine AS build
COPY ./main.go ./main.go
RUN go build -o /bin/output ./main.go
# Execute the GO binary
FROM scratch
COPY --from=build /bin/output /bin/output
CMD ["/bin/output"]
Build Stage (build)
We start with the official golang:1.24.1-alpine image image. We copy main.go
into the container (you can rename this) and compile it to /bin/output
.
Runtime Stage
The runtime stage uses FROM scratch
, meaning it has no base OS. scratch
is an empty image, and nothing else is present. This minimizes the final image size. We use the command COPY --from=build
which efficiently copies only the compiled binary from the build stage and set CMD ["/bin/output"]
as the entry command.
Building and Running the Docker Image
To build and run the image, use the following commands:
# Build the image (dot specifies the current directory as context)
docker build -t my-go-app:latest .
# Run the container
docker run my-go-app:latest
Signing off
We’ve covered a lot this time and our programs are becoming more and more sophisticated. We are progressing nicely, and the syntax is proving to be very easy to work with. Next time we’ll be looking at arrays, slices and maps. We’re really getting somewhere but at the same time we’ve only just scratched the surface.
Thanks for reading! If you’d like to connect, here are my Twitter, BlueSky, and LinkedIn profiles. Come say hi