Home >

Go By Assertion

Go By Assertion

A Go reference using unit testing assertions as opposed to print statements.

Posted on November 26, 2019 by Ernesto Garbarino

Introduction

This is a compact Go reference based on unit testing assertions as opposed to print statements—usually the norm. Comments are kept to a minimum; the reader is assumed to be familiarised with C-style languages (Java, C#, C++, etc.).

The order of topics is relatively bottom-up (basic topics appear before complex ones) but there are some circular dependencies. For example, covering control flow requires some understanding of basic types.

For convenience, the assertions are stylised as follows:

All source code snippets are linked to original files on GitHub at https://github.com/egarbarino/go-by-assertion/

Control Flow

Go, being an imperative language, offers a number of control flow statements, although relative small compared to other languages. For example, all forms of looping rely on the for keyword rather than while, foreach, etc.

If-Then

The syntax is if EXPR {...}. If statements don’t use parentheses in Go, unlike other C-like languages.

var counter = 0

// Simple If
if counter == 0 {
	counter++
}

// Assertions
1 ⇔ counter

Source: controlflow_test.go | Top

If-Then-Else

The syntax is if EXPR {...} else {...}.

var counter = 0

// Simple If
if counter == 0 {
	counter++
} else {
	counter--
}

// Assertions
1 ⇔ counter

Source: controlflow_test.go | Top

If-Then-Else If

The syntax is if EXPR {...} else if EXPR {...}. A final else {...}, as in the example below, may also be included.

var counter = 0

// Simple If
if counter == 0 {
	counter++
} else if counter == 10 {
	counter = counter * 10
} else if counter == 20 {
	counter = counter * 20
} else { // Optional
	counter--
}

// Assertions
1 ⇔ counter

Source: controlflow_test.go | Top

If-Scoped Variables

An if statement, in the form if VAR_DECLARATION ; EXPR {...}, introduces variables to its scope.

var counter = 0

// Assign
if i := 5; counter == 0 {
	counter = counter + i // i in scope here
} else {
	counter = counter - i // i in scope here
}

// --- i is not longer in scope here ---

// Assertions
5 ⇔ counter

Source: controlflow_test.go | Top

Switch

The regular switch EXPR {...} statement evaluates an expression EXPR and executes the first case whose value matches the reduction of said expression. Unlike other languages, the cases do not “cascade” and there is no need to terminate them with a break statement or similar.

The default keyword is used to label the case to be selected when all the other ones prefixed with case fail to match.

var result = ""

switch 1 + 1 {
case 0:
	result = "zero"
case 1:
	result = "one"
case 2:
	result = "two"
default: // Optional
	result = "some_number"
}

// Assertions
"two" ⇔ result

Source: controlflow_test.go | Top

Switch (Multi-Value Case)

The multi-value switch allows to match multiple values within a single case statement.

var result = ""

// Just cases
switch 2 * 3 {
case 0, 1, 2, 3, 4, 5, 6: // multiple values!
	result = "between_zero_and_six"
case 7:
	result = "seven"
default: // Optional
	result = "some_number"
}

// Assertions
"between_zero_and_six" ⇔ result

Source: controlflow_test.go | Top

Switch (Expression Cases)

This is a type of switch statement in which the cases contain boolean expressions rather then values. The first case whose expression reduces to true will be executed.

var result = ""

switch { // No expression here
case 2*3 == 1:
	result = "one"
case 2*3 == 6:
	result = "six"
default: // Optional
	result = "some_number"
}

// Assertions
"six" ⇔ result

Source: controlflow_test.go | Top

A more crude example is presented below:

var result = ""

switch { // No expression here
case false:
	result = "false1"
case false:
	result = "false2"
case true:
	result = "true1"
default: // Optional
	result = "true2"
}

// Assertions
"true1" ⇔ result

Source: controlflow_test.go | Top

Switch (Type Cases)

A switch statement in which the cases represent the types that a value may possess.

testType := func(i interface{}) string {
	switch t := i.(type) { // the t := assignment is optional
	case int:
		return "int"
	case string:
		return "string"
	default: // Optional
		return "other type: " + fmt.Sprintf("%T", t)
	}
}

// Assertions
"string" ⇔ testType("hello")
"int" ⇔ testType(45)
"other type: float64" ⇔ testType(1.53)

Source: controlflow_test.go | Top

Infinite Loop (While True Loop)

An infinite loop, in the form for {...} never terminates and must be short-circuited explicitly; for example, using break as in the example below.

var counter = 0

for {
	counter = counter + 1
	if counter == 3 {
		break
	}
}

// Assertions
3 ⇔ counter

Source: controlflow_test.go | Top

While Loop

A while loop has the form for EXPR {...} where the body is executed repeatedly as long as EXPR is true.

var counter = 0

for counter < 3 {
	counter = counter + 1
}

// Assertions
3 ⇔ counter

Source: controlflow_test.go | Top

Iteration Over Array Elements

This is accomplished by using the for index, value := range ARRAY {...} syntax where index is the array index starting from zero.

var indexSum = 0
var sum = 0

for index, currentValue := range [3]int{1, 2, 3} {
	indexSum = indexSum + index // 0 + 1 + 2 = 3
	sum = sum + currentValue    // 1 + 2 + 3 = 6
}

// Assertions
3 ⇔ indexSum
6 ⇔ sum

Source: controlflow_test.go | Top

Iteration Over Array Elements Using Only Index

This is like the regular iteration over an array except that the second value is ignored in the assignment: index := range ARRAY {...}

var sum = 0
var numbers = [3]int{1, 2, 3}

for index := range numbers {
	sum = sum + numbers[index] // array elements accessed by index!
}

// Assertions
6 ⇔ sum

Source: controlflow_test.go | Top

Iteration Over Array Elements Ignoring Index

In this case, the index value is explicitly ignored by using the underscore _ blank identifier symbol.

var sum = 0

for _, currentValue := range [3]int{1, 2, 3} {
	sum = sum + currentValue // 1 + 2 + 3 = 6
}

// Assertions
6 ⇔ sum

Source: controlflow_test.go | Top

Iteration Over Keys and Values of a Map

This is achieved using the key, value := range MAP {...} syntax.

var keys, values string

for k, v := range map[string]string{"A": "Argentina", "B": "Brazil"} {
	keys = keys + k
	values = values + v
}

// Assertions
true ⇔ strings.Contains(keys, "A")
true ⇔ strings.Contains(keys, "B")
true ⇔ strings.Contains(values, "Argentina")
true ⇔ strings.Contains(values, "Brazil")

Source: controlflow_test.go | Top

Iteration Over the Unicode Characters of a String

By prefixing an string with range, the for loop will iterate through the string’s Unicode characters as opposed to its raw bytes.

var word string

for _, v := range "😊 olleh" {
	word = string(v) + word
}

// Assertions
"hello 😊" ⇔ word

Source: controlflow_test.go | Top

Iteration Over The Bytes of a String

This is like iterating over an array in a regular C-like language.

var reversedHello = "😊 olleh"
var word string

for i := 0; i < len(reversedHello); i++ {
	word = string(reversedHello[i]) + word
}

// Assertions
"hello \u008a\u0098\u009fð" ⇔ word

Source: controlflow_test.go | Top

Regular For Loop

The regular for loop is exactly the same as that found in C-like languages except that no parentheses are used and that the variable(s) are initialised with := rather than =.

var counter = 0

for i := 0; i < 3; i++ {
	counter = counter + i // 0 + 1 + 2 = 3
}

// Assertions
3 ⇔ counter

Source: controlflow_test.go | Top

Defer

The defer STATEMENT syntax is used to enforce the execution of the statement STATEMENT before the function under which the declaration appears is exited. The execution order is that of a stack: last in, first out (LIFO).

var actions = ""

addAction := func(action string) {
	actions = actions + action
}

trackDefer := func() {
	addAction("1")
	defer addAction("[d1]")
	addAction("2")
	defer addAction("[d2]")
	addAction("3")
	defer addAction("[d3]")
	addAction("4")
}

actions = actions + "START-"
trackDefer()
actions = actions + "-END"

// Assertions
"START-1234[d3][d2][d1]-END" ⇔ actions

Source: controlflow_test.go | Top

Functions

Go functions are actually “procedures” rather than pure functions like in a FP language such as Haskell. It means that they are not necessarily idempotent; they may have side effects.

Simple Function

A simple function neither takes nor returns values.

var globalVar = ""

func simple() {
	globalVar = "changed"
}

// Assertions
"" ⇔ globalVar
simple()
"changed" ⇔ globalVar

Source: functions_test.go | Top

Simple Function With Return Value

The return value type is declared after the parenthesis: func name() <TYPE>. The value itself is returned using return <VALUE>.

func hello() string {
	return "hello"
}

// Assertions
"hello" ⇔ hello()

Source: functions_test.go | Top

Function With Multiple Return Values

Two or more values may be returned by a function; their type must be declared using the func name() (TYPE1, TYPE2, ...) syntax. When invoking the function, there is the flexibility to assign only the required values as shown below:

func returnTwo() (rune, bool) {
	return 'a', false
}

// Assertions (Two Values)
first, second := returnTwo()
'a' ⇔ first
false ⇔ second

// Assertions (First Value Only)
onlyFirst, _ := returnTwo()
'a' ⇔ onlyFirst

// Assertions (Second Value Only)
_, onlySecond := returnTwo()
false ⇔ onlySecond

Source: functions_test.go | Top

Function Arguments

Functions arguments are expressed using the func name(<VAR> <TYPE>) syntax. Multiple arguments may be declared by separating them with a comma as shown below:

var result = ""

func oneArgument(name string) {
	result = name
}

func twoArguments(name string, age int) {
	result = fmt.Sprintf("%s-%d", name, age)
}

// Assertions (One Argument)
oneArgument("Rob")
"Rob" ⇔ result

// Assertions (Two Arguments)
twoArguments("Rob", 36)
"Rob-36" ⇔ result

Source: functions_test.go | Top

Variadic Functions

Variadic functions take a varying number of arguments. The syntax is func name(<VAR> ...<TYPE>). Please note that <TYPE> is prefixed with three dots .... In the function’s body, <VAR> will appear like an array and can be iterated using the range keyword.

func sum(numbers ...int) int {
	var result int = 0
	for _, n := range numbers {
		result = result + n
	}
	return result
}

// Assertions
0 ⇔ sum()
1 ⇔ sum(1)
3 ⇔ sum(1, 2)
6 ⇔ sum(1, 2, 3)

Source: functions_test.go | Top

Anonymous Functions

Anonymous functions are declared without a custom name using the form func() {...} and can be assigned to variables or returned directly as values.

func operation(op string) func(int, int) int {
	switch op {
	case "sum":
		return func(v1 int, v2 int) int {
			return v1 + v2
		}
	case "mul":
		return func(v1 int, v2 int) int {
			return v1 * v2
		}
	default:
		panic("op should be either \"sum\" or \"mul\"")
	}
}

// Assertions

// Direct use
3 ⇔ operation("sum")(1, 2)
6 ⇔ operation("mul")(2, 3)

// Function extraction first
sum := operation("sum")
mul := operation("mul")
3 ⇔ sum(1, 2)
6 ⇔ mul(2, 3)

// Create Inline
3func(v1 int, v2 int) int { return v1 + v2 }(1, 2)

Source: functions_test.go | Top

Closures

Closures are a type of function that references variables from outside its body.

var counter = 0

incrementer := func() int {
	counter++
	return counter
}

// Assertions
1 ⇔ incrementer()
counter++
3 ⇔ incrementer()
incrementer()
5 ⇔ incrementer()
6 ⇔ incrementer()

Source: functions_test.go | Top

Basic Types

Basic types are also known as atomic types; they are those that cannot be broken up into smaller components such as integers, floats, etc. However, we also include string conversion to and from primitive types. Likewise, we also include the most typical built-in operators applicable to the basic type at hand.

Boolean (Bool) and Logical Operators

Boolean types are built-in and work with the typical logic operators such as AND (&&), OR (||), Negation (!), etc.

// Declaration
var MinBool bool = false
var MaxBool bool = true

// Logical Operators
false ⇔ !true
true ⇔ !false
truetrue || false
true ⇔ MinBool || MaxBool
falsetrue && false

Source: basictypes_test.go | Top

Signed Integers

Signed integers can accommodate negative numbers (and applicable arithmetic) but the trade off is losing one bit and therefore, half the capacity.

8-Bit

// Declaration
var MinInt8 int8 = -128
var MaxInt8 int8 = 127

// Bounds
true ⇔ MinInt8 == math.MinInt8
true ⇔ MaxInt8 == math.MaxInt8

Source: basictypes_test.go | Top

16-Bit

// Declaration
var MinInt16 int16 = -32768
var MaxInt16 int16 = 32767

// Bounds
true ⇔ MinInt16 == math.MinInt16
true ⇔ MaxInt16 == math.MaxInt16

Source: basictypes_test.go | Top

32-Bit (Rune)

// Declaration
var MinInt32 rune = -2147483648
var MinRune rune = MinInt32
var MaxInt32 int32 = 2147483647
var MaxRune int32 = MaxInt32

// Bounds
true ⇔ MinInt32 == math.MinInt32
true ⇔ MaxInt32 == math.MaxInt32
true ⇔ MinRune == math.MinInt32
true ⇔ MaxRune == math.MaxInt32

// Rune is an alias for int32
MaxRune ⇔ MinInt32-1
MinRune ⇔ MaxInt32+1

Source: basictypes_test.go | Top

64-Bit

// Declaration
var MinInt64 int64 = -9223372036854775808
var MaxInt64 int64 = 9223372036854775807

// Bounds
true ⇔ MinInt64 == math.MinInt64
true ⇔ MaxInt64 == math.MaxInt64

Source: basictypes_test.go | Top

General Integer

The size depends on the underlying architecture.

// Only on 64-bit architectures
var MinInt int = -9223372036854775808
var MaxInt int = 9223372036854775807

// Same as int64 on 64-bit architectures
MinInt ⇔ math.MinInt64
MaxInt ⇔ math.MaxInt64

// Overflow behaviour
MaxInt ⇔ MinInt-1
MinInt ⇔ MaxInt+1

Source: basictypes_test.go | Top

Unsigned Integers

Unsigned integers only store positive numbers (including zero) and offer larger capacity thanks to not having to use the sign bit.

8-Bit (Byte)

// Declaration
var MinUInt8 uint8 = 0
var MaxUInt8 uint8 = 255 // 2^8-1
var MinByte byte = MinUInt8
var MaxByte byte = MaxUInt8

// Bounds
true ⇔ MaxUInt8 == math.MaxUint8

// Overflow
true ⇔ math.MaxUint8 == MinUInt8-1
true ⇔ MinUInt8 == MaxUInt8+1

// byte is an alias for uint8
MaxByte ⇔ MinUInt8-1
MinByte ⇔ MaxUInt8+1

Source: basictypes_test.go | Top

16-Bit

// Decaration
var MinUInt16 uint16 = 0
var MaxUInt16 uint16 = 65535 // 2^16-1

// Bounds
true ⇔ MaxUInt16 == math.MaxUint16

// Overflow
MaxUInt16 ⇔ MinUInt16-1
MinUInt16 ⇔ MaxUInt16+1

Source: basictypes_test.go | Top

32-Bit

// Declaration
var MinUInt32 uint32 = 0
var MaxUInt32 uint32 = 4294967295 // 2^32-1

// Bounds
true ⇔ MaxUInt32 == math.MaxUint32

// Overflow
MaxUInt32 ⇔ MinUInt32-1
MinUInt32 ⇔ MaxUInt32+1

Source: basictypes_test.go | Top

64-Bit

// Declaration
var MinUInt64 uint64 = 0
var MaxUInt64 uint64 = 18446744073709551615 // 2^64-1

// Bounds
true ⇔ MaxUInt64 == math.MaxUint64

// Overflow
MaxUInt64 ⇔ MinUInt64-1
MinUInt64 ⇔ MaxUInt64+1

Source: basictypes_test.go | Top

General Unsigned Integer

Size is implementation-specific (either 32-bit or 64-bit)

// Declaration
var MinUint uint = 0
var MaxUint uint = 18446744073709551615

// Bounds: Same as uint64 on 64-bit architectures
true ⇔ math.MaxUint64 == MaxUint

// Overflow behaviour
MaxUint ⇔ MinUint-1
MinUint ⇔ MaxUint+1

Source: basictypes_test.go | Top

Unsigned Integer Pointer

Size is implementation-specific (either 32-bit or 64-bit)

// Declaration
var MinUintptr uintptr = 0
var MaxUintptr uintptr = 18446744073709551615

// Bounds: Same as uint64 on 64-bit architectures
true ⇔ math.MaxUint64 == MaxUintptr

// Overflow behaviour
MaxUintptr ⇔ MinUintptr-1
MinUintptr ⇔ MaxUintptr+1

Source: basictypes_test.go | Top

Float

The float type allows for a decimal component.

32-Bit

// Declaration
var MinFloat32 float32 = -1.401298464324817e-45
var MaxFloat32 float32 = 3.4028234663852886e+38

// Bounds
true ⇔ MinFloat32 == -math.SmallestNonzeroFloat32
true ⇔ MaxFloat32 == math.MaxFloat32

Source: basictypes_test.go | Top

64-Bit

// Declaration
var MinFloat64 float64 = -5e-324
var MaxFloat64 float64 = 1.7976931348623157e+308

// Bounds
true ⇔ MinFloat64 == -math.SmallestNonzeroFloat64
true ⇔ MaxFloat64 == math.MaxFloat64

Source: basictypes_test.go | Top

Complex

A type for Complex numbers.

64-Bit

// Declaration and Bounds
complex(1.401298464324817e-45, 1.401298464324817e-45) ⇔ complex(math.SmallestNonzeroFloat32, math.SmallestNonzeroFloat32)
complex(3.4028234663852886e+38, 3.4028234663852886e+38) ⇔ complex(math.MaxFloat32, math.MaxFloat32)

Source: basictypes_test.go | Top

128-Bit

// Declaration and Bounds
complex(5e-324, 5e-324) ⇔ complex(math.SmallestNonzeroFloat64, math.SmallestNonzeroFloat64)
complex(1.7976931348623157e+308, 1.7976931348623157e+308) ⇔ complex(math.MaxFloat64, math.MaxFloat64)

Source: basictypes_test.go | Top

Number Literals

Number literals may be expressed using a variety of notations (decimal, scientific, etc.) and bases (binary, octal, hex, etc.)

// Assertions
2550xFF              // Hex
2550XFF              // Hex
2550377              // Octal
2550o377             // Octal
2550O377             // Octal
255 ⇔ 0b11111111        // Binary
255 ⇔ 0B11111111        // Binary
0.5.5                // Float
0.500.5              // Float
0.00.                // Float
50.00.5e2            // Float w/Exponent
50.00.5E2            // Float w/Exponent
50.00.5E+2           // Float w/Exponent
0.0050.5E-2          // Float w/Exponent
complex128(1+2i) ⇔ 1+2i // Complex Number
10000001_000_000     // Digit seperator

Source: basictypes_test.go | Top

Number Arithmetic Operators

Arithmetic operators are overloaded for every underlying type. For example, the division operator / produces an integer result when the operands are integers, but a float one when they are float.

// Assertions
53+2                                 // Sum
13-2                                 // Subtraction
63*2                                 // Multiplication
13/2                                 // Integer Division
1.53.0/2.0                           // Float Division
25%3                                 // Modulo operator
2.0 ⇔ math.Sqrt(4)                      // Square Root
true ⇔ math.Pi > 3.14 && math.Pi < 3.15 // Pi Constant

// State mutating operators
var x = 3
x++ // Increase value by one
4 ⇔ x
x-- // Decrease value by one
3 ⇔ x

Source: basictypes_test.go | Top

Number Type Conversion

As expected, conversion from a smaller type to a larger one is value-preserving. The noteworthy thing is that conversion from a larger type to a small one results in truncation to zero whenever the value is too long to fit.

// Upcasting (Value-Preserving)
var smallValue byte = 5
var smallNegValue int8 = -5

// Assertions
true9000+uint(smallValue) == 9005
true9000+int(smallNegValue) == 8995
true9000+uint16(smallValue) == 9005
true9000+int16(smallNegValue) == 8995
true900000000+uint32(smallValue) == 900000005
true900000000+int32(smallNegValue) == 899999995
true9000000000000000000+uint64(smallValue) == 9000000000000000005
true9000000000000000000+int64(smallNegValue) == 8999999999999999995

// Down-casting (Value-Destructive)
var bigValue uint64 = 18446744073709551615    // 2^64
var bigNegValue int64 = -9223372036854775808  // 2^63
var bigNegValue2 int64 = -4611686018427387904 // 2^62
var bigNegValue3 int64 = -4294967296          // 2^32
uint32(4294967295) ⇔ uint32(bigValue)
int32(-5) ⇔ int32(smallNegValue)
int32(0) ⇔ int32(bigNegValue)  // Truncated to zero
int32(0) ⇔ int32(bigNegValue2) // Truncated to zero
int32(0) ⇔ int32(bigNegValue3) // Truncated to zero
uint16(65535) ⇔ uint16(bigValue)
uint8(255) ⇔ uint8(bigValue)

// Float and Integers
var floatValue float32 = 1.5
float32(1.0) ⇔ float32(1)
1int(floatValue)

Source: basictypes_test.go | Top

Rune Literals

Rune (Unicode) literals may be expressed using decimals, octal numbers, hexadecimal ones, escape codes, as well as official Unicode code points.

// Assertions
'A'rune(65)     // Rune value
'A' ⇔ '\101'       // Octal
'A''\x41'       // Hex
'A' ⇔ '\u0041'     // 16-bit Unicode
'A' ⇔ '\U00000041' // 32-bit Unicode
'😀' ⇔ '\U0001F600' // 32-bit Unicode
'\a''\x07'      // Bell
'\b''\x08'      // Backspace
'\f''\x0C'      // Form feed
'\n''\x0A'      // Line Feed
'\r''\x0D'      // Carriage Return
'\t''\x09'      // Horizontal Tab
'\v''\x0b'      // Vertial Tab
'\\''\x5c'      // Backslash
'\'''\x27'      // Single Quote

Source: basictypes_test.go | Top

String Conversion

String conversion is more explicit in Go than in other languages; different techniques are required depending on the underlying type.

// The string() function converts a Unicode code point!
"A"string(65)
"😀"string(0X0001F600)
"AB"string([]byte{65, 66})

// Integer to String
"15" ⇔ fmt.Sprintf("%d", 15)
"15" ⇔ strconv.Itoa(15)
"15" ⇔ strconv.FormatInt(15, 10)    // Base 10
"f" ⇔ strconv.FormatInt(15, 16)     // Base 16
"     15" ⇔ fmt.Sprintf("%7d", 15)  // Padding
"15     " ⇔ fmt.Sprintf("%-7d", 15) // Padding
7len(fmt.Sprintf("%7d", 15))

// String to Integer
if n, err := strconv.Atoi("15"); err == nil {
	15 ⇔ n
} else {
	t.Error("Unable to convert")
}
if n, err := strconv.Atoi("ex1!%5"); err == nil {
	t.Error("Conversion was not supposed to be successful", n)
} else {
	err ⇎ nil
}

// Float to String
"1.567000" ⇔ fmt.Sprintf("%f", 1.567)
"1.57" ⇔ fmt.Sprintf("%.2f", 1.567)
"   1.57" ⇔ fmt.Sprintf("%7.2f", 1.567)
7len(fmt.Sprintf("%7.2f", 1.567))

// String to Float
if n, err := strconv.ParseFloat("1.567", 32); err == nil {
	1.5670000314712524 ⇔ n
} else {
	t.Error("Unable to convert")
}
if n, err := strconv.ParseFloat("ex1!%5.67", 32); err == nil {
	t.Error("Conversion was not supposed to be successful", n)
} else {
	err ⇎ nil
}

Source: basictypes_test.go | Top

Constants

Constants are prefixed with the const keyword.

// Constants are immutable
const x byte = 5
const y = 7

// x++ // Won't compile

// Array-like like Slices are mutable and cannot be made constants
// const slice = []byte{'a', 'b', 'c'} // Won't compile

Source: basictypes_test.go | Top

Arrays

Arrays are immutable: once created, their size cannot be changed. They are passed by value by default.

Empty Array

An empty arrays contains zero elements.

var arrayEmpty1 [0]int
arrayEmpty2 := [...]int{}

// Assertions
arrayEmpty1 ⇔ arrayEmpty2
0len(arrayEmpty1)

Source: arrays_test.go | Top

Blank Array

A blank array is populated with the applicable zero element for the declared type.

var intArray [2]int
var stringArray [2]string
var fileArray [2]os.File

// Assertions
0 ⇔ intArray[0]
0 ⇔ intArray[1]
"" ⇔ stringArray[0]
"" ⇔ stringArray[1]
os.File{} ⇔ fileArray[0]
os.File{} ⇔ fileArray[1]

Source: arrays_test.go | Top

Array Literals

Array literals are array declarations in which the space for the array is allocated and its elements are declared in one single statement.

// Explicit length
arrayTwo := [2]string{"Einstein", "Newton"}
// No need to specify length
arrayThree := [...]string{"Einstein", "Newton", "Curie"}

// Assertions
2len(arrayTwo)
3len(arrayThree)

Source: arrays_test.go | Top

Multidimensional Arrays

In multidimensional arrays, the outer elements are arrays (which in turn may declare further arrays!)

array := [2][2]string{
	{"A0", "B0"}, // Excel-like values
	{"A1", "B1"}, // for intuition
}

// Assertions
"A0" ⇔ array[0][0]
"B0" ⇔ array[0][1]
"A1" ⇔ array[1][0]
"B1" ⇔ array[1][1]

Source: arrays_test.go | Top

Setting Elements’ Values

Elements are referred by their index, starting from zero.

var array [2]string

array[0] = "Einstein"
array[1] = "Newton"

// Assertions
"Einstein" ⇔ array[0]
"Newton" ⇔ array[1]

Source: arrays_test.go | Top

Iteration (Classic)

Arrays may be traversed using the traditional C-like approach: by getting their length and referencing each element by index.

array := [3]byte{'a', 'b', 'c'}
var result = ""
var counter = 0
for i := 0; i < len(array); i++ {
	counter++
	result = result + string(array[i])
}

// Assertions
3 ⇔ counter
"abc" ⇔ result

Source: arrays_test.go | Top

Iteration (Range)

Please see Control Flow

Pass By Value vs Pass By Reference

Arrays are passed by value by default.

array := [3]byte{'a', 'b', 'c'}

changeFirstElement := func(array [3]byte) {
	array[0] = 'x'
}

changeFirstElementByRef := func(array *[3]byte) {
	array[0] = 'x'
}

// Assertion #1
// Copying an array produces a brand new one
array2 := array
array2[0] = 'x'
byte('a') ⇔ array[0] // Rather than x

// Assertion #2
// Functions receive an array copy, rather than a reference
changeFirstElement(array)
byte('a') ⇔ array[0] // Rather than x

// Assertion #3
// Pass array's pointer
changeFirstElementByRef(&array)
byte('x') ⇔ array[0] // Rather than a

Source: arrays_test.go | Top

Slices

Slices are in principle “views” or “projections” over arrays created using the slice syntax, for example, [3]string{"red","green","blue"}[0:2] produces a slice that points to the {"red","green"} elements of the array. However, the idiomatic treatment of slices in Go is to always work with slices from the get go even when the initial slice could have been implemented as an array.

The key message is that a slice is a type in itself rather than an array with a lesser number of elements.

Slices vs Arrays

This example shows that when we get an array’s slice, a brand new type (hmm, a slice), is produced, rather than a smaller array.

var array = [3]string{"red", "green", "blue"}

// Assertions
[]string{"red", "green"} ⇔ array[0:2]     // Slice is a length-free type
[2]string{"red", "green"} ⇎ array[0:2] // Slice is not a smaller array

Source: slices_test.go | Top

Empty Slice

Unlike plain arrays, slices may be declared as empty but then populated using the append() function. The implementation is not, however, optimised for this use case: new arrays may be allocated depending on the slice’s initial capacity.

var sliceEmpty1 []int
var sliceEmpty2 = make([]int, 0)
var sliceEmpty3 = []int{}

// Assertions #1
// Prove slices have no elements
0len(sliceEmpty1)
0len(sliceEmpty2)
0len(sliceEmpty3)

// Assertions #2
// Append one element
[]int{5} ⇔ append(sliceEmpty1, 5)
[]int{5} ⇔ append(sliceEmpty2, 5)
[]int{5} ⇔ append(sliceEmpty3, 5)

Source: slices_test.go | Top

Make (Slice Allocation)

The make() function helps allocate a new slice by specifying the initial slice’s length and capacity. The specific syntax for slices is make([]<TYPE>, <LENGTH>, [<CAPACITY>]). The functions len() and cap() are used to obtain a slice’s length and capacity, respectively.

var slice1 = make([]int, 2)    // Capacity omitted
var slice2 = make([]int, 2, 2) // Equal length and capacity
var slice3 = make([]int, 1, 2) // Capacity larger then length
// var slice4 = make([]int, 3, 2) // Length larger than capacity (Compiler error)

// Assertions #1
[]int{0, 0} ⇔ slice1
2len(slice1)
2cap(slice1)

// Assertions #2
[]int{0, 0} ⇔ slice2
2len(slice2)
2cap(slice2)

// Assertions #3
[]int{0} ⇔ slice3
1len(slice3)
2cap(slice3)

Source: slices_test.go | Top

Slice Literals

A slice literal combines the allocation of a slice, and the setting of its elements, in one statement.

slice := []string{"Einstein", "Newton"}

// Assertions
2len(slice)

Source: slices_test.go | Top

Setting Elements’ Values

Like in the case of arrays, elements are referenced by index, starting from zero.

slice := []byte{'a', 'b', 'c'}

// Assertions
byte('a') ⇔ slice[0]
slice[0] = 'x' // Change first element!
byte('x') ⇔ slice[0]

Source: slices_test.go | Top

Append

The append(<SLICE>, <VALUE1>, <VALUE2>, ...) function allows adding new elements to an existing slice. It is often said that append() allows to grow a slice.

var sliceInt []int
var sliceStr []string

var sliceIntPrime = append(sliceInt, 15)
var sliceStrPrime = append(sliceStr, "Einstein", "Newton")

// Assertions
1len(sliceIntPrime)        // New length
2len(sliceStrPrime)        // New length
15 ⇔ sliceIntPrime[0]         // New element #0
"Einstein" ⇔ sliceStrPrime[0] // New element #0
"Newton" ⇔ sliceStrPrime[1]   // New element #1

Source: slices_test.go | Top

Copy

The copy(<DST_SLICE>, <SRC_SLICE>) function allows copying the elements from one slice onto another one.

// ------------- Same length --------------
src1 := []byte{'a', 'b', 'c'}
dst1 := []byte{'x', 'y', 'z'}
copy(dst1, src1)

// Assertions
[]byte{'a', 'b', 'c'} ⇔ dst1
dst1 ⇔ src1

// --- Source smaller than destination  ---
src2 := []byte{'a', 'b'}
dst2 := []byte{'x', 'y', 'z'}
copy(dst2, src2)

// Assertions
[]byte{'a', 'b', 'z'} ⇔ dst2
dst2 ⇎ src2

// ---- Source larger than destination ----
src3 := []byte{'a', 'b', 'c', 'd'}
dst3 := []byte{'x', 'y', 'z'}
copy(dst3, src3)

// Assertions
[]byte{'a', 'b', 'c'} ⇔ dst3
dst3 ⇎ src3

Source: slices_test.go | Top

Subsets (Slices, Splits, Trims)

There are various ways to obtain a subset of a slice; using the slice notation as well as helper functions.

// Assertions (Slice Notation)
byte('e') ⇔ []byte("hello")[1]
[]byte("ello") ⇔ []byte("hello")[1:]
[]byte("hel") ⇔ []byte("hello")[:3]
[]byte("ell") ⇔ []byte("hello")[1:4]
[]byte("ell") ⇔ []byte("hello")[1:4]

// Assertions (Helper Functions)
[][]byte{[]byte("a"), []byte("b"), []byte("c")} ⇔ bytes.Split([]byte("a b c"), []byte(" "))
[]byte("hello") ⇔ bytes.Trim([]byte("😊hello🍺"), "😊🍺")
[]byte("hello") ⇔ bytes.Trim([]byte("😊hello🍺"), "😊🍺")
[]byte("hello") ⇔ bytes.TrimSpace([]byte("\t \t hello\n"))

Source: slices_test.go | Top

Iteration (Classic)

A slice may be traversed just like an array by counting elements until reaching the slice’s length using a for() loop.

slice := []byte{'a', 'b', 'c'}
var result = ""
var counter = 0
for i := 0; i < len(slice); i++ {
	counter++
	result = result + string(slice[i])
}
3 ⇔ counter
"abc" ⇔ result

Source: slices_test.go | Top

Iteration (Range)

See Control Flow

Pass By Value vs Pass By Reference

Slices are passed to functions by reference by default.

changeFirstElement := func(slice []byte) {
	slice[0] = 'y'
}

slice := []byte{'a', 'b', 'c'}

// Assertion #1
// A copy of a slice still points to the original slice
slice2 := slice
slice2[0] = 'x'
byte('x') ⇔ slice[0] // Rather than a

// Assertion #2
// Functions receive an slice pointer, rather than a copy
changeFirstElement(slice)
byte('y') ⇔ slice[0] // Rather than a or x

// Assertion #3
// A slice's slice still points to the original slice
lastTwoLetters := slice[1:]
lastTwoLetters[0] = 'z'
byte('z') ⇔ slice[1] // Rather than b

Source: slices_test.go | Top

Strings

In Go, a string is a discrete type as opposed to a list, or an array, like in Haskell, or C, respectively. However, it behaves like a slice of bytes in most cases.

Length

A string’s length is obtained using the len() function.

// Assertions
0len("")
5len("hello")

Source: strings_test.go | Top

Substring and Rune Index

The following family of functions from the package strings provide the location of strings and characters (runes) in various contexts.

// Assertions
1 ⇔ strings.Index("hello fellows", "ell")
7 ⇔ strings.LastIndex("hello fellows", "ell")
2 ⇔ strings.IndexByte("hello", 'l')
3 ⇔ strings.LastIndexByte("hello", 'l')
4 ⇔ strings.IndexRune("🍷😊🍺🍷", '😊')
2 ⇔ strings.IndexAny("hi😊!", "😊🍺🍷")
6 ⇔ strings.IndexAny("🤣hi😊!", "😊🍺🍷")
12 ⇔ strings.LastIndexAny("🍷😊🍺🍷", "🍷")

Source: strings_test.go | Top

Predicates (Substring, Starts/Ends With)

The following family of functions from the package strings provide assertions about various substring membership cases.

// Assertions
true ⇔ strings.Contains("hello", "llo")
true ⇔ strings.ContainsAny("hello", "elxqwer")
true ⇔ strings.ContainsAny("hi😊!", "😊🍺🍷")
true ⇔ strings.HasPrefix("hello", "he")
true ⇔ strings.HasSuffix("hello", "lo")

Source: strings_test.go | Top

Subsets (Slices, Splits, Trims)

The slice syntax [start:end] and the below functions from the package strings provide subsets of a string.

// Assertions
"e"string("hello"[1])
"ello""hello"[1:]
"hel""hello"[:3]
"ell""hello"[1:4]
"ell""hello"[1:4]
[]string{"a", "b", "c"} ⇔ strings.Split("a b c", " ")
"hello" ⇔ strings.Trim("😊hello🍺", "😊🍺")
"hello" ⇔ strings.Trim("😊hello🍺", "😊🍺")
"hello" ⇔ strings.TrimSpace("\t \t hello\n")

Source: strings_test.go | Top

Replace

The Replace() function from the strings package substitutes one string with another.

// Assertions
"hello 😊😊" ⇔ strings.Replace("hello :):)", ":)", "😊", -1)
"hello 😊:)" ⇔ strings.Replace("hello :):)", ":)", "😊", 1)

Source: strings_test.go | Top

Case Conversion (Title, Upper, Lower)

The Title(), ToUpper(), and ToLower() functions from the strings package change a string’s case.

// Assertions
"This Is The End" ⇔ strings.Title("this is the end")
"HELLO" ⇔ strings.ToUpper("HellO")
"hello" ⇔ strings.ToLower("HellO")

Source: strings_test.go | Top

Join (Concat)

The Join() function from the strings package allows concatenating strings.

// Assertions
"Hello-world" ⇔ strings.Join([]string{"Hello", "world"}, "-")

Source: strings_test.go | Top

Repeat

The Repeat() function from the strings package creates a string by repeating another one a set number of times.

// Assertions
"hihihi" ⇔ strings.Repeat("hi", 3)

Source: strings_test.go | Top

String Formatting

The Sprtinf() function from the fmt package helps formatting various value types as well as adding padding (spaces) for alignment purposes.

"17" ⇔ fmt.Sprintf("%d", 17)             // Decimal
"11" ⇔ fmt.Sprintf("%x", 17)             // Hexadecimal
"21" ⇔ fmt.Sprintf("%o", 17)             // Octal
"10001" ⇔ fmt.Sprintf("%b", 17)          // Binary
"10001" ⇔ fmt.Sprintf("%b", 17)          // Binary
"1.750000" ⇔ fmt.Sprintf("%f", 1.75)     // Floating Point
"1.75" ⇔ fmt.Sprintf("%.2f", 1.75)       // Floating Point
"true" ⇔ fmt.Sprintf("%t", true)         // Boolean
"😊" ⇔ fmt.Sprintf("%c", '😊')             // Rune (Unicode point)
"hello" ⇔ fmt.Sprintf("%s", "hello")     // Rune (Unicode point)
"    o" ⇔ fmt.Sprintf("%5s", "o")        // Right Alignment
"h    " ⇔ fmt.Sprintf("%-5s", "h")       // Left Alignment
"'😊'" ⇔ fmt.Sprintf("%q", '😊')           // Quoted Rune
"'😊'" ⇔ fmt.Sprintf("%q", '😊')           // Quoted Rune
"\"hello\"" ⇔ fmt.Sprintf("%q", "hello") // Quoted String
"c64" ⇔ fmt.Sprintf("%v%v", "c", 64)     // Default String formatting
"int" ⇔ fmt.Sprintf("%T", 64)            // Inferred Value's Type
"%" ⇔ fmt.Sprintf("%%")                  // Literal Percent Sign

Source: strings_test.go | Top

Maps

Maps are associations of keys to values; both of which can be of any type. They are also known as hashes or hashmaps in other languages. Maps types have the form map[<KEY-TYPE>]<VALUE-TYPE>.

Empty Map

An empty map contains no key/value pairs.

var emptyMap1 = map[string]int{}
var emptyMap2 = make(map[string]int)

// Assertions
0len(emptyMap1)
0len(emptyMap2)
emptyMap1 ⇔ emptyMap2

Source: maps_test.go | Top

Map Literal

A map literal allocates a Map and sets an initial number of key/value pairs in one statement.

datesOfBirth := map[string]int{
	"Newton":   1643,
	"Faraday":  1791,
	"Einstein": 1879,
}

// Assertions
1643 ⇔ datesOfBirth["Newton"]
1791 ⇔ datesOfBirth["Faraday"]
1879 ⇔ datesOfBirth["Einstein"]

Source: maps_test.go | Top

Key/Value Insertion and Updating

Key/value pairs are added (or updated) to a map using the map[<KEY>] = <VALUE> syntax.

datesOfBirth := map[string]int{}

// Insertion of non-existing key/value pairs
datesOfBirth["Newton"] = 1543
datesOfBirth["Faraday"] = 1791
datesOfBirth["Einstein"] = 1879

// Assertions #1
1543 ⇔ datesOfBirth["Newton"]
1791 ⇔ datesOfBirth["Faraday"]
1879 ⇔ datesOfBirth["Einstein"]

// Update of existing key/value pair
datesOfBirth["Newton"] = 1643

// Assertions #2
1643 ⇔ datesOfBirth["Newton"]

Source: maps_test.go | Top

Deletion

Key/value pairs are deleted using the delete(<MAP>, <KEY>) function.

var datesOfBirth = map[string]int{
	"Newton":   1643,
	"Faraday":  1791,
	"Einstein": 1879,
}

delete(datesOfBirth, "Faraday")

// Assertions
0 ⇔ datesOfBirth["Faraday"]
2len(datesOfBirth)

Source: maps_test.go | Top

Membership Test

Key membership can be tested by checking the second return value when querying a key.

// Set up Initial Map
var datesOfBirth = map[string]int{
	"Newton":   1643,
	"Faraday":  1791,
	"Einstein": 1879,
}

// Test key membership
_, ok1 := datesOfBirth["Newton"]
_, ok2 := datesOfBirth["Curie"]

// Assertions
true ⇔ ok1
false ⇔ ok2

Source: maps_test.go | Top

Getting All Keys and Values

This is achived via iteration.

var datesOfBirth = map[string]int{
	"Newton":   1643,
	"Faraday":  1791,
	"Einstein": 1879,
}

// Get all keys
var keys = ""
for key := range datesOfBirth {
	keys = keys + key
}

// Get all values
var values = ""
for _, value := range datesOfBirth {
	values = values + strconv.Itoa(value)
}

// Assertions (Keys)
true ⇔ strings.Contains(keys, "Einstein")
true ⇔ strings.Contains(keys, "Newton")
true ⇔ strings.Contains(keys, "Faraday")

// Assertions (Values)
true ⇔ strings.Contains(values, "1879")
true ⇔ strings.Contains(values, "1643")
true ⇔ strings.Contains(values, "1791")

Source: maps_test.go | Top

Pointers

Pointers are references to values, or more complex objects such as structs, maps, etc. Unlike C, there is no pointer arithmetic or the notion that a pointer to an array is equivalent to a pointer to its first element.

Referencing and Dereferencing Pointers

A pointer to a value is obtained by prefixing the value with &. If the value is a pointer, the value is obtained by prefixing it with *.

a := 0
var b, c = &a, &a

// Assertions #1
0 ⇔ a    // a is the original value
0 ⇎ b // b is a pointer to a. It is not zero
0 ⇔ *b   // *b is the value of a
0 ⇎ c // c is a pointer to a. It is not zero
0 ⇔ *c   // *c is the value of a

// Change
*b++

// Assertions #2
1 ⇔ a  // a is the original value
1 ⇔ *b // *b is the value of a
1 ⇔ *c // *c is the value of a

Source: pointers_test.go | Top

New: Obtaining a Pointer Rather Than a Value

Variables allocated with the new() function are pointers rather than values.

var n = new(int)

// Assertions #1
0 ⇔ *n
0 ⇎ n // n is a pointer!

// Change
*n++

// Assertions #2
1 ⇔ *n
"0x" ⇔ fmt.Sprintf("%v", n)[:2] // n is a pointer!

Source: pointers_test.go | Top

Pointer vs Value Behaviour

The referencing and dereferencing operators assume that the target variable is a value, but not all objects behave like values.

// ------------------ Pointer Behaviour ------------------------

// Slices
slice := []int{0}
sliceCopy := slice
sliceCopy[0]++
1 ⇔ slice[0]
1 ⇔ sliceCopy[0]

// Maps
myMap := map[string]int{"a": 0}
myMapCopy := myMap
myMapCopy["a"]++
1 ⇔ myMap["a"]
1 ⇔ myMapCopy["a"]

// Functions
myFunc := func(n int) int { return n }
myFuncCopy := myFunc
myFunc(1) ⇔ myFuncCopy(1)

// ------------------- Value Behaviour ------------------------

// Structs
type NumberWrapper struct {
	n int
}
myStruct := NumberWrapper{n: 0}
myStructCopy := myStruct
myStructCopy.n++
0 ⇔ myStruct.n
1 ⇔ myStructCopy.n

// Arrays
array := [1]int{0}
arrayCopy := array
arrayCopy[0]++
0 ⇔ array[0]
1 ⇔ arrayCopy[0]

// Other built-in types
// Strings, floats, integers, etc.

Source: pointers_test.go | Top

Structs

Structs allow defining complex multi-attribute data structures made up of basic types as well as other structs too. A struct in Go is something in between a classic C struct and a JSON object in JavaScript (when instantiated).

Empty Struct Without Fields

Structs may serve as a type container; there is no need to declare any attributes.

type Empty struct{}

// Assertions
"structs.Empty" ⇔ fmt.Sprintf("%T", Empty{})

Source: structs_test.go | Top

Struct Initialisation

Structs, once declared, may be initialised using the literal syntax, Name{}, or the new() function, in which case a pointer is returned rather than a value.

type Employee struct {
	Name   string
	Salary int
}

e1 := Employee{}
e2 := new(Employee)

// Assertions
e1 ⇔ *e2
"" ⇔ e1.Name
0 ⇔ e2.Salary

Source: structs_test.go | Top

Struct Literals

Struct literals allow initialising a struct and populating its attributes in one statement. Attributes may be defined by name, or in a positional basis.

type Employee struct {
	Name   string
	Salary int
}

// ------------------ Positional Literal ---------------------

e1 := Employee{"Jazmin", 2000}

// Assertions
"Jazmin" ⇔ e1.Name
2000 ⇔ e1.Salary

// ---------------- Attribute-wise Literal -------------------

e2 := Employee{
	Name:   "Jazmin",
	Salary: 2000,
}

// Assertions
"Jazmin" ⇔ e2.Name
2000 ⇔ e2.Salary

// ------------------ Partial Literal (I) --------------------

// Partial Literal
e3 := Employee{
	Name: "Jazmin",
	// Salary: 2000, Omitted!!
}
// e3 := Employee{"Jazmin"} Illegal!

// Assertions
"Jazmin" ⇔ e3.Name
0 ⇔ e3.Salary // Zero value!

// ----------------- Partial Literal (II) --------------------

// Partial Literal (II)
e4 := Employee{
	// Name: "Jazmin", Omitted!!
	Salary: 2000,
}

// Assertions
"" ⇔ e4.Name // Zero value!
2000 ⇔ e4.Salary
Employee{Name: "", Salary: 2000} ⇔ e4 // In-line

Source: structs_test.go | Top

Struct Updates

Structs are updated by referencing their attributes using a dot-based notation like in other C-like languages.

type Employee struct {
	Name   string
	Salary int
}

e := Employee{Name: "Jazmin", Salary: 2000}

// Update attributes!
e.Name = "Luigi"
e.Salary = 1000

// Assertions
"Luigi" ⇔ e.Name
1000 ⇔ e.Salary

Source: structs_test.go | Top

Anonymous Struct

An anonymous struct combines the type definition and its instantiation in one statement.

jazmin := struct {
	Name   string
	Salary int
}{
	Name:   "Jazmin",
	Salary: 2000,
}

// Assertions
"Jazmin" ⇔ jazmin.Name
2000 ⇔ jazmin.Salary

Source: structs_test.go | Top

Anonymous Fields

Fields may be identified by their type (if different), avoiding the need to declare custom names.

type Employee struct {
	string
	int // types have to be distinct; can't repeat them
}

// Positional construction
e1 := Employee{"Jazmin", 2000}
// Field-wise construction (can use types as labels)
e2 := Employee{string: "Jazmin", int: 2000}

// Assertions
e1 ⇔ e2

Source: structs_test.go | Top

Pass By Reference vs Pass By Value

Structs are passed by value by default.

type Employee struct {
	Name   string
	Salary int
}

// ------------- Obtain Value (Default) ---------------

v := Employee{Name: "Jazmin", Salary: 2000}
var v2, v3 = v, v

// Assertions
2000 ⇔ v.Salary
v.Salary++
2001 ⇔ v.Salary
v2.Salary++
2001 ⇔ v2.Salary
v3.Salary++
2001 ⇔ v3.Salary
2001 ⇔ v.Salary

// --------------- Obtain Reference  ------------------

r := &Employee{Name: "Jazmin", Salary: 2000} // Reference!
var r2, r3 = r, r
2000 ⇔ r.Salary
r.Salary++
2001 ⇔ r.Salary
r2.Salary++
2002 ⇔ r2.Salary
r3.Salary++
2003 ⇔ r3.Salary
2003 ⇔ r.Salary // r = r2 = r3

Source: structs_test.go | Top

Nested Fields

A struct’s attribute type may be another struct.

type Address struct {
	Number     int
	StreetName string
	Postcode   string
}

type Employee struct {
	Name             string
	Salary           int
	PrimaryAddress   Address
	SecondaryAddress Address
}

a2 := Address{
	Number:     109,
	StreetName: "Larksfield",
	Postcode:   "TW200RA",
}

e := Employee{
	Name:   "Jazmin",
	Salary: 2000,
	PrimaryAddress: Address{
		Number:     101,
		StreetName: "Buckhurst",
		Postcode:   "IG9IJA",
	},
	SecondaryAddress: a2,
}

// Assertions
"IG9IJA" ⇔ e.PrimaryAddress.Postcode
"TW200RA" ⇔ e.SecondaryAddress.Postcode

Source: structs_test.go | Top

Nested Fields (Same Name as Struct)

A struct attribute that includes another struct type does not need to declare a custom name; the struct’s type will be used as the default name.

type Address struct {
	Number     int
	StreetName string
	Postcode   string
}
type Employee struct {
	Address // This will become an attribute named Address
	Name    string
	Salary  int
}

e := Employee{
	Name:   "Jazmin",
	Salary: 2000,
	Address: Address{
		Number:     101,
		StreetName: "Buckhurst",
		Postcode:   "IG9IJA",
	},
}

// Assertions
"IG9IJA" ⇔ e.Address.Postcode

Source: structs_test.go | Top

Nested Fields (Inline)

Nested struct types do not need to be declared externally, they may be inlined as well.

type Employee struct {
	Name    string
	Salary  int
	Address struct {
		Number     int
		StreetName string
		Postcode   string
	}
}

e1 := Employee{
	Name:   "Jazmin",
	Salary: 2000,
	Address: struct {
		Number     int
		StreetName string
		Postcode   string
	}{
		Number:     101,
		StreetName: "Buckhurst",
		Postcode:   "IG9IJA",
	},
}

// Assertions
"IG9IJA" ⇔ e1.Address.Postcode

Source: structs_test.go | Top

Methods

Methods are functions that operate against a given type and can, thus, be invoked using the dot notation against the relevant type using the form <VAR>.<METHOD>().

Method Declaration

Methods are declared by defining a top level function which receives the target type and specifies a return type as a function type that represents the method in the form func (<VAR> <TYPE>) <METHOD> where <TYPE> is the struct or type alias to which we want to add a method, and <METHOD> is the full function signature for the method. In this case, <VAR> will be a value, so any changes to the type will be local to the function. If we want the method to affect the provided value, then the pointer declaration should be used by prefixing the type with a star: *<TYPE>.

Let us now proceed with the example. First we declare a struct called Employee to which we will add two methods further on.

type Employee struct {
	Name   string
	Salary int
}

Source: methods_test.go | Top

Secondly, we add a method called FailToIncreaseSalaryBy() which allows raising the employee’s salary by the provided amount and returns the new effective salary. It is prefixed with Fail since the method receives a value and the changes only occur on a copy, rather than on the original value.

func (e Employee) FailToIncreaseSalaryBy(extraSalary int) int {
	e.Salary = e.Salary + extraSalary // Only changed within this method's scope
	return e.Salary
}

Source: methods_test.go | Top

Thirdly, we add a method called SucceedToIncreaseSalaryBy() which uses the pointer notation and that, unlike the previous method, succeeds in modifying the originally provided value’s attribute.

func (e *Employee) SucceedToIncreaseSalaryBy(extraSalary int) int {
	e.Salary = e.Salary + extraSalary // Passed value actually changed!
	return e.Salary
}

Source: methods_test.go | Top

Finally, we implement two sets of assertions to verify the differences between the two methods.

e := Employee{"Jazmin", 2000}

// Assertion #1
// Value-wise Method Call
var newSalary = e.FailToIncreaseSalaryBy(1000)
3000 ⇔ newSalary
2000 ⇔ e.Salary // Employee Salary not changed!

// Assertion #2
// Reference-wise Method Call (Side Effects!)
e.SucceedToIncreaseSalaryBy(1000)
3000 ⇔ e.Salary // Employee Salary changed!

Source: methods_test.go | Top

Struct Methods on Nested Structures

Whenever a struct embeds other structs, a method may change the inner struct only, resulting in the modification of the outer struct as well.

Let us see an example. First we declare the inner struct Address, and PublicEmployee which embeds it, and a method against Address to change the postcode called changePostCode().

type Address struct {
	Number     int
	StreetName string
	Postcode   string
}

type PublicEmployee struct {
	Name           string
	Salary         int
	PrimaryAddress Address
}

func (a *Address) ChangePostCode(newPostCode string) {
	a.Postcode = newPostCode // Passed value actually changed!
}

Source: methods_test.go | Top

We finally prove that when declaring a PublicEmployee value and changing the postcode of its embedded Address component, we effectively change the entire value.

e := PublicEmployee{
	Name:   "Jazmin",
	Salary: 2000,
	PrimaryAddress: Address{
		Number:     12,
		StreetName: "High street",
		Postcode:   "SW18RLN",
	},
}

// Assertions
"SW18RLN" ⇔ e.PrimaryAddress.Postcode // Before change
e.PrimaryAddress.ChangePostCode("TW18NLJ")            // change postcode!
"TW18NLJ" ⇔ e.PrimaryAddress.Postcode // After change

Source: methods_test.go | Top

Methods on Basic Types

Methods cannot be declared directly against built-in types such as string. The solution is creating a type alias using the type <NAME> <TYPE> notation.

type StringAlias string

func (s StringAlias) ToUpperCase() string { // Method against StringAlias
	return strings.ToUpper(string(s))
}
greeting := StringAlias("hello")

// Assertions
"HELLO" ⇔ greeting.ToUpperCase()

Source: methods_test.go | Top

Interfaces

Interfaces are a collection of method signatures that a type may implement.

Interface Definition

An interface is defined using the below notation:

type <NAME> interface {
  <METHOD1>
  <METHOD2>
  ...
}

In the below example, we create two types; employee and contractor, then declare an interface called costing, and finally implement said interface for both of the types.

type employee struct {
	personName string
	salary     int
}

type contractor struct {
	limitedCompany string
	dailyRate      int
}

type costingInterface interface { // Define interface (IF)!
	getYearlyCost() int
	// ... other methods here.
}

func (e employee) getYearlyCost() int { // Implement IF for employee
	return (e.salary * 103) / 100 // Pension contributions 3%
}

func (c contractor) getYearlyCost() int { // Implement IF for contractor
	return c.dailyRate * 5 * 40 // working weeks p/ year
}

// Use interface as a regular variable Type
var c1, c2 costingInterface
c1 = contractor{"RipOff Ltd", 500}
c2 = employee{"John Smith", 80000}

// Assertions
100000 ⇔ c1.getYearlyCost()
82400 ⇔ c2.getYearlyCost()

Source: interfaces_test.go | Top

The Empty Interface

The empty interface specifies zero methods and may hold values of any type.

// v can be any value type!
tellMeYourType := func(v interface{}) string {
	switch v.(type) {
	case string:
		return "string"
	case int:
		return "int"
	default:
		return "other"
	}
}

// All value types are compatible with interface{}!
var v1, v2, v3 interface{}
v1 = "hello"
v2 = 45
v3 = 1.5

// Assertions
"string" ⇔ tellMeYourType(v1)
"int" ⇔ tellMeYourType(v2)
"other" ⇔ tellMeYourType(v3)

Source: interfaces_test.go | Top

Errors

The Error Interface

Built-in functions that produce errors typically implement the error interface which has a method with the signature Error().

// Open a non-existing file
_, err := os.Open("non-existing-file.txt")

// Assertions
"open non-existing-file.txt: no such file or directory" ⇔ err.Error()

Source: errors_test.go | Top

Custom Errors

Customs errors are created using the errors.New() function.

func safeDiv(a int, b int) (int, error) {
	if b == 0 {
		return 0, errors.New("Can't divide by zero")
	}
	return a / b, nil
}

// Calculations
r, _ := safeDiv(12, 3)
_, err := safeDiv(12, 0)

// Assertions
4 ⇔ r
"Can't divide by zero" ⇔ err.Error()

Source: errors_test.go | Top

Wrapping Errors with String Formatting

// Open a non-existing file
_, err := os.Open("non-existing-file.txt")

// Annotate error
err2 := fmt.Errorf("Ouch! %s", err)

// Assertions
"Ouch! open non-existing-file.txt: no such file or directory" ⇔ err2.Error()

Source: errors_test.go | Top

Custom Error Struct

A custom struct type can be used to provide more details about a given error. In the example below, CustomError can provide details about the line number and row number where an error occured.

type CustomError struct {
	LineNumber int
	RowNumber  int
}

Source: errors_test.go | Top

We then implement the Error() method to comply with the error interface:

func (c CustomError) Error() string {
	return fmt.Sprintf("Failed at line %d, row %d", c.LineNumber, c.RowNumber)
}

Source: errors_test.go | Top

The idea is that all errors abide to the error interface. To test this principle, we define below a function which may fail either due to a system error from os.Open() or one using our custom CustomError type:

func doSomethingNasty(option int) error {
	if option == 1 {
		_, err := os.Open("non-existing-file.txt")
		return err
	}
	return &CustomError{LineNumber: 5, RowNumber: 30}
}

Source: errors_test.go | Top

We finally test the two different errors using the type assertion err.(*CustomerError).

var err error
var line, row int
var path string

// Trigger both doSomethingNasty(1) and doSomethingNasty(2)
for i := 1; i <= 2; i++ {
	err = doSomethingNasty(i)
	switch err.(type) {
	case *os.PathError:
		v, _ := err.(*os.PathError)
		path = v.Path
	case *CustomError:
		v, _ := err.(*CustomError)
		line = v.LineNumber
		row = v.RowNumber
	}
}

// Assertions
"non-existing-file.txt" ⇔ path
5 ⇔ line
30 ⇔ row

Source: errors_test.go | Top

Go Routines

Simple Goroutine

A goroutine is nothing more than a regular function that is run asynchronously by prefixing it with the go keyboard. For example: go myfunc().

In the below example, the execOrder strings captures the actual order of execution

// This function waits for a number of seconds and then appends
// an identifier to the `execOrder` string.
wasteTime := func(sleepSeconds int, execOrder *string, id string) {
	time.Sleep(time.Duration(sleepSeconds) * time.Second)
	*execOrder = *execOrder + id
}

execOrder := "A"

go wasteTime(7, &execOrder, "E") // will run last
go wasteTime(5, &execOrder, "D") // will run second
go wasteTime(3, &execOrder, "C") // will run first

execOrder = execOrder + "B"

// Wait 10 seconds to allow all goroutines to run.
time.Sleep(time.Duration(10) * time.Second)

execOrder = execOrder + "F"

// Assertions
"ABCDEF" ⇔ execOrder

Source: goroutines_test.go | Top

Synchronous Channels

Synchronous channels are first-in-first-out (FIFO) queues. A channel is created using make(chan <TYPE>):

Here we poll the queue using <-foodChannel. Messages may not arrive in sequence but we have added a sleep statement to platter() so that they do.

// Create foodChannel queue
var foodChannel = make(chan string)

// The below function adds one or more food items to the `foodChannel` queue.
platter := func(size int) {
	// Artificial wait
	time.Sleep(time.Duration(size) * time.Second)

	if size >= 1 {
		foodChannel <- "hummus" // Post "hummus" to the queue
	}
	if size >= 2 {
		foodChannel <- "falafel" // Post "falafel" to the queue
	}
	if size >= 3 {
		foodChannel <- fmt.Sprintf("%d pita", size-2) // Post "n pita" to the queue
	}
}

// Prepare various Platter Sizes
go platter(1)
go platter(2)
go platter(3)

// Assertions
"hummus" ⇔ <-foodChannel  // posted by platter(1)
"hummus" ⇔ <-foodChannel  // posted by platter(2)
"falafel" ⇔ <-foodChannel // posted by platter(2)
"hummus" ⇔ <-foodChannel  // posted by platter(3)
"falafel" ⇔ <-foodChannel // posted by platter(3)
"1 pita" ⇔ <-foodChannel  // posted by platter(3)

Source: sync_channels_test.go | Top

Synchronous Channels (Blocking Behaviour)

The following annotated code uses the execOrder string to demonstrate the order of execution using synchronous channels so that their “blocking” behaviour can be appreciated.

// Define a channel c
c := make(chan string)

execOrder := "A"

// Run a goroutine that posts "message" to c
go func() {

	execOrder = execOrder + "B"
	c <- "message" // It blocks until a receiver appears
	execOrder = execOrder + "E"

}()

// Wait
time.Sleep(time.Second)

// Poll queue: receive message
execOrder = execOrder + "C"
message := <-c
execOrder = execOrder + "D"

time.Sleep(time.Second) // Sleep 1 second

// Assertions
"message" ⇔ message
"ABCDE" ⇔ execOrder

Source: sync_channels_test.go | Top

Channel Buffering

By default, channels block the sender until a receiver collects the message. With buffering, messages can be “held” in the queue and extracted later, decoupling the sender from the receiver. This is accomplished by specifying the number of messages to be buffered as second argument (N) to make(chan <TYPE>, N).

c := make(chan string, 2)
c <- "first"  // Non-blocking
c <- "second" // Non-blocking
// c <- "third"  // This one would block

// Assertions
"first" ⇔ <-c
"second" ⇔ <-c

Source: sync_channels_test.go | Top

Channel Directions

Functions can specify whether channels provided as arguments can only send, receive, (or both send a receive). This is accomplished by specifying the channel signature as either <-chan <TYPE>, chan<-<TYPE>, or simply chan <TYPE>, respectively.

c := make(chan string, 1)

canOnlySend := func(c chan<- string, msg string) {
	c <- msg
	// msg := <-c // Won't compile
}
canOnlyReceive := func(c <-chan string) string {
	// c <- msg // Won't compile
	return (<-c)
}
canBothSendAndReceive := func(c chan string, msg string) string {
	c <- msg
	return (<-c)
}

// Assertions
canOnlySend(c, "hello")
"hello" ⇔ canOnlyReceive(c)
"hola!" ⇔ canBothSendAndReceive(c, "hola!")

Source: sync_channels_test.go | Top

Select

The select { ...} statement is similar to the switch statement, but it is used to wait for messages in multiple channels simultaneously.

In the below example, the message ping is passed to channel c1, c2, and finally c3 in each iteration of the for loop:

execOrder := "A"

c1 := make(chan string, 1)
c2 := make(chan string, 1)
c3 := make(chan string, 1)

c1 <- "ping"

execOrder = execOrder + "B"

for i := 0; i < 3; i++ { // Loop three times
	execOrder = execOrder + "-"
	select {
	case msg := <-c1: // Assignment is optional
		execOrder = execOrder + "c1(" + msg + ")"
		c2 <- msg
	case msg := <-c2:
		execOrder = execOrder + "c2(" + msg + ")"
		c3 <- msg
	case msg := <-c3:
		execOrder = execOrder + "c3(" + msg + ")"
	}
}

// Assertions
"AB-c1(ping)-c2(ping)-c3(ping)" ⇔ execOrder

Source: sync_channels_test.go | Top

Select and Timeouts

A select {...} statement will block until a message is received by one of the cases declared in it. It is possible to frame one of said cases as a timeout condition using the case: <- <Time> signature.

In the below example, the timeout case is hit in the first iteration, whereas the message receipt is hit is in the second one.

execOrder := "A"

c := make(chan string, 1)

for i := 0; i < 2; i++ { // Loop two times
	select {
	case <-c: // wait for any message
		execOrder = execOrder + "C"
	case <-time.After(1 * time.Second):
		execOrder = execOrder + "B"
	}
	c <- "ping"
}

// Assertions
"ABC" ⇔ execOrder

Source: sync_channels_test.go | Top

Select with “Send” Cases

The select {...} statement can also be used with send cases in the form case channel <- msg. This is helpful to avoid a blocking situation whenever there are no receivers associated with the channel.

In the below example, the timeout case is triggered because c is a synchronous channel and there are no receivers waiting on it.

execOrder := "A"

c := make(chan string) // synchronous channel!

select {
case c <- "ping": // would block because there is no receiver
	execOrder = execOrder + "Never"
case <-time.After(1 * time.Second): // therefore the result is a timeout
	execOrder = execOrder + "B"
}

// Assertions
"AB" ⇔ execOrder

Source: sync_channels_test.go | Top

Select with Defaults

Sometimes we want to tell whether there is a message pending to be retrieved (or a receiver for a channel to which we are sending a message) in an immediate manner. In this case, there is no need to create an artificial timeout, the keyword default can be used instead.

In the below example, the default case is triggered because c is a synchronous channel and there are no receivers waiting on it.

execOrder := "A"

c := make(chan string) // synchronous channel!

select {
case c <- "ping": // would block because there is no receiver
	execOrder = execOrder + "Never"
default: // therefore the result is the default case
	execOrder = execOrder + "B"
}

// Assertions
"AB" ⇔ execOrder

Source: sync_channels_test.go | Top

Opening and Closing Channels

Channels are open by default when created. They can explicitly be closed using the close(channel) statement. The status of a channel may be checked using the second return value of the message receive statement: msg, channelStatus <- channel. Channels can only be closed if they contain no pending messages in the buffer. Furthermore, channels can be closed only once.

In the below example we iterate two times through a declaration that obtains both the message and the status, and that closes the channel upon receiving the first message.

execOrder := "A"

c := make(chan string, 3)
c <- "msg1"

for i := 0; i < 2; i++ { // loop two times
	msg, channelStillOpen := <-c
	if channelStillOpen {
		execOrder = execOrder + "B(" + msg + ")"
		close(c)
	} else {
		execOrder = execOrder + "C(" + msg + ")"
	}
}

// Assertions
"AB(msg1)C()" ⇔ execOrder

Source: sync_channels_test.go | Top

Range over Channels

It is possible to iterate over the received messages on a channel using the range keyword.

execOrder := "A-"
c := make(chan string, 3)
c <- "m1"
c <- "m2"
c <- "m3"
close(c) // range will remain suspended unless we close the channel
for msg := range c {
	execOrder = execOrder + msg
}
"A-m1m2m3" ⇔ execOrder

Source: sync_channels_test.go | Top

Scheduled Tasks (Timers)

A timer, created via timer.NewTimer(Time) offers a mechanism to run goroutines in the future. The implementation, as appreciated below, relies on a blocking channel that forces the goroutine to wait until the specified time (5 seconds).

execOrder := "A"

timer := time.NewTimer(5 * time.Second)

futureFunc := func() {
	execOrder = execOrder + "B"
	<-timer.C // This is what makes the function wait!
	execOrder = execOrder + "D"
}

go futureFunc()
time.Sleep(2 * time.Second)

execOrder = execOrder + "C"

time.Sleep(4 * time.Second) // 6 seconds elapsed by now.

execOrder = execOrder + "E"

// Assertions
"ABCDE" ⇔ execOrder

Source: sync_channels_test.go | Top

Scheduled Tasks (Cancelling Timers)

A timer may be aborted by invoking timer.Stop(), preventing the waiting goroutine from executing the code after the channel receive statement.

execOrder := "A"

timer := time.NewTimer(5 * time.Second)

futureFunc := func() {
	execOrder = execOrder + "B"
	<-timer.C
	execOrder = execOrder + "Never" // will not be reached
}

go futureFunc()
time.Sleep(2 * time.Second)

execOrder = execOrder + "C"

timer.Stop() // Abort timer!

execOrder = execOrder + "D"

// Assertions
"ABCD" ⇔ execOrder

Source: sync_channels_test.go | Top

Recurrent Tasks (Tickers)

Recurrent tasks may be set up using time.NewTicker(time) and applied similarly to regular timers, except that the recurrent code must run inside some form of repeating loop as shown below.

Please note that the goroutine in the example terminates because the main function ends as well. It may be necessary, in a real world scenario, to implement a mechanism (i.e. a sentinel message via a channel) to force the repeating loop to exit.

execOrder := "A"

timer := time.NewTicker(time.Second) // 1 Second

repeatingFunc := func() {
	for {
		execOrder = execOrder + "<"
		<-timer.C
		execOrder = execOrder + ">"
	}
}

go repeatingFunc()

time.Sleep(3500 * time.Millisecond) // 3.5 seconds
timer.Stop()                        // Abort timer!

execOrder = execOrder + "B"

// Assertions
"A<><><><B" ⇔ execOrder

Source: sync_channels_test.go | Top

Race Conditions (Mutexes)

Concurrent goroutines that share a common variable may result in inconsistent behaviour whenever operations again said variable aren’t atomic.

In the below example, we execute 10,000 times both the increment() and decrement() goroutines, which should result in x == 0 at the end, but this is seldom the case.

var x = 0

var increment = func() {
	x = x + 1
}
var decrement = func() {
	x = x - 1
}

for i := 0; i < 10000; i++ {
	go increment()
	go decrement()
}

// Assertions
0 ⇎ x // There is a tiny chance this may be true :)

Source: sync_channels_test.go | Top

The reason why is that x = x - 1 is not an atomic operation; it may involve multiple steps such as the ones shown in the below pseudocode:

temp = get value of X
temp2 = 1 + temp2
x = temp2

The solution is to enclose the x = x + 1 statement with mutex.Lock() and mutex.Unlock() methods from sync.Mutex so that only one goroutine has access to either x = x + 1 or x = x - 1 at any given time:

var mutex sync.Mutex
var x = 0

var increment = func() {
	mutex.Lock()
	x = x + 1
	mutex.Unlock()
}
var decrement = func() {
	mutex.Lock()
	x = x - 1
	mutex.Unlock()
}

for i := 0; i < 10000; i++ {
	go increment()
	go decrement()
}

time.Sleep(5 * time.Second) // Sleep 5 seconds

// Assertions
0 ⇔ x

Source: sync_channels_test.go | Top

Race Conditions (Atomic Counters)

In the case of simple numerical counters, there is no need to set up mutexes, or channels. The atomic.Add<TYPE>(&counter, <N>) function allows to alter a counter in a thread-safe manner as shown in the example below. There are also other functions such as Store<TYPE>(&counter, <N>) which simply sets a new value rather than adding to an existing one.

var x int32 = 0
var increment = func() {
	atomic.AddInt32(&x, 1)
}
var decrement = func() {
	atomic.AddInt32(&x, -1)
}

for i := 0; i < 10000; i++ {
	go increment()
	go decrement()
}
time.Sleep(3 * time.Second)

// Assertions
int32(0) ⇔ atomic.LoadInt32(&x)
int32(0) ⇔ x // May be inconsistent if goroutines are still active

Source: sync_channels_test.go | Top

Wait Groups

Wait Groups allow to treat a swarm of concurrent goroutines as one logical unit of execution to spare the programmer from coordinating the orchestration of each individual goroutine.

The sync.WaitGroup type is used for this purpose and uses the below methods:

In the below example, we calculate the two times table in parallel, using sync.WaitGroup to coordinate the task:

var waitGroup sync.WaitGroup // Allocate a new Wait Group. pending := 0
twoTimesTable := make([]int, 10)

var double = func(number int, wg *sync.WaitGroup) {
	time.Sleep(3 * time.Second) // Artificial wait
	twoTimesTable[number] = (number + 1) * 2
	wg.Done() // Goroutine completed: pending--
}

for i := 0; i < 10; i++ {
	waitGroup.Add(1) // Add new pending goroutine: pending++
	go double(i, &waitGroup)
}

waitGroup.Wait() // Wait until pending == 0

// Assertions
[]int{2, 4, 6, 8, 10, 12, 14, 16, 18, 20} ⇔ twoTimesTable

Source: sync_channels_test.go | Top

Input/Output: Files and Streams

This section covers file access and input/output/error streams. A tmpRoot variable (e.g., pointing to /tmp in Linux) is assumed for all examples.

Creating a File

The Create() function from the os package is equivalent to the Unix touch command.

file, err := os.Create(tmpRoot + "example.txt")
if err != nil {
	t.Error("Can't create file: ", err)
}
file.Close()

// Assertions
nil ⇔ err

Source: files_test.go | Top

Obtaining a File’s Metadata

The Stat() function from the os package returns a fileInfo struct which has a number of methods that help inspect the file’s metadata.

// Create example.txt
Test_Create_File(t)

// Get File Metadata
fileInfo, err := os.Stat(tmpRoot + "example.txt")
if err != nil {
	t.Error("Can't access file: ", err)
}

// Assertions
"example.txt" ⇔ fileInfo.Name()
int64(0) ⇔ fileInfo.Size()
os.FileMode(0x1b6) ⇔ fileInfo.Mode()
true ⇔ fileInfo.ModTime().Year() >= 2019
false ⇔ fileInfo.IsDir()

Source: files_test.go | Top

Renaming a File

The Rename() function from the os package is equivalent to the Unix rename command.

// Create example.txt
Test_Create_File(t)

// Rename example.txt to deleteme.txt
err := os.Rename(tmpRoot+"example.txt", tmpRoot+"deleteme.txt")
if err != nil {
	t.Error("Can't rename file: ", err)
}

// Assertions
_, err = os.Stat(tmpRoot + "deleteme.txt") // Check file's presence
nil ⇔ err

Source: files_test.go | Top

Deleting a File

The Remove() function from the os package is equivalent to the Unix rm command.

// Create example.txt
Test_Create_File(t)

// Delete File
err := os.Remove(tmpRoot + "example.txt")
if err != nil {
	t.Error("Can't delete file: ", err)
}

// The example.txt should be absent
_, err = os.Stat(tmpRoot + "example.txt")

// Assertions
nil ⇎ err

Source: files_test.go | Top

Writing Bytes to a File

Data may be written to file by using the Write() or WriteString() methods offered by the File struct.

// Open File for Writing
file, err := os.Create(tmpRoot + "hello.txt")
if err != nil {
	t.Error("Can't open file: ", err)
}
defer file.Close()

// Write File
bytes := []byte("Hello 😀")
count, err2 := file.Write(bytes) // WriteString() also available
if err2 != nil {
	t.Error("Can't write to file: ", err2)
}

// Flush changes!
file.Sync()

// Assertions
10 ⇔ count // bytes written!

Source: files_test.go | Top

Reading Bytes from a File

Data may be read from a file by using the Read() method offered by the File struct.

// Create hello.txt
Test_Write_File(t)

// Open File
file, err := os.Open(tmpRoot + "hello.txt")
if err != nil {
	t.Error("Can't open file: ", err)
}
defer file.Close()

// Successful Read
bytes := make([]byte, 10)
count, err2 := file.Read(bytes)
if err2 != nil {
	t.Error("Can't open file: ", err2)
}

// EOF Error (No more bytes left to read)
var oneByteSlice = []byte{0}
_, err3 := file.Read(oneByteSlice)

// Assertions
"Hello 😀"string(bytes)
10 ⇔ count
io.EOF ⇔ err3

Source: files_test.go | Top

Writing Bytes to a File (Single Statement)

The WriteFile() function from the ioutil package allows creating and writing to a file using a single statement.

err := ioutil.WriteFile(tmpRoot+"hello.txt", []byte("Hello 😀"), 0666)
if err != nil {
	t.Error("Can't open file: ", err)
}

Source: files_test.go | Top

Read Bytes from a File Without Allocating a Slice

The ReadAll() function from the ioutil package returns a byte slice with the contents of a file, without requiring to allocate the slice in advance.

// Create hello.txt
Test_Write_File_IOUtil_Quick(t)

// Open file for reading
file, err := os.Open(tmpRoot + "hello.txt")
if err != nil {
	log.Fatal(err)
}
defer file.Close()

// Read File
bytes, err2 := ioutil.ReadAll(file) // results in bytes!
if err2 != nil {
	t.Error("Can't open file: ", err2)
}

// Assertions
"Hello 😀"string(bytes)
10len(bytes)

Source: files_test.go | Top

Reading Bytes form a File Without a File Handle

The ReadFile() function from the ioutil package allows reading the bytes form a file using a single statement, without requiring the allocation of a slice in advance and neither the obtention of a file handle.

// Create hello.txt
Test_Write_File_IOUtil_Quick(t)

// Open file and read bytes in one go!
bytes, err := ioutil.ReadFile(tmpRoot + "hello.txt")
if err != nil {
	t.Error("Can't open file: ", err)
}

// Assertions
"Hello 😀"string(bytes)
10len(bytes)

Source: files_test.go | Top

Writing to a File with Buffering

The Write() and WriteString() functions, for bytes and strings, respectively, from the bufio package, allow to write files with buffering.

// Open file For writing
fileHandle, err := os.Create(tmpRoot + "hello.txt")
if err != nil {
	t.Error("Can't open file: ", err)
}
defer fileHandle.Close()

// Write File
writer := bufio.NewWriter(fileHandle)
count, err2 := writer.WriteString("Hello 😀")
if err2 != nil {
	t.Error("Can't open file: ", err2)
}

// Make sure changes are written to disk!
writer.Flush()

// Assertions
10 ⇔ count

Source: files_test.go | Top

Reading from a File with Buffering

The Read() and ReadString() functions, for bytes and strings, respectively, from the bufio package, allow to read files with buffering.

// Open file for reading
file, err := os.Open(tmpRoot + "hello.txt")
if err != nil {
	t.Error("Can't open file: ", err)
}
defer file.Close()

// Read File
reader := bufio.NewReader(file)
line, err2 := reader.ReadString('o')
if err2 != nil {
	t.Error("Can't open file: ", err2)
}

// EOF Error (No more lines left to read)
_, err3 := reader.ReadString('o')

// Assertions
"Hello" ⇔ line
io.EOF ⇔ err3

Source: files_test.go | Top

Reading Lines from a File

The NewScanner() function, from the bufio package, allows iterating through the lines contained in a file.

// First generate example file
contents := []byte("line1\nline2\nline3\n")
err := ioutil.WriteFile(tmpRoot+"lines.txt", contents, 0666)
if err != nil {
	t.Error("Can't open file: ", err)
}

// Open File For Reading
file, err := os.Open(tmpRoot + "lines.txt")
if err != nil {
	t.Error("Can't open file: ", err)
}
defer file.Close()

// Use Scanner
input := bufio.NewScanner(file)
var lineBuffer = ""
for input.Scan() {
	lineBuffer = lineBuffer + input.Text()
}

// Assertions
"line1line2line3" ⇔ lineBuffer

Source: files_test.go | Top

Interacting with Stdin, Stdout, and Stderr

The os package provides the Stdin, Stdout, and Stderr streams. The below example reads all data from Stdin, converts said data to upper case, and then writes the results to Stdout. It then uses the Stderr stream to report the number of bytes read.

func main() {
	// Read all Stdin
	data, _ := ioutil.ReadAll(os.Stdin)
	// Convert data to upper case and write to Stdout
	count, _ := os.Stdout.WriteString(strings.ToUpper(string(data)))
	// Write the number of bytes read to Stderr
	os.Stderr.WriteString(fmt.Sprintf("Bytes read: %d", count))
}

Source: iostreams.go | Top

Interacting with Shell Commands

It is possible to interact with commands by using the Command() function from the exec package. Other than passing arguments as separate function arguments to Command(), piping data in and out of commands involves connecting the Stdin, Stdout, and Stderr streams declared by the Cmd struct.

// Specify command
cmd := exec.Command("go", "run", "iostreams.go")

// Specify stdin input
cmd.Stdin = strings.NewReader("hello")

// Declare a buffer to capture stdout from command
var stdout bytes.Buffer
cmd.Stdout = &stdout

// Declare a buffer to capture stderr from command
var stderr bytes.Buffer
cmd.Stderr = &stderr

// Run Command
err := cmd.Run()
if err != nil {
	t.Error("Error:", err)
}

// Assertions
"HELLO" ⇔ stdout.String()
"Bytes read: 5" ⇔ stderr.String()

Source: iostreams_test.go | Top

Arguments

A command’s arguments are found in the Args slice from the os package. The first element is the command name whereas the second one, third and so on, are the arguments. In the example below, we check the command and arguments generated by the test runner.

var arguments, command string
for i := 0; i < len(os.Args); i++ {
	if i == 0 {
		command = os.Args[0]
	} else if i == 1 {
		arguments += os.Args[i]
	} else {
		arguments += " " + os.Args[i]
	}
}

// Assertions
fmt.Printf(command)
fmt.Printf(arguments)
true ⇔ strings.Contains(command, "arguments.test")
true ⇔ strings.Contains(arguments, "test.timeout")

Source: arguments_test.go | Top