Enhance Your Go Skills: Develop a Quiz Game Using Go Routines

Enhance Your Go Skills: Develop a Quiz Game Using Go Routines

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!