Go

Google created a programming language. What's it like?

Overview

Go is a systems programming language that first appeared in 2009. Most notably:

Here is the language rationale from the Go Programming Language FAQ:

Go is an attempt to combine the ease of programming of an interpreted, dynamically typed language with the efficiency and safety of a statically typed, compiled language. It also aims to be modern, with support for networked and multicore computing. Finally, it is intended to be fast: it should take at most a few seconds to build a large executable on a single computer. To meet these goals required addressing a number of linguistic issues: an expressive but lightweight type system; concurrency and garbage collection; rigid dependency specification; and so on. These cannot be addressed well by libraries or tools; a new language was called for.

Go has an official definition.

Like any modern language, it is currently evolving. If interested, check out the version history.

Getting Started

Let's start with Hello, world:

hello.go
package main

import "fmt"

func main() {
    fmt.Println("Hello, world")
}

To build and run:

$ go run hello.go
Hello, world

or you can do it the long way:

$ go build hello.go && ./hello
Hello, world

And another little script:

fib.go
package main

import "fmt"
import "os"
import "strconv"

func main() {
    n, error := strconv.Atoi(os.Args[1])
    if error != nil {
        fmt.Println("A single integer commandline argument is required")
    } else {
        a, b := 0, 1
        for b <= n {
            fmt.Println(b)
            a, b = b, a+b
        }
    }
}
$ go run fib.go 100
1
1
2
3
5
8
13
21
34
55
89
$ go run fib.go dog
A single integer commandline argument is required
Exercise: The program crashes with an error if run without any command arguments. Fix it.

And another:

triple.go
package main

import "fmt"

func main() {
    for c := 1; c <= 100; c++ {
        for b := 1; b <= c; b++ {
            for a := 1; a <= b; a++ {
                if a*a+b*b == c*c {
                    fmt.Printf("(%d,%d,%d)\n", a, b, c)
                }
            }
        }
    }
}

Now, here are a few more sample programs. You can browse the list just to see what Go “looks like” rather than to learn from the examples.

Learning Go

You should spend the time to take the interactive tour at gloang.org. It is very good.

Exercise: Do the entire tour. Seriously, it is the best way to get started and learn Go.

Also, go through the excellent Go By Example. It isn't interactive like Google's Tour of Go; however, the annotations beside the example code are really nice.

You can also experiment a little using the Go Playground. There's no actual REPL for Go, but things compile so fast it probably doesn't matter too much.

Don’t miss the main Go page. You can find everything from here. After watching any intro videos and taking any tours go to the doc page and go through the items in the "Learning Go" section.

When ready to learn everything, see the great portal Awesome Go.

Language Highlights

Go is known for what it leaves out as much as for what it has. You’ll notice the language is quite small.

Syntax

There are very few keywords compared to most mainstream languages. Just these:

break
case
chan
const
continue
default
defer
else
fallthrough
for
func
go
goto
if
import
interface
map
package
range
return
select
struct
switch
type
var

There aren’t that many operators, either. While C++ has over 50, Go has less that half that. Here is the operator precedence chart, from highest precedence to lowest:

Operators Associativity Description
+-^! unary plus, negation, bitwise complement, logical not
*/%<<>>&&^ L multiplication, division, modulo, left shift, right shift, binary and, binary and-not
+-|^ L addition, subtraction, binary or, binary xor
==!=<<=>>= L equals, not equals, less, less or equal, greater, greater or equal
&& L short-circuit and
|| L short-circuit or
Semicolons

While semicolons terminate many syntactic constructs, you don’t have to type them into your source code. Go will automatically insert a semicolon into the token stream at the end of a non-blank line if the last token on the line is an identifier, integer literal, floating-point literal, imaginary literal, rune literal, string literal, break, continue, fallthrough, return,++, --, ), ], or }.

For complete details of Go syntax, see the Go Programming Language Specification.

