Go By Assertion
A Go reference using unit testing assertions as opposed to print statements.
- Introduction
- Control Flow
- If-Then
- If-Then-Else
- If-Then-Else If
- If-Scoped Variables
- Switch
- Switch (Multi-Value Case)
- Switch (Expression Cases)
- Switch (Type Cases)
- Infinite Loop (While True Loop)
- While Loop
- Iteration Over Array Elements
- Iteration Over Array Elements Using Only Index
- Iteration Over Array Elements Ignoring Index
- Iteration Over Keys and Values of a Map
- Iteration Over the Unicode Characters of a String
- Iteration Over The Bytes of a String
- Regular For Loop
- Defer
- Functions
- Basic Types
- Arrays
- Slices
- Strings
- Maps
- Pointers
- Structs
- Methods
- Interfaces
- Errors
- Go Routines
- Simple Goroutine
- Synchronous Channels
- Synchronous Channels (Blocking Behaviour)
- Channel Buffering
- Channel Directions
- Select
- Select and Timeouts
- Select with “Send” Cases
- Select with Defaults
- Opening and Closing Channels
- Range over Channels
- Scheduled Tasks (Timers)
- Scheduled Tasks (Cancelling Timers)
- Recurrent Tasks (Tickers)
- Race Conditions (Mutexes)
- Race Conditions (Atomic Counters)
- Wait Groups
- Input/Output: Files and Streams
- Creating a File
- Obtaining a File’s Metadata
- Renaming a File
- Deleting a File
- Writing Bytes to a File
- Reading Bytes from a File
- Writing Bytes to a File (Single Statement)
- Read Bytes from a File Without Allocating a Slice
- Reading Bytes form a File Without a File Handle
- Writing to a File with Buffering
- Reading from a File with Buffering
- Reading Lines from a File
- Interacting with Stdin, Stdout, and Stderr
- Interacting with Shell Commands
- Arguments
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.
Source: controlflow_test.go | Top
If-Then-Else
The syntax is if EXPR {...} else {...}
.
var counter = 0
// Simple If
if counter == 0 {
counter++
} else {
counter--
}
// Assertions
1 ⇔ counter
Source: controlflow_test.go | Top
If-Then-Else If
The syntax is if EXPR {...} else if EXPR {...}
. A final else {...}
, as in the example below, may also be included.
var counter = 0
// Simple If
if counter == 0 {
counter++
} else if counter == 10 {
counter = counter * 10
} else if counter == 20 {
counter = counter * 20
} else { // Optional
counter--
}
// Assertions
1 ⇔ counter
Source: controlflow_test.go | Top
If-Scoped Variables
An if statement, in the form if VAR_DECLARATION ; EXPR {...}
, introduces variables to its scope.
var counter = 0
// Assign
if i := 5; counter == 0 {
counter = counter + i // i in scope here
} else {
counter = counter - i // i in scope here
}
// --- i is not longer in scope here ---
// Assertions
5 ⇔ counter
Source: controlflow_test.go | Top
Switch
The regular switch EXPR {...}
statement evaluates an expression EXPR
and executes the first case whose value matches the reduction of said expression. Unlike other languages, the cases do not “cascade” and there is no need to terminate them with a break
statement or similar.
The default
keyword is used to label the case to be selected when all the other ones prefixed with case
fail to match.
var result = ""
switch 1 + 1 {
case 0:
result = "zero"
case 1:
result = "one"
case 2:
result = "two"
default: // Optional
result = "some_number"
}
// Assertions
"two" ⇔ result
Source: controlflow_test.go | Top
Switch (Multi-Value Case)
The multi-value switch allows to match multiple values within a single case statement.
var result = ""
// Just cases
switch 2 * 3 {
case 0, 1, 2, 3, 4, 5, 6: // multiple values!
result = "between_zero_and_six"
case 7:
result = "seven"
default: // Optional
result = "some_number"
}
// Assertions
"between_zero_and_six" ⇔ result
Source: controlflow_test.go | Top
Switch (Expression Cases)
This is a type of switch statement in which the cases contain boolean expressions rather then values. The first case whose expression reduces to true will be executed.
var result = ""
switch { // No expression here
case 2*3 == 1:
result = "one"
case 2*3 == 6:
result = "six"
default: // Optional
result = "some_number"
}
// Assertions
"six" ⇔ result
Source: controlflow_test.go | Top
A more crude example is presented below:
var result = ""
switch { // No expression here
case false:
result = "false1"
case false:
result = "false2"
case true:
result = "true1"
default: // Optional
result = "true2"
}
// Assertions
"true1" ⇔ result
Source: controlflow_test.go | Top
Switch (Type Cases)
A switch statement in which the cases represent the types that a value may possess.
testType := func(i interface{}) string {
switch t := i.(type) { // the t := assignment is optional
case int:
return "int"
case string:
return "string"
default: // Optional
return "other type: " + fmt.Sprintf("%T", t)
}
}
// Assertions
"string" ⇔ testType("hello")
"int" ⇔ testType(45)
"other type: float64" ⇔ testType(1.53)
Source: controlflow_test.go | Top
Infinite Loop (While True Loop)
An infinite loop, in the form for {...}
never terminates and must be short-circuited explicitly; for example, using break
as in the example below.
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.
Source: controlflow_test.go | Top
Iteration Over Array Elements
This is accomplished by using the for index, value := range ARRAY {...}
syntax where index is the array index starting from zero.
var indexSum = 0
var sum = 0
for index, currentValue := range [3]int{1, 2, 3} {
indexSum = indexSum + index // 0 + 1 + 2 = 3
sum = sum + currentValue // 1 + 2 + 3 = 6
}
// Assertions
3 ⇔ indexSum
6 ⇔ sum
Source: controlflow_test.go | Top
Iteration Over Array Elements Using Only Index
This is like the regular iteration over an array except that the second value is ignored in the assignment: index := range ARRAY {...}
var sum = 0
var numbers = [3]int{1, 2, 3}
for index := range numbers {
sum = sum + numbers[index] // array elements accessed by index!
}
// Assertions
6 ⇔ sum
Source: controlflow_test.go | Top
Iteration Over Array Elements Ignoring Index
In this case, the index value is explicitly ignored by using the underscore _
blank identifier symbol.
var sum = 0
for _, currentValue := range [3]int{1, 2, 3} {
sum = sum + currentValue // 1 + 2 + 3 = 6
}
// Assertions
6 ⇔ sum
Source: controlflow_test.go | Top
Iteration Over Keys and Values of a Map
This is achieved using the key, value := range MAP {...}
syntax.
var keys, values string
for k, v := range map[string]string{"A": "Argentina", "B": "Brazil"} {
keys = keys + k
values = values + v
}
// Assertions
true ⇔ strings.Contains(keys, "A")
true ⇔ strings.Contains(keys, "B")
true ⇔ strings.Contains(values, "Argentina")
true ⇔ strings.Contains(values, "Brazil")
Source: controlflow_test.go | Top
Iteration Over the Unicode Characters of a String
By prefixing an string with range
, the for
loop will iterate through the string’s Unicode characters as opposed to its raw bytes.
var word string
for _, v := range "😊 olleh" {
word = string(v) + word
}
// Assertions
"hello 😊" ⇔ word
Source: controlflow_test.go | Top
Iteration Over The Bytes of a String
This is like iterating over an array in a regular C-like language.
var reversedHello = "😊 olleh"
var word string
for i := 0; i < len(reversedHello); i++ {
word = string(reversedHello[i]) + word
}
// Assertions
"hello \u008a\u0098\u009fð" ⇔ word
Source: controlflow_test.go | Top
Regular For Loop
The regular for loop is exactly the same as that found in C-like languages except that no parentheses are used and that the variable(s) are initialised with :=
rather than =
.
var counter = 0
for i := 0; i < 3; i++ {
counter = counter + i // 0 + 1 + 2 = 3
}
// Assertions
3 ⇔ counter
Source: controlflow_test.go | Top
Defer
The defer STATEMENT
syntax is used to enforce the execution of the statement STATEMENT
before the function under which the declaration appears is exited. The execution order is that of a stack: last in, first out (LIFO).
var actions = ""
addAction := func(action string) {
actions = actions + action
}
trackDefer := func() {
addAction("1")
defer addAction("[d1]")
addAction("2")
defer addAction("[d2]")
addAction("3")
defer addAction("[d3]")
addAction("4")
}
actions = actions + "START-"
trackDefer()
actions = actions + "-END"
// Assertions
"START-1234[d3][d2][d1]-END" ⇔ actions
Source: controlflow_test.go | Top
Functions
Go functions are actually “procedures” rather than pure functions like in a FP language such as Haskell. It means that they are not necessarily idempotent; they may have side effects.
Simple Function
A simple function neither takes nor returns values.
var globalVar = ""
func simple() {
globalVar = "changed"
}
// Assertions
"" ⇔ globalVar
simple()
"changed" ⇔ globalVar
Source: functions_test.go | Top
Simple Function With Return Value
The return value type is declared after the parenthesis: func name() <TYPE>
. The value itself is returned using return <VALUE>
.
Source: functions_test.go | Top
Function With Multiple Return Values
Two or more values may be returned by a function; their type must be declared using the func name() (TYPE1, TYPE2, ...)
syntax. When invoking the function, there is the flexibility to assign only the required values as shown below:
func returnTwo() (rune, bool) {
return 'a', false
}
// Assertions (Two Values)
first, second := returnTwo()
'a' ⇔ first
false ⇔ second
// Assertions (First Value Only)
onlyFirst, _ := returnTwo()
'a' ⇔ onlyFirst
// Assertions (Second Value Only)
_, onlySecond := returnTwo()
false ⇔ onlySecond
Source: functions_test.go | Top
Function Arguments
Functions arguments are expressed using the func name(<VAR> <TYPE>)
syntax. Multiple arguments may be declared by separating them with a comma as shown below:
var result = ""
func oneArgument(name string) {
result = name
}
func twoArguments(name string, age int) {
result = fmt.Sprintf("%s-%d", name, age)
}
// Assertions (One Argument)
oneArgument("Rob")
"Rob" ⇔ result
// Assertions (Two Arguments)
twoArguments("Rob", 36)
"Rob-36" ⇔ result
Source: functions_test.go | Top
Variadic Functions
Variadic functions take a varying number of arguments. The syntax is func name(<VAR> ...<TYPE>)
. Please note that <TYPE>
is prefixed with three dots ...
. In the function’s body, <VAR>
will appear like an array and can be iterated using the range
keyword.
func sum(numbers ...int) int {
var result int = 0
for _, n := range numbers {
result = result + n
}
return result
}
// Assertions
0 ⇔ sum()
1 ⇔ sum(1)
3 ⇔ sum(1, 2)
6 ⇔ sum(1, 2, 3)
Source: functions_test.go | Top
Anonymous Functions
Anonymous functions are declared without a custom name using the form func() {...}
and can be assigned to variables or returned directly as values.
func operation(op string) func(int, int) int {
switch op {
case "sum":
return func(v1 int, v2 int) int {
return v1 + v2
}
case "mul":
return func(v1 int, v2 int) int {
return v1 * v2
}
default:
panic("op should be either \"sum\" or \"mul\"")
}
}
// Assertions
// Direct use
3 ⇔ operation("sum")(1, 2)
6 ⇔ operation("mul")(2, 3)
// Function extraction first
sum := operation("sum")
mul := operation("mul")
3 ⇔ sum(1, 2)
6 ⇔ mul(2, 3)
// Create Inline
3 ⇔ 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.
var counter = 0
incrementer := func() int {
counter++
return counter
}
// Assertions
1 ⇔ incrementer()
counter++
3 ⇔ incrementer()
incrementer()
5 ⇔ incrementer()
6 ⇔ incrementer()
Source: functions_test.go | Top
Basic Types
Basic types are also known as atomic types; they are those that cannot be broken up into smaller components such as integers, floats, etc. However, we also include string conversion to and from primitive types. Likewise, we also include the most typical built-in operators applicable to the basic type at hand.
Boolean (Bool) and Logical Operators
Boolean types are built-in and work with the typical logic operators such as AND (&&
), OR (||
), Negation (!
), etc.
// Declaration
var MinBool bool = false
var MaxBool bool = true
// Logical Operators
false ⇔ !true
true ⇔ !false
true ⇔ true || false
true ⇔ MinBool || MaxBool
false ⇔ 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
// Declaration
var MinInt8 int8 = -128
var MaxInt8 int8 = 127
// Bounds
true ⇔ MinInt8 == math.MinInt8
true ⇔ MaxInt8 == math.MaxInt8
Source: basictypes_test.go | Top
16-Bit
// Declaration
var MinInt16 int16 = -32768
var MaxInt16 int16 = 32767
// Bounds
true ⇔ MinInt16 == math.MinInt16
true ⇔ MaxInt16 == math.MaxInt16
Source: basictypes_test.go | Top
32-Bit (Rune)
// Declaration
var MinInt32 rune = -2147483648
var MinRune rune = MinInt32
var MaxInt32 int32 = 2147483647
var MaxRune int32 = MaxInt32
// Bounds
true ⇔ MinInt32 == math.MinInt32
true ⇔ MaxInt32 == math.MaxInt32
true ⇔ MinRune == math.MinInt32
true ⇔ MaxRune == math.MaxInt32
// Rune is an alias for int32
MaxRune ⇔ MinInt32-1
MinRune ⇔ MaxInt32+1
Source: basictypes_test.go | Top
64-Bit
// Declaration
var MinInt64 int64 = -9223372036854775808
var MaxInt64 int64 = 9223372036854775807
// Bounds
true ⇔ MinInt64 == math.MinInt64
true ⇔ MaxInt64 == math.MaxInt64
Source: basictypes_test.go | Top
General Integer
The size depends on the underlying architecture.
// Only on 64-bit architectures
var MinInt int = -9223372036854775808
var MaxInt int = 9223372036854775807
// Same as int64 on 64-bit architectures
MinInt ⇔ math.MinInt64
MaxInt ⇔ math.MaxInt64
// Overflow behaviour
MaxInt ⇔ MinInt-1
MinInt ⇔ MaxInt+1
Source: basictypes_test.go | Top
Unsigned Integers
Unsigned integers only store positive numbers (including zero) and offer larger capacity thanks to not having to use the sign bit.
8-Bit (Byte)
// Declaration
var MinUInt8 uint8 = 0
var MaxUInt8 uint8 = 255 // 2^8-1
var MinByte byte = MinUInt8
var MaxByte byte = MaxUInt8
// Bounds
true ⇔ MaxUInt8 == math.MaxUint8
// Overflow
true ⇔ math.MaxUint8 == MinUInt8-1
true ⇔ MinUInt8 == MaxUInt8+1
// byte is an alias for uint8
MaxByte ⇔ MinUInt8-1
MinByte ⇔ MaxUInt8+1
Source: basictypes_test.go | Top
16-Bit
// Decaration
var MinUInt16 uint16 = 0
var MaxUInt16 uint16 = 65535 // 2^16-1
// Bounds
true ⇔ MaxUInt16 == math.MaxUint16
// Overflow
MaxUInt16 ⇔ MinUInt16-1
MinUInt16 ⇔ MaxUInt16+1
Source: basictypes_test.go | Top
32-Bit
// Declaration
var MinUInt32 uint32 = 0
var MaxUInt32 uint32 = 4294967295 // 2^32-1
// Bounds
true ⇔ MaxUInt32 == math.MaxUint32
// Overflow
MaxUInt32 ⇔ MinUInt32-1
MinUInt32 ⇔ MaxUInt32+1
Source: basictypes_test.go | Top
64-Bit
// Declaration
var MinUInt64 uint64 = 0
var MaxUInt64 uint64 = 18446744073709551615 // 2^64-1
// Bounds
true ⇔ MaxUInt64 == math.MaxUint64
// Overflow
MaxUInt64 ⇔ MinUInt64-1
MinUInt64 ⇔ MaxUInt64+1
Source: basictypes_test.go | Top
General Unsigned Integer
Size is implementation-specific (either 32-bit or 64-bit)
// Declaration
var MinUint uint = 0
var MaxUint uint = 18446744073709551615
// Bounds: Same as uint64 on 64-bit architectures
true ⇔ math.MaxUint64 == MaxUint
// Overflow behaviour
MaxUint ⇔ MinUint-1
MinUint ⇔ MaxUint+1
Source: basictypes_test.go | Top
Unsigned Integer Pointer
Size is implementation-specific (either 32-bit or 64-bit)
// Declaration
var MinUintptr uintptr = 0
var MaxUintptr uintptr = 18446744073709551615
// Bounds: Same as uint64 on 64-bit architectures
true ⇔ math.MaxUint64 == MaxUintptr
// Overflow behaviour
MaxUintptr ⇔ MinUintptr-1
MinUintptr ⇔ MaxUintptr+1
Source: basictypes_test.go | Top
Float
The float type allows for a decimal component.
32-Bit
// Declaration
var MinFloat32 float32 = -1.401298464324817e-45
var MaxFloat32 float32 = 3.4028234663852886e+38
// Bounds
true ⇔ MinFloat32 == -math.SmallestNonzeroFloat32
true ⇔ MaxFloat32 == math.MaxFloat32
Source: basictypes_test.go | Top
64-Bit
// Declaration
var MinFloat64 float64 = -5e-324
var MaxFloat64 float64 = 1.7976931348623157e+308
// Bounds
true ⇔ MinFloat64 == -math.SmallestNonzeroFloat64
true ⇔ MaxFloat64 == math.MaxFloat64
Source: basictypes_test.go | Top
Complex
A type for Complex numbers.
64-Bit
// Declaration and Bounds
complex(1.401298464324817e-45, 1.401298464324817e-45) ⇔ complex(math.SmallestNonzeroFloat32, math.SmallestNonzeroFloat32)
complex(3.4028234663852886e+38, 3.4028234663852886e+38) ⇔ complex(math.MaxFloat32, math.MaxFloat32)
Source: basictypes_test.go | Top
128-Bit
// Declaration and Bounds
complex(5e-324, 5e-324) ⇔ complex(math.SmallestNonzeroFloat64, math.SmallestNonzeroFloat64)
complex(1.7976931348623157e+308, 1.7976931348623157e+308) ⇔ complex(math.MaxFloat64, math.MaxFloat64)
Source: basictypes_test.go | Top
Number Literals
Number literals may be expressed using a variety of notations (decimal, scientific, etc.) and bases (binary, octal, hex, etc.)
// Assertions
255 ⇔ 0xFF // Hex
255 ⇔ 0XFF // Hex
255 ⇔ 0377 // Octal
255 ⇔ 0o377 // Octal
255 ⇔ 0O377 // Octal
255 ⇔ 0b11111111 // Binary
255 ⇔ 0B11111111 // Binary
0.5 ⇔ .5 // Float
0.5 ⇔ 00.5 // Float
0.0 ⇔ 0. // Float
50.0 ⇔ 0.5e2 // Float w/Exponent
50.0 ⇔ 0.5E2 // Float w/Exponent
50.0 ⇔ 0.5E+2 // Float w/Exponent
0.005 ⇔ 0.5E-2 // Float w/Exponent
complex128(1+2i) ⇔ 1+2i // Complex Number
1000000 ⇔ 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.
// Assertions
5 ⇔ 3+2 // Sum
1 ⇔ 3-2 // Subtraction
6 ⇔ 3*2 // Multiplication
1 ⇔ 3/2 // Integer Division
1.5 ⇔ 3.0/2.0 // Float Division
2 ⇔ 5%3 // Modulo operator
2.0 ⇔ math.Sqrt(4) // Square Root
true ⇔ math.Pi > 3.14 && math.Pi < 3.15 // Pi Constant
// State mutating operators
var x = 3
x++ // Increase value by one
4 ⇔ x
x-- // Decrease value by one
3 ⇔ x
Source: basictypes_test.go | Top
Number Type Conversion
As expected, conversion from a smaller type to a larger one is value-preserving. The noteworthy thing is that conversion from a larger type to a small one results in truncation to zero whenever the value is too long to fit.
// Upcasting (Value-Preserving)
var smallValue byte = 5
var smallNegValue int8 = -5
// Assertions
true ⇔ 9000+uint(smallValue) == 9005
true ⇔ 9000+int(smallNegValue) == 8995
true ⇔ 9000+uint16(smallValue) == 9005
true ⇔ 9000+int16(smallNegValue) == 8995
true ⇔ 900000000+uint32(smallValue) == 900000005
true ⇔ 900000000+int32(smallNegValue) == 899999995
true ⇔ 9000000000000000000+uint64(smallValue) == 9000000000000000005
true ⇔ 9000000000000000000+int64(smallNegValue) == 8999999999999999995
// Down-casting (Value-Destructive)
var bigValue uint64 = 18446744073709551615 // 2^64
var bigNegValue int64 = -9223372036854775808 // 2^63
var bigNegValue2 int64 = -4611686018427387904 // 2^62
var bigNegValue3 int64 = -4294967296 // 2^32
uint32(4294967295) ⇔ uint32(bigValue)
int32(-5) ⇔ int32(smallNegValue)
int32(0) ⇔ int32(bigNegValue) // Truncated to zero
int32(0) ⇔ int32(bigNegValue2) // Truncated to zero
int32(0) ⇔ int32(bigNegValue3) // Truncated to zero
uint16(65535) ⇔ uint16(bigValue)
uint8(255) ⇔ uint8(bigValue)
// Float and Integers
var floatValue float32 = 1.5
float32(1.0) ⇔ float32(1)
1 ⇔ 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.
// Assertions
'A' ⇔ rune(65) // Rune value
'A' ⇔ '\101' // Octal
'A' ⇔ '\x41' // Hex
'A' ⇔ '\u0041' // 16-bit Unicode
'A' ⇔ '\U00000041' // 32-bit Unicode
'😀' ⇔ '\U0001F600' // 32-bit Unicode
'\a' ⇔ '\x07' // Bell
'\b' ⇔ '\x08' // Backspace
'\f' ⇔ '\x0C' // Form feed
'\n' ⇔ '\x0A' // Line Feed
'\r' ⇔ '\x0D' // Carriage Return
'\t' ⇔ '\x09' // Horizontal Tab
'\v' ⇔ '\x0b' // Vertial Tab
'\\' ⇔ '\x5c' // Backslash
'\'' ⇔ '\x27' // Single Quote
Source: basictypes_test.go | Top
String Conversion
String conversion is more explicit in Go than in other languages; different techniques are required depending on the underlying type.
// The string() function converts a Unicode code point!
"A" ⇔ string(65)
"😀" ⇔ string(0X0001F600)
"AB" ⇔ string([]byte{65, 66})
// Integer to String
"15" ⇔ fmt.Sprintf("%d", 15)
"15" ⇔ strconv.Itoa(15)
"15" ⇔ strconv.FormatInt(15, 10) // Base 10
"f" ⇔ strconv.FormatInt(15, 16) // Base 16
" 15" ⇔ fmt.Sprintf("%7d", 15) // Padding
"15 " ⇔ fmt.Sprintf("%-7d", 15) // Padding
7 ⇔ len(fmt.Sprintf("%7d", 15))
// String to Integer
if n, err := strconv.Atoi("15"); err == nil {
15 ⇔ n
} else {
t.Error("Unable to convert")
}
if n, err := strconv.Atoi("ex1!%5"); err == nil {
t.Error("Conversion was not supposed to be successful", n)
} else {
err ⇎ nil
}
// Float to String
"1.567000" ⇔ fmt.Sprintf("%f", 1.567)
"1.57" ⇔ fmt.Sprintf("%.2f", 1.567)
" 1.57" ⇔ fmt.Sprintf("%7.2f", 1.567)
7 ⇔ len(fmt.Sprintf("%7.2f", 1.567))
// String to Float
if n, err := strconv.ParseFloat("1.567", 32); err == nil {
1.5670000314712524 ⇔ n
} else {
t.Error("Unable to convert")
}
if n, err := strconv.ParseFloat("ex1!%5.67", 32); err == nil {
t.Error("Conversion was not supposed to be successful", n)
} else {
err ⇎ nil
}
Source: basictypes_test.go | Top
Constants
Constants are prefixed with the const
keyword.
// Constants are immutable
const x byte = 5
const y = 7
// x++ // Won't compile
// Array-like like Slices are mutable and cannot be made constants
// const slice = []byte{'a', 'b', 'c'} // Won't compile
Source: basictypes_test.go | Top
Arrays
Arrays are immutable: once created, their size cannot be changed. They are passed by value by default.
Empty Array
An empty arrays contains zero elements.
var arrayEmpty1 [0]int
arrayEmpty2 := [...]int{}
// Assertions
arrayEmpty1 ⇔ arrayEmpty2
0 ⇔ len(arrayEmpty1)
Source: arrays_test.go | Top
Blank Array
A blank array is populated with the applicable zero element for the declared type.
var intArray [2]int
var stringArray [2]string
var fileArray [2]os.File
// Assertions
0 ⇔ intArray[0]
0 ⇔ intArray[1]
"" ⇔ stringArray[0]
"" ⇔ stringArray[1]
os.File{} ⇔ fileArray[0]
os.File{} ⇔ fileArray[1]
Source: arrays_test.go | Top
Array Literals
Array literals are array declarations in which the space for the array is allocated and its elements are declared in one single statement.
// Explicit length
arrayTwo := [2]string{"Einstein", "Newton"}
// No need to specify length
arrayThree := [...]string{"Einstein", "Newton", "Curie"}
// Assertions
2 ⇔ len(arrayTwo)
3 ⇔ len(arrayThree)
Source: arrays_test.go | Top
Multidimensional Arrays
In multidimensional arrays, the outer elements are arrays (which in turn may declare further arrays!)
array := [2][2]string{
{"A0", "B0"}, // Excel-like values
{"A1", "B1"}, // for intuition
}
// Assertions
"A0" ⇔ array[0][0]
"B0" ⇔ array[0][1]
"A1" ⇔ array[1][0]
"B1" ⇔ array[1][1]
Source: arrays_test.go | Top
Setting Elements’ Values
Elements are referred by their index, starting from zero.
var array [2]string
array[0] = "Einstein"
array[1] = "Newton"
// Assertions
"Einstein" ⇔ array[0]
"Newton" ⇔ array[1]
Source: arrays_test.go | Top
Iteration (Classic)
Arrays may be traversed using the traditional C-like approach: by getting their length and referencing each element by index.
array := [3]byte{'a', 'b', 'c'}
var result = ""
var counter = 0
for i := 0; i < len(array); i++ {
counter++
result = result + string(array[i])
}
// Assertions
3 ⇔ counter
"abc" ⇔ result
Source: arrays_test.go | Top
Iteration (Range)
Please see Control Flow
Pass By Value vs Pass By Reference
Arrays are passed by value by default.
array := [3]byte{'a', 'b', 'c'}
changeFirstElement := func(array [3]byte) {
array[0] = 'x'
}
changeFirstElementByRef := func(array *[3]byte) {
array[0] = 'x'
}
// Assertion #1
// Copying an array produces a brand new one
array2 := array
array2[0] = 'x'
byte('a') ⇔ array[0] // Rather than x
// Assertion #2
// Functions receive an array copy, rather than a reference
changeFirstElement(array)
byte('a') ⇔ array[0] // Rather than x
// Assertion #3
// Pass array's pointer
changeFirstElementByRef(&array)
byte('x') ⇔ array[0] // Rather than a
Source: arrays_test.go | Top
Slices
Slices are in principle “views” or “projections” over arrays created using the slice syntax, for example, [3]string{"red","green","blue"}[0:2]
produces a slice that points to the {"red","green"}
elements of the array. However, the idiomatic treatment of slices in Go is to always work with slices from the get go even when the initial slice could have been implemented as an array.
The key message is that a slice is a type in itself rather than an array with a lesser number of elements.
Slices vs Arrays
This example shows that when we get an array’s slice, a brand new type (hmm, a slice), is produced, rather than a smaller array.
var array = [3]string{"red", "green", "blue"}
// Assertions
[]string{"red", "green"} ⇔ array[0:2] // Slice is a length-free type
[2]string{"red", "green"} ⇎ array[0:2] // Slice is not a smaller array
Source: slices_test.go | Top
Empty Slice
Unlike plain arrays, slices may be declared as empty but then populated using the append()
function. The implementation is not, however, optimised for this use case: new arrays may be allocated depending on the slice’s initial capacity.
var sliceEmpty1 []int
var sliceEmpty2 = make([]int, 0)
var sliceEmpty3 = []int{}
// Assertions #1
// Prove slices have no elements
0 ⇔ len(sliceEmpty1)
0 ⇔ len(sliceEmpty2)
0 ⇔ len(sliceEmpty3)
// Assertions #2
// Append one element
[]int{5} ⇔ append(sliceEmpty1, 5)
[]int{5} ⇔ append(sliceEmpty2, 5)
[]int{5} ⇔ append(sliceEmpty3, 5)
Source: slices_test.go | Top
Make (Slice Allocation)
The make()
function helps allocate a new slice by specifying the initial slice’s length and capacity. The specific syntax for slices is make([]<TYPE>, <LENGTH>, [<CAPACITY>])
. The functions len()
and cap()
are used to obtain a slice’s length and capacity, respectively.
var slice1 = make([]int, 2) // Capacity omitted
var slice2 = make([]int, 2, 2) // Equal length and capacity
var slice3 = make([]int, 1, 2) // Capacity larger then length
// var slice4 = make([]int, 3, 2) // Length larger than capacity (Compiler error)
// Assertions #1
[]int{0, 0} ⇔ slice1
2 ⇔ len(slice1)
2 ⇔ cap(slice1)
// Assertions #2
[]int{0, 0} ⇔ slice2
2 ⇔ len(slice2)
2 ⇔ cap(slice2)
// Assertions #3
[]int{0} ⇔ slice3
1 ⇔ len(slice3)
2 ⇔ 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.
Source: slices_test.go | Top
Setting Elements’ Values
Like in the case of arrays, elements are referenced by index, starting from zero.
slice := []byte{'a', 'b', 'c'}
// Assertions
byte('a') ⇔ slice[0]
slice[0] = 'x' // Change first element!
byte('x') ⇔ slice[0]
Source: slices_test.go | Top
Append
The append(<SLICE>, <VALUE1>, <VALUE2>, ...)
function allows adding new elements to an existing slice. It is often said that append()
allows to grow a slice.
var sliceInt []int
var sliceStr []string
var sliceIntPrime = append(sliceInt, 15)
var sliceStrPrime = append(sliceStr, "Einstein", "Newton")
// Assertions
1 ⇔ len(sliceIntPrime) // New length
2 ⇔ len(sliceStrPrime) // New length
15 ⇔ sliceIntPrime[0] // New element #0
"Einstein" ⇔ sliceStrPrime[0] // New element #0
"Newton" ⇔ sliceStrPrime[1] // New element #1
Source: slices_test.go | Top
Copy
The copy(<DST_SLICE>, <SRC_SLICE>)
function allows copying the elements from one slice onto another one.
// ------------- Same length --------------
src1 := []byte{'a', 'b', 'c'}
dst1 := []byte{'x', 'y', 'z'}
copy(dst1, src1)
// Assertions
[]byte{'a', 'b', 'c'} ⇔ dst1
dst1 ⇔ src1
// --- Source smaller than destination ---
src2 := []byte{'a', 'b'}
dst2 := []byte{'x', 'y', 'z'}
copy(dst2, src2)
// Assertions
[]byte{'a', 'b', 'z'} ⇔ dst2
dst2 ⇎ src2
// ---- Source larger than destination ----
src3 := []byte{'a', 'b', 'c', 'd'}
dst3 := []byte{'x', 'y', 'z'}
copy(dst3, src3)
// Assertions
[]byte{'a', 'b', 'c'} ⇔ dst3
dst3 ⇎ src3
Source: slices_test.go | Top
Subsets (Slices, Splits, Trims)
There are various ways to obtain a subset of a slice; using the slice notation as well as helper functions.
// Assertions (Slice Notation)
byte('e') ⇔ []byte("hello")[1]
[]byte("ello") ⇔ []byte("hello")[1:]
[]byte("hel") ⇔ []byte("hello")[:3]
[]byte("ell") ⇔ []byte("hello")[1:4]
[]byte("ell") ⇔ []byte("hello")[1:4]
// Assertions (Helper Functions)
[][]byte{[]byte("a"), []byte("b"), []byte("c")} ⇔ bytes.Split([]byte("a b c"), []byte(" "))
[]byte("hello") ⇔ bytes.Trim([]byte("😊hello🍺"), "😊🍺")
[]byte("hello") ⇔ bytes.Trim([]byte("😊hello🍺"), "😊🍺")
[]byte("hello") ⇔ bytes.TrimSpace([]byte("\t \t hello\n"))
Source: slices_test.go | Top
Iteration (Classic)
A slice may be traversed just like an array by counting elements until reaching the slice’s length using a for()
loop.
slice := []byte{'a', 'b', 'c'}
var result = ""
var counter = 0
for i := 0; i < len(slice); i++ {
counter++
result = result + string(slice[i])
}
3 ⇔ counter
"abc" ⇔ result
Source: slices_test.go | Top
Iteration (Range)
See Control Flow
Pass By Value vs Pass By Reference
Slices are passed to functions by reference by default.
changeFirstElement := func(slice []byte) {
slice[0] = 'y'
}
slice := []byte{'a', 'b', 'c'}
// Assertion #1
// A copy of a slice still points to the original slice
slice2 := slice
slice2[0] = 'x'
byte('x') ⇔ slice[0] // Rather than a
// Assertion #2
// Functions receive an slice pointer, rather than a copy
changeFirstElement(slice)
byte('y') ⇔ slice[0] // Rather than a or x
// Assertion #3
// A slice's slice still points to the original slice
lastTwoLetters := slice[1:]
lastTwoLetters[0] = 'z'
byte('z') ⇔ slice[1] // Rather than b
Source: slices_test.go | Top
Strings
In Go, a string is a discrete type as opposed to a list, or an array, like in Haskell, or C, respectively. However, it behaves like a slice of bytes in most cases.
Length
A string’s length is obtained using the len()
function.
Source: strings_test.go | Top
Substring and Rune Index
The following family of functions from the package strings
provide the location of strings and characters (runes) in various contexts.
// Assertions
1 ⇔ strings.Index("hello fellows", "ell")
7 ⇔ strings.LastIndex("hello fellows", "ell")
2 ⇔ strings.IndexByte("hello", 'l')
3 ⇔ strings.LastIndexByte("hello", 'l')
4 ⇔ strings.IndexRune("🍷😊🍺🍷", '😊')
2 ⇔ strings.IndexAny("hi😊!", "😊🍺🍷")
6 ⇔ strings.IndexAny("🤣hi😊!", "😊🍺🍷")
12 ⇔ strings.LastIndexAny("🍷😊🍺🍷", "🍷")
Source: strings_test.go | Top
Predicates (Substring, Starts/Ends With)
The following family of functions from the package strings
provide assertions about various substring membership cases.
// Assertions
true ⇔ strings.Contains("hello", "llo")
true ⇔ strings.ContainsAny("hello", "elxqwer")
true ⇔ strings.ContainsAny("hi😊!", "😊🍺🍷")
true ⇔ strings.HasPrefix("hello", "he")
true ⇔ strings.HasSuffix("hello", "lo")
Source: strings_test.go | Top
Subsets (Slices, Splits, Trims)
The slice syntax [start:end]
and the below functions from the package strings
provide subsets of a string.
// Assertions
"e" ⇔ string("hello"[1])
"ello" ⇔ "hello"[1:]
"hel" ⇔ "hello"[:3]
"ell" ⇔ "hello"[1:4]
"ell" ⇔ "hello"[1:4]
[]string{"a", "b", "c"} ⇔ strings.Split("a b c", " ")
"hello" ⇔ strings.Trim("😊hello🍺", "😊🍺")
"hello" ⇔ strings.Trim("😊hello🍺", "😊🍺")
"hello" ⇔ strings.TrimSpace("\t \t hello\n")
Source: strings_test.go | Top
Replace
The Replace()
function from the strings
package substitutes one string with another.
// Assertions
"hello 😊😊" ⇔ strings.Replace("hello :):)", ":)", "😊", -1)
"hello 😊:)" ⇔ strings.Replace("hello :):)", ":)", "😊", 1)
Source: strings_test.go | Top
Case Conversion (Title, Upper, Lower)
The Title()
, ToUpper()
, and ToLower()
functions from the strings
package change a string’s case.
// Assertions
"This Is The End" ⇔ strings.Title("this is the end")
"HELLO" ⇔ strings.ToUpper("HellO")
"hello" ⇔ strings.ToLower("HellO")
Source: strings_test.go | Top
Join (Concat)
The Join()
function from the strings
package allows concatenating strings.
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.
Source: strings_test.go | Top
String Formatting
The Sprtinf()
function from the fmt
package helps formatting various value types as well as adding padding (spaces) for alignment purposes.
"17" ⇔ fmt.Sprintf("%d", 17) // Decimal
"11" ⇔ fmt.Sprintf("%x", 17) // Hexadecimal
"21" ⇔ fmt.Sprintf("%o", 17) // Octal
"10001" ⇔ fmt.Sprintf("%b", 17) // Binary
"10001" ⇔ fmt.Sprintf("%b", 17) // Binary
"1.750000" ⇔ fmt.Sprintf("%f", 1.75) // Floating Point
"1.75" ⇔ fmt.Sprintf("%.2f", 1.75) // Floating Point
"true" ⇔ fmt.Sprintf("%t", true) // Boolean
"😊" ⇔ fmt.Sprintf("%c", '😊') // Rune (Unicode point)
"hello" ⇔ fmt.Sprintf("%s", "hello") // Rune (Unicode point)
" o" ⇔ fmt.Sprintf("%5s", "o") // Right Alignment
"h " ⇔ fmt.Sprintf("%-5s", "h") // Left Alignment
"'😊'" ⇔ fmt.Sprintf("%q", '😊') // Quoted Rune
"'😊'" ⇔ fmt.Sprintf("%q", '😊') // Quoted Rune
"\"hello\"" ⇔ fmt.Sprintf("%q", "hello") // Quoted String
"c64" ⇔ fmt.Sprintf("%v%v", "c", 64) // Default String formatting
"int" ⇔ fmt.Sprintf("%T", 64) // Inferred Value's Type
"%" ⇔ fmt.Sprintf("%%") // Literal Percent Sign
Source: strings_test.go | Top
Maps
Maps are associations of keys to values; both of which can be of any type. They are also known as hashes or hashmaps in other languages. Maps types have the form map[<KEY-TYPE>]<VALUE-TYPE>
.
Empty Map
An empty map contains no key/value pairs.
var emptyMap1 = map[string]int{}
var emptyMap2 = make(map[string]int)
// Assertions
0 ⇔ len(emptyMap1)
0 ⇔ len(emptyMap2)
emptyMap1 ⇔ emptyMap2
Source: maps_test.go | Top
Map Literal
A map literal allocates a Map and sets an initial number of key/value pairs in one statement.
datesOfBirth := map[string]int{
"Newton": 1643,
"Faraday": 1791,
"Einstein": 1879,
}
// Assertions
1643 ⇔ datesOfBirth["Newton"]
1791 ⇔ datesOfBirth["Faraday"]
1879 ⇔ datesOfBirth["Einstein"]
Source: maps_test.go | Top
Key/Value Insertion and Updating
Key/value pairs are added (or updated) to a map using the map[<KEY>] = <VALUE>
syntax.
datesOfBirth := map[string]int{}
// Insertion of non-existing key/value pairs
datesOfBirth["Newton"] = 1543
datesOfBirth["Faraday"] = 1791
datesOfBirth["Einstein"] = 1879
// Assertions #1
1543 ⇔ datesOfBirth["Newton"]
1791 ⇔ datesOfBirth["Faraday"]
1879 ⇔ datesOfBirth["Einstein"]
// Update of existing key/value pair
datesOfBirth["Newton"] = 1643
// Assertions #2
1643 ⇔ datesOfBirth["Newton"]
Source: maps_test.go | Top
Deletion
Key/value pairs are deleted using the delete(<MAP>, <KEY>)
function.
var datesOfBirth = map[string]int{
"Newton": 1643,
"Faraday": 1791,
"Einstein": 1879,
}
delete(datesOfBirth, "Faraday")
// Assertions
0 ⇔ datesOfBirth["Faraday"]
2 ⇔ len(datesOfBirth)
Source: maps_test.go | Top
Membership Test
Key membership can be tested by checking the second return value when querying a key.
// Set up Initial Map
var datesOfBirth = map[string]int{
"Newton": 1643,
"Faraday": 1791,
"Einstein": 1879,
}
// Test key membership
_, ok1 := datesOfBirth["Newton"]
_, ok2 := datesOfBirth["Curie"]
// Assertions
true ⇔ ok1
false ⇔ ok2
Source: maps_test.go | Top
Getting All Keys and Values
This is achived via iteration.
var datesOfBirth = map[string]int{
"Newton": 1643,
"Faraday": 1791,
"Einstein": 1879,
}
// Get all keys
var keys = ""
for key := range datesOfBirth {
keys = keys + key
}
// Get all values
var values = ""
for _, value := range datesOfBirth {
values = values + strconv.Itoa(value)
}
// Assertions (Keys)
true ⇔ strings.Contains(keys, "Einstein")
true ⇔ strings.Contains(keys, "Newton")
true ⇔ strings.Contains(keys, "Faraday")
// Assertions (Values)
true ⇔ strings.Contains(values, "1879")
true ⇔ strings.Contains(values, "1643")
true ⇔ strings.Contains(values, "1791")
Source: maps_test.go | Top
Pointers
Pointers are references to values, or more complex objects such as structs, maps, etc. Unlike C, there is no pointer arithmetic or the notion that a pointer to an array is equivalent to a pointer to its first element.
Referencing and Dereferencing Pointers
A pointer to a value is obtained by prefixing the value with &
. If the value is a pointer, the value is obtained by prefixing it with *
.
a := 0
var b, c = &a, &a
// Assertions #1
0 ⇔ a // a is the original value
0 ⇎ b // b is a pointer to a. It is not zero
0 ⇔ *b // *b is the value of a
0 ⇎ c // c is a pointer to a. It is not zero
0 ⇔ *c // *c is the value of a
// Change
*b++
// Assertions #2
1 ⇔ a // a is the original value
1 ⇔ *b // *b is the value of a
1 ⇔ *c // *c is the value of a
Source: pointers_test.go | Top
New: Obtaining a Pointer Rather Than a Value
Variables allocated with the new()
function are pointers rather than values.
var n = new(int)
// Assertions #1
0 ⇔ *n
0 ⇎ n // n is a pointer!
// Change
*n++
// Assertions #2
1 ⇔ *n
"0x" ⇔ fmt.Sprintf("%v", n)[:2] // n is a pointer!
Source: pointers_test.go | Top
Pointer vs Value Behaviour
The referencing and dereferencing operators assume that the target variable is a value, but not all objects behave like values.
// ------------------ Pointer Behaviour ------------------------
// Slices
slice := []int{0}
sliceCopy := slice
sliceCopy[0]++
1 ⇔ slice[0]
1 ⇔ sliceCopy[0]
// Maps
myMap := map[string]int{"a": 0}
myMapCopy := myMap
myMapCopy["a"]++
1 ⇔ myMap["a"]
1 ⇔ myMapCopy["a"]
// Functions
myFunc := func(n int) int { return n }
myFuncCopy := myFunc
myFunc(1) ⇔ myFuncCopy(1)
// ------------------- Value Behaviour ------------------------
// Structs
type NumberWrapper struct {
n int
}
myStruct := NumberWrapper{n: 0}
myStructCopy := myStruct
myStructCopy.n++
0 ⇔ myStruct.n
1 ⇔ myStructCopy.n
// Arrays
array := [1]int{0}
arrayCopy := array
arrayCopy[0]++
0 ⇔ array[0]
1 ⇔ arrayCopy[0]
// Other built-in types
// Strings, floats, integers, etc.
Source: pointers_test.go | Top
Structs
Structs allow defining complex multi-attribute data structures made up of basic types as well as other structs too. A struct in Go is something in between a classic C struct and a JSON object in JavaScript (when instantiated).
Empty Struct Without Fields
Structs may serve as a type container; there is no need to declare any attributes.
Source: structs_test.go | Top
Struct Initialisation
Structs, once declared, may be initialised using the literal syntax, Name{}
, or the new()
function, in which case a pointer is returned rather than a value.
type Employee struct {
Name string
Salary int
}
e1 := Employee{}
e2 := new(Employee)
// Assertions
e1 ⇔ *e2
"" ⇔ e1.Name
0 ⇔ e2.Salary
Source: structs_test.go | Top
Struct Literals
Struct literals allow initialising a struct and populating its attributes in one statement. Attributes may be defined by name, or in a positional basis.
type Employee struct {
Name string
Salary int
}
// ------------------ Positional Literal ---------------------
e1 := Employee{"Jazmin", 2000}
// Assertions
"Jazmin" ⇔ e1.Name
2000 ⇔ e1.Salary
// ---------------- Attribute-wise Literal -------------------
e2 := Employee{
Name: "Jazmin",
Salary: 2000,
}
// Assertions
"Jazmin" ⇔ e2.Name
2000 ⇔ e2.Salary
// ------------------ Partial Literal (I) --------------------
// Partial Literal
e3 := Employee{
Name: "Jazmin",
// Salary: 2000, Omitted!!
}
// e3 := Employee{"Jazmin"} Illegal!
// Assertions
"Jazmin" ⇔ e3.Name
0 ⇔ e3.Salary // Zero value!
// ----------------- Partial Literal (II) --------------------
// Partial Literal (II)
e4 := Employee{
// Name: "Jazmin", Omitted!!
Salary: 2000,
}
// Assertions
"" ⇔ e4.Name // Zero value!
2000 ⇔ e4.Salary
Employee{Name: "", Salary: 2000} ⇔ e4 // In-line
Source: structs_test.go | Top
Struct Updates
Structs are updated by referencing their attributes using a dot-based notation like in other C-like languages.
type Employee struct {
Name string
Salary int
}
e := Employee{Name: "Jazmin", Salary: 2000}
// Update attributes!
e.Name = "Luigi"
e.Salary = 1000
// Assertions
"Luigi" ⇔ e.Name
1000 ⇔ e.Salary
Source: structs_test.go | Top
Anonymous Struct
An anonymous struct combines the type definition and its instantiation in one statement.
jazmin := struct {
Name string
Salary int
}{
Name: "Jazmin",
Salary: 2000,
}
// Assertions
"Jazmin" ⇔ jazmin.Name
2000 ⇔ jazmin.Salary
Source: structs_test.go | Top
Anonymous Fields
Fields may be identified by their type (if different), avoiding the need to declare custom names.
type Employee struct {
string
int // types have to be distinct; can't repeat them
}
// Positional construction
e1 := Employee{"Jazmin", 2000}
// Field-wise construction (can use types as labels)
e2 := Employee{string: "Jazmin", int: 2000}
// Assertions
e1 ⇔ e2
Source: structs_test.go | Top
Pass By Reference vs Pass By Value
Structs are passed by value by default.
type Employee struct {
Name string
Salary int
}
// ------------- Obtain Value (Default) ---------------
v := Employee{Name: "Jazmin", Salary: 2000}
var v2, v3 = v, v
// Assertions
2000 ⇔ v.Salary
v.Salary++
2001 ⇔ v.Salary
v2.Salary++
2001 ⇔ v2.Salary
v3.Salary++
2001 ⇔ v3.Salary
2001 ⇔ v.Salary
// --------------- Obtain Reference ------------------
r := &Employee{Name: "Jazmin", Salary: 2000} // Reference!
var r2, r3 = r, r
2000 ⇔ r.Salary
r.Salary++
2001 ⇔ r.Salary
r2.Salary++
2002 ⇔ r2.Salary
r3.Salary++
2003 ⇔ r3.Salary
2003 ⇔ r.Salary // r = r2 = r3
Source: structs_test.go | Top
Nested Fields
A struct’s attribute type may be another struct.
type Address struct {
Number int
StreetName string
Postcode string
}
type Employee struct {
Name string
Salary int
PrimaryAddress Address
SecondaryAddress Address
}
a2 := Address{
Number: 109,
StreetName: "Larksfield",
Postcode: "TW200RA",
}
e := Employee{
Name: "Jazmin",
Salary: 2000,
PrimaryAddress: Address{
Number: 101,
StreetName: "Buckhurst",
Postcode: "IG9IJA",
},
SecondaryAddress: a2,
}
// Assertions
"IG9IJA" ⇔ e.PrimaryAddress.Postcode
"TW200RA" ⇔ e.SecondaryAddress.Postcode
Source: structs_test.go | Top
Nested Fields (Same Name as Struct)
A struct attribute that includes another struct type does not need to declare a custom name; the struct’s type will be used as the default name.
type Address struct {
Number int
StreetName string
Postcode string
}
type Employee struct {
Address // This will become an attribute named Address
Name string
Salary int
}
e := Employee{
Name: "Jazmin",
Salary: 2000,
Address: Address{
Number: 101,
StreetName: "Buckhurst",
Postcode: "IG9IJA",
},
}
// Assertions
"IG9IJA" ⇔ e.Address.Postcode
Source: structs_test.go | Top
Nested Fields (Inline)
Nested struct types do not need to be declared externally, they may be inlined as well.
type Employee struct {
Name string
Salary int
Address struct {
Number int
StreetName string
Postcode string
}
}
e1 := Employee{
Name: "Jazmin",
Salary: 2000,
Address: struct {
Number int
StreetName string
Postcode string
}{
Number: 101,
StreetName: "Buckhurst",
Postcode: "IG9IJA",
},
}
// Assertions
"IG9IJA" ⇔ e1.Address.Postcode
Source: structs_test.go | Top
Methods
Methods are functions that operate against a given type and can, thus, be invoked using the dot notation against the relevant type using the form <VAR>.<METHOD>()
.
Method Declaration
Methods are declared by defining a top level function which receives the target type and specifies a return type as a function type that represents the method in the form func (<VAR> <TYPE>) <METHOD>
where <TYPE>
is the struct or type alias to which we want to add a method, and <METHOD>
is the full function signature for the method. In this case, <VAR>
will be a value, so any changes to the type will be local to the function. If we want the method to affect the provided value, then the pointer declaration should be used by prefixing the type with a star: *<TYPE>
.
Let us now proceed with the example. First we declare a struct called Employee
to which we will add two methods further on.
Source: methods_test.go | Top
Secondly, we add a method called FailToIncreaseSalaryBy()
which allows raising the employee’s salary by the provided amount and returns the new effective salary. It is prefixed with Fail
since the method receives a value and the changes only occur on a copy, rather than on the original value.
func (e Employee) FailToIncreaseSalaryBy(extraSalary int) int {
e.Salary = e.Salary + extraSalary // Only changed within this method's scope
return e.Salary
}
Source: methods_test.go | Top
Thirdly, we add a method called SucceedToIncreaseSalaryBy()
which uses the pointer notation and that, unlike the previous method, succeeds in modifying the originally provided value’s attribute.
func (e *Employee) SucceedToIncreaseSalaryBy(extraSalary int) int {
e.Salary = e.Salary + extraSalary // Passed value actually changed!
return e.Salary
}
Source: methods_test.go | Top
Finally, we implement two sets of assertions to verify the differences between the two methods.
e := Employee{"Jazmin", 2000}
// Assertion #1
// Value-wise Method Call
var newSalary = e.FailToIncreaseSalaryBy(1000)
3000 ⇔ newSalary
2000 ⇔ e.Salary // Employee Salary not changed!
// Assertion #2
// Reference-wise Method Call (Side Effects!)
e.SucceedToIncreaseSalaryBy(1000)
3000 ⇔ e.Salary // Employee Salary changed!
Source: methods_test.go | Top
Struct Methods on Nested Structures
Whenever a struct embeds other structs, a method may change the inner struct only, resulting in the modification of the outer struct as well.
Let us see an example. First we declare the inner struct Address
, and PublicEmployee
which embeds it, and a method against Address
to change the postcode called changePostCode()
.
type Address struct {
Number int
StreetName string
Postcode string
}
type PublicEmployee struct {
Name string
Salary int
PrimaryAddress Address
}
func (a *Address) ChangePostCode(newPostCode string) {
a.Postcode = newPostCode // Passed value actually changed!
}
Source: methods_test.go | Top
We finally prove that when declaring a PublicEmployee
value and changing the postcode of its embedded Address
component, we effectively change the entire value.
e := PublicEmployee{
Name: "Jazmin",
Salary: 2000,
PrimaryAddress: Address{
Number: 12,
StreetName: "High street",
Postcode: "SW18RLN",
},
}
// Assertions
"SW18RLN" ⇔ e.PrimaryAddress.Postcode // Before change
e.PrimaryAddress.ChangePostCode("TW18NLJ") // change postcode!
"TW18NLJ" ⇔ e.PrimaryAddress.Postcode // After change
Source: methods_test.go | Top
Methods on Basic Types
Methods cannot be declared directly against built-in types such as string. The solution is creating a type alias using the type <NAME> <TYPE>
notation.
type StringAlias string
func (s StringAlias) ToUpperCase() string { // Method against StringAlias
return strings.ToUpper(string(s))
}
greeting := StringAlias("hello")
// Assertions
"HELLO" ⇔ greeting.ToUpperCase()
Source: methods_test.go | Top
Interfaces
Interfaces are a collection of method signatures that a type may implement.
Interface Definition
An interface is defined using the below notation:
type <NAME> interface {
<METHOD1>
<METHOD2>
...
}
In the below example, we create two types; employee
and contractor
, then declare an interface called costing
, and finally implement said interface for both of the types.
type employee struct {
personName string
salary int
}
type contractor struct {
limitedCompany string
dailyRate int
}
type costingInterface interface { // Define interface (IF)!
getYearlyCost() int
// ... other methods here.
}
func (e employee) getYearlyCost() int { // Implement IF for employee
return (e.salary * 103) / 100 // Pension contributions 3%
}
func (c contractor) getYearlyCost() int { // Implement IF for contractor
return c.dailyRate * 5 * 40 // working weeks p/ year
}
// Use interface as a regular variable Type
var c1, c2 costingInterface
c1 = contractor{"RipOff Ltd", 500}
c2 = employee{"John Smith", 80000}
// Assertions
100000 ⇔ c1.getYearlyCost()
82400 ⇔ c2.getYearlyCost()
Source: interfaces_test.go | Top
The Empty Interface
The empty interface specifies zero methods and may hold values of any type.
// v can be any value type!
tellMeYourType := func(v interface{}) string {
switch v.(type) {
case string:
return "string"
case int:
return "int"
default:
return "other"
}
}
// All value types are compatible with interface{}!
var v1, v2, v3 interface{}
v1 = "hello"
v2 = 45
v3 = 1.5
// Assertions
"string" ⇔ tellMeYourType(v1)
"int" ⇔ tellMeYourType(v2)
"other" ⇔ tellMeYourType(v3)
Source: interfaces_test.go | Top
Errors
The Error Interface
Built-in functions that produce errors typically implement the error
interface which has a method with the signature Error()
.
// Open a non-existing file
_, err := os.Open("non-existing-file.txt")
// Assertions
"open non-existing-file.txt: no such file or directory" ⇔ err.Error()
Source: errors_test.go | Top
Custom Errors
Customs errors are created using the errors.New()
function.
func safeDiv(a int, b int) (int, error) {
if b == 0 {
return 0, errors.New("Can't divide by zero")
}
return a / b, nil
}
// Calculations
r, _ := safeDiv(12, 3)
_, err := safeDiv(12, 0)
// Assertions
4 ⇔ r
"Can't divide by zero" ⇔ err.Error()
Source: errors_test.go | Top
Wrapping Errors with String Formatting
// Open a non-existing file
_, err := os.Open("non-existing-file.txt")
// Annotate error
err2 := fmt.Errorf("Ouch! %s", err)
// Assertions
"Ouch! open non-existing-file.txt: no such file or directory" ⇔ err2.Error()
Source: errors_test.go | Top
Custom Error Struct
A custom struct type can be used to provide more details about a given error. In the example below, CustomError
can provide details about the line number and row number where an error occured.
Source: errors_test.go | Top
We then implement the Error()
method to comply with the error
interface:
func (c CustomError) Error() string {
return fmt.Sprintf("Failed at line %d, row %d", c.LineNumber, c.RowNumber)
}
Source: errors_test.go | Top
The idea is that all errors abide to the error
interface. To test this principle, we define below a function which may fail either due to a system error from os.Open()
or one using our custom CustomError
type:
func doSomethingNasty(option int) error {
if option == 1 {
_, err := os.Open("non-existing-file.txt")
return err
}
return &CustomError{LineNumber: 5, RowNumber: 30}
}
Source: errors_test.go | Top
We finally test the two different errors using the type assertion err.(*CustomerError)
.
var err error
var line, row int
var path string
// Trigger both doSomethingNasty(1) and doSomethingNasty(2)
for i := 1; i <= 2; i++ {
err = doSomethingNasty(i)
switch err.(type) {
case *os.PathError:
v, _ := err.(*os.PathError)
path = v.Path
case *CustomError:
v, _ := err.(*CustomError)
line = v.LineNumber
row = v.RowNumber
}
}
// Assertions
"non-existing-file.txt" ⇔ path
5 ⇔ line
30 ⇔ row
Source: errors_test.go | Top
Go Routines
Simple Goroutine
A goroutine is nothing more than a regular function that is run asynchronously by prefixing it with the go
keyboard. For example: go myfunc()
.
In the below example, the execOrder
strings captures the actual order of execution
// This function waits for a number of seconds and then appends
// an identifier to the `execOrder` string.
wasteTime := func(sleepSeconds int, execOrder *string, id string) {
time.Sleep(time.Duration(sleepSeconds) * time.Second)
*execOrder = *execOrder + id
}
execOrder := "A"
go wasteTime(7, &execOrder, "E") // will run last
go wasteTime(5, &execOrder, "D") // will run second
go wasteTime(3, &execOrder, "C") // will run first
execOrder = execOrder + "B"
// Wait 10 seconds to allow all goroutines to run.
time.Sleep(time.Duration(10) * time.Second)
execOrder = execOrder + "F"
// Assertions
"ABCDEF" ⇔ execOrder
Source: goroutines_test.go | Top
Synchronous Channels
Synchronous channels are first-in-first-out (FIFO) queues. A channel is created using make(chan <TYPE>)
:
Here we poll the queue using <-foodChannel
. Messages may not arrive in sequence but we have added a sleep statement to platter()
so that they do.
// Create foodChannel queue
var foodChannel = make(chan string)
// The below function adds one or more food items to the `foodChannel` queue.
platter := func(size int) {
// Artificial wait
time.Sleep(time.Duration(size) * time.Second)
if size >= 1 {
foodChannel <- "hummus" // Post "hummus" to the queue
}
if size >= 2 {
foodChannel <- "falafel" // Post "falafel" to the queue
}
if size >= 3 {
foodChannel <- fmt.Sprintf("%d pita", size-2) // Post "n pita" to the queue
}
}
// Prepare various Platter Sizes
go platter(1)
go platter(2)
go platter(3)
// Assertions
"hummus" ⇔ <-foodChannel // posted by platter(1)
"hummus" ⇔ <-foodChannel // posted by platter(2)
"falafel" ⇔ <-foodChannel // posted by platter(2)
"hummus" ⇔ <-foodChannel // posted by platter(3)
"falafel" ⇔ <-foodChannel // posted by platter(3)
"1 pita" ⇔ <-foodChannel // posted by platter(3)
Source: sync_channels_test.go | Top
Synchronous Channels (Blocking Behaviour)
The following annotated code uses the execOrder
string to demonstrate the order of execution using synchronous channels so that their “blocking” behaviour can be appreciated.
// Define a channel c
c := make(chan string)
execOrder := "A"
// Run a goroutine that posts "message" to c
go func() {
execOrder = execOrder + "B"
c <- "message" // It blocks until a receiver appears
execOrder = execOrder + "E"
}()
// Wait
time.Sleep(time.Second)
// Poll queue: receive message
execOrder = execOrder + "C"
message := <-c
execOrder = execOrder + "D"
time.Sleep(time.Second) // Sleep 1 second
// Assertions
"message" ⇔ message
"ABCDE" ⇔ execOrder
Source: sync_channels_test.go | Top
Channel Buffering
By default, channels block the sender until a receiver collects the message. With buffering, messages can be “held” in the queue and extracted later, decoupling the sender from the receiver. This is accomplished by specifying the number of messages to be buffered as second argument (N) to make(chan <TYPE>, N)
.
c := make(chan string, 2)
c <- "first" // Non-blocking
c <- "second" // Non-blocking
// c <- "third" // This one would block
// Assertions
"first" ⇔ <-c
"second" ⇔ <-c
Source: sync_channels_test.go | Top
Channel Directions
Functions can specify whether channels provided as arguments can only send, receive, (or both send a receive). This is accomplished by specifying the channel signature as either <-chan <TYPE>
, chan<-<TYPE>
, or simply chan <TYPE>
, respectively.
c := make(chan string, 1)
canOnlySend := func(c chan<- string, msg string) {
c <- msg
// msg := <-c // Won't compile
}
canOnlyReceive := func(c <-chan string) string {
// c <- msg // Won't compile
return (<-c)
}
canBothSendAndReceive := func(c chan string, msg string) string {
c <- msg
return (<-c)
}
// Assertions
canOnlySend(c, "hello")
"hello" ⇔ canOnlyReceive(c)
"hola!" ⇔ canBothSendAndReceive(c, "hola!")
Source: sync_channels_test.go | Top
Select
The select { ...}
statement is similar to the switch
statement, but it is used to wait for messages in multiple channels simultaneously.
In the below example, the message ping is passed to channel c1
, c2
, and finally c3
in each iteration of the for
loop:
execOrder := "A"
c1 := make(chan string, 1)
c2 := make(chan string, 1)
c3 := make(chan string, 1)
c1 <- "ping"
execOrder = execOrder + "B"
for i := 0; i < 3; i++ { // Loop three times
execOrder = execOrder + "-"
select {
case msg := <-c1: // Assignment is optional
execOrder = execOrder + "c1(" + msg + ")"
c2 <- msg
case msg := <-c2:
execOrder = execOrder + "c2(" + msg + ")"
c3 <- msg
case msg := <-c3:
execOrder = execOrder + "c3(" + msg + ")"
}
}
// Assertions
"AB-c1(ping)-c2(ping)-c3(ping)" ⇔ execOrder
Source: sync_channels_test.go | Top
Select and Timeouts
A select {...}
statement will block until a message is received by one of the cases declared in it. It is possible to frame one of said cases as a timeout condition using the case: <- <Time>
signature.
In the below example, the timeout case is hit in the first iteration, whereas the message receipt is hit is in the second one.
execOrder := "A"
c := make(chan string, 1)
for i := 0; i < 2; i++ { // Loop two times
select {
case <-c: // wait for any message
execOrder = execOrder + "C"
case <-time.After(1 * time.Second):
execOrder = execOrder + "B"
}
c <- "ping"
}
// Assertions
"ABC" ⇔ execOrder
Source: sync_channels_test.go | Top
Select with “Send” Cases
The select {...}
statement can also be used with send cases in the form case channel <- msg
. This is helpful to avoid a blocking situation whenever there are no receivers associated with the channel.
In the below example, the timeout case is triggered because c
is a synchronous channel and there are no receivers waiting on it.
execOrder := "A"
c := make(chan string) // synchronous channel!
select {
case c <- "ping": // would block because there is no receiver
execOrder = execOrder + "Never"
case <-time.After(1 * time.Second): // therefore the result is a timeout
execOrder = execOrder + "B"
}
// Assertions
"AB" ⇔ execOrder
Source: sync_channels_test.go | Top
Select with Defaults
Sometimes we want to tell whether there is a message pending to be retrieved (or a receiver for a channel to which we are sending a message) in an immediate manner. In this case, there is no need to create an artificial timeout, the keyword default
can be used instead.
In the below example, the default case is triggered because c
is a synchronous channel and there are no receivers waiting on it.
execOrder := "A"
c := make(chan string) // synchronous channel!
select {
case c <- "ping": // would block because there is no receiver
execOrder = execOrder + "Never"
default: // therefore the result is the default case
execOrder = execOrder + "B"
}
// Assertions
"AB" ⇔ execOrder
Source: sync_channels_test.go | Top
Opening and Closing Channels
Channels are open by default when created. They can explicitly be closed using the close(channel)
statement. The status of a channel may be checked using the second return value of the message receive statement: msg, channelStatus <- channel
. Channels can only be closed if they contain no pending messages in the buffer. Furthermore, channels can be closed only once.
In the below example we iterate two times through a declaration that obtains both the message and the status, and that closes the channel upon receiving the first message.
execOrder := "A"
c := make(chan string, 3)
c <- "msg1"
for i := 0; i < 2; i++ { // loop two times
msg, channelStillOpen := <-c
if channelStillOpen {
execOrder = execOrder + "B(" + msg + ")"
close(c)
} else {
execOrder = execOrder + "C(" + msg + ")"
}
}
// Assertions
"AB(msg1)C()" ⇔ execOrder
Source: sync_channels_test.go | Top
Range over Channels
It is possible to iterate over the received messages on a channel using the range
keyword.
execOrder := "A-"
c := make(chan string, 3)
c <- "m1"
c <- "m2"
c <- "m3"
close(c) // range will remain suspended unless we close the channel
for msg := range c {
execOrder = execOrder + msg
}
"A-m1m2m3" ⇔ execOrder
Source: sync_channels_test.go | Top
Scheduled Tasks (Timers)
A timer, created via timer.NewTimer(Time)
offers a mechanism to run goroutines in the future. The implementation, as appreciated below, relies on a blocking channel that forces the goroutine to wait until the specified time (5 seconds).
execOrder := "A"
timer := time.NewTimer(5 * time.Second)
futureFunc := func() {
execOrder = execOrder + "B"
<-timer.C // This is what makes the function wait!
execOrder = execOrder + "D"
}
go futureFunc()
time.Sleep(2 * time.Second)
execOrder = execOrder + "C"
time.Sleep(4 * time.Second) // 6 seconds elapsed by now.
execOrder = execOrder + "E"
// Assertions
"ABCDE" ⇔ execOrder
Source: sync_channels_test.go | Top
Scheduled Tasks (Cancelling Timers)
A timer may be aborted by invoking timer.Stop()
, preventing the waiting goroutine from executing the code after the channel receive statement.
execOrder := "A"
timer := time.NewTimer(5 * time.Second)
futureFunc := func() {
execOrder = execOrder + "B"
<-timer.C
execOrder = execOrder + "Never" // will not be reached
}
go futureFunc()
time.Sleep(2 * time.Second)
execOrder = execOrder + "C"
timer.Stop() // Abort timer!
execOrder = execOrder + "D"
// Assertions
"ABCD" ⇔ execOrder
Source: sync_channels_test.go | Top
Recurrent Tasks (Tickers)
Recurrent tasks may be set up using time.NewTicker(time)
and applied similarly to regular timers, except that the recurrent code must run inside some form of repeating loop as shown below.
Please note that the goroutine in the example terminates because the main function ends as well. It may be necessary, in a real world scenario, to implement a mechanism (i.e. a sentinel message via a channel) to force the repeating loop to exit.
execOrder := "A"
timer := time.NewTicker(time.Second) // 1 Second
repeatingFunc := func() {
for {
execOrder = execOrder + "<"
<-timer.C
execOrder = execOrder + ">"
}
}
go repeatingFunc()
time.Sleep(3500 * time.Millisecond) // 3.5 seconds
timer.Stop() // Abort timer!
execOrder = execOrder + "B"
// Assertions
"A<><><><B" ⇔ execOrder
Source: sync_channels_test.go | Top
Race Conditions (Mutexes)
Concurrent goroutines that share a common variable may result in inconsistent behaviour whenever operations again said variable aren’t atomic.
In the below example, we execute 10,000 times both the increment()
and decrement()
goroutines, which should result in x == 0
at the end, but this is seldom the case.
var x = 0
var increment = func() {
x = x + 1
}
var decrement = func() {
x = x - 1
}
for i := 0; i < 10000; i++ {
go increment()
go decrement()
}
// Assertions
0 ⇎ x // There is a tiny chance this may be true :)
Source: sync_channels_test.go | Top
The reason why is that x = x - 1
is not an atomic operation; it may involve multiple steps such as the ones shown in the below pseudocode:
temp = get value of X
temp2 = 1 + temp2
x = temp2
The solution is to enclose the x = x + 1
statement with mutex.Lock()
and mutex.Unlock()
methods from sync.Mutex
so that only one goroutine has access to either x = x + 1
or x = x - 1
at any given time:
var mutex sync.Mutex
var x = 0
var increment = func() {
mutex.Lock()
x = x + 1
mutex.Unlock()
}
var decrement = func() {
mutex.Lock()
x = x - 1
mutex.Unlock()
}
for i := 0; i < 10000; i++ {
go increment()
go decrement()
}
time.Sleep(5 * time.Second) // Sleep 5 seconds
// Assertions
0 ⇔ x
Source: sync_channels_test.go | Top
Race Conditions (Atomic Counters)
In the case of simple numerical counters, there is no need to set up mutexes, or channels. The atomic.Add<TYPE>(&counter, <N>)
function allows to alter a counter in a thread-safe manner as shown in the example below. There are also other functions such as Store<TYPE>(&counter, <N>)
which simply sets a new value rather than adding to an existing one.
var x int32 = 0
var increment = func() {
atomic.AddInt32(&x, 1)
}
var decrement = func() {
atomic.AddInt32(&x, -1)
}
for i := 0; i < 10000; i++ {
go increment()
go decrement()
}
time.Sleep(3 * time.Second)
// Assertions
int32(0) ⇔ atomic.LoadInt32(&x)
int32(0) ⇔ x // May be inconsistent if goroutines are still active
Source: sync_channels_test.go | Top
Wait Groups
Wait Groups allow to treat a swarm of concurrent goroutines as one logical unit of execution to spare the programmer from coordinating the orchestration of each individual goroutine.
The sync.WaitGroup
type is used for this purpose and uses the below methods:
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:
var waitGroup sync.WaitGroup // Allocate a new Wait Group. pending := 0
twoTimesTable := make([]int, 10)
var double = func(number int, wg *sync.WaitGroup) {
time.Sleep(3 * time.Second) // Artificial wait
twoTimesTable[number] = (number + 1) * 2
wg.Done() // Goroutine completed: pending--
}
for i := 0; i < 10; i++ {
waitGroup.Add(1) // Add new pending goroutine: pending++
go double(i, &waitGroup)
}
waitGroup.Wait() // Wait until pending == 0
// Assertions
[]int{2, 4, 6, 8, 10, 12, 14, 16, 18, 20} ⇔ twoTimesTable
Source: sync_channels_test.go | Top
Input/Output: Files and Streams
This section covers file access and input/output/error streams. A tmpRoot
variable (e.g., pointing to /tmp
in Linux) is assumed for all examples.
Creating a File
The Create()
function from the os
package is equivalent to the Unix touch
command.
file, err := os.Create(tmpRoot + "example.txt")
if err != nil {
t.Error("Can't create file: ", err)
}
file.Close()
// Assertions
nil ⇔ err
Source: files_test.go | Top
Obtaining a File’s Metadata
The Stat()
function from the os
package returns a fileInfo
struct which has a number of methods that help inspect the file’s metadata.
// Create example.txt
Test_Create_File(t)
// Get File Metadata
fileInfo, err := os.Stat(tmpRoot + "example.txt")
if err != nil {
t.Error("Can't access file: ", err)
}
// Assertions
"example.txt" ⇔ fileInfo.Name()
int64(0) ⇔ fileInfo.Size()
os.FileMode(0x1b6) ⇔ fileInfo.Mode()
true ⇔ fileInfo.ModTime().Year() >= 2019
false ⇔ fileInfo.IsDir()
Source: files_test.go | Top
Renaming a File
The Rename()
function from the os
package is equivalent to the Unix rename
command.
// Create example.txt
Test_Create_File(t)
// Rename example.txt to deleteme.txt
err := os.Rename(tmpRoot+"example.txt", tmpRoot+"deleteme.txt")
if err != nil {
t.Error("Can't rename file: ", err)
}
// Assertions
_, err = os.Stat(tmpRoot + "deleteme.txt") // Check file's presence
nil ⇔ err
Source: files_test.go | Top
Deleting a File
The Remove()
function from the os
package is equivalent to the Unix rm
command.
// Create example.txt
Test_Create_File(t)
// Delete File
err := os.Remove(tmpRoot + "example.txt")
if err != nil {
t.Error("Can't delete file: ", err)
}
// The example.txt should be absent
_, err = os.Stat(tmpRoot + "example.txt")
// Assertions
nil ⇎ err
Source: files_test.go | Top
Writing Bytes to a File
Data may be written to file by using the Write()
or WriteString()
methods offered by the File
struct.
// Open File for Writing
file, err := os.Create(tmpRoot + "hello.txt")
if err != nil {
t.Error("Can't open file: ", err)
}
defer file.Close()
// Write File
bytes := []byte("Hello 😀")
count, err2 := file.Write(bytes) // WriteString() also available
if err2 != nil {
t.Error("Can't write to file: ", err2)
}
// Flush changes!
file.Sync()
// Assertions
10 ⇔ count // bytes written!
Source: files_test.go | Top
Reading Bytes from a File
Data may be read from a file by using the Read()
method offered by the File
struct.
// Create hello.txt
Test_Write_File(t)
// Open File
file, err := os.Open(tmpRoot + "hello.txt")
if err != nil {
t.Error("Can't open file: ", err)
}
defer file.Close()
// Successful Read
bytes := make([]byte, 10)
count, err2 := file.Read(bytes)
if err2 != nil {
t.Error("Can't open file: ", err2)
}
// EOF Error (No more bytes left to read)
var oneByteSlice = []byte{0}
_, err3 := file.Read(oneByteSlice)
// Assertions
"Hello 😀" ⇔ string(bytes)
10 ⇔ count
io.EOF ⇔ err3
Source: files_test.go | Top
Writing Bytes to a File (Single Statement)
The WriteFile()
function from the ioutil
package allows creating and writing to a file using a single statement.
err := ioutil.WriteFile(tmpRoot+"hello.txt", []byte("Hello 😀"), 0666)
if err != nil {
t.Error("Can't open file: ", err)
}
Source: files_test.go | Top
Read Bytes from a File Without Allocating a Slice
The ReadAll()
function from the ioutil
package returns a byte slice with the contents of a file, without requiring to allocate the slice in advance.
// Create hello.txt
Test_Write_File_IOUtil_Quick(t)
// Open file for reading
file, err := os.Open(tmpRoot + "hello.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
// Read File
bytes, err2 := ioutil.ReadAll(file) // results in bytes!
if err2 != nil {
t.Error("Can't open file: ", err2)
}
// Assertions
"Hello 😀" ⇔ string(bytes)
10 ⇔ 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.
// Create hello.txt
Test_Write_File_IOUtil_Quick(t)
// Open file and read bytes in one go!
bytes, err := ioutil.ReadFile(tmpRoot + "hello.txt")
if err != nil {
t.Error("Can't open file: ", err)
}
// Assertions
"Hello 😀" ⇔ string(bytes)
10 ⇔ 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.
// Open file For writing
fileHandle, err := os.Create(tmpRoot + "hello.txt")
if err != nil {
t.Error("Can't open file: ", err)
}
defer fileHandle.Close()
// Write File
writer := bufio.NewWriter(fileHandle)
count, err2 := writer.WriteString("Hello 😀")
if err2 != nil {
t.Error("Can't open file: ", err2)
}
// Make sure changes are written to disk!
writer.Flush()
// Assertions
10 ⇔ count
Source: files_test.go | Top
Reading from a File with Buffering
The Read()
and ReadString()
functions, for bytes and strings, respectively, from the bufio
package, allow to read files with buffering.
// Open file for reading
file, err := os.Open(tmpRoot + "hello.txt")
if err != nil {
t.Error("Can't open file: ", err)
}
defer file.Close()
// Read File
reader := bufio.NewReader(file)
line, err2 := reader.ReadString('o')
if err2 != nil {
t.Error("Can't open file: ", err2)
}
// EOF Error (No more lines left to read)
_, err3 := reader.ReadString('o')
// Assertions
"Hello" ⇔ line
io.EOF ⇔ err3
Source: files_test.go | Top
Reading Lines from a File
The NewScanner()
function, from the bufio
package, allows iterating through the lines contained in a file.
// First generate example file
contents := []byte("line1\nline2\nline3\n")
err := ioutil.WriteFile(tmpRoot+"lines.txt", contents, 0666)
if err != nil {
t.Error("Can't open file: ", err)
}
// Open File For Reading
file, err := os.Open(tmpRoot + "lines.txt")
if err != nil {
t.Error("Can't open file: ", err)
}
defer file.Close()
// Use Scanner
input := bufio.NewScanner(file)
var lineBuffer = ""
for input.Scan() {
lineBuffer = lineBuffer + input.Text()
}
// Assertions
"line1line2line3" ⇔ lineBuffer
Source: files_test.go | Top
Interacting with Stdin, Stdout, and Stderr
The os
package provides the Stdin
, Stdout
, and Stderr
streams. The below example reads all data from Stdin
, converts said data to upper case, and then writes the results to Stdout
. It then uses the Stderr
stream to report the number of bytes read.
func main() {
// Read all Stdin
data, _ := ioutil.ReadAll(os.Stdin)
// Convert data to upper case and write to Stdout
count, _ := os.Stdout.WriteString(strings.ToUpper(string(data)))
// Write the number of bytes read to Stderr
os.Stderr.WriteString(fmt.Sprintf("Bytes read: %d", count))
}
Source: iostreams.go | Top
Interacting with Shell Commands
It is possible to interact with commands by using the Command()
function from the exec
package. Other than passing arguments as separate function arguments to Command()
, piping data in and out of commands involves connecting the Stdin
, Stdout
, and Stderr
streams declared by the Cmd
struct.
// Specify command
cmd := exec.Command("go", "run", "iostreams.go")
// Specify stdin input
cmd.Stdin = strings.NewReader("hello")
// Declare a buffer to capture stdout from command
var stdout bytes.Buffer
cmd.Stdout = &stdout
// Declare a buffer to capture stderr from command
var stderr bytes.Buffer
cmd.Stderr = &stderr
// Run Command
err := cmd.Run()
if err != nil {
t.Error("Error:", err)
}
// Assertions
"HELLO" ⇔ stdout.String()
"Bytes read: 5" ⇔ stderr.String()
Source: iostreams_test.go | Top
Arguments
A command’s arguments are found in the Args
slice from the os
package. The first element is the command name whereas the second one, third and so on, are the arguments. In the example below, we check the command and arguments generated by the test runner.
var arguments, command string
for i := 0; i < len(os.Args); i++ {
if i == 0 {
command = os.Args[0]
} else if i == 1 {
arguments += os.Args[i]
} else {
arguments += " " + os.Args[i]
}
}
// Assertions
fmt.Printf(command)
fmt.Printf(arguments)
true ⇔ strings.Contains(command, "arguments.test")
true ⇔ strings.Contains(arguments, "test.timeout")
Source: arguments_test.go | Top