Number Guessing Game
This tutorial will guide you to the basic programming of Go by creating a number guessing application.
Overview
- Go programmers keep all their Go code in a single workspace.
- A workspace contains multiple Go projects.
- Each project contains many packages.
- A package contains multiple Go source files in a single directory.
- The path to the package directory determines its import path.
Workspace - A Golang Concept
- A workspace is a directory with two sub-directories at its root
- src - Contains Go source files
- bin - Contains executable commands
This directory usually is
$HOME/go.
Example of how a workspace looks like
bin/
- mycommand
- yourcommand
src/
- golang.org/x/example/
- .git/
- pkg/
- main.go
- github.com/yourusername/yourproject/
- .git/
- pkg/
- main.go
... (more projects are omitted) ...
There are 2 binaries inside the bin/ directory which are mycommand and yourcommand.
The source code lies inside the src/ directory.
GOPATH environment variable
GOPATH specifies the location of your Go workspace.
- For Unix, it defaults to
$HOME/go - For Windows, it defaults to
C:\Users\YourName\go
You can view GOPATH environment variable by typing the following command.
go env GOPATH
Import paths
A Go package has a unique import path. The import path corresponds to its location inside a workspace or in a remote repository.
If you keep your package in a remote repository (like GitHub). Its import path would be github.com/user/yourpackage.
Please keep in mind that you don't need to upload your code to a remote repository. It's a good habit to organize your code as if you will publish it someday.
Setting up a number guessing app
Open a terminal and create a new directory named guessthenumber.
mkdir guessthenumber
Change your current directory to guessthenumber.
cd guessthenumber
Initialize a Go module by executing the following command.
go mod init github.com/example/guessthenumber
Please note that
github.com/example/guessthenumberis just a mock URL. In real projects, we'll use our remote repository URL instead. For example,github.com/apiplustech/core.
After that, create a new file called main.go at the root directory of this project.
touch main.go
Then, open the file and insert the following code.
package main
func main() {
}
The first line is package main. This defines the package name when it is imported in another project. On line 3~5 is the main function of the program. Go will always find the func main() function and use it as the program entrypoint.
Say hello
Let's code your first Go program. To print to the terminal, insert the following code.
package main
import "fmt"
func main() {
fmt.Println("Sup, world!")
}
On line 3, a package named "fmt" is imported. This package contains utility functions to format strings and print values to the terminal.
On line 6, we call a function from the "fmt" package called "Println". This function will print the received arguments to the terminal.
At this point, you can see that Go code doesn't need a semicolon (;) at the end of each statement.
To run the code, please type the following command.
go run main.go
You should see the following result.
$ ~/go/src/github.com/yourusername/guessthenumber > go run main.go
Sup, world!
Variables
In Go, you can declare a variable in two ways.
- First, defining using the word
var. The syntax isvar variableName variableType. - Second, defining using the shorthand operator
:=. The syntax isvariableName := <values>.
Let's start making an number guessing app. We'll first create a new variable called guess. Please insert the following line in main.go.
var guess int
Note that argument name is placed before argument data type. This is the common convention that appears in modern programming language like Kotlin, Rust and Go. If you want to know why, Here is the article for you. go declaration syntax
If you want to use the shorthand operator, you can declare a variable like this too. Go will implicitly assign variable type and value automatically.
guess := 0
Receiving user input
Function Scan() inside "fmt" package is used to parse user input from the terminal.
fmt.Scan(&guess)
The ampersand sign & is called "dereference operator" or "address of operator". As the name implied, the operator is used to get the address of the variable, aka "pointer". The Scan() function takes a pointer as an argument because it needs to modify the value of that variable.
Generating a random number
Go package called "math/rand" is used for generating random number for our number guessing game.
seed := time.Now().UnixNano()
source := rand.NewSource(seed)
randInstance := rand.New(source)
goal := randInstance.Intn(cap)
Here is the explaination for the above snippet.
seed := time.Now().UnixNano(): This line gets the current Unix timestamp in nanoseconds and assigns it to the variable seed. The Unix timestamp is the number of seconds since January 1, 1970 UTC.source := rand.NewSource(seed): This line creates a new source of randomness from the seed value. The rand.NewSource() function takes a seed value as its argument and returns a new source of randomness.randInstance := rand.New(source): This line creates a new random number generator from the source of randomness. The rand.New() function takes a source of randomness as its argument and returns a new random number generator.goal := randInstance.Intn(cap): This line generates a random integer between 0 and cap (inclusive). The rand.Intn() function takes a maximum value as its argument and returns a random integer between 0 and the maximum value (exclusive).
In summary, the code seed := time.Now().UnixNano() line by line does the following:
- Sets the seed value for the random number generator.
- Creates a new source of randomness.
- Creates a new instance of the random number generator.
- Generates a random integer between 0 and
cap(exclusive).
Function
The key to create something big is to abstract chunks of granular details, bunch them together into one piece, and then combine the piece together to make even bigger stuff. Declaring function could help you achieve that.
Here is the function declaration example for randInt().
// This function returns random integer from 0 to cap
func randInt(cap int) int {
seed := time.Now().UnixNano()
source := rand.NewSource(seed)
randInstance := rand.New(source)
goal := randInstance.Intn(cap + 1)
return goal
}
The function signature also follows name-before-type style like variable declaration.
Conditional Statement
Go provide two ways for flow controlling, that is.
if-elseswitch
if-else in Go is similar to others, but the parenthesis around condition is opted out.
if guess == goal {
fmt.Println("Yay!, you got it right.")
} else if guess > goal {
fmt.Println("Nay, your guess is too large.")
} else {
fmt.Println("Nah, your guess is too small.")
}
switch in Go is much of a different than other c-style programming language because the break; statement is opted out. Also, case statement in Go can be non-constant value.
switch {
case guess == goal:
fmt.Println("Yay!, you got it right.")
case guess > goal:
fmt.Println("Nay, your guess is too large.")
default:
fmt.Println("Nah, your guess is too small.")
}
Loop
The Go programming language is aims to be short and concise programming language. So Go only supports for loop, but you can replicate while loop using for like this.
infiniteLoop:
for {
fmt.Scan(&guess)
switch {
case guess == goal:
fmt.Println("Yay!, you got it right.")
break infiniteLoop
case guess > goal:
fmt.Println("Nay, your guess is too large.")
default:
fmt.Println("Nah, your guess is too small.")
}
}
The infiniteLoop: label at line 1 is needed because Go will confused when you call break statement in line 7. Go compiler doesn't know whether you want to break the outer loop or the switch control flow.
If you let the player guess for four attempts at max, then you could do the following.
loop:
for try := 0; try < 4; try++ {
fmt.Scan(&guess)
switch {
case guess == goal:
fmt.Println("Yay!, you got it right.")
break loop
case guess > goal:
fmt.Println("Nay, your guess is too large.")
default:
fmt.Println("Nah, your guess is too small.")
}
}
Putting them all together
Combines all snippet we've written, we get.
package main
import (
"fmt"
"math"
"math/rand"
"time"
)
// This function returns random integer from 0 to cap
func randInt(cap int) int {
seed := time.Now().UnixNano()
source := rand.NewSource(seed)
randInstance := rand.New(source)
goal := randInstance.Intn(cap + 1)
return goal
}
func main() {
cap := 0
fmt.Print("Hi, this is a number guessing game. Please enter the upper bound:")
fmt.Scan(&cap)
goal := randInt(cap)
guess := 0
maximumAttempts := int(math.Ceil(math.Log2(float64(cap))))
for try := 0; try < maximumAttempts; try++ {
fmt.Print("Please Enter your guess:")
fmt.Scan(&guess)
switch {
case guess == goal:
fmt.Println("Yay!, you got it right.")
return
case guess > goal:
fmt.Println("Nay, your guess is too large.")
default:
fmt.Println("Nah, your guess is too small.")
}
}
fmt.Println("You ran out of attempts, Game Over.")
}
Where to go next?
Great!. You just finished your very first Go project. However, we have just learnt the surface of the Go programming language. If you want to take a comprehensive tour on Go, please navigate to the tour of go.
Finishing Touch
The number of maximum attempts is carefully chosen so that the player can always win if he/she is a perfect logician. Can you figure out the way to always win this game?