Exercise: If you are familiar with JavaScript, you may know it too has a rule for automatic semicolon insertion, but JavaScript’s rules create at least four well-known terribly unintuitive and unexpected behaviors. For each of these, find out whether Go is subject to the same problems.

Predeclared Identifiers

These identifiers come already declared for you:

Standard Library

Go programs must run in a package (they start in main) and there are a bunch of standard packages that you can import from.

Please browse the list of standard packages to see what's available.

Four of them are featured here:

misc.go
package main

import (
    "fmt"
    "math/rand"
    "strings"
    "time"
)

func main() {
    fmt.Printf("%q\n", strings.Split("a:bcd:ef", ":"))
    r := rand.New(rand.NewSource(time.Now().UnixNano()))
    fmt.Println(r.Int31())
}

Writing Functions

When writing function signatures, you must supply the argument type(s) and (if present) the return type(s). You can name the return value(s) too. Variadic functions are supported by prefixing the parameter type with .... This will roll up the arguments into a single slice object.

example-functions.go
package main

import "fmt"

func add(x int, y int) int {
    return x + y
}

// Multiple parameters of the same type can be shortened to
func divide(x, y int) int {
    return x / y
}

// Multiple return values
func swap(x, y string) (string, string) {
    return y, x
}

// Named return values
func divmod(x, y int) (quo, rem int) {
    quo = x / y
    rem = x % y
    return
}

// Variadic functions (aribtrary number of arguments)
func sumOfFloats(a ...float64) float64 {
    total := 0.0
    for _, x := range a {
        total += x
    }
    return total
}

func main() {
    fmt.Println(add(1, 2), divide(33, 10))
    one, two := swap("ho", "hi")
    three, four := divmod(97, 25)
    fmt.Println(one, two, three, four)
    fmt.Println(sumOfFloats(8, 4.3, -2, 11.9))
}

Variables

Variables are declared with var. There is some degree of type inference. There is a default initial value for each type. This is rather cool, right? Inside a function, a short variable declaration is allowed (but never outside a function).

vars.go
package main

import "fmt"

var x int = 10
var y int                   // value is 0
var z = 12                  // type inferred to be int
var a, b, c bool            // multiple vars declared at once
var p, q float64 = 8.9, 2.3 // multiple vars with initializers

var (
    message = "O noes"
    start   complex128
)

func main() {
    var s rune = '$' // nothing special here
    t := "hello"     // short var declaration, same as var t string = "hello"

    fmt.Println(x, y, z, a, b, c, p, q, s, t, message, start)
}

Types

Go has a rich set of static types. Here are the predeclared types:

TypeDescriptionZero value
bool Contains only the values true and false false
uint8 Unsigned 8-bit integers (0 to 255) 0
uint16 Unsigned 16-bit integers (0 to 65535) 0
uint32 Unsigned 32-bit integers (0 to 4294967295) 0
uint64 Unsigned 64-bit integers (0 to 18446744073709551615) 0
int8 Signed 8-bit integers (-128 to 127) 0
int16 Signed 16-bit integers (-32768 to 32767) 0
int32 Signed 32-bit integers (-2147483648 to 2147483647) 0
int64 Signed 64-bit integers (-9223372036854775808 to 9223372036854775807) 0
float32 IEEE-754 32-bit floating-point numbers 0
float64 IEEE-754 64-bit floating-point numbers 0
complex64 Complex numbers with float32 real and imaginary parts (0+0i)
complex128 Complex numbers with float64 real and imaginary parts (0+0i)
byte Alias for uint8 0
rune Alias for int32 0
uint IMPLEMENTATION-SPECIFIC either 32 or 64 bits 0
int IMPLEMENTATION-SPECIFIC same size as uint 0
uintptr IMPLEMENTATION-SPECIFIC an unsigned integer large enough to store the uninterpreted bits of a pointer value 0x0
string Immutable sequences of bytes ""
error Defined as: type error interface {Error() string} nil
any Alias for interface{}, the empty interface nil
comparable Interface denoting the set of all non-interface types whose instances are strictly comparable. This type can only be used in the context of a type constraint. nil

