Master Go (Exersizes) - part 1

Time To Practice: if, switch, and for

Time To Practice: If, Switch, And For

The “Time To Practice” lectures contain mini-exercises for recapitulating the previous lecture(s) and strengthen the knowledge you just learned.

The programming tasks are intentionally not very complex. If they feel too easy for you, keep in mind that the tasks first and foremost shall help you getting a feeling for the language - after all, practicing is the most efficient way of internalizing what you have learned.

So here we go:

Part 1: if and for

What to do

Read the task description, create a new file named collatz.go in your editor, then copy & paste the code below into, and complete the function so that it accomplishes the task.

Execute the file in a shell or command prompt by cd’ing to the directory where your collatz.go file is and running

go run collatz.go

Optional: To test if your function returns correct results, download collatz_test.go , place it into the same directory as collatz.go , and run

go test

Your collatz() function is then tested against a list of different inputs and results. This is Go’s built-in unit testing feature; you will learn more about it in section 4 of this course.

Task: Collatz Conjecture

Pick a number n > 1. Apply the following process repeatedly until n becomes 1.

  • If n is even, divide it by 2.
  • If n is odd, multiply it by 3, then add 1.

Return the number of steps needed.

In case you need the remainder of a division, use “ a % b ”.

package main
import (
    "fmt"
    "os"
    "strconv"
)
func collatz(n int) int {
    count := 0
    // Your code here!
    return count
}
func main() {
    var n int
    var err error
    if len(os.Args) > 1 {  // Read the number from the command line
        n, err = strconv.Atoi(os.Args[1])
        if err != nil {
            fmt.Println(err)
            return
        }
    } else {  // Read the number interactively
        fmt.Println("Input a number > 1: ")
        _, err := fmt.Scanf("%d", &n)
        if err != nil {
            fmt.Println(err)
            return
        }
    }
    if n <= 1 {
        fmt.Println("n must be larger than 1.")
        return
    }
    c := collatz(n)
    fmt.Println(n, "requires", c, "steps to reach 1.")
}

Part 2: switch

What to do

Similar to the Collatz Conjecture part, create a new file in your editor called fizzbuzz.go , copy and paste the code from below into that file, and implement the task. Then run

go run fizzbuzz.go

to see if your code works.

The Task: FizzBuzz

Write a program that plays the game of FizzBuzz. The rules are simple:

  • Count from 1 to a given number n.
  • Print out each number, with the following exceptions:
    • If the number is divisible by 3, print “Fizz” instead of the number.
    • If the number is divisible by 5, print “Buzz”.
    • If the number is divisible by 15, print “FizzBuzz”.

The code below reads a number from the command line (default is 50 if no number is given) and calls function fizzbuzz with that number.

Fill the body of fizzbuzz , using a for loop and a switch statement.

package main
import (
    "fmt"
    "os"
    "strconv"
)
func fizzbuzz(n int) {
    // Your code here!
}
func main() {
    n := 50
    if len(os.Args) > 1 {
        max, err := strconv.Atoi(os.Args[1])
        if err == nil {
            n = max
        }
    }
    fizzbuzz(n)
}


A Quick Intro to Using Libraries

A quick introduction to using libraries

Before we move on to look at input and output, let’s have a quick look on libraries in Go, and how to use them.

In the lecture where we built our first Go application, we learned that Go code is organized into packages . We created a package named main that represented our main program.

We also imported a package named fmt (usually pronounced as either “fumpt” or “format”). This is a library package that provides a number of functions for formatted input and output.

As you may know from other programming languages, libraries can contain external functions - those that are available to the code that uses the library - and internal functions that cannot be accessed from the outside. In most languages, keywords like public and private are used to separate public functions from private functions.

Go distinguishes between external and internal functions through spelling alone.

  • All public functions, types and variables must start with an uppercase letter.
  • All private functions, types and variables must start with a lowercase letter.

This is why

fmt.Println()

(as seen in the previous lectures) has an uppercase “P”.

We will have a more detailed look into packages in Section 4; for now it is sufficient to know that all public entities in a library package start with an uppercase letter.

Links

Language Reference: Exported identifiers

Time To Practice: Printing And Scanning

