Go By Assertion

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)
becomesa ⇔ b
assert.NotEqual(t, a, b)
becomesa ⇎ 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 untilpending == 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