There are facilities for you to create new types:

Kind of typeExamplesIs nil a value?Zero Value
Array[7]intNoAn array of the defined length with all elements set ot their zero values
Slice[]intYesnil
Structstruct {X int; Y int}NoStruct with the defined shape having all fields with their zero values
Mapmap[string]intYesnil
Functionfunc(int)intYesnil
Channelchan intYesnil
Pointer*intYesnil
Interfaceinterface {Save()}Yesnil

Numbers, booleans, runes, strings, arrays, and structs are value types. They are always copied when assigned and passed and returned.

Pointers, slices, maps, channels, functions, and interfaces are reference types. They are not copied when assigned and passed and returned. Sadly, the value nil is a value of each reference type. This means that like Java, the static and dynamic types are not quite synced up, meaning you may need lots of runtime checking for nil.

val_and_ref.go
package main

func main() {
    // Arrays are value types
    a := [3]int{1, 2, 3}
    b := a
    b[1] = 1000
    println(a[1], b[1]) // 2 1000

    // Structs are value types
    s := struct {
        x int
        y int
    }{1, 2}
    t := s
    t.y = 1000
    println(s.y, t.y) // 2 1000

    // Maps are reference types
    m := map[string]int{"x": 1, "y": 2}
    n := m
    n["y"] = 1000
    println(m["y"], n["y"]) // 1000 1000
    m = nil
    println(m["y"]) // 0 (safe to read from nil map)
    // But writing to a nil map will cause a panic

    // Slices are reference types
    x := []int{1, 2, 3}
    y := x
    y[1] = 1000
    println(x[1], y[1]) // 1000 1000
    x = nil
    println(len(x)) // 0 (length of nil slice)
    // But writing to a nil slice will cause a panic
}

Note the subtle differences between the empty slice and the nil slice and between the empty map and the nil map.

Conditionals

Read about if and switch at Go By Example.

Loops

Write loops with the for statement. There is no while or repeat keyword. There is only for. But there are lots of forms. The following table lists most, but not all, of them:

FormDescription
for {...}infinite loop, runs until a break, return, or panic
for x < 10 {...}like “while x < 10” in other languages
for i := 0; i < 10; {...}
for i < 10; i+=2 {...}
for i := 0; i < 10; i+=2 {...}
iterates while a condition is true, with an optional statement before the whole loop and an optional statement at the end of each iteration. If the initial statement is a declaration, the scope the variable is local to the loop.
for range 10 {...}does the body 10 times
for i := range 10 {...}does the body 10 times, with a variable i taking on the values 0, 1, ..., 9. The variable i is declared at the beginning of the statement and is local to the block.
for range aSlice {...}
for i := range aSlice {...}
for i, v := range aSlice {...}
for _, v := range aSlice {...}
Iterates through a slice, optionally binding i and/or v to the index and value of each element, respectively.
for range aString {...}
for i := range aString {...}
for i, c := range aString {...}
for _, c := range aString {...}
Iterates through the runes of a string, optionally binding i and/or c to the index and rune of each element, respectively.
for range aMap {...}
for k := range aMap {...}
for k, v := range aMap {...}
for _, v := range aMap {...}
Iterates through the key-value pairs of a map, optionally binding k and/or v to the key and value of each element, respectively.
for range aChannel {...}
for e := range aChannel {...}
Receives values on a channel until closed, optionally binding e to the value received.

Note that just as with the if statement, variables declared at the top of the statement, before the block, are local to the block.

Loops have break and continue, too. Loops can be exited early with break, return, or a panic.

Read about the for statement at Go By Example.

Pointers

The syntax is pretty conventional here: Use & to make a pointer to something, and use * to dererence the pointer.

