Building a CLI Quiz Application in Go
Introduction
In this project, we'll be building a CLI tool—a quiz application that reads quiz questions from a customizable CSV file and outputs the number of correct and incorrect answers from the user within a customizable time limit. My strategy for this challenge is to utilize available resources on the internet (excluding the solution) to build the application efficiently.
Tools and Packages
Go provides a flag
package for handling command-line arguments, and we'll also be using the encoding/csv
and os
packages to read CSV files. After a quick search, it's clear that Go's flag
package is well-suited for our needs.
Reading CSV Records
I've created a function to read records from a CSV file and return a [][]string
. Here's the code:
// Reads the CSV file and returns its content as a slice of slices of strings
func ReadCSV(path string) ([][]string, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
reader := csv.NewReader(file)
records, err := reader.ReadAll()
if err != nil {
return nil, err
}
return records, nil
}
Command-line Arguments with flag
Package
To read command-line arguments, I'm using the flag
package. We specify how we want our flags to be present and then instruct the package to parse the arguments, updating the referenced variables in our program.
Implementing the Quiz Logic
With a list of records from the CSV file and user-specified flags, I iterate through each record to present questions to the user.
Here is the `main()` function:
func main() {
var path string
var duration int
flag.StringVar(&path, "filepath", "", "Specify a CSV file containing records")
flag.IntVar(&duration, "duration", 30, "Specify a quiz duration")
flag.Parse()
records, err := ReadCSV(path)
if err != nil {
log.Fatal("Error while reading the file", err)
}
fmt.Println("Successfully! Parsed the Questions file")
for i := 3; i > 0; i -= 1 {
fmt.Printf("Starting the Quiz in %v...\n", i)
time.Sleep(time.Second)
}
fmt.Print("\n\n\n")
var correctCount int
var incorrectCount int
for i, record := range records {
fmt.Printf("Problem #%v: %v - ", i+1, record[0])
var userInput string
fmt.Scan(&userInput)
if userInput == record[1] {
correctCount += 1
} else {
incorrectCount += 1
}
}
fmt.Println("You have successfully completed the quiz game.")
fmt.Printf("Your Score: %v out of %v", correctCount, correctCount+incorrectCount)
}
Now we have a basic quiz application ready but, we still don't have the functionality of making the time limit of quiz customizable.
I am using channels to block the main from exiting until the user alloted time for the quiz is elapsed.
doneC := make(chan struct{})
// kind of a timer which sends of a signal to doneC channel to
// which another go routinte is listening for stopping the quiz
go func() {
time.Sleep(time.Duration(duration * int(time.Second)))
doneC <- struct{}{}
}()
The `main()` function is modified like this:
func main() {
var path string
var duration int
flag.StringVar(&path, "file", "problems.csv", "Specify a CSV file containing records")
flag.IntVar(&duration, "limit", 30, "Specify a quiz duration")
flag.Parse()
records, err := ReadCSV(path)
shuffle(records)
if err != nil {
log.Fatal("Error while reading the file", err)
}
fmt.Println("Successfully! Parsed the Questions file")
for i := 3; i > 0; i -= 1 {
fmt.Printf("Starting the Quiz in %v...\n", i)
time.Sleep(time.Second)
}
fmt.Print("\n\n\n")
var correctCount int
var incorrectCount int
doneC := make(chan struct{})
// kind of a timer which sends of a signal to doneC channel to
// which another go routinte is listening for stopping the quiz
go func() {
time.Sleep(time.Duration(duration * int(time.Second)))
doneC <- struct{}{}
}()
for i, record := range records {
select {
case <-doneC:
goto end
default:
fmt.Printf("Problem #%v: %v - ", i+1, record[0])
var userInput string
fmt.Scan(&userInput)
if userInput == record[1] {
correctCount += 1
} else {
incorrectCount += 1
}
}
}
end:
fmt.Println("You have successfully completed the quiz game.")
fmt.Printf("Your Score: %v out of %v", correctCount, correctCount+incorrectCount)
}
You can notice I have added a shuffle functionality as well. I have implemented my own function for shuffling the quiz questions each time it is run.
// This is an acceptable implementation
func shuffle(records [][]string) {
// Using the Durstenfeld Shuffle (Modern Fisher Yates Shuffle)
lastUnshuffledIdx := len(records) - 1
for i := 0; i < len(records); i++ {
j := rand.Intn(len(records) - i)
records[j], records[lastUnshuffledIdx] = records[lastUnshuffledIdx], records[j]
lastUnshuffledIdx--
}
}
You can read more about this here.
Finally, this is what I am playing with:
You can access my code on GitHub here.
Conclusion
This marks the end of our journey to build a CLI quiz application in Go. Stay tuned for more updates and keep the puns coming!
Subscribe to my newsletter
Read articles from Aditya Kumar Singh directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Aditya Kumar Singh
Aditya Kumar Singh
Passionate and driven undergrad senior pursuing bachelors in computer science with a focus on making a tangible impact in technology. Knowledge in diverse domains including Blockchain, Cloud Computing, Web Development, ML, and academic research. Worked as an SDE backend intern at Mercari Tokyo on microservices architecture with Go, gRPC, Kubernetes, Terraform and Datadog in an agile team environment. Also worked with Go and gRPC as an LFX Mentee at Hyperledger, where I contributed to the integration of BDLS (a new BFT consensus protocol) into the ordering service of Hyperledger Fabric. Eager collaborator with a knack for problem-solving and continuous learner mindset. Let's connect and explore opportunities to collaborate! 🚀