Learning objectives
In the previous lesson, we learned about how Go implements while loops and integrated a while loop into a Factorial()
function.
In the main text, we also saw that we could write a function to calculate n! using a for loop, as shown by the following pseudocode function AnotherFactorial()
.
AnotherFactorial(n) p ← 1 for every integer i between 1 and n p ← p·i return p
In this lesson, we will learn how Go implements for loops, , which will prepare us to implement our two GCD algorithms from the main text.
Set up
In the previous lesson, you created a file called main.go
in the go/src/loops
directory. We will continue to edit main.go
in this lesson.
Code along video
Beneath the video, we provide a detailed summary of the topics and code covered in the code along.
At the bottom of this page, you will have the opportunity to validate your work via auto-graded assessments that evaluate the functions covered in the code along.
Although we strongly suggest completing the code along on your own, you can find completed code from the code along in our course code repository.
Code along summary
Another factorial function and for loops in Go
Now that we have learned a bit about while loops in Go, let us see how Go implements for loops. In particular, we can implement alternate versions of both Factorial()
and SumFirstNIntegers()
using for loops.
Recall the AnotherFactorial()
pseudocode function from the main text, shown below, which uses a for loop.
AnotherFactorial(n) p ← 1 for every integer i between 1 and n p ← p·i return p
Our implementation of this function in Go is shown below — as we might guess, the for loop will be confined by brackets in the same way that while loops are. But Go’s syntax for for loops is a little trickier than its notation for while loops, which we should explain.
//AnotherFactorial takes an integer n as input and returns n! = n*(n-1)*(n-2)*...*2*1, using a for loop func AnotherFactorial(n int) int { if n < 0 { panic("Error: Negative input given to AnotherFactorial") } p := 1 // for every integer i between 1 and n, p = p*i for i := 1; i <= n; i++ { p *= i } return p }
Let’s zoom in on the for loop itself, which uses the shorthand syntax p *= i
introduced in the previous lesson.
// for every integer i between 1 and n, p = p*i for i := 1; i <= n; i++ { p *= i }
The for loop consists of three parts.
- The initialization step: (
i := 1
): this declares a variable that will be created before the for loop is ever entered. This variable will have scope of the loop. - The condition (
i <= n
): this condition must be true for us to enter the loop each time - The postcondition (
i++
): After completing the loop once, we take this action.
Note: Not all three parts of the for loop must be present. We will see more exotic examples of this later in the course, but for now, note that Go’s implementation of a while loop is simply a for loop that consists only of the condition and that excludes the initialization and postcondition.
Summing the first n integers using a for loop
Let’s now write a different version of the SumFirstNIntegers()
function from the previous lesson uses a for loop. This function, AnotherSum()
, is shown below.
//AnotherSum takes an integer n as input and returns the sum of the first n positive integers, using a for loop func AnotherSum(n int) int { if n < 0 { panic("Error: negative integer given to AnotherSum") } sum := 0 for k := 1; k <= n; k++ { sum += k } return sum }
STOP: PrintAnotherFactorial(5)
andAnotherSum(10)
infunc main()
. Ensure that these function calls print120
and55
, respectively.
Note: A (likely apocryphal) story about 19th century mathematician Carl Friedrich Gauss is that his primary school teacher, annoyed by young Carl’s precociousness, set him the task of summing the first 100 positive integers. Gauss quickly responded with the correct answer: 5,050. He had noticed that 1 and 100 sum to 101, as do 2 and 99, as do 3 and 98, and so on, ending with 50 and 51. There are 50 such pairs of integers summing to 101, so that the correct answer is 50 · 101 = 5,050. Gauss’s approach can be generalized so that we can compute the sum of the first n integers for any value of n without using any loops at all: the answer is simply n · (n+1)/2.
A stylistic point about variable scope in for and while loops
You might be wondering why we would write two different functions to produce the same task. Is one of Factorial()
or AnotherFactorial()
better than the other, or can we use a while loop and for loop interchangeably?
Let’s temporarily add a statement to print i
immediately before the return
statement in both Factorial()
and AnotherFactorial()
as shown below, and then compile our code. What happens?
//AnotherFactorial takes an integer n as input and returns n! = n*(n-1)*(n-2)*...*2*1, using a for loop func AnotherFactorial(n int) int { if n < 0 { panic("Error: Negative input given to AnotherFactorial") } p := 1 // for every integer i between 1 and n, p = p*i for i := 1; i <= n; i++ { p *= i } fmt.Println("i is currently", i) return p } //Factorial takes an integer n as input and returns n! = n*(n-1)*(n-2)*...*2*1 func Factorial(n int) int { if n < 0 { // let's handle negative input panic("Error: Negative input given to Factorial()!") } p := 1 i := 1 // Go doesn't have a while keyword, and it uses "for" instead for i <= n { p = p*i //i = i+1 } fmt.Println("i is currently", i) return p }
This code will not compile! In Factorial()
, we define the variable i
outside of the loop, and so its scope (a concept introduced in the previous lesson) is the entire function. However, in AnotherFactorial()
, the scope of i
is confined to the loop and cannot be accessed outside.
Although these two functions may seem the same,
is objectively superior because AnotherFactorial()
i
is needed only within the loop and should not exist outside of this loop. After all, imagine what would happen in Factorial()
if we wanted to use another while loop with a counter variable named i
.
This point is a minor one, but it hints at an important stylistic point, which is that variables should the scope that is appropriate for them and should not exist outside of where they are needed. We will keep this point in mind as we begin to code larger projects.
Variations of for loops
The for loop in AnotherFactorial()
is not required to have an initialization step i := 1
, a condition of i <= n
, and a postcondition of incrementing i
each time through the loop. We could just as easily have initialized using i := n
, tested the condition i >= 1
, and decremented i
each time through the loop. This updated loop is shown in the following alternative version of AnotherFactorial()
.
//AnotherFactorial takes an integer n as input and returns n! = n*(n-1)*(n-2)*...*2*1, using a for loop func AnotherFactorial(n int) int { if n < 0 { panic("Error: Negative input given to AnotherFactorial") } p := 1 // for every integer i between 1 and n, p = p*i for i := n; i >= 1; i-- { p *= i } return p }
STOP: How wouldAnotherSum()
need to change if we applied the same approach to edit its for loop?
Exercise: Write a functionSumEven()
that takes an integerk
as input and returns the sum of all even positive integers up to and possibly includingk
.
Recall from the main text that an integer p
is a divisor of an integer n
if the remainder after dividing n
by p is zero. Furthermore, we asked you to write pseudocode functions
and IntegerDivision()
, both of which have integer parameters Remainder()
n
and p
, and which respectively return the integer component and the remainder when dividing n
by p
. Fortunately, Go has built-in operations for both of these functions: integer division is given by n/p
(as we have already seen), and the remainder after this division is given by
.n%p
Note: The remainder operationis often called “n modulo p”.
n%p
We can therefore implement SumEven()
by ranging over all integers j
between 2 and k
, and checking if j
is even using the test if j % 2 == 0
. We add each such j
to a variable sum
, which we will return.
//SumEven takes an integer k as input and returns the sum of every even integer up to and including n func SumEven(k int) int { sum := 0 // for every even integer j between 1 and k, sum = sum + j for j := 2; j <= k; j++ { if j % 2 == 0 { sum += j } } return sum }
Yet we can solve this problem much faster by ignoring all odd integers entirely. To do so, we replace the postcondition j++
with j += 2
; accordingly, because we add 2 to j
each time, we know that j
will always be even.
//SumEven takes an integer k as input and returns the sum of every even integer up to and including n func SumEven(k int) int { sum := 0 // for every even integer j between 1 and k, sum = sum + j for j := 2; j <= k; j += 2 { sum += j } return sum }
A point about underflow
Consider the following code. It seems like an innocuous countdown in which we print 10, then 9, then 8, until we print 0, followed by “Blast off!”
func main() { var i uint = 10 for ; i >= 0; i-- { fmt.Println(i) } fmt.Println("Blast off!") }
Yet when we run this code below, we see that this desired behavior is not what happens at all.
The program eventually kills the process after printing an enormous number of negative numbers. If we were to run this code on our own computers, we would need to use Control+C
to stop the process.
The problem with the above code is that i
is declared to be equal to 10, but a variable having type uint
can never be negative, so that when i
reaches 0, the condition i >= 0
is true. After printing 0, when the postcondition decrements i
, underflow causes i
to become the largest integer that Go stores on your computer. But this integer is still greater than or equal to zero! As a result, the condition i >= 0
is always true when i
has type uint
, and the code contains an infinite loop in disguise.
This code indicates one reason why we will rarely use variables of type uint
, even if we are working with variables that should not take negative values. Some languages, like Java, expressly have no support for unsigned integers. After all, we can always handle negative values where they are not wanted by using error messages.
You made it!
Thus ends our introduction to the basics of Go. You should be proud of making it this far because although this introduction is relatively short, everything else that we learn about in this course will build upon it.
We are now ready to return to start implementing our two nontrivial algorithms from the main text. In the next lesson, we begin with Euclid’s algorithm for calculating the GCD of two integers.
Check your work from the code along
We provide autograders in the window below (or via a direct link) allowing you to check your work for the following functions:
AnotherFactorial()
SumEven()