simple-pointers.go
package main

import "fmt"

func uselessTriple(x int) {
    x *= 3 // Yep, this is completely useless
}

func reallyTriple(x *int) {
    *x *= 3 // Changes the referent of the argument
}

func main() {
    x := 3
    y := &x          // Perfectly okay to point to a local variable
    uselessTriple(x) // x does not change
    fmt.Println(x)   // Prints 3
    reallyTriple(y)  // You should draw a picture of what's going on
    fmt.Println(x)   // Yep, prints 9
}

Structs

Read about structs at Go By Example.

Arrays and Slices

Read about arrays and slices at Go By Example.

Maps

Read about maps at Go By Example.

Exercise: Go does not have a native optional type, so how does the language deal with the possibility of a key not being in a map?

Functions as Values

Functions are first-class citizens in Go. You can assign them to variables, pass them as arguments, and return them from other functions. Here is a simple example:

twice.go
package main

import "fmt"

func twice(f func(int) int, x int) int {
    return f(f(x))
}

func main() {
    square := func(x int) int { return x * x }
    fmt.Println(twice(square, 5))
    fmt.Println(twice(func(x int) int { return x * 2 }, 5))
}

Functions can be closures too. You can write your own versions of map, filter, reduce, and friends, but such things are rare in the kind of applications Go is used for. Even one of the Go creators, Rob Pike, says you should just use for loops for this kind of thing.

Methods

A method is a function with a receiver. Generally the receiver is a pointer but doesn't have to be. By making it a pointer you allow the method to be a mutator, and you don't incur cost of a copy. Let’s look at two examples. The first is a completely immutable Quaternion type:

quaternion.go
package quaternions

import (
    "errors"
    "fmt"
    "math"
    "strings"
)

type Quaternion struct {
    A, B, C, D float64
}

var (
    Zero = Quaternion{0.0, 0.0, 0.0, 0.0}
    I    = Quaternion{0.0, 1.0, 0.0, 0.0}
    J    = Quaternion{0.0, 0.0, 1.0, 0.0}
    K    = Quaternion{0.0, 0.0, 0.0, 1.0}
)

func NewQuaternion(a, b, c, d float64) (Quaternion, error) {
    if math.IsNaN(a) || math.IsNaN(b) || math.IsNaN(c) || math.IsNaN(d) {
        return Quaternion{}, errors.New("coefficients cannot be NaN")
    }
    return Quaternion{a, b, c, d}, nil
}

func (q Quaternion) Add(other Quaternion) Quaternion {
    return Quaternion{
        A: q.A + other.A,
        B: q.B + other.B,
        C: q.C + other.C,
        D: q.D + other.D,
    }
}

func (q Quaternion) Multiply(other Quaternion) Quaternion {
    return Quaternion{
        A: other.A*q.A - other.B*q.B - other.C*q.C - other.D*q.D,
        B: other.A*q.B + other.B*q.A - other.C*q.D + other.D*q.C,
        C: other.A*q.C + other.B*q.D + other.C*q.A - other.D*q.B,
        D: other.A*q.D - other.B*q.C + other.C*q.B + other.D*q.A,
    }
}

func (q Quaternion) Conjugate() Quaternion {
    return Quaternion{q.A, -q.B, -q.C, -q.D}
}

func (q Quaternion) Coefficients() []float64 {
    return []float64{q.A, q.B, q.C, q.D}
}

func (q Quaternion) String() string {
    coefficients := q.Coefficients()
    units := []string{"", "i", "j", "k"}
    var builder strings.Builder
    for i, c := range coefficients {
        if c != 0 {
            if c < 0 {
                builder.WriteString("-")
            } else if builder.Len() > 0 {
                builder.WriteString("+")
            }
            if math.Abs(c) != 1.0 || units[i] == "" {
                builder.WriteString(fmt.Sprintf("%g", math.Abs(c)))
            }
            builder.WriteString(units[i])
        }
    }
    if builder.Len() == 0 {
        return "0"
    }
    return builder.String()
}

