crazyc4t's blog

Gophercise #1 "Quiz" Walkthrough!

What’s up guys! It’s crazyc4t here, once again, I’m sorry for not being fully active these past couple of weeks, but I’m here for you guys and I’m bringing you the best way to learn golang, and that is coding with it!

What is Gophercises?

Gophercises is a webpage made by Jon Calhoun, an experienced web developer with go, and we takes us through a series of exercises to code with golang making us improve our way to code in go by exploring topics like channels, concurrency, methods, using the standard library and more! The best thing of all is that is free! So you won’t lose anything trying it out, instead you will have tons of fun like I did!

In gophercises there are a lot of exercises ranging from an order from 1 through 20, and of course they increase in difficulty, the exercises are solved by the author in case you get lost so you can get a lead, but also the author adds some additional exercises to the exercise, making you go for the extra mile.

The “Quiz” Exercise

This exercise is based on the idea of a quiz, hence the name for it, it is a program that reads questions and answers from a csv file and count how many questions did the user got right or wrong, and ask the following questions no matter if the answer was right or wrong, all of these fields are customizable through flags, the format of the csv is as follows:

5+5,10
7+3,10
1+1,2
8+3,11
1+2,3
8+6,14
3+1,4
1+4,5
5+1,6
2+3,5
3+3,6
2+4,6
5+2,7

Printing at the end of the program the results of the user, after completing that, the second part is to add a timer to the exercise, being the default flag 30 seconds, but it needs to be customizable to the end user, and without any delay, cut the quiz if the time runs out and don’t wait if the user is going to answer or not the question, so ungiven answers are counted as wrong answers.

Bonus Exercises

Here’s the official repo for the quiz exercise with detailed instructions and solutions from various students: https://github.com/gophercises/quiz

I suggest that you first try it out on your own before reading my solution, give your best shot! 頑張って!

The solution

I will explain the code detail by detail, so no worries about it!

Import the required packages

We will be using:

 1package main
 2
 3import (
 4	"encoding/csv"
 5	"flag"
 6	"fmt"
 7	"log"
 8	"math/rand"
 9	"os"
10	"strings"
11	"time"
12
13	"github.com/zakaria-chahboun/cute"
14)

Structs for handling flags

We create a struct of the type Exercise that has a question and an answer, and the struct Flag that has a Csv, Limit and Shuffle.

We create a function to check our errors that we will use later, and to parse the flags we will save each data received in a varible of type “Flag”, and return it’s pointer since we used the dereferenced variable (the memory address)

 1// Struct of the exercise
 2type Exercise struct {
 3	Question, Answer string
 4}
 5
 6// Struct of the flags
 7type Flags struct {
 8	Csv     string
 9	Limit   int
10	Shuffle bool
11}
12
13// Check my errors
14func checkErr(description string, err error) {
15	if err != nil {
16		log.Println(description, err)
17	}
18}
19
20// Save each flag result in an variable type Flag, parse it and return it's pointer.
21func parsing() Flags {
22	options := new(Flags)
23	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
24	flag.StringVar(&options.Csv, "csv", "problems.csv", "Insert the filepath of the csv file.")
25	flag.IntVar(&options.Limit, "limit", 30, "Insert time limit of the quiz")
26	flag.BoolVar(&options.Shuffle, "shuffle", false, "Wether the order of the csv file is shuffled or not")
27	flag.Parse()
28	return *options
29}

Reading the csv and creating the shuffle feature

In the read function create a varible that will store the csv file from the flag, defer it until we no longer need it, create a new reader and read all the fields at once, after that check if there were any errors, and return the whole reading that is a map of type string.

In the serialize function read the whole data and “serialize” it to a type Exercise, meaning reading the whole data and assign each field if it belongs to the answer or question field.