The broad range of formatting options available for printing and scanning can be overwhelming. Plus, the programming languages you know might do it differently.

Practicing is the best strategy to get familiar with Go’s format options.

For the following practice, open the page

https://golang.org/pkg/fmt/

in your browser. Here you find all available formatting placeholders (called “verbs”) that you need for the tasks.

Part 1: Printing

What to do

Inspect the following Printf() statements. Each has a comment that shows the desired output format.

package main
import "fmt"
func main() {
    // Print RGB values...
    r, g, b := 124, 87, 3
    // ...as #7c5703  (specifying hex format, fixed width, and leading zeroes)
    // Hint: don't forget to add a newline at the end of the format string.
    fmt.Printf("", r, g, b)
    // ...as rgb(124, 87, 3)
    fmt.Printf("", r, g, b)
    // ...as rgb(124, 087, 003) (specifying fixed width and leading zeroes)
    fmt.Printf("", r, g, b)
    // ...as rgb(48%, 34%, 1%) (specifying a literal percent sign)
    fmt.Printf("", r, g, b)
    // Print the type of r.
    fmt.Printf("", r)
}

Copy the code into your editor (or download printing.go from here). Then fill the format strings so that the output matches the format in the comments.

Part 2: Scanning

What to do

In the code below, str1 and str2 contain strings with some numbers to read. Complete each Sscanf statement with format string and variables to read into.

package main
import "fmt"
func main() {
    var n1, n2, n3, n4 int
    var f1 float64
    // Scan the card number.
    str1 := "Card number: 1234 5678 0123 4567"
    _, err := fmt.Sscanf(str1, "", ... )
    if err != nil {
        fmt.Println(err)
    }
    fmt.Printf("%04d %04d %04d %04d\n", n1, n2, n3, n4)
    // Scan the numeric values into a floating-point variable, and an integer.
    str2 := "Brightness is 50.0% (hex #7ffff)"
    _, err = fmt.Sscanf(str2, "", ... )
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(f1, n1)
}

Bonus task: Advanced printing

What to do

Print the same variable twice , once as a decimal value and once as a hex value, without repeating the variable in Printf ’s argument list.

Hint: Search for “Explicit argument indexes” in the fmt documentation!

package main
import "fmt"
func main() {
    n := 49374
    fmt.Printf("", n)
}

Time To Practice: strings

Time To Practice: strings

Intro

The world is full of different langauges, alphabets, and writing systems, and this is fascinating. The downside is that string handling has become more difficult than it was in the last millenium, where pure and simple ASCII was the predominant text encoding - at least in English speaking countries.

Luckily, Go comes with Unicode support out of the box. So let’s solve a text conversion task with Unicode in mind.

Your task

Write code that turns a string into an acronym. For example: Turn “Pan Galactic Gargle Blaster” into “PGGB”.

Remember that not all string functionality is unicode-aware. Inspect the unicode package from the standard library at https://golang.org/pkg/unicode/ - it may contain some of the functions you need.

package main

import (
    "fmt"
    "os"
    "strings"
    "unicode"
)

func acronym(s string) (acr string) {

    // TODO: Your code here

}

func main() {
    s := "Pan Galactic Gargle Blaster" 
    if len(os.Args) > 1 {
        s = strings.Join(os.Args, " ")
    }
    fmt.Println(acronym(s))
}

Find the solutions in stringssolution.go, attached to this lecture.

Time To Practice: Function Behavior

Task 1: Variadic functions

Write a function longest() that takes an arbitrary number of strings and prints the length of the longest one.

func longest(/* TODO */) int {
    // TODO
}

func main() {
    fmt.Println(longest("Six", "sleek", "swans", "swam", "swiftly", "southwards"))
}

Task 2: Scope

Are you sure you remember all the scope levels? Especially in and around loops…

This task is an analytical one: The code below apparently uses the same variable s all over the place! This cannot be, or can it?

Look closely, and try to identify all scope levels in this code. Then rename the variables so that each scope has its unique variable name instead of s and no shadowing occurs anymore.

func main() {
    s := "abcde"
    for _, s := range s {
        s := unicode.ToUpper(s)
        fmt.Print(string(s))
    }
    fmt.Println("\n" + s)
}

