A well-written program has a well-organized structure.
In Go, interfaces describe behavior, and are a key abstraction. A primary Go abstraction for a stream of data is the io.Reader interface. It allows us to substitute any type that satisfies the io.Reader interface, for example, os.File, bytes.Buffer, bytes.Reader, strings.Reader, bufio.Reader, and so on.
Consider reading the Gophercises Quiz Game problems.csv.
A problem is
type problem struct {
question, answer string
}
For the io.Reader interface we have
func readProblems(r io.Reader) ([]problem, error) {
rv := csv.NewReader(r)
rv.FieldsPerRecord = 2
rv.ReuseRecord = true
var problems []problem
for {
rec, err := rv.Read()
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
problems = append(problems,
problem{
question: rec[0],
answer: rec[1],
},
)
}
return problems[:len(problems):len(problems)], nil
}
For reading a file, we use os.File to satisfy the io.Reader interface.
func loadProblems(filename string) ([]problem, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
problems, err := readProblems(f)
if err != nil {
return nil, err
}
return problems, nil
}
For testing, we use bytes.Reader to satisfy the io.Reader interface.
var readProblemsTests = []struct {
line string
want problem
}{
{
line: "5+5,10\n",
want: problem{question: "5+5", answer: "10"},
},
// ...
}
func TestReadProblems(t *testing.T) {
var file []byte
for _, tt := range readProblemsTests {
file = append(file, tt.line...)
}
r := bytes.NewReader(file)
got, err := readProblems(r)
if err != nil {
t.Errorf("readProblems: got %v, want %v", err, nil)
}
// ...
}
The following are sample data, program, and test to illustrate the usage of the readProblems function for the Gophercises Quiz Game.
readcsv.csv
:
5+5,10
1+1,2
8+3,11
1+2,3
8+6,14
3+1,4
1+4,5
readcsv.go
:
package main
import (
"encoding/csv"
"fmt"
"io"
"log"
"math/rand"
"os"
"time"
)
type problem struct {
question, answer string
}
func (p problem) String() string {
return fmt.Sprintf(
"{question: %q, answer: %q}",
p.question, p.answer,
)
}
func readProblems(r io.Reader) ([]problem, error) {
rv := csv.NewReader(r)
rv.FieldsPerRecord = 2
rv.ReuseRecord = true
var problems []problem
for {
rec, err := rv.Read()
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
problems = append(problems,
problem{
question: rec[0],
answer: rec[1],
},
)
}
return problems[:len(problems):len(problems)], nil
}
func loadProblems(filename string) ([]problem, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
problems, err := readProblems(f)
if err != nil {
return nil, err
}
return problems, nil
}
func shuffleProblems(l []problem) {
rand.Seed(time.Now().UnixNano())
rand.Shuffle(len(l),
func(i, j int) {
l[i], l[j] = l[j], l[i]
},
)
}
func printProblems(problems []problem) {
fmt.Println(len(problems), "problems")
for _, p := range problems {
fmt.Println(p.String())
}
}
func main() {
// TODO: use flag
filename := `readcsv.csv`
problems, err := loadProblems(filename)
if err != nil {
log.Fatal(err)
}
// TODO: use flag
shuffle := true
if shuffle {
shuffleProblems(problems)
}
printProblems(problems)
}
Output:
$ go run readcsv.go
7 problems
{question: "1+4", answer: "5"}
{question: "5+5", answer: "10"}
{question: "8+6", answer: "14"}
{question: "1+1", answer: "2"}
{question: "3+1", answer: "4"}
{question: "1+2", answer: "3"}
{question: "8+3", answer: "11"}
$
readcsv_test.go
:
package main
import (
"bytes"
"testing"
)
var readProblemsTests = []struct {
line string
want problem
}{
{
line: "5+5,10\n",
want: problem{question: "5+5", answer: "10"},
},
{
line: "1+1,2\n",
want: problem{question: "1+1", answer: "2"},
},
}
func TestReadProblems(t *testing.T) {
var file []byte
for _, tt := range readProblemsTests {
file = append(file, tt.line...)
}
r := bytes.NewReader(file)
got, err := readProblems(r)
if err != nil {
t.Errorf("readProblems: got %v, want %v", err, nil)
}
if len(got) != len(readProblemsTests) {
t.Errorf("problems: got %v, want %v", len(got), len(readProblemsTests))
}
for i, tt := range readProblemsTests {
if got[i] != tt.want {
t.Errorf("problem: got %v, want %v", got[i], tt.want)
}
}
}
Output:
$ go test readcsv_test.go readcsv.go -v
=== RUN TestReadProblems
--- PASS: TestReadProblems (0.00s)
PASS
$
ADDENDUM
Comment: Regarding the readcsv.go file, in the readProblems function
why did you return problems[:len(problems):len(problems)] instead of
problems? Also, when closing the file in the loadProblems function,
does the defer wait until the loadProblems function has returned or
until the main function is done? – Anthony Gedeon
The Go Programming Language
Specification
Appending to and copying
slices
The variadic function append appends zero or more values x to s of
type S, which must be a slice type, and returns the resulting slice,
also of type S.
If the capacity of s is not large enough to fit the additional values,
append allocates a new, sufficiently large underlying array that fits
both the existing slice elements and the additional values. Otherwise,
append re-uses the underlying array.
Slice expressions
Full slice expressions
For an array, pointer to array, or slice a (but not a string), the
primary expression
a[low : high : max]
constructs a slice of the same type, and with the same length and
elements as the simple slice expression a[low : high]. Additionally,
it controls the resulting slice's capacity by setting it to max - low.
Only the first index may be omitted; it defaults to 0.
Hyrum's Law
An observation on Software Engineering
Put succinctly, the observation is this:
With a sufficient number of users of an API,
it does not matter what you promise in the contract:
all observable behaviors of your system
will be depended on by somebody.
Obligatory XKCD: https://xkcd.com/1172/
We should avoid exposing a particular API (function or method) implementation. This particular implementation of readProblems uses the append built-in function, which may create excess capacity. Other implementations may return the exact capacity. By Hyrum's Law, we ensure that this implementation, like other implementations, returns the exact capacity:
problems[:len(problems):len(problems)]
The Go Programming Language
Specification
Defer statements
A "defer" statement invokes a function whose execution is deferred to
the moment the surrounding function returns, either because the
surrounding function executed a return statement, reached the end of
its function body, or because the corresponding goroutine is
panicking.
It's important to ensure that scarce resources, like file handles, are released as soon as they are no longer needed. The defer statement to close the file in the loadProblems function is deferred to the moment the loadProblems function returns.