Go By Assertion

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

Table of Contents

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:

  • assert.Equal(t, a, b) becomes a ⇔ b
  • assert.NotEqual(t, a, b) becomes a ⇎ b

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.

1var counter = 0
2
3// Simple If
4if counter == 0 {
5	counter++
6}
7
8// Assertions
91  counter

Source: controlflow_test.go | Top

If-Then-Else

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

 1var counter = 0
 2
 3// Simple If
 4if counter == 0 {
 5	counter++
 6} else {
 7	counter--
 8}
 9
10// Assertions
111  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.

 1var counter = 0
 2
 3// Simple If
 4if counter == 0 {
 5	counter++
 6} else if counter == 10 {
 7	counter = counter * 10
 8} else if counter == 20 {
 9	counter = counter * 20
10} else { // Optional
11	counter--
12}
13
14// Assertions
151  counter

Source: controlflow_test.go | Top

If-Scoped Variables

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

 1var counter = 0
 2
 3// Assign
 4if i := 5; counter == 0 {
 5	counter = counter + i // i in scope here
 6} else {
 7	counter = counter - i // i in scope here
 8}
 9
10// --- i is not longer in scope here ---
11
12// Assertions
135  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.

 1var result = ""
 2
 3switch 1 + 1 {
 4case 0:
 5	result = "zero"
 6case 1:
 7	result = "one"
 8case 2:
 9	result = "two"
10default: // Optional
11	result = "some_number"
12}
13
14// Assertions
15"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.

 1var result = ""
 2
 3// Just cases
 4switch 2 * 3 {
 5case 0, 1, 2, 3, 4, 5, 6: // multiple values!
 6	result = "between_zero_and_six"
 7case 7:
 8	result = "seven"
 9default: // Optional
10	result = "some_number"
11}
12
13// Assertions
14"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.

 1var result = ""
 2
 3switch { // No expression here
 4case 2*3 == 1:
 5	result = "one"
 6case 2*3 == 6:
 7	result = "six"
 8default: // Optional
 9	result = "some_number"
10}
11
12// Assertions
13"six"  result

Source: controlflow_test.go | Top

A more crude example is presented below:

 1var result = ""
 2
 3switch { // No expression here
 4case false:
 5	result = "false1"
 6case false:
 7	result = "false2"
 8case true:
 9	result = "true1"
10default: // Optional
11	result = "true2"
12}
13
14// Assertions
15"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.

 1testType := func(i interface{}) string {
 2	switch t := i.(type) { // the t := assignment is optional
 3	case int:
 4		return "int"
 5	case string:
 6		return "string"
 7	default: // Optional
 8		return "other type: " + fmt.Sprintf("%T", t)
 9	}
10}
11
12// Assertions
13"string"  testType("hello")
14"int"  testType(45)
15"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.

 1var counter = 0
 2
 3for {
 4	counter = counter + 1
 5	if counter == 3 {
 6		break
 7	}
 8}
 9
10// Assertions
113  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.

1var counter = 0
2
3for counter < 3 {
4	counter = counter + 1
5}
6
7// Assertions
83  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.

 1var indexSum = 0
 2var sum = 0
 3
 4for index, currentValue := range [3]int{1, 2, 3} {
 5	indexSum = indexSum + index // 0 + 1 + 2 = 3
 6	sum = sum + currentValue    // 1 + 2 + 3 = 6
 7}
 8
 9// Assertions
103  indexSum
116  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 {...}

1var sum = 0
2var numbers = [3]int{1, 2, 3}
3
4for index := range numbers {
5	sum = sum + numbers[index] // array elements accessed by index!
6}
7
8// Assertions
96  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.

1var sum = 0
2
3for _, currentValue := range [3]int{1, 2, 3} {
4	sum = sum + currentValue // 1 + 2 + 3 = 6
5}
6
7// Assertions
86  sum

Source: controlflow_test.go | Top

Iteration Over Keys and Values of a Map

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

 1var keys, values string
 2
 3for k, v := range map[string]string{"A": "Argentina", "B": "Brazil"} {
 4	keys = keys + k
 5	values = values + v
 6}
 7
 8// Assertions
 9true  strings.Contains(keys, "A")
10true  strings.Contains(keys, "B")
11true  strings.Contains(values, "Argentina")
12true  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.

1var word string
2
3for _, v := range "😊 olleh" {
4	word = string(v) + word
5}
6
7// Assertions
8"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.

1var reversedHello = "😊 olleh"
2var word string
3
4for i := 0; i < len(reversedHello); i++ {
5	word = string(reversedHello[i]) + word
6}
7
8// Assertions
9"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 =.

1var counter = 0
2
3for i := 0; i < 3; i++ {
4	counter = counter + i // 0 + 1 + 2 = 3
5}
6
7// Assertions
83  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).

 1var actions = ""
 2
 3addAction := func(action string) {
 4	actions = actions + action
 5}
 6
 7trackDefer := func() {
 8	addAction("1")
 9	defer addAction("[d1]")
10	addAction("2")
11	defer addAction("[d2]")
12	addAction("3")
13	defer addAction("[d3]")
14	addAction("4")
15}
16
17actions = actions + "START-"
18trackDefer()
19actions = actions + "-END"
20
21// Assertions
22"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.

 1var globalVar = ""
 2
 3func simple() {
 4	globalVar = "changed"
 5}
 6
 7// Assertions
 8""  globalVar
 9simple()
10"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>.

1func hello() string {
2	return "hello"
3}
4
5// Assertions
6"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:

 1func returnTwo() (rune, bool) {
 2	return 'a', false
 3}
 4
 5// Assertions (Two Values)
 6first, second := returnTwo()
 7'a'  first
 8false  second
 9
10// Assertions (First Value Only)
11onlyFirst, _ := returnTwo()
12'a'  onlyFirst
13
14// Assertions (Second Value Only)
15_, onlySecond := returnTwo()
16false  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:

 1var result = ""
 2
 3func oneArgument(name string) {
 4	result = name
 5}
 6
 7func twoArguments(name string, age int) {
 8	result = fmt.Sprintf("%s-%d", name, age)
 9}
10
11// Assertions (One Argument)
12oneArgument("Rob")
13"Rob"  result
14
15// Assertions (Two Arguments)
16twoArguments("Rob", 36)
17"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.

 1func sum(numbers ...int) int {
 2	var result int = 0
 3	for _, n := range numbers {
 4		result = result + n
 5	}
 6	return result
 7}
 8
 9// Assertions
