How to program a guessing game with Go

Let's jump into Go by working through a hands-on project together. This tutorial will introduce you to a few common Go concepts such as variabes, error handling, loops, packages, and more by showing you how to use them to tackle a classic beginner programming problem.

In this tutorial, we’ll build a “Guess the number” game in Go. In this game, the program will generate a random integer between 1 and 100, and then prompt the player to enter a guess.

After a guess is entered, the program will notify the player if the guess was higher or lower than the random number, and prompt the player for another guess. Otherwise, if the guess is correct, a congratulatory message will be printed on the screen and the program will exit.

The objective here is to give you your first bit of experience with writing a Go program, and for you to gain a basic understanding of what writing a Go program involves.

At the end of this tutorial, you should have a program that works in a similar manner to the demo below:

Prerequisites

You need to have Go installed on your computer. You can visit the official Go website to view installation instructions for your operating system.

Project setup

Create a new directory anywhere on your filesystem, and change into it with the commands below:

$ mkdir guessing-game
$ cd guessing-game

Next, initialise your project with a go.mod file. Replace <username> with your GitHub username.

$ go mod init github.com/<username>/guessing-game

Finally, create a main.go file in your project root, and open it in your favourite text editor. You will write all the code for this project in this file.

$ vim main.go

How the game works

As you can observe from the demo, when the program runs, a random integer between 1 and 100 is generated. The player is subsequently prompted to guess what number was generated and feedback is provided to the player if the guess is higher or lower than the secret number. If the guess is equal to the secret number, a congratulatory message is printed and the program exits.

Let’s start by generating a random number between 1 and 100 in the next section.

Generate a random number

To generate a random number, we’ll be making use of the math/rand package from the standard library. Modify your main.go file shown below:

main.go
package main

import (
	"fmt"
	"math/rand"
)

func main() {
	min, max := 1, 100
	secretNumber := rand.Intn(max-min) + min
	fmt.Println("The secret number is", secretNumber)
}

Let’s go over the code line by line. The first step is to declare the package that this file belongs to. In this case, it’s the main package. The main package is special in Go as it serves as the entry point for every executable program.

import (
	"fmt"
	"math/rand"
)

To print text to the terminal, we need to bring the fmt package into scope. It provides several methods that we can leverage to print the result of an operation to the standard output. We also need to import the rand package before we can use it. When importing more than one package, you can place the package names inside parenthesis. This prevents you from repeating the import keyword on every line.

Note that the rand package was imported as math/rand. This is because it is nested inside the math package as a subdirectory. The convention in Go is that package name is the same as the last element in the import path.