The second is a mutable stack. The receiver is always a pointer:

stack.go
package stacks

import "fmt"

type Stack[T any] struct {
    elements []T
}

func NewStack[T any]() *Stack[T] {
    return &Stack[T]{}
}

func (s *Stack[T]) Push(value T) {
    s.elements = append(s.elements, value)
}

func (s *Stack[T]) Pop() (T, error) {
    if len(s.elements) == 0 {
        var zero T
        return zero, fmt.Errorf("stack is empty")
    }
    top := s.elements[len(s.elements)-1]
    s.elements = s.elements[:len(s.elements)-1]
    return top, nil
}

func (s *Stack[T]) Size() int {
    return len(s.elements)
}

func (s *Stack[T]) IsEmpty() bool {
    return len(s.elements) == 0
}

Interfaces

An interface is a collection of method signatures. You don't have to say that a type impements an interface, the compiler will check that all the methods are implemented when you try to assign (or pass) an object to a variable that is given an interface for a type.

shapes.go
package main

import (
    "fmt"
    "math"
)

type Shape interface {
    perimeter() float64
    area() float64
}

type Circle struct {
    radius float64
}

func (c Circle) perimeter() float64 {
    return 2.0 * math.Pi * c.radius
}

func (c Circle) area() float64 {
    return math.Pi * c.radius * c.radius
}

type Rectangle struct {
    length, width float64
}

func (r Rectangle) perimeter() float64 {
    return 2.0 * (r.width + r.length)
}

func (r Rectangle) area() float64 {
    return r.width * r.length
}

func showDetails(s Shape) {
    fmt.Printf("%T perimeter is %g and area is %g\n", s, s.perimeter(), s.area())
}

func main() {
    showDetails(Rectangle{5.5, 20.3})
    showDetails(Circle{10})
}
$ go run shapes.go
main.Rectangle perimeter is 51.6 and area is 111.65
main.Circle perimeter is 62.83185307179586 and area is 314.1592653589793

Errors

Rather than throwing exceptions, returning error codes, or returning result enums (say, like Swift), the convention in Go is for failable functions to simply return two values: the first is the value that should be returned on success, and the second is either nil on success or a value of a type implementing error on failure.

There are a few examples of this above.

Defer

Go has a defer statement that schedules a function call to be run after the function it is in returns. This is useful for cleanup tasks, like closing a file or unlocking a mutex. Read about defer at Go By Example.

Panic

A panic is a run time error that normally is fatal and nonrecoverable. It is similar to throwing an exception in other languages. In some rare cases, you can recover from a panic using the recover function. Read about panic at Go By Example.

Concurrency

Go’s concurrency features are awesome. Go was designed with concurrent applications in mind. It’s helpful organize your study of Go concurrency around three big themes:

Goroutines

A goroutine is essentially a lightweight thread

Channels

For message passing and synchronization between goroutines

Synchronization Mechanisms

Mutexes, wait groups, atomics, condition variables, and more

Goroutines

All code runs on a goroutine. All of the examples above have just one goroutine, the main one. The entire program stops when the main goroutine stops, so you almost always need a way for the main goroutine to wait for the others to finish. One approach is to use a channel:

01-channels.go
package main

import (
    "fmt"
    "time"
)

var done = make(chan bool)

func printChars(c rune, n int) {
    for i := 0; i < n; i++ {
        fmt.Printf("%c", c)
        time.Sleep(time.Millisecond)
    }
    done <- true
}

func main() {
    go printChars('0', 500)
    go printChars('1', 500)
    <-done
    <-done
    fmt.Println()
}

Another is to use a wait group:

01-waitgroups.go
package main

import (
    "fmt"
    "sync"
    "time"
)