100  sum()
111  sum(1)
123  sum(1, 2)
136  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.

 1func operation(op string) func(int, int) int {
 2	switch op {
 3	case "sum":
 4		return func(v1 int, v2 int) int {
 5			return v1 + v2
 6		}
 7	case "mul":
 8		return func(v1 int, v2 int) int {
 9			return v1 * v2
10		}
11	default:
12		panic("op should be either \"sum\" or \"mul\"")
13	}
14}
15
16// Assertions
17
18// Direct use
193  operation("sum")(1, 2)
206  operation("mul")(2, 3)
21
22// Function extraction first
23sum := operation("sum")
24mul := operation("mul")
253  sum(1, 2)
266  mul(2, 3)
27
28// Create Inline
293  func(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.

 1var counter = 0
 2
 3incrementer := func() int {
 4	counter++
 5	return counter
 6}
 7
 8// Assertions
 91  incrementer()
10counter++
113  incrementer()
12incrementer()
135  incrementer()
146  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.

 1// Declaration
 2var MinBool bool = false
 3var MaxBool bool = true
 4
 5// Logical Operators
 6false  !true
 7true  !false
 8true  true || false
 9true  MinBool || MaxBool
10false  true && 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

1// Declaration
2var MinInt8 int8 = -128
3var MaxInt8 int8 = 127
4
5// Bounds
6true  MinInt8 == math.MinInt8
7true  MaxInt8 == math.MaxInt8

Source: basictypes_test.go | Top

16-Bit

1// Declaration
2var MinInt16 int16 = -32768
3var MaxInt16 int16 = 32767
4
5// Bounds
6true  MinInt16 == math.MinInt16
7true  MaxInt16 == math.MaxInt16

Source: basictypes_test.go | Top

32-Bit (Rune)

 1// Declaration
 2var MinInt32 rune = -2147483648
 3var MinRune rune = MinInt32
 4var MaxInt32 int32 = 2147483647
 5var MaxRune int32 = MaxInt32
 6
 7// Bounds
 8true  MinInt32 == math.MinInt32
 9true  MaxInt32 == math.MaxInt32
10true  MinRune == math.MinInt32
11true  MaxRune == math.MaxInt32
12
13// Rune is an alias for int32
14MaxRune  MinInt32-1
15MinRune  MaxInt32+1

Source: basictypes_test.go | Top

64-Bit

1// Declaration
2var MinInt64 int64 = -9223372036854775808
3var MaxInt64 int64 = 9223372036854775807
4
5// Bounds
6true  MinInt64 == math.MinInt64
7true  MaxInt64 == math.MaxInt64

Source: basictypes_test.go | Top

General Integer

The size depends on the underlying architecture.

 1// Only on 64-bit architectures
 2var MinInt int = -9223372036854775808
 3var MaxInt int = 9223372036854775807
 4
 5// Same as int64 on 64-bit architectures
 6MinInt  math.MinInt64
 7MaxInt  math.MaxInt64
 8
 9// Overflow behaviour
10MaxInt  MinInt-1
11MinInt  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)

 1// Declaration
 2var MinUInt8 uint8 = 0
 3var MaxUInt8 uint8 = 255 // 2^8-1
 4var MinByte byte = MinUInt8
 5var MaxByte byte = MaxUInt8
 6
 7// Bounds
 8true  MaxUInt8 == math.MaxUint8
 9
10// Overflow
11true  math.MaxUint8 == MinUInt8-1
12true  MinUInt8 == MaxUInt8+1
13
14// byte is an alias for uint8
15MaxByte  MinUInt8-1
16MinByte  MaxUInt8+1

Source: basictypes_test.go | Top

16-Bit

 1// Decaration
 2var MinUInt16 uint16 = 0
 3var MaxUInt16 uint16 = 65535 // 2^16-1
 4
 5// Bounds
 6true  MaxUInt16 == math.MaxUint16
 7
 8// Overflow
 9MaxUInt16  MinUInt16-1
10MinUInt16  MaxUInt16+1

Source: basictypes_test.go | Top

32-Bit

 1// Declaration
 2var MinUInt32 uint32 = 0
 3var MaxUInt32 uint32 = 4294967295 // 2^32-1
 4
 5// Bounds
 6true  MaxUInt32 == math.MaxUint32
 7
 8// Overflow
 9MaxUInt32  MinUInt32-1
10MinUInt32  MaxUInt32+1

Source: basictypes_test.go | Top

64-Bit

 1// Declaration
 2var MinUInt64 uint64 = 0
 3var MaxUInt64 uint64 = 18446744073709551615 // 2^64-1
 4
 5// Bounds
 6true  MaxUInt64 == math.MaxUint64
 7
 8// Overflow
 9MaxUInt64  MinUInt64-1
10MinUInt64  MaxUInt64+1

Source: basictypes_test.go | Top

General Unsigned Integer

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

 1// Declaration
 2var MinUint uint = 0
 3var MaxUint uint = 18446744073709551615
 4
 5// Bounds: Same as uint64 on 64-bit architectures
 6true  math.MaxUint64 == MaxUint
 7
 8// Overflow behaviour
 9MaxUint  MinUint-1
10MinUint  MaxUint+1

Source: basictypes_test.go | Top

Unsigned Integer Pointer

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

 1// Declaration
 2var MinUintptr uintptr = 0
 3var MaxUintptr uintptr = 18446744073709551615
 4
 5// Bounds: Same as uint64 on 64-bit architectures
 6true  math.MaxUint64 == MaxUintptr
 7
 8// Overflow behaviour
 9MaxUintptr  MinUintptr-1
10MinUintptr  MaxUintptr+1

Source: basictypes_test.go | Top

Float

The float type allows for a decimal component.

32-Bit

1// Declaration
2var MinFloat32 float32 = -1.401298464324817e-45
3var MaxFloat32 float32 = 3.4028234663852886e+38
4
5// Bounds
6true  MinFloat32 == -math.SmallestNonzeroFloat32
7true  MaxFloat32 == math.MaxFloat32

Source: basictypes_test.go | Top

64-Bit

1// Declaration
2var MinFloat64 float64 = -5e-324
3var MaxFloat64 float64 = 1.7976931348623157e+308
4
5// Bounds
6true  MinFloat64 == -math.SmallestNonzeroFloat64
7true  MaxFloat64 == math.MaxFloat64

Source: basictypes_test.go | Top

Complex

A type for Complex numbers.

64-Bit

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

Source: basictypes_test.go | Top

128-Bit

1// Declaration and Bounds
2complex(5e-324, 5e-324)  complex(math.SmallestNonzeroFloat64, math.SmallestNonzeroFloat64)
3complex(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.)

 1// Assertions
 2255  0xFF              // Hex
 3255  0XFF              // Hex
 4255  0377              // Octal
 5255  0o377             // Octal
 6255  0O377             // Octal
 7255  0b11111111        // Binary
 8255  0B11111111        // Binary
 90.5  .5                // Float
100.5  00.5              // Float
110.0  0.                // Float
1250.0  0.5e2            // Float w/Exponent
1350.0  0.5E2            // Float w/Exponent
1450.0  0.5E+2           // Float w/Exponent
150.005  0.5E-2          // Float w/Exponent
16complex128(1+2i)  1+2i // Complex Number
171000000  1_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.

 1// Assertions
 25  3+2                                 // Sum
 31  3-2                                 // Subtraction
 46  3*2                                 // Multiplication
 51  3/2                                 // Integer Division
 61.5  3.0/2.0                           // Float Division
 72  5%3                                 // Modulo operator
 82.0  math.Sqrt(4)                      // Square Root
 9true  math.Pi > 3.14 && math.Pi < 3.15 // Pi Constant
10
11// State mutating operators
12var x = 3
13x++ // Increase value by one
144  x
15x-- // Decrease value by one
163  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.

 1// Upcasting (Value-Preserving)
 2var smallValue byte = 5
 3var smallNegValue int8 = -5
 4
 5// Assertions
 6true  9000+uint(smallValue) == 9005
 7true  9000+int(smallNegValue) == 8995
 8true  9000+uint16(smallValue) == 9005
 9true  9000+int16(smallNegValue) == 8995