Time To Practice - Function Values and Closures

Time To Practice: Function Values and Closures

Task 1: Two closures

Intro

As you learned in the lecture about function values and closures, a closure can reference the outer function’s variables even after the outer function has terminated.

But what happens if the outer function generates and returns two closures?

Do they access the same outer variables, or does each of them get its own copy?

Your task

Copy and paste the code from below into your editor and name the file “closures.go”.

Add code to newClosure so that it returns two closures. The first one is of type func() , the second one of type func() int .

Both closures shall modify an integer variable defined in the outer function as follows:

The first closure shall just set the outer variable to 5. It returns nothing.

The second closure shall multiply the outer variable by 7 and return the value.

main() calls newClosure to create the new closures, and then calls both closures and prints out the result.

Run the code via

go run closures.go

and inspect the outcome - is this what you have expected?

package main
import "fmt"
func newClosures() (func(), func() int) {
    a := 0
    // Your code here!
}
func main() {
    f1, f2 := newClosures()
    f1() // sets "a" to 5
    n := f2() // multiplies "a" by 7 - is f2's internal value of "a" 0 or 5 before the call? 
    fmt.Println(n)
}

Task 2: Clever tracing with “defer”

Intro

The defer keyword allows to specify a function that is called whenever the current function ends. What if we could call one function at the beginning of the current function, and one at the end, with only one function call?

Like so:

func f() {
    trace("f")
    fmt.Println("Doing something")
}

And when calling function f() it would print:

Entering f
Doing something
Leaving f

With a tricky use of the defer statement and a closure, we can do that!

Your task

Write a function trace() that receives a string - the name of the current function - and does the following:

  1. Print “Entering ” where is the string parameter that trace receives
  2. Create and return a function that prints “Leaving ”

Then call trace() via the defer keyword in such a way that trace() runs immediately, and returns its result to defer .

func trace(name string) func() {
    // TODO:
    // 1. Print "Entering <name>"
    // 2. return a func() that prints "Leaving <name>" 
}
func f() {
    defer // TODO: add trace() here so the defer receives the returned function
    fmt.Println("Doing something")
}
func main() {
    fmt.Println("Before f")
    f()
    fmt.Println("After f")
}

Time To Practice: Slices

Time To Practice: Slices

Task 1: Appending through a pointer

appendOne is a simple function that takes a pointer to a slice of int s and appends a 1 to this slice.

func appendOne(s *[]int) {
    *s = append(*s, 1)
}
func main() {
    s1 := []int{0, 0, 0, 0}
    // s1 := make([]int, 4, 8) // capacity is twice the initial size
    s2 := s1
    fmt.Printf("Before appendOne:\ns1: %v\ns2: %v\n", s1, s2)
    appendOne(&s1)
    fmt.Printf("After appendOne:\ns1: %v\ns2: %v\n", s1, s2)
    s1[0] = 2
    fmt.Printf("After changing s1:\ns1: %v\ns2: %v\n", s1, s2)
}

Does appendOne(&s2) also change s1 ?

Create a new .go file, add the above code, and run the file via go run .

Then change the first two lines of main() to:


  // s1 := []int{0, 0, 0, 0}
    s1 := make([]int, 4, 8) // capacity is twice the initial size

Repeat the test.

If one of the results (or both) is different from what you expected, inspect the properties of s1 and s2 (address, length and capacity) - they might give you a hint about what’s going on.

Task 2: The slice is copied! Or is it?

Go has pass-by-value semantics, as we learned in the lecture about functions and pointers. The following code passes a slice to the function changeSlice() . The slice is not passed as a pointer like in the first task, so apparently it is passed by value, and the local variable s is therefore just a copy of s1 that is passed to changeSlice() in main() .

func changeSlice1(s []int) {
    s[0] = 7
}
func main() {
    s1 := []int{1}
    fmt.Println("s1 before changeSlice1:", s1)
    changeSlice1(s1)
    fmt.Println("s1 after changeSlice1:", s1)
}

Still, assigning a new value to s[0] changes the value of s1[0] as well.

Why?

Task 3: A little change

A slight modification of task #2: Instead of assigning a new value to s[0] , we set s to a new slice of ints. Now s1 is not affected by the change.

