In this blog we will be developing a quiz game which will eventually help us understand the concept of go routines. For this program, we will need a CSV file containing questions and answers of the following format:
5+5,10
10+5,15
12+3,15
14+9,23
5+6,11
10+6,16
12+4,16
5+1,6
10+0,10
12+2,14
14+10,24
5+14,19
10+4,14
12+20,32
1+4,5
In order to pass a CSV file while executing the program we will consume the flag
package. This package will help us to take arguments from CLI.
csvFilename := flag.String("csv", "problems.csv", "a csv file for the format of 'question,answer'")
Here first, second & third parameter defines the flag name, default value, and usage respectively. flag.string()
returns the address of the string variable that stores the value. After getting the csvFilename from CLI we need to parse the value and open the CSV file.
flag.Parse()
file, err := os.Open(*csvFilename)
if err != nil {
exit(fmt.Sprintf("Failed to open the csv file: %s", *csvFilename))
}
After opening the CSV file we need to read through it, therefore we will be using encoding/csv
package.
r := csv.NewReader(file)
lines, err := r.ReadAll()
if err != nil {
exit("Failed to parse the CSV file")
}
Here we, have assigned a reader to the file, and r.ReadAll()
iterates over the CSV file and returns slice of slices , as shown below.
[[5+5 10] [10+5 15] [12+3 15] [14+9 23] [5+6 11] [10+6 16] [12+4 16] [5+1 6] [10+0 10] [12+2 14] [14+10 24] [5+14 19] [10+4 14] [12+20 32] [1+4 5]]
But we want the data in below format , therefore we will make another slice of objects by writing a custom function.
type problem struct {
q string
a string
}
problems := parseLines(lines)
func parseLines(lines [][]string) []problem {
ret := make([]problem, len(lines))
for i, line := range lines {
ret[i] = problem{
q: line[0],
a: strings.TrimSpace(line[1]),
}
}
return ret
}
Now we are ready to take input and calculate results. Easy stuff, define and initialise correct
variable to keep track of correct answers, run a loop over problems
slice and take input from user, check if it’s correct on not and update the correct
variable accordingly.
correct := 0
for i, p := range problems {
fmt.Printf("Problem #%d: %s = \n", i+1, p.q)
if answer == p.a {
correct++
}
}
fmt.Printf("You scored %d out of %d. \n", correct, len(problems))
Holla ! Quiz game ready is ready but , there’s something missing and that’s timer. When timer runs out, game automatically quits. We will be implementing timer using time
package & go routines
.
Let’s include timer as a flag
csvFilename := flag.String("csv", "problems.csv", "a csv file for the format of 'question,answer'")
+ timeLimit := flag.Int("limit", 30, "The time limit for the quiz in seconds")
flag.Parse()
problems := parseLines(lines)
+ timer := time.NewTimer(time.Duration(*timeLimit) * time.Second)
After parsing the CSV files and generating the problems slice we should start our timer. Here , NewTimer
function creates a new Timer that will send the current time on its channel
after provided duration d.
In order to work with go routines we need to know about channels. So, channels are passage through which go routines can talk. They are basically typed conduit. We send and receive data from one go routine to another using these channels.
There are few types of channel, that is out of scope of this blog but do check this out golang-channels-blog.
In our for loop which iterates over questions
we need to create an answer channel
which will hold answers from CLI and implement the go routine which will be responsible to take input and send it to the answer channel.
correct := 0
for i, p := range problems {
fmt.Printf("Problem #%d: %s = \n", i+1, p.q)
answerCh := make(chan string)
go func() {
var answer string
fmt.Scanf("%s\n", &answer)
answerCh <- answer
}()
// ----------
}
After this we need to use select block to check if timer runs out or user has given correct answer. The select
statement allows go routine to wait on multiple communication operations.
Therefore, we would be checking if timer
channel responds or answer
channel responds.
For understanding select block read through this select-block article
for i, p := range problems {
fmt.Printf("Problem #%d: %s = \n", i+1, p.q)
answerCh := make(chan string)
go func() {
var answer string
fmt.Scanf("%s\n", &answer)
answerCh <- answer
}()
+ select {
+ case <-timer.C:
+ fmt.Printf("You scored %d out of %d. \n", correct, len(problems))
+ return
+ case answer := <-answerCh:
+ if answer == p.a {
+ correct++
+ }
+ }
}
This would look similar to switch case from javascript but it performs bit differently here.
Once the command reaches select statement it waits for any of the two cases to get ready to be executed. If answers
go routine, sends data to answer channel it is received here in main routine and assigned to answer variable in case 2 of select statement.
If user is not answering to the question and sitting idle, the answer
channel won’t be able to send any data to it’s channel and the timer
channel will send the current time in time
channel and it will be received here in main routine in case 1. Program quits and display the result.
If user keeps on answering within the time limit, loop exits naturally and result is printed on the console.
Let’s understand Arrows->
answerCh <- answer
=> Here the answer value is being sent to the answer channel
answer := <- answerCh
=> Here answer value from the answer channel is being recieved and assigned to answer variable.
Find source code here https://github.com/whiletrueee/gophercises
This blog post and the quiz game project are inspired by the exercises from Gophercises, a fantastic resource for learning Go through practical, hands-on projects. Gophercises has been instrumental in helping me understand and apply various Go programming concepts, including goroutines and concurrency.
Happy Coding!