10true  900000000+uint32(smallValue) == 900000005
11true  900000000+int32(smallNegValue) == 899999995
12true  9000000000000000000+uint64(smallValue) == 9000000000000000005
13true  9000000000000000000+int64(smallNegValue) == 8999999999999999995
14
15// Down-casting (Value-Destructive)
16var bigValue uint64 = 18446744073709551615    // 2^64
17var bigNegValue int64 = -9223372036854775808  // 2^63
18var bigNegValue2 int64 = -4611686018427387904 // 2^62
19var bigNegValue3 int64 = -4294967296          // 2^32
20uint32(4294967295)  uint32(bigValue)
21int32(-5)  int32(smallNegValue)
22int32(0)  int32(bigNegValue)  // Truncated to zero
23int32(0)  int32(bigNegValue2) // Truncated to zero
24int32(0)  int32(bigNegValue3) // Truncated to zero
25uint16(65535)  uint16(bigValue)
26uint8(255)  uint8(bigValue)
27
28// Float and Integers
29var floatValue float32 = 1.5
30float32(1.0)  float32(1)
311  int(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.

 1// Assertions
 2'A'  rune(65)     // Rune value
 3'A'  '\101'       // Octal
 4'A'  '\x41'       // Hex
 5'A'  '\u0041'     // 16-bit Unicode
 6'A'  '\U00000041' // 32-bit Unicode
 7'😀'  '\U0001F600' // 32-bit Unicode
 8'\a'  '\x07'      // Bell
 9'\b'  '\x08'      // Backspace
10'\f'  '\x0C'      // Form feed
11'\n'  '\x0A'      // Line Feed
12'\r'  '\x0D'      // Carriage Return
13'\t'  '\x09'      // Horizontal Tab
14'\v'  '\x0b'      // Vertial Tab
15'\\'  '\x5c'      // Backslash
16'\''  '\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.

 1// The string() function converts a Unicode code point!
 2"A"  string(65)
 3"😀"  string(0X0001F600)
 4"AB"  string([]byte{65, 66})
 5
 6// Integer to String
 7"15"  fmt.Sprintf("%d", 15)
 8"15"  strconv.Itoa(15)
 9"15"  strconv.FormatInt(15, 10)    // Base 10
10"f"  strconv.FormatInt(15, 16)     // Base 16
11"     15"  fmt.Sprintf("%7d", 15)  // Padding
12"15     "  fmt.Sprintf("%-7d", 15) // Padding
137  len(fmt.Sprintf("%7d", 15))
14
15// String to Integer
16if n, err := strconv.Atoi("15"); err == nil {
17	15  n
18} else {
19	t.Error("Unable to convert")
20}
21if n, err := strconv.Atoi("ex1!%5"); err == nil {
22	t.Error("Conversion was not supposed to be successful", n)
23} else {
24	err  nil
25}
26
27// Float to String
28"1.567000"  fmt.Sprintf("%f", 1.567)
29"1.57"  fmt.Sprintf("%.2f", 1.567)
30"   1.57"  fmt.Sprintf("%7.2f", 1.567)
317  len(fmt.Sprintf("%7.2f", 1.567))
32
33// String to Float
34if n, err := strconv.ParseFloat("1.567", 32); err == nil {
35	1.5670000314712524  n
36} else {
37	t.Error("Unable to convert")
38}
39if n, err := strconv.ParseFloat("ex1!%5.67", 32); err == nil {
40	t.Error("Conversion was not supposed to be successful", n)
41} else {
42	err  nil
43}

Source: basictypes_test.go | Top

Constants

Constants are prefixed with the const keyword.

1// Constants are immutable
2const x byte = 5
3const y = 7
4
5// x++ // Won't compile
6
7// Array-like like Slices are mutable and cannot be made constants
8// 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.

1var arrayEmpty1 [0]int
2arrayEmpty2 := [...]int{}
3
4// Assertions
5arrayEmpty1  arrayEmpty2
60  len(arrayEmpty1)

Source: arrays_test.go | Top

Blank Array

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

 1var intArray [2]int
 2var stringArray [2]string
 3var fileArray [2]os.File
 4
 5// Assertions
 60  intArray[0]
 70  intArray[1]
 8""  stringArray[0]
 9""  stringArray[1]
10os.File{}  fileArray[0]
11os.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.

1// Explicit length
2arrayTwo := [2]string{"Einstein", "Newton"}
3// No need to specify length
4arrayThree := [...]string{"Einstein", "Newton", "Curie"}
5
6// Assertions
72  len(arrayTwo)
83  len(arrayThree)

Source: arrays_test.go | Top

Multidimensional Arrays

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

 1array := [2][2]string{
 2	{"A0", "B0"}, // Excel-like values
 3	{"A1", "B1"}, // for intuition
 4}
 5
 6// Assertions
 7"A0"  array[0][0]
 8"B0"  array[0][1]
 9"A1"  array[1][0]
10"B1"  array[1][1]

Source: arrays_test.go | Top

Setting Elements’ Values

Elements are referred by their index, starting from zero.

1var array [2]string
2
3array[0] = "Einstein"
4array[1] = "Newton"
5
6// Assertions
7"Einstein"  array[0]
8"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.

 1array := [3]byte{'a', 'b', 'c'}
 2var result = ""
 3var counter = 0
 4for i := 0; i < len(array); i++ {
 5	counter++
 6	result = result + string(array[i])
 7}
 8
 9// Assertions
103  counter
11"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.

 1array := [3]byte{'a', 'b', 'c'}
 2
 3changeFirstElement := func(array [3]byte) {
 4	array[0] = 'x'
 5}
 6
 7changeFirstElementByRef := func(array *[3]byte) {
 8	array[0] = 'x'
 9}
10
11// Assertion #1
12// Copying an array produces a brand new one
13array2 := array
14array2[0] = 'x'
15byte('a')  array[0] // Rather than x
16
17// Assertion #2
18// Functions receive an array copy, rather than a reference
19changeFirstElement(array)
20byte('a')  array[0] // Rather than x
21
22// Assertion #3
23// Pass array's pointer
24changeFirstElementByRef(&array)
25byte('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.

1var array = [3]string{"red", "green", "blue"}
2
3// Assertions
4[]string{"red", "green"}  array[0:2]     // Slice is a length-free type
5[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.

 1var sliceEmpty1 []int
 2var sliceEmpty2 = make([]int, 0)
 3var sliceEmpty3 = []int{}
 4
 5// Assertions #1
 6// Prove slices have no elements
 70  len(sliceEmpty1)
 80  len(sliceEmpty2)
 90  len(sliceEmpty3)
10
11// Assertions #2
12// Append one element
13[]int{5}  append(sliceEmpty1, 5)
14[]int{5}  append(sliceEmpty2, 5)
15[]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.

 1var slice1 = make([]int, 2)    // Capacity omitted
 2var slice2 = make([]int, 2, 2) // Equal length and capacity
 3var slice3 = make([]int, 1, 2) // Capacity larger then length
 4// var slice4 = make([]int, 3, 2) // Length larger than capacity (Compiler error)
 5
 6// Assertions #1
 7[]int{0, 0}  slice1
 82  len(slice1)
 92  cap(slice1)
10
11// Assertions #2
12[]int{0, 0}  slice2
132  len(slice2)
142  cap(slice2)
15
16// Assertions #3
17[]int{0}  slice3
181  len(slice3)
192  cap(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.

1slice := []string{"Einstein", "Newton"}
2
3// Assertions
42  len(slice)

Source: slices_test.go | Top

Setting Elements’ Values

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

1slice := []byte{'a', 'b', 'c'}
2
3// Assertions
4byte('a')  slice[0]
5slice[0] = 'x' // Change first element!
6byte('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.

 1var sliceInt []int
 2var sliceStr []string
 3
 4var sliceIntPrime = append(sliceInt, 15)
 5var sliceStrPrime = append(sliceStr, "Einstein", "Newton")
 6
 7// Assertions
 81  len(sliceIntPrime)        // New length
 92  len(sliceStrPrime)        // New length
1015  sliceIntPrime[0]         // New element #0
11"Einstein"  sliceStrPrime[0] // New element #0
12"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.

 1// ------------- Same length --------------
 2src1 := []byte{'a', 'b', 'c'}
 3dst1 := []byte{'x', 'y', 'z'}
 4copy(dst1, src1)
 5
 6// Assertions
 7[]byte{'a', 'b', 'c'}  dst1
 8dst1  src1
 9
10// --- Source smaller than destination  ---
11src2 := []byte{'a', 'b'}
12dst2 := []byte{'x', 'y', 'z'}
13copy(dst2, src2)
14
15// Assertions
16[]byte{'a', 'b', 'z'}  dst2
17dst2  src2
18
19// ---- Source larger than destination ----
20src3 := []byte{'a', 'b', 'c', 'd'}
21dst3 := []byte{'x', 'y', 'z'}
22copy(dst3, src3)
23
24// Assertions
25[]byte{'a', 'b', 'c'}  dst3
26dst3  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.

 1// Assertions (Slice Notation)
 2byte('e')  []byte("hello")[1]
 3[]byte("ello")  []byte("hello")[1:]
 4[]byte("hel")  []byte("hello")[:3]
 5[]byte("ell")  []byte("hello")[1:4]
 6[]byte("ell")  []byte("hello")[1:4]
 7
 8// Assertions (Helper Functions)
 9[][]byte{[]byte("a"), []byte("b"), []byte("c")}  bytes.Split([]byte("a b c"), []byte(" "))
10[]byte("hello")  bytes.Trim([]byte("😊hello🍺"), "😊🍺")
11[]byte("hello")  bytes.Trim([]byte("😊hello🍺"), "😊🍺")
12[]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.

1slice := []byte{'a', 'b', 'c'}
2var result = ""
3var counter = 0
4for i := 0; i < len(slice); i++ {
5	counter++
6	result = result + string(slice[i])
7}
83  counter
9"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.

 1changeFirstElement := func(slice []byte) {
 2	slice[0] = 'y'
 3}
 4
 5slice := []byte{'a', 'b', 'c'}
 6
 7// Assertion #1
 8// A copy of a slice still points to the original slice
 9slice2 := slice
10slice2[0] = 'x'
11byte('x')  slice[0] // Rather than a
12
13// Assertion #2
14// Functions receive an slice pointer, rather than a copy
15changeFirstElement(slice)
16byte('y')  slice[0] // Rather than a or x
17
18// Assertion #3
19// A slice's slice still points to the original slice
20lastTwoLetters := slice[1:]
21lastTwoLetters[0] = 'z'
22byte('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.

1// Assertions
20  len("")
35  len("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.

1// Assertions
21  strings.Index("hello fellows", "ell")
37  strings.LastIndex("hello fellows", "ell")
42  strings.IndexByte("hello", 'l')
53  strings.LastIndexByte("hello", 'l')
64  strings.IndexRune("🍷😊🍺🍷", '😊')
72  strings.IndexAny("hi😊!", "😊🍺🍷")
86  strings.IndexAny("🤣hi😊!", "😊🍺🍷")
912  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.

1// Assertions
2true  strings.Contains("hello", "llo")
3true  strings.ContainsAny("hello", "elxqwer")
4true  strings.ContainsAny("hi😊!", "😊🍺🍷")
5true  strings.HasPrefix("hello", "he")
6true  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.

 1// Assertions
 2"e"  string("hello"[1])
 3"ello"  "hello"[1:]
 4"hel"  "hello"[:3]
 5"ell"  "hello"[1:4]
 6"ell"  "hello"[1:4]
 7[]string{"a", "b", "c"}  strings.Split("a b c", " ")
 8"hello"  strings.Trim("😊hello🍺", "😊🍺")
 9"hello"  strings.Trim("😊hello🍺", "😊🍺")
10"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.

1// Assertions
2"hello 😊😊"  strings.Replace("hello :):)", ":)", "😊", -1)
3"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.

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

Source: strings_test.go | Top

Join (Concat)

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

1// Assertions
2"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.

1// Assertions
2"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.

 1"17"  fmt.Sprintf("%d", 17)             // Decimal
 2"11"  fmt.Sprintf("%x", 17)             // Hexadecimal
 3"21"  fmt.Sprintf("%o", 17)             // Octal
 4"10001"  fmt.Sprintf("%b", 17)          // Binary
 5"10001"  fmt.Sprintf("%b", 17)          // Binary
 6"1.750000"  fmt.Sprintf("%f", 1.75)     // Floating Point
 7"1.75"  fmt.Sprintf("%.2f", 1.75)       // Floating Point
 8"true"  fmt.Sprintf("%t", true)         // Boolean
 9"😊"  fmt.Sprintf("%c", '😊')             // Rune (Unicode point)
10"hello"  fmt.Sprintf("%s", "hello")     // Rune (Unicode point)
11"    o"  fmt.Sprintf("%5s", "o")        // Right Alignment
12"h    "  fmt.Sprintf("%-5s", "h")       // Left Alignment
13"'😊'"  fmt.Sprintf("%q", '😊')           // Quoted Rune
14"'😊'"  fmt.Sprintf("%q", '😊')           // Quoted Rune
15"\"hello\""  fmt.Sprintf("%q", "hello") // Quoted String
16"c64"  fmt.Sprintf("%v%v", "c", 64)     // Default String formatting
17"int"  fmt.Sprintf("%T", 64)            // Inferred Value's Type
18"%"  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.

1var emptyMap1 = map[string]int{}
2var emptyMap2 = make(map[string]int)
3
4// Assertions
50  len(emptyMap1)
60  len(emptyMap2)
7emptyMap1  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.

 1datesOfBirth := map[string]int{
 2	"Newton":   1643,
 3	"Faraday":  1791,
 4	"Einstein": 1879,
 5}
 6
 7// Assertions
 81643  datesOfBirth["Newton"]
 91791  datesOfBirth["Faraday"]
101879  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.

 1datesOfBirth := map[string]int{}
 2
 3// Insertion of non-existing key/value pairs
 4datesOfBirth["Newton"] = 1543
 5datesOfBirth["Faraday"] = 1791
 6datesOfBirth["Einstein"] = 1879
 7
 8// Assertions #1
 91543  datesOfBirth["Newton"]
101791  datesOfBirth["Faraday"]
111879  datesOfBirth["Einstein"]
12
13// Update of existing key/value pair
14datesOfBirth["Newton"] = 1643
15
16// Assertions #2
171643  datesOfBirth["Newton"]

Source: maps_test.go | Top

Deletion

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

 1var datesOfBirth = map[string]int{
 2	"Newton":   1643,
 3	"Faraday":  1791,
 4	"Einstein": 1879,
 5}
 6
 7delete(datesOfBirth, "Faraday")
 8
 9// Assertions
100  datesOfBirth["Faraday"]
112  len(datesOfBirth)

Source: maps_test.go | Top

Membership Test

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

 1// Set up Initial Map
 2var datesOfBirth = map[string]int{
 3	"Newton":   1643,
 4	"Faraday":  1791,
 5	"Einstein": 1879,
 6}
 7
 8// Test key membership
 9_, ok1 := datesOfBirth["Newton"]
10_, ok2 := datesOfBirth["Curie"]
11
12// Assertions
13true  ok1
14false  ok2

Source: maps_test.go | Top

Getting All Keys and Values

This is achived via iteration.

 1var datesOfBirth = map[string]int{
 2	"Newton":   1643,
 3	"Faraday":  1791,
 4	"Einstein": 1879,
 5}
 6
 7// Get all keys
 8var keys = ""
 9for key := range datesOfBirth {
10	keys = keys + key
11}
12
13// Get all values
14var values = ""
15for _, value := range datesOfBirth {
16	values = values + strconv.Itoa(value)
17}
18
19// Assertions (Keys)
20true  strings.Contains(keys, "Einstein")
21true  strings.Contains(keys, "Newton")
22true  strings.Contains(keys, "Faraday")
23
24// Assertions (Values)
25true  strings.Contains(values, "1879")
26true  strings.Contains(values, "1643")
27true  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 *.

 1a := 0
 2var b, c = &a, &a
 3
 4// Assertions #1
 50  a    // a is the original value
 60  b // b is a pointer to a. It is not zero
 70  *b   // *b is the value of a
 80  c // c is a pointer to a. It is not zero
 90  *c   // *c is the value of a
10
11// Change
12*b++
13
14// Assertions #2
151  a  // a is the original value
161  *b // *b is the value of a
171  *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.

 1var n = new(int)
 2
 3// Assertions #1
 40  *n
 50  n // n is a pointer!
 6
 7// Change
 8*n++
 9
10// Assertions #2
111  *n
12"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.

 1// ------------------ Pointer Behaviour ------------------------
 2
 3// Slices
 4slice := []int{0}
 5sliceCopy := slice
 6sliceCopy[0]++
 71  slice[0]
 81  sliceCopy[0]
 9
10// Maps
11myMap := map[string]int{"a": 0}
12myMapCopy := myMap
13myMapCopy["a"]++
141  myMap["a"]
151  myMapCopy["a"]
16
17// Functions
18myFunc := func(n int) int { return n }
19myFuncCopy := myFunc
20myFunc(1)  myFuncCopy(1)
21
22// ------------------- Value Behaviour ------------------------
23
24// Structs
25type NumberWrapper struct {
26	n int
27}
28myStruct := NumberWrapper{n: 0}
29myStructCopy := myStruct
30myStructCopy.n++
310  myStruct.n
321  myStructCopy.n
33
34// Arrays
35array := [1]int{0}
36arrayCopy := array
37arrayCopy[0]++
380  array[0]
391  arrayCopy[0]
40
41// Other built-in types
42// 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.

1type Empty struct{}
2
3// Assertions
4"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.

 1type Employee struct {
 2	Name   string
 3	Salary int
 4}
 5
 6e1 := Employee{}
 7e2 := new(Employee)
 8
 9// Assertions
10e1  *e2
11""  e1.Name
120  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.

 1type Employee struct {
 2	Name   string
 3	Salary int
 4}
 5
 6// ------------------ Positional Literal ---------------------
 7
 8e1 := Employee{"Jazmin", 2000}
 9
10// Assertions
11"Jazmin"  e1.Name
122000  e1.Salary
13
14// ---------------- Attribute-wise Literal -------------------
15
16e2 := Employee{
17	Name:   "Jazmin",
18	Salary: 2000,
19}
20
21// Assertions
22"Jazmin"  e2.Name
232000  e2.Salary
24
25// ------------------ Partial Literal (I) --------------------
26
27// Partial Literal
28e3 := Employee{
29	Name: "Jazmin",
30	// Salary: 2000, Omitted!!
31}
32// e3 := Employee{"Jazmin"} Illegal!
33
34// Assertions
35"Jazmin"  e3.Name
360  e3.Salary // Zero value!
37
38// ----------------- Partial Literal (II) --------------------
39
40// Partial Literal (II)
41e4 := Employee{
42	// Name: "Jazmin", Omitted!!
43	Salary: 2000,
44}
45
46// Assertions
47""  e4.Name // Zero value!
482000  e4.Salary
49Employee{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.

 1type Employee struct {
 2	Name   string
 3	Salary int
 4}
 5
 6e := Employee{Name: "Jazmin", Salary: 2000}
 7
 8// Update attributes!
 9e.Name = "Luigi"
10e.Salary = 1000
11
12// Assertions
13"Luigi"  e.Name
141000  e.Salary

Source: structs_test.go | Top

Anonymous Struct

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

 1jazmin := struct {
 2	Name   string
 3	Salary int
 4}{
 5	Name:   "Jazmin",
 6	Salary: 2000,
 7}
 8
 9// Assertions
10"Jazmin"  jazmin.Name
112000  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.

 1type Employee struct {
 2	string
 3	int // types have to be distinct; can't repeat them
 4}
 5
 6// Positional construction
 7e1 := Employee{"Jazmin", 2000}
 8// Field-wise construction (can use types as labels)
 9e2 := Employee{string: "Jazmin", int: 2000}
10
11// Assertions
12e1  e2

Source: structs_test.go | Top

Pass By Reference vs Pass By Value

Structs are passed by value by default.

 1type Employee struct {
 2	Name   string
 3	Salary int
 4}
 5
 6// ------------- Obtain Value (Default) ---------------
 7
 8v := Employee{Name: "Jazmin", Salary: 2000}
 9var v2, v3 = v, v
10
11// Assertions
122000  v.Salary
13v.Salary++
142001  v.Salary
15v2.Salary++
162001  v2.Salary
17v3.Salary++
182001  v3.Salary
192001  v.Salary
20
21// --------------- Obtain Reference  ------------------
22
23r := &Employee{Name: "Jazmin", Salary: 2000} // Reference!
24var r2, r3 = r, r
252000  r.Salary
26r.Salary++
272001  r.Salary
28r2.Salary++
292002  r2.Salary
30r3.Salary++
312003  r3.Salary
322003  r.Salary // r = r2 = r3

Source: structs_test.go | Top

Nested Fields

A struct’s attribute type may be another struct.

 1type Address struct {
 2	Number     int
 3	StreetName string
 4	Postcode   string
 5}
 6
 7type Employee struct {
 8	Name             string
 9	Salary           int
10	PrimaryAddress   Address
11	SecondaryAddress Address
12}
13
14a2 := Address{
15	Number:     109,
16	StreetName: "Larksfield",
17	Postcode:   "TW200RA",
18}
19
20e := Employee{
21	Name:   "Jazmin",
22	Salary: 2000,
23	PrimaryAddress: Address{
24		Number:     101,
25		StreetName: "Buckhurst",
26		Postcode:   "IG9IJA",
27	},
28	SecondaryAddress: a2,
29}
30
31// Assertions
32"IG9IJA"  e.PrimaryAddress.Postcode
33"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.

 1type Address struct {
 2	Number     int
 3	StreetName string
 4	Postcode   string
 5}
 6type Employee struct {
 7	Address // This will become an attribute named Address
 8	Name    string
 9	Salary  int
10}
11
12e := Employee{
13	Name:   "Jazmin",
14	Salary: 2000,
15	Address: Address{
16		Number:     101,
17		StreetName: "Buckhurst",
18		Postcode:   "IG9IJA",
19	},
20}
21
22// Assertions
23"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.

 1type Employee struct {
 2	Name    string
 3	Salary  int
 4	Address struct {
 5		Number     int
 6		StreetName string
 7		Postcode   string
 8	}
 9}
10
11e1 := Employee{
12	Name:   "Jazmin",
13	Salary: 2000,
14	Address: struct {
15		Number     int
16		StreetName string
17		Postcode   string
18	}{
19		Number:     101,
20		StreetName: "Buckhurst",
21		Postcode:   "IG9IJA",
22	},
23}
24
25// Assertions
26"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.

1type Employee struct {
2	Name   string
3	Salary int
4}

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.

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

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.

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

Source: methods_test.go | Top

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

 1e := Employee{"Jazmin", 2000}
 2
 3// Assertion #1
 4// Value-wise Method Call
 5var newSalary = e.FailToIncreaseSalaryBy(1000)
 63000  newSalary
 72000  e.Salary // Employee Salary not changed!
 8
 9// Assertion #2
10// Reference-wise Method Call (Side Effects!)
11e.SucceedToIncreaseSalaryBy(1000)
123000  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().

 1type Address struct {
 2	Number     int
 3	StreetName string
 4	Postcode   string
 5}
 6
 7type PublicEmployee struct {
 8	Name           string
 9	Salary         int
10	PrimaryAddress Address
11}
12
13func (a *Address) ChangePostCode(newPostCode string) {
14	a.Postcode = newPostCode // Passed value actually changed!
15}

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.

 1e := PublicEmployee{
 2	Name:   "Jazmin",
 3	Salary: 2000,
 4	PrimaryAddress: Address{
 5		Number:     12,
 6		StreetName: "High street",
 7		Postcode:   "SW18RLN",
 8	},
 9}
10
11// Assertions
12"SW18RLN"  e.PrimaryAddress.Postcode // Before change
13e.PrimaryAddress.ChangePostCode("TW18NLJ")            // change postcode!
14"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.

1type StringAlias string
2
3func (s StringAlias) ToUpperCase() string { // Method against StringAlias
4	return strings.ToUpper(string(s))
5}
6greeting := StringAlias("hello")
7
8// Assertions
9"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:

1type <NAME> interface {
2  <METHOD1>
3  <METHOD2>
4  ...
5}

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.

 1type employee struct {
 2	personName string
 3	salary     int
 4}
 5
 6type contractor struct {
 7	limitedCompany string
 8	dailyRate      int
 9}
10
11type costingInterface interface { // Define interface (IF)!
12	getYearlyCost() int
13	// ... other methods here.
14}
15
16func (e employee) getYearlyCost() int { // Implement IF for employee
17	return (e.salary * 103) / 100 // Pension contributions 3%
18}
19
20func (c contractor) getYearlyCost() int { // Implement IF for contractor
21	return c.dailyRate * 5 * 40 // working weeks p/ year
22}
23
24// Use interface as a regular variable Type
25var c1, c2 costingInterface
26c1 = contractor{"RipOff Ltd", 500}
27c2 = employee{"John Smith", 80000}
28
29// Assertions
30100000  c1.getYearlyCost()
3182400  c2.getYearlyCost()

Source: interfaces_test.go | Top

The Empty Interface

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

 1// v can be any value type!
 2tellMeYourType := func(v interface{}) string {
 3	switch v.(type) {
 4	case string:
 5		return "string"
 6	case int:
 7		return "int"
 8	default:
 9		return "other"
10	}
11}
12
13// All value types are compatible with interface{}!
14var v1, v2, v3 interface{}
15v1 = "hello"
16v2 = 45
17v3 = 1.5
18
19// Assertions
20"string"  tellMeYourType(v1)
21"int"  tellMeYourType(v2)
22"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().

1// Open a non-existing file
2_, err := os.Open("non-existing-file.txt")
3
4// Assertions
5"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.

 1func safeDiv(a int, b int) (int, error) {
 2	if b == 0 {
 3		return 0, errors.New("Can't divide by zero")
 4	}
 5	return a / b, nil
 6}
 7
 8// Calculations
 9r, _ := safeDiv(12, 3)
10_, err := safeDiv(12, 0)
11
12// Assertions
134  r
14"Can't divide by zero"  err.Error()

Source: errors_test.go | Top

Wrapping Errors with String Formatting

1// Open a non-existing file
2_, err := os.Open("non-existing-file.txt")
3
4// Annotate error
5err2 := fmt.Errorf("Ouch! %s", err)
6
7// Assertions
8"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.

1type CustomError struct {
2	LineNumber int
3	RowNumber  int
4}

Source: errors_test.go | Top

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

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

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:

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

Source: errors_test.go | Top

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

 1var err error
 2var line, row int
 3var path string
 4
 5// Trigger both doSomethingNasty(1) and doSomethingNasty(2)
 6for i := 1; i <= 2; i++ {
 7	err = doSomethingNasty(i)
 8	switch err.(type) {
 9	case *os.PathError:
10		v, _ := err.(*os.PathError)
11		path = v.Path
12	case *CustomError:
13		v, _ := err.(*CustomError)
14		line = v.LineNumber
15		row = v.RowNumber
16	}
17}
18
19// Assertions
20"non-existing-file.txt"  path
215  line
2230  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

 1// This function waits for a number of seconds and then appends
 2// an identifier to the `execOrder` string.
 3wasteTime := func(sleepSeconds int, execOrder *string, id string) {
 4	time.Sleep(time.Duration(sleepSeconds) * time.Second)
 5	*execOrder = *execOrder + id
 6}
 7
 8execOrder := "A"
 9
10go wasteTime(7, &execOrder, "E") // will run last
11go wasteTime(5, &execOrder, "D") // will run second
12go wasteTime(3, &execOrder, "C") // will run first
13
14execOrder = execOrder + "B"
15
16// Wait 10 seconds to allow all goroutines to run.
17time.Sleep(time.Duration(10) * time.Second)
18
19execOrder = execOrder + "F"
20
21// Assertions
22"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.

 1// Create foodChannel queue
 2var foodChannel = make(chan string)
 3
 4// The below function adds one or more food items to the `foodChannel` queue.
 5platter := func(size int) {
 6	// Artificial wait
 7	time.Sleep(time.Duration(size) * time.Second)
 8
 9	if size >= 1 {
10		foodChannel <- "hummus" // Post "hummus" to the queue
11	}
12	if size >= 2 {
13		foodChannel <- "falafel" // Post "falafel" to the queue
14	}
15	if size >= 3 {
16		foodChannel <- fmt.Sprintf("%d pita", size-2) // Post "n pita" to the queue
17	}
18}
19
20// Prepare various Platter Sizes
21go platter(1)
22go platter(2)
23go platter(3)
24
25// Assertions
26"hummus"  <-foodChannel  // posted by platter(1)
27"hummus"  <-foodChannel  // posted by platter(2)
28"falafel"  <-foodChannel // posted by platter(2)
29"hummus"  <-foodChannel  // posted by platter(3)
30"falafel"  <-foodChannel // posted by platter(3)
31"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.

 1// Define a channel c
 2c := make(chan string)
 3
 4execOrder := "A"
 5
 6// Run a goroutine that posts "message" to c
 7go func() {
 8
 9	execOrder = execOrder + "B"
10	c <- "message" // It blocks until a receiver appears
11	execOrder = execOrder + "E"
12
13}()
14
15// Wait
16time.Sleep(time.Second)
17
18// Poll queue: receive message
19execOrder = execOrder + "C"
20message := <-c
21execOrder = execOrder + "D"
22
23time.Sleep(time.Second) // Sleep 1 second
24
25// Assertions
26"message"  message
27"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).

1c := make(chan string, 2)
2c <- "first"  // Non-blocking
3c <- "second" // Non-blocking
4// c <- "third"  // This one would block
5
6// Assertions
7"first"  <-c
8"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.

 1c := make(chan string, 1)
 2
 3canOnlySend := func(c chan<- string, msg string) {
 4	c <- msg
 5	// msg := <-c // Won't compile
 6}
 7canOnlyReceive := func(c <-chan string) string {
 8	// c <- msg // Won't compile
 9	return (<-c)
10}
11canBothSendAndReceive := func(c chan string, msg string) string {
12	c <- msg
13	return (<-c)
14}
15
16// Assertions
17canOnlySend(c, "hello")
18"hello"  canOnlyReceive(c)
19"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:

 1execOrder := "A"
 2
 3c1 := make(chan string, 1)
 4c2 := make(chan string, 1)
 5c3 := make(chan string, 1)
 6
 7c1 <- "ping"
 8
 9execOrder = execOrder + "B"
10
11for i := 0; i < 3; i++ { // Loop three times
12	execOrder = execOrder + "-"
13	select {
14	case msg := <-c1: // Assignment is optional
15		execOrder = execOrder + "c1(" + msg + ")"
16		c2 <- msg
17	case msg := <-c2:
18		execOrder = execOrder + "c2(" + msg + ")"
19		c3 <- msg
20	case msg := <-c3:
21		execOrder = execOrder + "c3(" + msg + ")"
22	}
23}
24
25// Assertions
26"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.

 1execOrder := "A"
 2
 3c := make(chan string, 1)
 4
 5for i := 0; i < 2; i++ { // Loop two times
 6	select {
 7	case <-c: // wait for any message
 8		execOrder = execOrder + "C"
 9	case <-time.After(1 * time.Second):
10		execOrder = execOrder + "B"
11	}
12	c <- "ping"
13}
14
15// Assertions
16"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.

 1execOrder := "A"
 2
 3c := make(chan string) // synchronous channel!
 4
 5select {
 6case c <- "ping": // would block because there is no receiver
 7	execOrder = execOrder + "Never"
 8case <-time.After(1 * time.Second): // therefore the result is a timeout
 9	execOrder = execOrder + "B"
10}
11
12// Assertions
13"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.

 1execOrder := "A"
 2
 3c := make(chan string) // synchronous channel!
 4
 5select {
 6case c <- "ping": // would block because there is no receiver
 7	execOrder = execOrder + "Never"
 8default: // therefore the result is the default case
 9	execOrder = execOrder + "B"
10}
11
12// Assertions
13"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.

 1execOrder := "A"
 2
 3c := make(chan string, 3)
 4c <- "msg1"
 5
 6for i := 0; i < 2; i++ { // loop two times
 7	msg, channelStillOpen := <-c
 8	if channelStillOpen {
 9		execOrder = execOrder + "B(" + msg + ")"
10		close(c)
11	} else {
12		execOrder = execOrder + "C(" + msg + ")"
13	}
14}
15
16// Assertions
17"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.

 1execOrder := "A-"
 2c := make(chan string, 3)
 3c <- "m1"
 4c <- "m2"
 5c <- "m3"
 6close(c) // range will remain suspended unless we close the channel
 7for msg := range c {
 8	execOrder = execOrder + msg
 9}
10"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).

 1execOrder := "A"
 2
 3timer := time.NewTimer(5 * time.Second)
 4
 5futureFunc := func() {
 6	execOrder = execOrder + "B"
 7	<-timer.C // This is what makes the function wait!
 8	execOrder = execOrder + "D"
 9}
10
11go futureFunc()
12time.Sleep(2 * time.Second)
13
14execOrder = execOrder + "C"
15
16time.Sleep(4 * time.Second) // 6 seconds elapsed by now.
17
18execOrder = execOrder + "E"
19
20// Assertions
21"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.

 1execOrder := "A"
 2
 3timer := time.NewTimer(5 * time.Second)
 4
 5futureFunc := func() {
 6	execOrder = execOrder + "B"
 7	<-timer.C
 8	execOrder = execOrder + "Never" // will not be reached
 9}
10
11go futureFunc()
12time.Sleep(2 * time.Second)
13
14execOrder = execOrder + "C"
15
16timer.Stop() // Abort timer!
17
18execOrder = execOrder + "D"
19
20// Assertions
21"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.

 1execOrder := "A"
 2
 3timer := time.NewTicker(time.Second) // 1 Second
 4
 5repeatingFunc := func() {
 6	for {
 7		execOrder = execOrder + "<"
 8		<-timer.C
 9		execOrder = execOrder + ">"
10	}
11}
12
13go repeatingFunc()
14
15time.Sleep(3500 * time.Millisecond) // 3.5 seconds
16timer.Stop()                        // Abort timer!
17
18execOrder = execOrder + "B"
19
20// Assertions
21"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.

 1var x = 0
 2
 3var increment = func() {
 4	x = x + 1
 5}
 6var decrement = func() {
 7	x = x - 1
 8}
 9
10for i := 0; i < 10000; i++ {
11	go increment()
12	go decrement()
13}
14
15// Assertions
160  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:

1temp = get value of X
2temp2 = 1 + temp2
3x = 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:

 1var mutex sync.Mutex
 2var x = 0
 3
 4var increment = func() {
 5	mutex.Lock()
 6	x = x + 1
 7	mutex.Unlock()
 8}
 9var decrement = func() {
10	mutex.Lock()
11	x = x - 1
12	mutex.Unlock()
13}
14
15for i := 0; i < 10000; i++ {
16	go increment()
17	go decrement()
18}
19
20time.Sleep(5 * time.Second) // Sleep 5 seconds
21
22// Assertions
230  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.

 1var x int32 = 0
 2var increment = func() {
 3	atomic.AddInt32(&x, 1)
 4}
 5var decrement = func() {
 6	atomic.AddInt32(&x, -1)
 7}
 8
 9for i := 0; i < 10000; i++ {
10	go increment()
11	go decrement()
12}
13time.Sleep(3 * time.Second)
14
15// Assertions
16int32(0)  atomic.LoadInt32(&x)
17int32(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:

  • Add(N) - tell how many goroutines are pending: pending++
  • Done() - tell that a goroutine has finished: pending--
  • Wait() - block flow until pending == 0

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

 1var waitGroup sync.WaitGroup // Allocate a new Wait Group. pending := 0
 2twoTimesTable := make([]int, 10)
 3
 4var double = func(number int, wg *sync.WaitGroup) {
 5	time.Sleep(3 * time.Second) // Artificial wait
 6	twoTimesTable[number] = (number + 1) * 2
 7	wg.Done() // Goroutine completed: pending--
 8}
 9
10for i := 0; i < 10; i++ {
11	waitGroup.Add(1) // Add new pending goroutine: pending++
12	go double(i, &waitGroup)
13}
14
15waitGroup.Wait() // Wait until pending == 0
16
17// Assertions
18[]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.

1file, err := os.Create(tmpRoot + "example.txt")
2if err != nil {
3	t.Error("Can't create file: ", err)
4}
5file.Close()
6
7// Assertions
8nil  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.

 1// Create example.txt
 2Test_Create_File(t)
 3
 4// Get File Metadata
 5fileInfo, err := os.Stat(tmpRoot + "example.txt")
 6if err != nil {
 7	t.Error("Can't access file: ", err)
 8}
 9
10// Assertions
11"example.txt"  fileInfo.Name()
12int64(0)  fileInfo.Size()
13os.FileMode(0x1b6)  fileInfo.Mode()
14true  fileInfo.ModTime().Year() >= 2019
15false  fileInfo.IsDir()

Source: files_test.go | Top

Renaming a File

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

 1// Create example.txt
 2Test_Create_File(t)
 3
 4// Rename example.txt to deleteme.txt
 5err := os.Rename(tmpRoot+"example.txt", tmpRoot+"deleteme.txt")
 6if err != nil {
 7	t.Error("Can't rename file: ", err)
 8}
 9
10// Assertions
11_, err = os.Stat(tmpRoot + "deleteme.txt") // Check file's presence
12nil  err

Source: files_test.go | Top

Deleting a File

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

 1// Create example.txt
 2Test_Create_File(t)
 3
 4// Delete File
 5err := os.Remove(tmpRoot + "example.txt")
 6if err != nil {
 7	t.Error("Can't delete file: ", err)
 8}
 9
10// The example.txt should be absent
11_, err = os.Stat(tmpRoot + "example.txt")
12
13// Assertions
14nil  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.

 1// Open File for Writing
 2file, err := os.Create(tmpRoot + "hello.txt")
 3if err != nil {
 4	t.Error("Can't open file: ", err)
 5}
 6defer file.Close()
 7
 8// Write File
 9bytes := []byte("Hello 😀")
10count, err2 := file.Write(bytes) // WriteString() also available
11if err2 != nil {
12	t.Error("Can't write to file: ", err2)
13}
14
15// Flush changes!
16file.Sync()
17
18// Assertions
1910  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.

 1// Create hello.txt
 2Test_Write_File(t)
 3
 4// Open File
 5file, err := os.Open(tmpRoot + "hello.txt")
 6if err != nil {
 7	t.Error("Can't open file: ", err)
 8}
 9defer file.Close()
10
11// Successful Read
12bytes := make([]byte, 10)
13count, err2 := file.Read(bytes)
14if err2 != nil {
15	t.Error("Can't open file: ", err2)
16}
17
18// EOF Error (No more bytes left to read)
19var oneByteSlice = []byte{0}
20_, err3 := file.Read(oneByteSlice)
21
22// Assertions
23"Hello 😀"  string(bytes)
2410  count
25io.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.

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

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.

 1// Create hello.txt
 2Test_Write_File_IOUtil_Quick(t)
 3
 4// Open file for reading
 5file, err := os.Open(tmpRoot + "hello.txt")
 6if err != nil {
 7	log.Fatal(err)
 8}
 9defer file.Close()
10
11// Read File
12bytes, err2 := ioutil.ReadAll(file) // results in bytes!
13if err2 != nil {
14	t.Error("Can't open file: ", err2)
15}
16
17// Assertions
18"Hello 😀"  string(bytes)
1910  len(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.

 1// Create hello.txt
 2Test_Write_File_IOUtil_Quick(t)
 3
 4// Open file and read bytes in one go!
 5bytes, err := ioutil.ReadFile(tmpRoot + "hello.txt")
 6if err != nil {
 7	t.Error("Can't open file: ", err)
 8}
 9
10// Assertions
11"Hello 😀"  string(bytes)
1210  len(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.

 1// Open file For writing
 2fileHandle, err := os.Create(tmpRoot + "hello.txt")
 3if err != nil {
 4	t.Error("Can't open file: ", err)
 5}
 6defer fileHandle.Close()
 7
 8// Write File
 9writer := bufio.NewWriter(fileHandle)
10count, err2 := writer.WriteString("Hello 😀")
11if err2 != nil {
12	t.Error("Can't open file: ", err2)
13}
14
15// Make sure changes are written to disk!
16writer.Flush()
17
18// Assertions
1910  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.

 1// Open file for reading
 2file, err := os.Open(tmpRoot + "hello.txt")
 3if err != nil {
 4	t.Error("Can't open file: ", err)
 5}
 6defer file.Close()
 7
 8// Read File
 9reader := bufio.NewReader(file)
10line, err2 := reader.ReadString('o')
11if err2 != nil {
12	t.Error("Can't open file: ", err2)
13}
14
15// EOF Error (No more lines left to read)
16_, err3 := reader.ReadString('o')
17
18// Assertions
19"Hello"  line
20io.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.

 1// First generate example file
 2contents := []byte("line1\nline2\nline3\n")
 3err := ioutil.WriteFile(tmpRoot+"lines.txt", contents, 0666)
 4if err != nil {
 5	t.Error("Can't open file: ", err)
 6}
 7
 8// Open File For Reading
 9file, err := os.Open(tmpRoot + "lines.txt")
10if err != nil {
11	t.Error("Can't open file: ", err)
12}
13defer file.Close()
14
15// Use Scanner
16input := bufio.NewScanner(file)
17var lineBuffer = ""
18for input.Scan() {
19	lineBuffer = lineBuffer + input.Text()
20}
21
22// Assertions
23"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.

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

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.

 1// Specify command
 2cmd := exec.Command("go", "run", "iostreams.go")
 3
 4// Specify stdin input
 5cmd.Stdin = strings.NewReader("hello")
 6
 7// Declare a buffer to capture stdout from command
 8var stdout bytes.Buffer
 9cmd.Stdout = &stdout
10
11// Declare a buffer to capture stderr from command
12var stderr bytes.Buffer
13cmd.Stderr = &stderr
14
15// Run Command
16err := cmd.Run()
17if err != nil {
18	t.Error("Error:", err)
19}
20
21// Assertions
22"HELLO"  stdout.String()
23"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.

 1var arguments, command string
 2for i := 0; i < len(os.Args); i++ {
 3	if i == 0 {
 4		command = os.Args[0]
 5	} else if i == 1 {
 6		arguments += os.Args[i]
 7	} else {
 8		arguments += " " + os.Args[i]
 9	}
10}
11
12// Assertions
13fmt.Printf(command)
14fmt.Printf(arguments)
15true  strings.Contains(command, "arguments.test")
16true  strings.Contains(arguments, "test.timeout")

Source: arguments_test.go | Top

Before You Leave

🤘 Subscribe to my 100% spam-free newsletter!

website counters