func changeSlice2(s []int) {
    s = []int{7}
}
func main() {
    s1 := []int{1}
    fmt.Println("s1 before changeSlice2:", s1)
    changeSlice2(s1)
    fmt.Println("s1 after changeSlice2:", s1)
}

Why does this behave differently than the code in task #2?

Task 4: An append() gotcha

The built-in function append() is a convenient way of appending data to a slice without worrying about the capacity. But in the code below it does not seem to work as intended:

func main() {
    src := []int{}
    src = append(src, 0)
    src = append(src, 1)
    src = append(src, 2)
    dest1 := append(src, 3)
    dest2 := append(src, 4)
    fmt.Println(src, dest1, dest2)
}

Run this code and inspect the output. What would you have expected? Why do you see this result instead?

Find the solutions and the answers in slicesolutions.go , attached to this lecture.

Time To Practice: fun with maps

Time To Practice: Maps

Maps are a pretty versatile data structure, and make a good ad-hoc storage in many situations. In this Time To Practice, we want to create a word counter.

Task 1: count words

Implement a function that receives a string and a map[string]int . The function shall split the string into words, turn the words into lowercase, and adds the word to the map and/or increases the counter for this word.

Tips

  1. What could be the easiest way of increasing a counter for a map element? Would it matter whether or not the element is already in the map or whether it has to be added?
    Remember moons["Jupiter"]++ and the fact that a map element springs into existence if it does not exist. (An advantage of the “zero value” concept in Go.)
  2. After splitting the string, the resulting words may still have punctuation attached - quotation marks, colons, question marks, etc. Trim all these away using strings.Trim(<wordvariable>, " \t\n\"'.,:;?!()- .
  3. Turn each word into lowercase before counting it - have a look into the strings package for a suitable function.

This is the function to fill:

func count(s string, m map[string]int) {
    // your code here
}

Task 2: Print the word counts

Write a function that receives a map[string]int and prints out each key (the word) together with its value (the count), but only if the count is greater than 1.

func printCount(m map[string]int) {
    // your code here
}

Find these two Go files attached to this lecture:

  • maps.go as a starting point. It contains a main() that feeds your functions from either a file (if you pass a file name on the command line) or from a test text at the end of the file.
  • mapssolution.go that contains one possible way of implementing the two functions. If your solution differs from this–don’t worry, as long as it counts like it should.

Happy coding!

Time To Practice: structures

Time To Practice: structs

Intro

Custom, configurable types are a central aspect of flexibility in programming languages. The struct type is THE classic customizable type. In its pure form, it is nothing but a collection of other types, but in conjunction with embedding and methods–the topics of the upcoming lessions–, structs turn into a very versatile building block of apps and libraries.

Part 1: are these structs comparable?

Two instances of a struct type can be compared to each other as long as each of their fields is a comparable type.

For example, two instances of this struct…

type s struct {
    n int
}

…are comparable because an integer is a comparable type.

Task: verify if instances of the following structs are comparable

Without running the code, can you tell for each of the following structs if two instances of that struct are comparable to each other?

type s1 struct {
    n int
    b bool
}
s11 := s1{n: 4, b: true}
s12 := s1{n: 4, b: true}
fmt.Println(s11 == s12) // Does this line compile?
type s2 struct {
    r []rune
}
s21 := s2{r: []rune{'a', 'b', '🎵'}}
s22 := s2{r: []rune{'a', 'b', '🎶'}}
fmt.Println(s21 == s22) // Does this line compile?
type s3 struct {
    r [3]rune
}
s31 := s3{r: [3]rune{'a', 'b', '🎵'}}
s32 := s3{r: [3]rune{'a', 'b', '🎶'}}
fmt.Println(s31 == s32) // Does this line compile?

Part 2: fun with empty structs

Can a struct contain no elements at all? It can, and then it is called an empty struct .

type emptyStruct struct{}

An empty struct takes zero bytes of memory, and so does a struct of empty structs. We can test this with a function from the unsafe package, unsafe.Sizeof(<variable>) .

type metaEmptyStruct struct {
    A struct{}
    B struct{}
}
fmt.Println("Size of emptyStruct:", unsafe.Sizeof(emptyStruct{}))
fmt.Println("Size of metaEmptyStruct:", unsafe.Sizeof(metaEmptyStruct{}))

Even a slice of empty structs consumes no memory except for the slice header.

type sliceOfNothings []struct{}
sOfN := make(sliceOfNothings, math.MaxInt64)

Even when instantiating the slice with a length and a capacity, it still only consumes memory for the header.

voidSlice := make([]struct{}, 1000, 5000)

The literal value of an empty struct looks a bit funny though:

empty := struct{}{}

The first pair of parens are the empty struct definition body, and the second pair belong to the empty literal value.

Now empty structs seem a bit superfluous–or can we maybe do something useful with them?

Well, there are indeed some use cases for empty structs. In the following two tasks, you will explore two of those use cases.

Task 1: define a Set type

Go has no native Set type. A set is a container for values where each value can only be stored once. An array of ints, for example, can have multiple elements with value 37, but in a set, there can be only one such element.

Your task is to create a data structure that can hold elements of a given type, but each value exists only once within a given instance of this data structure. Inserting the same value a second time does not change the contents of the set.

Hint: Which of the available “container” types–arrays, slices, or maps–could be a suitable data structure?

Another hint: Do not think too complicated! The solution is really nothing but a single type declaration:

type ...

and the declared type somehow includes a struct{} type.

Task 2: create an integer iterator

Problem: The standard for loop for i := 0; i < 10; i++ looks soo oldskool. You want a sleeker way of iterating over ints, like the Java folks do it. You want an iterator that returns the numbers from 0 to n , one number at a time.

Your task is to write a function iter(n int) <return value> that takes a number n and returns someting that can be passed to a range loop, like so:

for i := range iter(7) {
    ...
}

So the result of iter(7) must be something that a range loop can loop over n times. Of course, empty structs play a role here. Remember the zero-sized data types discussed in the intro.

Bonus task: a duplicate finder

As a bonus task, use a Set type for finding duplicate lines in a text file.

Write a small program that opens a file, reads the file line by line, and verifies if the line already exists in the set. If so, it shall print the line; otherwise, it shall add the line to the set.

Tip for scanning text files: Use a bufio.Scanner . The following functions and methods help with easy scanning:

  • os.Open(string) opens a file at path path and returns an os.File and an error value.
  • bufio.NewScanner(io.Reader) takes an io.Reader and creates a scanner. An os.File can act as an io.Reader !
  • The scanner has these methods (among others):
    • Scan() advances to the next token and returns a boolean true on success. Handy for use in a for loop!
    • Text() returns the current token as a string.

Try starting from a blank file! There is no template file included for this bonus task. See the extra file “structbonussolution.go” for a possible solution.

Happy coding!

Time To Practice: a TerminalWriter

Time To Practice: a terminal writer

Intro

The io.Writer interface is as simple as can get, yet it has great abstraction capabilities. Whatever you want to write to–standard output, a file, a network connection, a byte buffer, and so on–is abstracted away by a single, uniform method named Write() .

And you can even write your own output facilities that can be used anywhere an io.Writer interface is accepted.

For example, imagine you frequently need to write to the terminal but not across the entire width of the terminal window. To solve this problem once and for all, you decide to implement a TerminalWriter that writes text to the terminal at a given width.

Your task

Start from the code below (also downloadable as “iowriter.go”) and implement the Write method of TerminalWriter .

It must meet the following requirements:

  • It writes to os.Stdout . (You can use the standard fmt.Print/f/ln family here, or os.Stdout.Write() .)
  • After tw.width bytes, start a new line.
  • When the function encounters any error during writing, return an appropriate error, and the number of bytes successfully written.

Bonus task 1: The poet

Make the text break at word boundaries.

(No solution provided.)

Bonus task 2: The world citizen

Start the next line after n Unicode characters rather than n bytes.

To simplify this task, only consider Latin scripts (where all glyphs have the same width - at least with a non-proportional font as used in terminals).

The packages “unicode/utf8” from the standard library (most notably) and “golang.org/x/text/width” may prove useful. (As well as maybe other packages of golang.org/x/text.)

Time To Practice: Interfaces - write a data container

Time to Practice: Write a universal data container

Intro

Sometimes you need a certain type of container–a list, a tree, a queue, etc–for a number of different data types. The core functionality–inserting and retrieving values, maybe also sorting, searching, etc–shall remain unchanged for whatever actual data type is going to be stored. An empty interface ( interface{} ) seems ideal for this. It declares no methods, hence any type satisfies this interface.

Task 1: Write a simple queue

Your first task is to write a simple first-in-first-out (FIFO) container, also called a queue. Below is some stub code to complete. Replace all TODO labels with your code.

Tip: A slice comes handy as the basis for a queue. Do you remember how to append a value to a slice, and how to remove the first element of a slice?

type Queue TODO

// PutAny adds an element to the queue.
func (c *Queue) PutAny(elem interface{}) {
    TODO: append elem to the queue
}

// GetAny removes an element from the queue.
// If the queue is empty, GetAny returns an error.
func (c *Queue) GetAny() (interface{}, err) {
    TODO: fetch the first element's value, and then remove the first element from the queue.
    If the queue is already empty, return the zero value of interface{} and an error.
}

Task 2: Write a wrapper to gain run-time type safety

Imagine you need a queue of integers. Create a type IntQueue based on the generic Queue type, and implement Put and Get methods that append an int to the queue and return an int from the queue, respectively.

type IntQueue TODO

func (ic *IntQueue) Put(n int) {
    TODO: Put n into the generic queue.
}

func (ic *IntQueue) Get() (int, err) {
    TODO: Dequeue an element, and return it as an int
}

Your code should make the main function run without errors, and without wrong results, for two loop iterations. At the third iteration, you should see the expected error message.

func main() {
    ic := IntQueue{}
    ic.Put(7)
    ic.Put(42)

    for i := 0; i < 3; i++ { 
        elem, err := ic.Get()
        if err != nil {
            fmt.Println("Error:", err)
            return
        }
        fmt.Printf("Got: %d (%[1]T)\n", elem)
    }
}

Time To Practice: It depends…

Time To Practice: It depends…

Now it is your turn to use the dep tool. As you have seen in the lecture, the dep tool is designed for ease of use, with only three subcommands - init , ensure , and status . Still, under the hood, dependency management is hard (“hard” is maybe not the right word; the core problem of dependency management–whether all direct and transitive dependencies of an application can be satisfied with no conflicts–is “NP-complete”, which is “harder than hard”).

But I digress. If you want to read more on this, follow this link.

Scenario

You create a new application that uses the famous “version” package featured in the lecture. The “version” package is at version 2.0.1, but you know that this new major version has some severe bugs, and so you decide to stay with 1.x for now. So you need to ensure (pun intended) to not use this version but version 1.2.0 instead, which is the highest available version below 2.0.

Your task

Step 1: Install dep

First, install the dep tool:

go get -u github.com/golang/dep/cmd/dep

Then type

dep help

to verify if dep has been installed properly.

Step 2: Create a project

Now create a new Go project in your $GOPATH/src (or %GOPATH%\src if on Windows).

Create main.go:

package main

import (
    "fmt"

    "github.com/appliedgocourses/version"
)

func main() {
    fmt.Println(version.Semver())
}

Step 3: Initialize dep

In a shell or command prompt, cd to your project and type

dep init

Verify if you see these new items in your project:

  • Gopkg.toml - the TOML, or “manifest” file
  • Gopkg.lock - the Lock file
  • vendor/ - the directory that stores the requested version of each package.

Now run

dep status

You should see that the version package is pinned to version 2.0.1, and the constraint is ^2.0.1 . In contrast to the lecture, we do not need to run dep ensure -add anymore. Why?

The reason is that in the lecture, main.go did not have any import statement when running dep init. I did this intentionally to demonstrate the use of dep ensure -add , but in this Time To Practice, we want to head for the most efficient steps.

Takeaway: dep init already scans for existing dependencies and updates the TOML file and the lock file accordingly.

Also note that dep init already has downloaded the required dependencies - in our case, just the version package. If you run

go run main.go

you should get the output 2.0.1 .

Step 4: Adjust the constraint

But we wanted to use version 1.2.x until the bugs in 2.0 are fixed, right? So let’s change the constraint.

Open Gopkg.toml and find the [[constraint]] section (below all the default comments). Change the version string to ^1.2.0 and save the file.

Then run

dep ensure

and verify the result through

dep status

and

go run main.go

And that’s it! Dependency management with just three commands (and with editing a file).

Testing - Subtests, parallel tests, and table-driven tests

Subtests

The testing package provides support for subtests that enhance the testing experience in several ways.

Subtests

  • enable better handling of failures
  • allow more fine-grained control on running tests
  • provide the basis for running tests in parallel
  • can help writing simpler test code

The core of subtests is the Run() function.

func (t *T) Run(name string, f func(t *T)) bool

Run() receives a name and a function, and returns a boolean that indicates whether the function call succeeded.

Like with top-level test functions, if a test function contains multiple subtests, if a subtest fails it does not prevent other subtests from running.

A typical use case for subtests is the use of setup and tear-down code.

func TestSomeComplexFunction(t *testing.T) {
    // add setup code here
    t.Run("Subtest 1", func(t *testing.T) { ... })
    t.Run("Subtest 2", func(t *testing.T) { ... })
    t.Run("Subtest 3", func(t *testing.T) { ... })
    // add tear-down code here
}

Parallel tests

Subtests make it easy to run tests in parallel. Simply place

t.Parallel()

as the first call inside a subtest function. As a result, all subtests within the same test function that call t.Parallel() can run in parallel. They do NOT run in parallel with any subtests that do not call t.Parallel() .

When running the tests, you can limit the number of subtests that run in parallel via the -parallel flag.

go test -parallel n

The default is the available number of (logical) CPU cores.

Note: There is a caveat with t.Parallel() when calling t.Run() within a loop. This scenario usually occurs with table-driven tests, so the explanation of this caveat follows after the section on table-driven tests.

Table-driven tests

Coding every test case by hand is tedious; and in most cases, you’ll want to run a given test on a number of different input values, especially if the range of possible input values contains important edge cases (such as: an empty string, a nil pointer, maximum and minimum values, a slice exactly as large as the underlying array, etc). You also will want to include input values for which the test must fail.

Table-driven tests can deal with all these different cases easily. The basic idea is to provide a slice of several input values and feed these values to the test case through a loop.

We could now walk through writing a table-driven test manually, but I want to take another approach and start with an existing tool.

The gotests tool

The gotests tool generates test function templates from your code. These templates implement table-driven tests as well as subtesting.

You can run gotests from the command line but it really shines when used via an editor plugin.

For example, in Visual Stuio Code, you can simply right-click at any part of a function and select

“Go: Generate Unit Tests for Function”

to generate a test function. Let’s take our simple Scan() function from the go test lecture and change it to return an error if the input string is empty.

func Scan(s string) []string {
    return strings.Split(s, " ") // wrong
    // return strings.Fields(s)  // correct
}

We do this here to be able to inlude tests that are expected to have the tested function return an error.

After running gotests , the generated test function looks like this:

func TestScan(t *testing.T) {
    type args struct {
        s string
    }
    tests := []struct {
        name    string
        args    args
        want    []string
        wantErr bool
    }{
    // TODO: Add test cases.
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := Scan(tt.args.s)
            if (err != nil) != tt.wantErr {
                t.Errorf("Scan() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if !reflect.DeepEqual(got, tt.want) {
                t.Errorf("Scan() = %v, want %v", got, tt.want)
            }
        })
    }
}