func printChars(c rune, n int, wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 0; i < n; i++ {
        fmt.Printf("%c", c)
        time.Sleep(time.Millisecond)
    }
}

func main() {
    var wg sync.WaitGroup
    wg.Add(2)
    go printChars('0', 500, &wg)
    go printChars('1', 500, &wg)
    wg.Wait()
    fmt.Println()
}

Channels

Here are the things to know about channels:

In an unbuffered channel, the sender and receiver synchronize on a single value and they block for each other. (This autosync can be exploited to not require locks or condition variables in many cases.) We saw an example above.

A buffered channel has a capacity. Senders only block if the channel is full; if not full, they write immediately and proceed, even if there is no receiver is ready! Receivers only block if the channel is empty:

TODO Buffered channel example

Here is quick comparison of unbuffered and buffered channels:

FeatureUnbuffered ChannelBuffered Channel
CapacityZero capacityFixed, user-defined capacity
SendingBlocks until receivedBlocks only if buffer is full
ReceivingBlocks until sentBlocks only if buffer is empty
SynchronizationStrict, synchronous rendezvousAsynchronous (within buffer capacity)
Use CaseStrict synchronization, handshaking, rendezvousDecoupling, producer-consumer patterns, rate limiting, asynchronous communication, fire and forget

As channels are a form of message passing, Go has ways to do balking and timeout sends, and you can also write servers that can timeout or balk on receives. Here is an example showing the different kinds of sends (blocking, nonblocking, and timeout):

senders.go
package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var ch = make(chan struct{})
    var wg sync.WaitGroup
    wg.Add(1)

    go func() {
        defer wg.Done()

        // Non blocking send, receiver will not be ready
        select {
        case ch <- struct{}{}:
            fmt.Println("Receiver was immediately ready, message sent")
        default:
            fmt.Println("Receiver was not immediately ready")
        }

        // Two second timeout, receiver will not be ready
        select {
        case ch <- struct{}{}:
            fmt.Println("Receiver ready within 2 seconds, message sent")
        case <-time.After(2 * time.Second):
            fmt.Println("Timeout after 2 seconds")
        }

        // Ten second timeout, receiver will be ready
        select {
        case ch <- struct{}{}:
            fmt.Println("Receiver ready within 10 seconds, message sent")
        case <-time.After(10 * time.Second):
            fmt.Println("Timeout after 10 seconds")
        }

        // This nonblocking send will successfully send a message
        select {
        case ch <- struct{}{}:
            fmt.Println("Receiver was immediately ready, message sent")
        default:
            fmt.Println("Receiver was not immediately ready")
        }

        // Blocking send, will eventually be serviced
        ch <- struct{}{}
        fmt.Println("Blocking send completed, message sent")
    }()

    // Enough time for the first two sends to find the receiver not ready
    time.Sleep(3 * time.Second)

    // Service the 10s timeout then the next non-blocking send
    <-ch
    <-ch

    // Wait a bit before servicing the final blocking send
    time.Sleep(5 * time.Second)
    <-ch
    wg.Wait()
}

The select statement is also used for nonblocking and timed receives. But it can also be multi-way, servicing any available channel:

TODO server-side analog of the previous example

Go By Example has good examples of the select statement, timeouts, and nonblocking operations.

In some cases, a sender may wish to close a channel. This means you won’t be sending anything else on the channel (if you do, it will cause a panic). Closing a channel allows receivers to detect this and stop waiting for new values. Here is how a receiver can detect a close:

TODO Close detection

A receiver can also iterate over a channel using the for statement, which will stop when the channel is closed:

TODO For range over channel

When programming with channels, cleaning up and closing down channels gracefully is quite important. Here’s a fairly comprehensive article covering many issues and tactics for managing channels cleanly.

Exercise: Read the article! Then, read this one.

Timers and Tickers