func main(){

As discussed in the last article, the main function is the entry point to the program. It is called automatically when you run the program.

min, max := 1, 100

This above statement represents a short variable declaration in Go and it can only be used inside functions. Here, we’re creating two new variables (min and max) and assigning the values 1 and 100 to them respectively. The min and max variables represent the range within which the random number will be generated.

secretNumber := rand.Intn(max-min) + min

Here’s where we generate a secret number within the min and max constraints. The rand package exports an Intn method that returns a psuedo-random positive number between 0 and its argument which should be a positive integer. In this case, the argument is 99 (max - min), so the range of numbers that can be generated will be between 0 and 98. min is added to the output of rand.Intn() so as to change the range to 1…99 instead.

Note that the max number is exclusive in the range while min is inclusive. If you wanted numbers between 1-100, you have to change the max variable to 101.

fmt.Println("The secret number is", secretNumber)

The fmt package exposes a Println method which prints its arguments to the console and adds a newline at the end. If there are more than one arguments, they are separated with a space.

Save your main.go file and run the program in the terminal using go run main.go. You should get a similar output to the one shown below:

$ go run main.go
The secret number is 24
$ go run main.go
The secret number is 24
$ go run main.go
The secret number is 24
$ go run main.go
The secret number is 24

It appears that the same number is printed to the screen each time. This is definitely not what we want. But why is this happening? There’s a simple explanation.

The numbers that the rand package generates are based on a specific initial value called its seed. The default seed is 1, so unless you change it, you’ll always get the same output. The seed needs to be something that’s unique and always changing, so that when the program is run multiple times, you’ll get better random values. A popular choice for this initial seed is the current time in nanoseconds which will most probably be a different value on each execution.

Here’s how to change the initial seed of the rand generator:

// note that `rand.Seed()` expects an `int64` type so make sure
// that whatever you pass into it produces an `int64` value.
// Also, it needs to be called before any other methods from the
// `rand` package
rand.Seed(time.Now().UnixNano())

Here’s how your main.go file should look like after adding the code snippet above:

main.go
package main

import (
	"fmt"
	"math/rand"
	"time"
)

func main() {
	min, max := 1, 100
	rand.Seed(time.Now().UnixNano())
	secretNumber := rand.Intn(max-min) + min
	fmt.Println("The secret number is", secretNumber)
}

Notice the introduction of the time package amongst the imports. This is the package that provides functionality for measuring and displaying time in Go. It exports a Now() method that returns the current time and the UnixNano() method which returns the number of nanoseconds elapsed since January 1, 1970 UTC as an int64 value.

Try running the program again. You should get different random numbers, and they should all be numbers between 1 and 100.

$ go run main.go
The secret number is 91
$ go run main.go
The secret number is 77
$ go run main.go
The secret number is 84
$ go run main.go
The secret number is 6

Read user input from the terminal

The next step is to allow the user to enter a guess and then check that it’s in the expected format (that is, it must be an integer).

Here’s the code that helps us achieve that:

main.go
package main

import (
	"bufio"
	"fmt"
	"math/rand"
	"os"
	"strconv"
	"strings"
	"time"
)

func main() {
	min, max := 1, 100
	rand.Seed(time.Now().UnixNano())
	secretNumber := rand.Intn(max-min) + min
	fmt.Println(secretNumber)

	fmt.Println("Guess a number between 1 and 100")
	fmt.Println("Please input your guess")

	reader := bufio.NewReader(os.Stdin)
	input, err := reader.ReadString('\n')
	if err != nil {
		fmt.Println("An error occured while reading input. Please try again", err)
		return
	}

	input = strings.TrimSuffix(input, "\n")

	guess, err := strconv.Atoi(input)
	if err != nil {
		fmt.Println("Invalid input. Please enter an integer value")
		return
	}

	fmt.Println("Your guess is", guess)
}

Let’s go over each of the new additions so as to understand what they all do:

fmt.Println("Guess a number between 1 and 100")
fmt.Println("Please input your guess")

The above lines introduces the game and prompts the player to enter a guess.

The next step is to accept input from the player. The bufio and os packages are what we’ll be using to capture the user’s input. Notice how they’re both imported at the top of the file.

reader := bufio.NewReader(os.Stdin)

The above line of code declares a new variable called reader and initialises it to the return value of bufio.NewReader(os.Stdin).

The NewReader() method of bufio takes a value that implements the  io.Reader interface. os.Stdin represents an open File that points to the standard input file descriptor, and also implements the io.Reader interface which is why we’re able to pass it as an argument to NewReader().

input, err := reader.ReadString('\n')

The NewReader() method returns a bufio.Reader struct which has a method called ReadString() which takes in a delimiter (the newline character \n in this case). This method reads the user’s input until the first occurrence of the delimiter in the string, and returns two values: a string containing the data up to and including the delimiter, and an error (if any). The former is stored in the input variable while the latter value (the error) stored in the err variable.

if err != nil {
	fmt.Println("An error occured while reading input. Please try again", err)
	return
}

The above snippet handles the error that might occur when using the ReadString method. This is the idiomatic way to handle errors in Go. Before using the result of ReadString, the error value is checked to see if it’s not nil. If so, a message is printed to the screen and the main function returns causing the program to exit.

input = strings.TrimSuffix(input, "\n")

Assuming err is nil, program execution will move to the next line shown above. Remember that ReadString() includes the delimiter in the result value. This means that the input will contain the newline character in addition to whatever the player entered.

For example, if the player types 30 and presses Enter, the value of input will be 30\n. But we don’t want this /n part in the input so we need to get rid of it. This can be achieved using the TrimSuffix() method from the strings package. All we need to do is pass the string and the trailing suffix. It will then return the provided the string without the suffix, so 30\n will become just 30.

guess, err := strconv.Atoi(input)

The next step we need to convert the input string to an integer so that we can compare it numerically to the secretNumber variable created in the last section.

To convert a string to an integer in Go, The Atoi() method from the strconv package is employed. This method attempts to convert the provided string into an integer and returns an integer and an error which are assigned to guess and err respectively.

if err != nil {
	fmt.Println("Invalid input. Please enter an integer value")
	return
}

The usage of Atoi() can easily result in an error which must be handled appropriately. For example, if the player’s input contained an alphabet, there would be no way to convert it to an integer. Therefore we need to stop the program and remind the player to provide an integer input.

fmt.Println("Your guess is", guess)

But if there is no error, the player’s guess will be printed on the screen.

Save the file and run the program using go run main.go. You should get a similar output to the one below:

$ go run main.go
45
Guess a number between 1 and 100
Please input your guess
20
Your guess is 20

Compare the player’s guess to the secret number

Now that we have both the player’s guess, and the secret number, it’s time to compare them. We’ll provide feedback to the player if the guess is higher or lower than the secret number. If the guess is equal to the secret number, we’ll print out a congratulatory message and exit the program.

Add the following code to the end of your main function below the line that prints the guess variable:

main.go
//...
func main() {
  // ...

	if guess > secretNumber {
		fmt.Println("Your guess is bigger than the secret number. Try again")
	} else if guess < secretNumber {
		fmt.Println("Your guess is smaller than the secret number. Try again")
	} else {
		fmt.Println("Correct, you Legend!")
	}
}

Run the program a few times to verify that the code works as expected.

$ go run main.go
The secret number is 72
Guess a number between 1 and 100
Please input your guess
65
Your guess is 65
Your guess is smaller than the secret number. Try again
$ go run main.go
The secret number is 85
Guess a number between 1 and 100
Please input your guess
93
Your guess is 93
Your guess is bigger than the secret number. Try again
$ go run main.go
The secret number is 78
Guess a number between 1 and 100
Please input your guess
78
Your guess is 78
Correct, you Legend!

Our program mostly works but the player can only input one guess, and the program exits regardless of whether the guess is correct or not. Let’s change this behaviour so that the player is able to continue guessing until the correct number is guessed. We will also inform the player of the number of attempts made before winning the game.

Change your main function as shown below:

main.go
func main() {
	min, max := 1, 100
	rand.Seed(time.Now().UnixNano())
	secretNumber := rand.Intn(max-min) + min
	fmt.Println("The secret number is", secretNumber)

	fmt.Println("Guess a number between 1 and 100")
	fmt.Println("Please input your guess")

	attempts := 0
	for {
		attempts++
		reader := bufio.NewReader(os.Stdin)
		input, err := reader.ReadString('\n')
		if err != nil {
			fmt.Println("An error occured while reading input. Please try again", err)
			continue
		}

		input = strings.TrimSuffix(input, "\n")

		guess, err := strconv.Atoi(input)
		if err != nil {
			fmt.Println("Invalid input. Please enter an integer value")
			continue
		}

		fmt.Println("Your guess is", guess)

		if guess > secretNumber {
			fmt.Println("Your guess is bigger than the secret number. Try again")
		} else if guess < secretNumber {
			fmt.Println("Your guess is smaller than the secret number. Try again")
		} else {
			fmt.Println("Correct, you Legend! You guessed right after", attempts, "attempts")
			break
		}
	}
}

We created a for loop above, and moved all the code below the line that prompts the user to input a guess into it.

In Go, using a for loop without a loop condition creates an infinite loop, and the only way to break out of this sort of loop is by using the break keyword which we’re doing when the player guesses correctly. This also causes the program to exit.

Notice the attempts variable declaration just before the loop. It’s how we keep track of the number of times a player had to guess before making a correct guess. When game is won, the number of attempts is printed in the congratulatory message.

Also note that the error handling from the Atoi() and ReadString() methods have been changed slightly to use the continue keyword in place of return to ensure that loop skips to its next iteration when an invalid input is received instead of exiting.

If you run the program now, you’ll be able to play the game and enter several guesses without the program exiting.

$ go run main.go
The secret number is 20
Guess a number between 1 and 100
Please input your guess
26
Your guess is 26
Your guess is bigger than the secret number. Try again
Please input your guess
22
Your guess is 22
Your guess is bigger than the secret number. Try again
Please input your guess
20
Your guess is 20
Correct, you Legend! You guessed right after 3 attempts

Let’s wrap up the game by deleting the line that prints out the secretNumber to the screen. The final code is shown below:

main.go
package main

import (
	"bufio"
	"fmt"
	"math/rand"
	"os"
	"strconv"
	"strings"
	"time"
)

func main() {
	min, max := 1, 100
	rand.Seed(time.Now().UnixNano())
	secretNumber := rand.Intn(max-min) + min

	fmt.Println("Guess a number between 1 and 100")
	fmt.Println("Please input your guess")

	attempts := 0
	for {
		attempts++
		reader := bufio.NewReader(os.Stdin)
		input, err := reader.ReadString('\n')
		if err != nil {
			fmt.Println("An error occured while reading input. Please try again", err)
			continue
		}

		input = strings.TrimSuffix(input, "\n")

		guess, err := strconv.Atoi(input)
		if err != nil {
			fmt.Println("Invalid input. Please enter an integer value")
			continue
		}

		fmt.Println("Your guess is", guess)

		if guess > secretNumber {
			fmt.Println("Your guess is bigger than the secret number. Try again")
		} else if guess < secretNumber {
			fmt.Println("Your guess is smaller than the secret number. Try again")
		} else {
			fmt.Println("Correct, you Legend! You guessed right after", attempts, "attempts")
			break
		}
	}
}

Wrap up

Congratulations! You’ve successfully created a guessing game in Go and learnt many new concepts such as variables, loops, functions, control flow, error handling and more along the way.

If you have any questions, please leave a comment below and I’ll get back to you. Thanks for reading!