Let’s look at the different parts in detail.

The test function starts with a struct that contains all function parameters of the function to be tested. In case of our Scan() functions, this is just a single string.

type args struct {
    s string
}

Next, a variable named tests is defined. This variable is a slice of an anonymous struct type with three fields:

  • name : The name of the test case (choose something meaningful)
  • args : The input parameters (which is the args struct defined just before)
  • want : The expected result
  • wantErr : A flag that specifies if the tested function is expected to return an error
tests := []struct {
        name    string
        args    args
        want    []string
        wantErr bool
    }{
    // TODO: Add test cases.
    }

Here we can add our test cases. A single test case looks like this:

{
    {
        name: "Single white spaces", 
        args: args{s: "a b c"}, 
        want: []string{"a", "b", "c"},
        wantErr: false,
    },
}

Now let’s add further tests:

{
        {
            name:    "Single white spaces",
            args:    args{s: "a b c"},
            want:    []string{"a", "b", "c"},
            wantErr: false,
        },
        {
            name:    "Multiple white spaces",
            args:    args{s: "a   b  t  c"},
            want:    []string{"a", "b", "c"},
            wantErr: false,
        },
        {
            name:    "Leading and trailing white spaces",
            args:    args{s: "    a   b  tn  c  t "},
            want:    []string{"a", "b", "c"},
            wantErr: false,
        },
        {
            name:    "Only white spaces",
            args:    args{s: "  t  nt    t"},
            want:    []string{},
            wantErr: false,
        },
        {
            name:    "Empty input",
            args:    args{s: ""},
            want:    nil,
            wantErr: true, // here we expect an error
        },
        {
            name:    "No white spaces",
            args:    args{s: "abc"},
            want:    []string{"abc"},
            wantErr: false,
        },
}