Go provides timers and tickers for executing jobs once or repeatedly, respectively, in the future. Creating a timer or ticker creates a channel that you can read from when the next time is reached (as close as possible) and the date becomes ready. Read about timers and tickers at Go By Example.

Protecting Shared Resources

If you have an application with shared data that multiple goroutines are reading and writing concurrently, you might have data races. There are a few things you can employ to avoid them:

Learning More

Here are some more thorough overviews and lessons on Go concurrency:

You might also be interested in articles from the Go blog:

Unit Testing

Go tests come in two forms—traditional unit tests and testable examples. The testable examples are awesome. They say they are best for simple cases only, but I dunno, I use them all the time. Here’s the test for the quaternions package above:

quaternion_test.go
package quaternions

import "fmt"

func ExampleQuaternion() {
    q := Quaternion{3.5, 2.25, -100.0, -1.25}
    fmt.Println(q.A, q.B, q.C, q.D)
    fmt.Println(q.Coefficients())
    // Output: 3.5 2.25 -100 -1.25
    // [3.5 2.25 -100 -1.25]
}

func ExampleArithmetic() {
    q1 := Quaternion{1.0, 3.0, 5.0, 2.0};
    q2 := Quaternion{-2.0, 2.0, 8.0, -1.0};
    q3 := Quaternion{-1.0, 5.0, 13.0, 1.0};
    q4 := Quaternion{-46.0, -25.0, 5.0, 9.0};
    fmt.Println(q1.Add(q2) == q3)
    fmt.Println(q1.Multiply(q2) == q4)
    fmt.Println(q1.Add(Zero) == q1)
    fmt.Println(q1.Multiply(Zero) == Zero)
    fmt.Println(I.Multiply(J) == K)
    fmt.Println(J.Multiply(K) == I)
    fmt.Println(J.Add(I) == Quaternion{0.0, 1.0, 1.0, 0.0})
    // Output: true
    // true
    // true
    // true
    // true
    // true
    // true
}

func ExampleStrings() {
    fmt.Println(Zero)
    fmt.Println(J)
    fmt.Println(K.Conjugate())
    fmt.Println(J.Conjugate().Multiply(Quaternion{2.0, 0.0, 0.0, 0.0}))
    fmt.Println(J.Add(K))
    fmt.Println(Quaternion{0.0, -1.0, 0.0, 2.25})
    fmt.Println(Quaternion{-20.0, -1.75, 13.0, -2.25})
    fmt.Println(Quaternion{-1.0, -2.0, 0.0, 0.0})
    fmt.Println(Quaternion{1.0, 0.0, -2.0, 5.0})
    // Output: 0
    // j
    // -k
    // -2j
    // j+k
    // -i+2.25k
    // -20-1.75i+13j-2.25k
    // -1-2i
    // 1-2j+5k
}

Run like so:

$ go test quaternion.go quaternion_test.go
ok

For traditional unit tests, you have test functions accepting pointers to a *testing.T:

stats.go
package stats

import "errors"

func Average(a []float64) (float64, error) {
    if len(a) == 0 {
        return 0, errors.New("No average for empty collection")
    }
    total := 0.0
    for _, x := range a {
        total += x
    }
    return total / float64(len(a)), nil
}

And here's the test:

stats_test.go
package stats

import "testing"

func TestAverage(t *testing.T) {
    average, error := Average([]float64{10.0, 1.0, 4.0})
    if average != 5.0 && error != nil {
        t.Errorf("Average of [10, 1, 4] should be 5")
    }
    _, error = Average([]float64{})
    if error == nil {
        t.Errorf("Average of empty slice should return an error")
    }
}

To test:

$ go test stats.go stats_test.go
ok

This works, and is good enough for school, but in general, testing should be part of building your applications according to the standard Go project layout, which you'll learn about in the video in the next section.

Writing Large Go Applications

Go has an awesome ecosystem for building and managing massive software projects.

Here is how you can build an app using your own packages.