After that create the shuffle function where it receives a slice of data type Exercise, where it takes the length of the slice, and the indexes of the slice, being i and j, changing the index position of each value stored in the slice, if we remember correctly the Exercise data type stores a question and an answer, so when using the shuffle function, it will change the position of an exercise in the exercises slice, not a question or answer, but the whole exercise.

 1// Read csv all at once
 2func read() [][]string {
 3	csvFile, err := os.Open(parsing().Csv)
 4	checkErr("Error opening file", err)
 5
 6	defer csvFile.Close() // Do not close it until finished
 7
 8	r := csv.NewReader(csvFile) // New reader
 9	r.FieldsPerRecord = -1      // Read all fields, do not expect a fixed number
10	reading, err := r.ReadAll() // Data read
11	checkErr("Error reading file", err)
12	return reading // Give the data read as a result
13}
14
15// From the data read, create a slice that supports values type Exercise
16func serialize(data [][]string) []Exercise {
17	var Exercises []Exercise
18	var exercise Exercise
19	for _, record := range data {
20		exercise.Question = record[0]
21		exercise.Answer = record[1]
22		Exercises = append(Exercises, exercise)
23	}
24	return Exercises
25}
26
27// From an slice of type Exercise, shuffle the order of the exercises
28func shuffleOrder(exercises []Exercise) {
29	rand.Shuffle(len(exercises), func(i, j int) {
30		exercises[i], exercises[j] = exercises[j], exercises[i]
31	})
32}

The cute output

Using the cute library, we can create a list that will print out the quiz results, including:

All of this being stored in the quizResults function.

1// Creates cute list that prints quiz results
2func quizResults(correct, questions int) {
3	results := cute.NewList(cute.BrightBlue, "Quiz Results!")
4	results.Addf(cute.BrightGreen, "Correct Answers: %d", correct)
5	results.Addf(cute.BrightRed, "Incorrect Answers: %d", questions-correct)
6	results.Addf(cute.BrightPurple, "Total Questions: %d", questions)
7	results.Print()
8}

The func main

Here it gets complex, so let’s break it down step by step:

  1. Create timer, serialize the whole csv reading, check if the user wants shuffling.
  2. Range the exercises, and print it to the user, receive the answer in a channel, avoiding incorrect time.
  3. Use the concurrent select keyword, where if the timer channel runs out, it will print the results and exit.
  4. If an answer is received, check if it is correct or not, count it and store it.
  5. If the user ended the quiz before the time, print the results and exit the program.
 1func main() {
 2	var reply string
 3	var correct, questions int
 4
 5	timer := time.NewTimer(time.Duration(parsing().Limit) * time.Second) // Timer duration
 6	Exercises := serialize(read())                                       // Slice with exercises
 7
 8	if parsing().Shuffle {
 9		shuffleOrder(Exercises) // if true, shuffle order
10	}
11
12	for i := range Exercises { // iterate over the exercises slice
13		cute.Println("Exercise", Exercises[i].Question, "= ")
14		answerCh := make(chan string) // create a channel where it will receive the answer
15		go func() {                   // while the timer run, listen to the answer
16			fmt.Scanln(&reply)
17			answerCh <- reply // store the scanned reply in the channel
18		}()
19		select {
20		case <-timer.C: // when the timer runs out
21			fmt.Println("") // print new line for pretty output
22			cute.Println("Time is over!")
23			quizResults(correct, questions) // print cute quiz result
24			os.Exit(0)                      // exit with success
25		case reply := <-answerCh: // store the answer channel in the reply variable
26			if strings.TrimSpace(reply) == Exercises[i].Answer { // trim spaces of the reply to avoid incorrect replies
27				cute.Println("Correct!")
28				correct++ // count the correct replies
29			} else {
30				cute.Println("Incorrect!", "Your reply:", strings.TrimSpace(reply), "Answer:", Exercises[i].Answer) // if incorrect, show the correct answer
31			}
32			questions++ // count the questions
33		}
34	}
35	quizResults(correct, questions)
36}

Gophercise #1 Solved!

There you have it homeboi!! Hope you enjoyed my solution of the gophercise, I will leave my github repo where I stored the solution if you want to check the whole code out, give a star or fork it, I’m so glad to write blog posts again so I will be here soon, expect tons of blog posts coming!

Github repo: https://github.com/crazyc4t/quiz

#Go