The generated test code feeds all these test input values into the test case within a loop:

 for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := Scan(tt.args.s)
            if (err != nil) != tt.wantErr {
                t.Errorf("Scan() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if !reflect.DeepEqual(got, tt.want) {
                t.Errorf("Scan() = %v, want %v", got, tt.want)
            }
        })
    }

The loop dissected:

  1. The loop iterates over our tests slice.
for _, tt := range tests {
}

The loop body calls t.Run() , which is nice as we can now add t.Parallel() if we want to execute parallel tests.

  t.Run(tt.name, func(t *testing.T) {
    })

The t.Run() function calls our Scan() function.

        got, err := Scan(tt.args.s)

Then test if Scan() returns an unexpected error:

  if (err != nil) != tt.wantErr {
                t.Errorf("Scan() error = %v, wantErr %v", err, tt.wantErr)
                return
            }

And finally, compare the actual result against the expected one from the want field and raise an error if they differ:

   if !reflect.DeepEqual(got, tt.want) {
                t.Errorf("Scan() = %v, want %v", got, tt.want)
            }

If you compare this with our original, manually written TestScan() function, you can see that the table-driven test style takes only very little effort to cover a much broader range of test cases than a manually written test.

And there is even no need for modifying the test code; we only need to supply various inputs and expected results.

Back to the t.Parallel caveat

Now it is time to look into the caveat that was mentioned in the section on parallel tests.

Here is the outline of the generated test loop again, with a call to t.Parallel() that enables concurrent tests:

for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // Inside the subtest function. This is a closure that has
            // access to tt.
            t.Parallel()
            // Define tests using tt
        })
    }

This spawns as many goroutines as there are elements in tests .

The goroutines do not necessarily start immediately. In fact, their start can be postponed as long as the loop is running, especially on a single-core machine. Let’s assume this happens here. The loop finishes, and all subtests start running in goroutines.

Since the subtest function is a closure, it can access tt for use in tests. However, tt is a loop variable, and every iteration assigns a new value to tt . At the end of the loop, tt contains the last element of tests , and so all of the goroutines access the last element only!

To ensure that each goroutine gets access to the correct element from tests , we have to copy the tt loop variable into another variable at each iteration.

for _, tt := range tests {
        tt := tt // local variable tt overshadows loop variable tt
        t.Run(tt.name, func(t *testing.T) {
            // Inside the subtest function. This is a closure that has
            // access to tt.
            t.Parallel()
            // Define tests using tt
        })
    }

The assignment tt := tt may look like a no-op, but in fact it creates a new local variable tt with the current value of the loop variable tt being copied over . (If this seems too strange to you, simply use another name for the local variable, e.g. tst := tt , and have the subtests use tst instead of tt .)

Now the subtest closure accesses its own copy of tt that contains the current element of tests .

I can imagine that this can be a bit difficult to understand, but don’t worry - the lecture “Concurrency 3 - two caveats” in section 5 discusses this problem in detail.

Summary

  • Subtests allow setup and tear-down code for a group of related tests
  • Subtests allow running tests in parallel
  • Table-driven tests are a great way of covering a large range of test cases with very little effort

Misc notes

Subtests were introduced in Go version 1.7.