While reading and doing exercises, etc of this book, I also look at quite a few other resources, as it helps to solidify concepts for me. When I use other resources, I specify what they are.
"Since the introduction of Go in 2009, there have been several changes in how Go developers organize their code and their dependencies. Because of this churn, there’s lots of conflicting advice, and most of it is obsolete."
"Go still expects there to be a single workspace for third-party Go tools installed via go install
"
"By default, this workspace is located in $HOME/go, with source code for these tools stored in $HOME/go/src and the compiled binaries in $HOME/go/bin. You can use this default or specify a different workspace by setting the $GOPATH
environment variable."
"Whether or not you use the default location, it’s a good idea to explicitly define GOPATH
and to put the $GOPATH/bin directory in your executable path. Explicitly defining GOPATH
makes it clear where your Go workspace is located and adding $GOPATH/bin to your executable path makes it easier to run third-party tools installed via go install"
If using asdf, the GOPATH
is defined for you. For example if you are using golang 1.20
(as specified in your .tool-versions file), then your GOPATH
will look like:
> go env GOPATH
GOPATH="/home/<you>/.asdf/installs/golang/1.20/packages"
If using zsh, there are are couple of tweaks that need to be made to the .zshrc file, including adding the GOPATH/bin
to your system path, this is done with the following line in the .zshrc:
export PATH=$(go env GOPATH)/bin:$PATH
If using asdf, the following advice does not apply:
(If you are using zsh, add these lines to .zshrc instead):
export GOPATH=$HOME/go
export PATH=$PATH:$GOPATH/bin
- Builds the binary
- Executes the binary
- Deletes the binary after your program finishes
go run
compiles your code into a binary and saves the file to a temporary directory, by default the systems default temporary directory. You can override this directory by modifying the $GOTMPDIR
environment variable. To view all of your go environment variables, just run: go env
.
If you want to locate the exact temporary directory where Go is storing these artifacts on your system, you can use the os.TempDir()
function in your Go code to retrieve the path to the system's temporary directory. Here's an example:
package main
import (
"fmt"
"os"
)
func main() {
tempDir := os.TempDir()
fmt.Printf("Temporary directory: %s\n", tempDir)
}
go build
go build <packages>
or
go build -o <custom-binary-name> <packages>
Pg 5 of "Learning Go an Idiomatic approach..." says:
The go install
command takes an argument, which is the location of the source code repository of the project you want to install, followed by an @
and the version of the tool you want (if you just want to get the latest version, use @latest
). It then downloads, compiles, and installs the tool into your $GOPATH/bin directory.
To view all of your go environment variables, just run: go env
.
It looks like the source is downloaded from a proxy (as defined by the value of the $GOPROXY
environment variable) if it exists into:
- $GOPATH/pkg/mod/cache/download/github.com/rakyll/hey/@v/v0.1.4.zip as an archive file
- $GOPATH/pkg/mod/github.com/rakyll/hey@v0.1.4/ as the decompressed archive (the hydrated source)
- and then compiled into the $GOPATH/bin directory
Also discussed in Effective Go: Formatting.
"Go programs use tabs to indent, and it is a syntax error if the opening brace is not on the same line as the declaration or command that begins the block."
go fmt
"reformats your code to match the standard format"
VS Code with the official Golang plugin uses goimports
which is better.
Like JavaScript, the Go lexer uses semicolon insertion (ASI). Unlike JavaScript, Go developers never add semicolons other than places such as for
loop clauses, to separate the initialiser, condition, and continuation elements. They are also necessary to separate multiple statements on a line, should you write code that way.
golint is deprecated, drop-in replacement is https://github.com/mgechev/revive .
Use go vet
instead.
golangci-lint runs multiple (configurable which) tools in parallel
The recommendation is: start off using `go vet`` as a required part of your automated build process and revive as part of your code review process (since revive might have false positives and false negatives, you can’t require your team to fix every issue it reports). Once you are used to their recommendations, try out golangci-lint and tweak its settings until it works for your team.
The Go wiki Go IDEs and Editors Discusses:
- Visual Studio Code
Uses: - GoLand IDE from JetBrains
- The Go Playground
Go developers typically use Makefiles.
If using asdf, just follow the asdf documentation. You can have as many versions of Go installed as you like, and specific versions can be run from specific directories and below simply by adding a .tool-versions file (containing the binary's name and version) in the directory of your choosing.
Go assigns a default zero value to any variable that is declared but not assigned a value.
The following lower or upper case prefixes are used to indicate different bases:
0b
: binary (base two)0o
: octal (base eight).0
can also be used, but obviously it can cause confusion0x
: hexadecimal (base sixteen)
Underscores _
can be used to separate parts of a number, for example you can separate thousands in base ten (1_234
). The underscores have no effect on the value of the number. Underscores can't be at the beginning, end, or next to each other in a number.
6.03e23
is equivalent to6.03
x10
23
in decimal notation0x6.03p23
represents a hexadecimal floating-point number, equivalent to6.03
×2
23
. The use of "p" in this context is specific to Go's syntax for hexadecimal floating-point literals
Single and double quotes are not interchangeable in Go.
Rune literals represent characters and are surrounded by single quotes.
Rune literals can be written as:
'a'
: single unicode character'\141'
: 8-bit octal number'\x61'
: 8-bit hexadecimal number'\u0061'
: 16-bit hexadecimal number'\U00000061'
: 32-bit unicode number
The most useful backslash escape rune literals:
'\n'
: newline'\t'
: tab'\''
: single quote'\"'
: double quote'\\'
: backslash
"`Practically speaking, use base ten to represent your number literals and, unless the context makes your code clearer, try to avoid using any of the hexadecimal escapes for rune literals. Octal representations are rare, mostly used to represent POSIX permission flag values (such as 0o777 for rwxrwxrwx). Hexadecimal and binary are sometimes used for bit filters or networking and infrastructure applications."
The interpreted string literal: "Greetings and\n\"Salutations\""
ends up being:
Greetings and
"Salutations"
"If you need to include backslashes, double quotes, or newlines in your string, use a raw string literal. These are delimited with backquotes (`
) and can contain any literal character except a backquote. When using a raw string literal, we write our multiline greeting like so:"
`Greetings and
"Salutations"`
There are signed and unsigned integers from one to four bytes:
Type name | Alias | Value range |
---|---|---|
int8 |
-128 to 127 | |
int16 |
-32768 to 32767 | |
int32 |
int (most 32-bit CPU), rune |
-2147483648 to 2147483647 |
int64 |
int (most 64-bit CPU) |
-9223372036854775808 to 9223372036854775807 |
uint8 |
byte |
0 to 255 |
uint16 |
0 to 65536 | |
uint32 |
uint (most 32-bit CPU) |
0 to 4294967295 |
uint64 |
uint (most 64-bit CPU) |
0 to 18446744073709551615 |
int
, on a 32-bit CPU is a 32-bit signed integer like an int32
.
On most 64-bit CPUs, int is a 64-bit signed integer like an int64
.
Because int
isn’t consistent from platform to platform, it is a compile-time error to assign, compare, or perform mathematical operations between an int
and an int32
or int64
without a type conversion.
Go supports 32-bit signed integers as int
on CPU architectures: amd64p32, mips64p32, and mips64p32le.
rune
(covered on pg 26) and uintptr
(covered in ch 14) are special names for integer types also.
Go has all the usual bit-manipulation operators found in other languages.
The floating point types are:
float32
float64
Unless you have to be compatible with an existing format, use float64
. Floating point literals have a default type of float64
.
In most cases you shouldn't use a floating point number at all.
Go floating point numbers have a huge range, but they cannot store every value in that range; they store the nearest approximation. Because floats aren’t exact, they can only be used in situations where inexact values are acceptable or the rules of floating point are well understood.
If you have to use a float, you can use ==
and !=
to compare floats, but don’t do it.
Due to the inexact nature of floats, two floating point values might not be equal when you think they should be.
Instead, define a maximum allowed variance and see if the difference between two floats is less than that.
These are built-in conversion expressions, not functions as they look like.
:=
can be used to assign values to new and existing variables, as long as there is one new variable on the lefthand side of the :=
.
Beware of creating new shadow variables with :=
when you think you are reusing an existing variable.
In situations like this, explicitly declare all of your new variables with var
to make it clear which variables are new, and then use the assignment operator (=
) to assign values to both new and old variables.
While var
and :=
allow you to declare multiple variables on the same line, only use this style when assigning multiple values returned from a function or the comma ok idiom.
Also covered in:
Created at compile time, as opposed to variables at runtime.
const
in Go is very limited. Constants in Go are a way to give names to literals. They can only hold values that the compiler can figure out at compile time.
Here’s what an untyped constant declaration looks like:
const x = 10
All of the following assignments are legal:
var y int = x
var z float64 = x
var d byte = x
Here’s what a typed constant declaration looks like:
const typedX int = 10
This constant can only be assigned directly to an int
.
Effective Go: Unused imports and variables discusses temporarily reading and storing to the _
, this allows the compilation to succeed.
Also discussed in The Go wiki CodeReviewComments#variable-names, which to an extent goes against what history has taught us about non-descriptive variable names. Bob Martin, Steve McConnell, other greats, even myself, have written about short, unmemorable names many times, favouring write-time convenience over read-time convenience is often a mistake. Use descriptive names rather than adding non-executable comments to explain what something does. Although I agree that short variable names are fine in some cases, such cases are mentioned in this Go wiki section.
Initialisms are covered in:
- The Go wiki CodeReviewComments#initialisms
- Pg 28, 2.1. Names of "The Go Programming Language"
MixedCaps is also discussed in:
- The Go wiki CodeReviewComments#mixed-caps
- Effective Go: MixedCaps
No snake_case, use PascalCase for exported identifiers, use camelCase for unexported identifiers.
Unicode characters are allowed for variable naming, but don't use them.
Effective Go: Constructors and composite literals discusses composite literals.
Using [n]
or [...]
makes an array. Using []
makes a slice.
In arrays and slices you can create what's known as a sparse array or slice.
Array:
var x = [12]int{1, 5: 4, 6, 10: 100, 15}
Creates an array of 12 ints with the following values: [1, 0, 0, 0, 0, 4, 6, 0, 0, 0, 100, 15].
Slice:
var x = []int{1, 5: 4, 6, 10: 100, 15}
Creates a slice of 12 ints with the following values: [1, 0, 0, 0, 0, 4, 6, 0, 0, 0, 100, 15].
Also covered in:
- Effective Go: Arrays
- Pg 81, 4.1. Arrays of "The Go Programming Language"
- Go for JavaScript Developers: Types - Arrays / Slices Discusses the differences between arrays in Go vs JavaScript, including how slices fit
- Go by Example: Arrays
==
and !=
can be used for equality checking in arrays but not slices, deep equality checking also works.
"Go only has one-dimensional arrays and slices, but you can simulate multidimensional arrays or slices:"
var x [2][3]int
"This declares x
to be an array of length 2 whose type is an array of ints of length 3."
"Go considers the size of the array to be part of the type of the array.
This makes an array that’s declared to be [3]int
a different type from an array that’s declared to be [4]int
.
This also means that you cannot use a variable to specify the size of an array, because types must be resolved at compile time, not at runtime."
"What’s more, you can’t use a type conversion to convert arrays of different sizes to identical types. Because you can’t convert arrays of different sizes into each other, you can’t write a function that works with arrays of any size and you can’t assign arrays of different sizes to the same variable."
Also covered in:
- Effective Go: Slices
- Pg 84, 4.2. Slices of "The Go Programming Language"
- Go by Example: Slices
- "100 Go Mistakes and How to Avoid Them" Not understanding slice length and capacity (#20)
- "100 Go Mistakes and How to Avoid Them" Inefficient slice initialization (#21)
- "100 Go Mistakes and How to Avoid Them" Being confused about nil vs. empty slice (#22)
- "100 Go Mistakes and How to Avoid Them" Not properly checking if a slice is empty (#23)
- "100 Go Mistakes and How to Avoid Them" Not making slice copies correctly (#24)
- "100 Go Mistakes and How to Avoid Them" Unexpected side effects using slice append (#25)
- "100 Go Mistakes and How to Avoid Them" Slices and memory leaks (#26)
- "100 Go Mistakes and How to Avoid Them" Slices and memory leaks (#26)
"The length is not part of the type for a slice. This removes the limitations of arrays."
Inspiration for the following example was from Effective Go: Two-dimensional slices:
package main
import (
"fmt"
)
func main() {
var slice [][][]byte
slice = [][][]byte{
{
{'c'},
{'a', 't'},
},
{
{'d'},
{'o', 'g'},
},
}
fmt.Println("Contents of the three-dimensional slice:")
for i := 0; i < len(slice); i++ {
fmt.Printf("Level %d:\n", i+1)
for j := 0; j < len(slice[i]); j++ {
fmt.Println(string(slice[i][j]))
}
fmt.Println()
}
}
The Go wiki CodeReviewComments#declaring-empty-slices also discusses nil slices vs non-nil, and when to use which.
// Declares a nil slice:
var t []string
vs
//Declares a non-nill slice:
t := []string{}
Slices can not be compared with ==
or !=
. "The only thing you can compare a slice with is nil
".
"The reflect
package contains a function called DeepEqual
that can compare almost anything, including slices.
It’s primarily intended for testing, but you could use it to compare slices if you needed to."
Also covered in:
- The built-in documentation
- The spec under Appending to and copying slices
- Effective Go:
append
- Pg 88, 4.2.1. The
append
Function of "The Go Programming Language" - "100 Go Mistakes and How to Avoid Them" Unexpected side effects using slice append (#25)
- "100 Go Mistakes and How to Avoid Them" Creating data races with append (#69)
The built-in function make
creates slices, maps, and channels only.
Effective Go: Allocation with make, has some low value examples.
If you use a variable to specify a capacity with make
that’s smaller than the length, your program will panic at runtime.
Declaring a slice that might stay nil:
var data []int
Create a slice using an empty slice literal
var x = []int{}
// or
x := []int{}
If you have some starting values, or if a slice’s values aren’t going to change, then a slice literal is a good choice
data := []int{2, 4, 6, 8}
There are several permutations using make
. Specifying nonzero length, zero length and nonzero capacity. Check the three possibilities in the book.
Another not-so-great part of the Go language...
"When you take a slice from a slice, you are not making a copy of the data. Instead, you now have two variables that are sharing memory."
// ...
x := []int{1, 2, 3, 4}
y := x[:2] // y == [1, 2]
z := x[1:] // z == [2, 3, 4]
x[1] = 20
// x now == [1, 20, 3, 4]
// y now == [1, 20]
// z now == [20, 3, 4]
y[0] = 10
// x now == [10, 20, 3, 4]
// y now == [10, 20]
// z now = [20, 3, 4]
z[1] = 30
// x now == [10, 20, 30, 4]
// y now == [10, 20]
// z == [20, 30, 4]
// ...
Changing x
modified both y
and z
, while changes to y
and z
modified x
.
Slicing slices gets extra confusing when combined with append:
x := []int{1, 2, 3, 4}
y := x[:2] // y == [1, 2]
fmt.Println(len(x), len(y), cap(x), cap(y)) // 4, 2, 4, 4
y = append(y, 30)
// x now == [1, 2, 30, 4]
// y now == [1, 2, 30]
"To avoid complicated slice situations, you should either never use append with a sub‐slice or make sure that append doesn’t cause an overwrite by using a full slice expression."
"The full slice expression (x[:2:2]
, x[2:4:4]
) includes a third part , which indicates the last position in the parent slice’s capacity that’s available for the subslice."
It's well worth reading and understanding "Example 3-7. Even more confusing slices" on pg 45.
"If you have an array, you can take a slice from it using a slice expression."
"be aware that taking a slice from an array has the same memory-sharing properties as taking a slice from a slice."
Using the built-in copy
function rectifies the above problems with copying the memory locations.
Also covered in:
- Effective Go: Maps, which also covers "The comma ok Idiom", discussed soon in "Learning Go an Idiomatic approach..."
- Pg 93, 4.3. Maps of "The Go Programming Language"
- Go for JavaScript Developers: Types - Dictionaries Discusses the differences between how and what JavaScript and Go implement and use Dictionaries in the form of JS Object, JS Map, JS WeakMap, and Go Maps
- "100 Go Mistakes and How to Avoid Them" Inefficient map initialization (#27)
- "100 Go Mistakes and How to Avoid Them" Maps and memory leaks (#28)
- "100 Go Mistakes and How to Avoid Them" Making wrong assumptions during map iterations (ordering and map insert during iteration) (#33)
- Go by Example: Maps
- The spec under Map types discusses the types that can be used for map keys and values
- The spec under Deletion of map elements discusses deleting map elements
- The spec under Making slices, maps and channels discusses the creation of maps using the built-in
make
function
This section should have discussed Redeclaration and reassignment, thankfully we have coverage in Effective Go: Redeclaration and reassignment, be sure to read it.
Also discussed in Effective Go: if
Also discussed in Effective Go: for
Also covered in:
- Effective Go: Switch
- Go for JavaScript Developers: Flow control statements - Switch, specifically the fact that in Go,
fallthrough
must be specified if desired - Pg 23, 1.8. Loose Ends of "The Go Programming Language"
Also covered in:
- Effective Go: Multiple return values, bit of a stupid example though
- The Go Wiki CodeReviewComments#dont-panic discusses using error and multiple return values
- Pg 124, 5.3. Multiple Return Values of "The Go Programming Language"
- Go by Example: Multiple Return Values
- Go for JavaScript Developers: Functions - Multiple returns
Also covered in:
- Effective Go: Named result parameters, bit of a stupid example though
- Also called named result parameters, in The Go Wiki CodeReviewComments#named-result-parameters, which is mostly about how it affects godoc
- "100 Go Mistakes and How to Avoid Them" Never using named result parameters (#43)
- "100 Go Mistakes and How to Avoid Them" Unintended side effects with named result parameters (#44)
Also covered in:
- Effective Go: Defer has an interesting last example
- Go by Example: Defer
- Pg 143, 5.8. Deferred Function Calls of "The Go Programming Language"
- "100 Go Mistakes and How to Avoid Them" Using defer inside a loop (#35)
- "100 Go Mistakes and How to Avoid Them" Ignoring how defer arguments and receivers are evaluated (argument evaluation, pointer, and value receivers) (#47)
- "100 Go Mistakes and How to Avoid Them" Not handling defer errors (#54)
Also called naked returns, and covered in The Go Wiki CodeReviewComments#naked-returns. Doesn't say anything about not using them though.
The Go Wiki CodeReviewComments#pass-values says: "Don't pass pointers as function arguments just to save a few bytes. If a function refers to its argument x only as *x throughout, then the argument shouldn't be a pointer."
"Go for Javascript Developers" says: "In Go, there are value types, reference types, and pointers. References types are slices, maps, and channels. All the rest are value types, but have the ability "to be referenced" with pointers."
Because Slices in Go are reference types, a slice's value is a reference.
Any modifications made to the slice within the function will affect the original slice in the calling function. However, when you append an element to a slice, a new underlying array is created if the capacity of the current array is exceeded, and the original slice's reference does not change.
In the modSlice
function, when you append 10 to s
, it exceeds the capacity of the original array, and a new array is created to accommodate the additional element. The s
slice inside the modSlice
function now points to a new array, but the s
slice in the main
function remains unchanged because slices are passed by value (a copy of the slice header is passed) and not by reference.
"Learning Go an Idiomatic approach..." has an example (var x = new(int)
) of the new
built-in function, which is also covered in:
-
The spec under Slice types also has an interesting example that the following "two expressions are equivalent:"
make([]int, 50, 100) new([100]int)[0:50]
The spec also has some other examples of the
new
built-in -
Pg 34, 2.3.3. The
new
Function of "The Go Programming Language" -
Go for JavaScript Developers: Keywords & Syntax Comparison - new keyword
Effective Go: Interface Names discusses how to name single method interfaces.
- The Go Wiki CodeReviewComments#pass-values discusses when to pass a value receiver vs reference receiver
- Effective Go: Initialization has an example of using a
String()
method with a constant type using theiota
enumerator
As Effective Go: Methods mentions:
- "methods can be defined for any named type (except a pointer or an interface); the receiver does not have to be a struct."
- "The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers."
- The Go Wiki CodeReviewComments#receiver-names discusses receiver names
- The Go Wiki CodeReviewComments#receiver-type discusses when to use value vs pointer receiver type
- "100 Go Mistakes and How to Avoid Them" Not knowing which type of receiver to use (#42) also discusses when to use value vs pointer receiver type
- Pg 158, 6.2. Methods with a Pointer Receiver of "The Go Programming Language" discusses when to pass a value receiver vs reference receiver
The following "100 Go Mistakes and How to Avoid Them" are also worth a read:
- Returning a nil receiver (#45)
- Ignoring how defer arguments and receivers are evaluated (argument evaluation, pointer, and value receivers) (#47)
"do not write getter and setter methods for Go structs, unless you need them to meet an interface"
Also discussed in:
- Effective Go: Getters
- "100 Go Mistakes and How to Avoid Them" Overusing getters and setters (#4)
- Pg 169, 6.6. Encapsulation of "The Go Programming Language"
"You can embed any type within a struct"
Effective Go: "Only interfaces can be embedded within interfaces"
Also covered in:
- The Go wiki CodeReviewComments#comment-sentences
- Effective Go: Interfaces
- "100 Go Mistakes and How to Avoid Them" Interface pollution (#5), Interface on the producer side (#6)
- Pg 171, Interfaces of "The Go Programming Language"
Also covered in:
- Effective Go: Type switch has one example.
- Pg 205, 7.10. Type Assertions of "The Go Programming Language"
- Pg 210, 7.13. Type Switches of "The Go Programming Language"
In-band errors are covered in the Go wiki CodeReviewComments#in-band-errors.
Error strings are:
- Covered in the Go wiki CodeReviewComments#error-strings
- Briefly touched on in Effective Go: Errors
Also covered in:
- The Go wiki CodeReviewComments#dont-panic which also links to Effective Go: Errors
- Effective Go: Panic
- Effective Go: Recover
- "100 Go Mistakes and How to Avoid Them" Panicking (#48)
- Pg 148, 5.9. Panic of "The Go Programming Language"
- Go for JavaScript Developers: error-handling
The Go wiki CodeReviewComments#examples discusses adding examples.
A topic that is somewhat thin in the "Learning Go an Idiomatic approach..." book is printing, this has coverage in:
Effective Go: Unused imports and variables discusses temporarily reading and storing to the _
, this allows the compilation to succeed.
Also covered in:
- The Go wiki CodeReviewComments#package-names which also links to Effective Go: Package names and The Go Blog Package names
- Pg 289, 10.6. Packages and Naming of "The Go Programming Language"
The Go wiki:
- CodeReviewComments#crypto-rand discusses math/rand and crypto/rand
- CodeReviewComments#imports discusses aliasing imports
- CodeReviewComments#import-dot discusses importing all the exported identifiers (functions, variables, types, etc) from a package into the current scope, effectively allowing you to use them without specifying the package name
Also covered in "100 Go Mistakes and How to Avoid Them" Ignoring package name collisions (#14)
Also covered in:
- The Go wiki CodeReviewComments#comment-sentences and CodeReviewComments#doc-comments which also links to Effective Go: Commentary
- The Go wiki CodeReviewComments#package-comments and CodeReviewComments#doc-comments
- "100 Go Mistakes and How to Avoid Them" Missing code documentation (#15)
- Pg 296, 10.7.4. Documenting Packages of "The Go Programming Language"
Also covered in the Go wiki CodeReviewComments#import-blank
The Go wiki CodeReviewComments#synchronous-functions discusses preferring synchronous functions. This of course means waiting/blocking.
Go has two styles of concurrent programming:
- Goroutines and channels, which support communicating sequential processes or CSP. A go community saying: "Share memory by communicating; do not communicate by sharing memory". This style is covered in 8. Goroutines and Channels of "The Go Programming Language"
- The more traditional model of shared memory multithreding. This style is covered in 9. Concurrency with Shared Variables of "The Go Programming Language"
"If you are not sure if concurrency will help, first write your code serially, and then write a benchmark to compare performance with a concurrent implementation."
Operating System schedules threads across CPU cores. A process can own multiple threads. a thread can own many goroutines (lightweight processes).
- Pg 205 says "Goroutines are lightweight processes managed by the Go runtime" Just about every other source says that Goroutines are more like lightweight threads, including "The Go Programming Language" pg 218
The crux of this section is that goroutines are faster and lighter (in terms of resources) because they are managed by the Go runtime scheduler instead of the operating system (as in OS managed threads). The Go runtime scheduler is obviously closer to your code, and knows more about your code than the operating system.
I have a full example code for this.
Pg 218, 8.1. Goroutines of "The Go Programming Language" says "The go statement itself completes immediately:"
f() // call f(); wait for it to return
go f() // create a new goroutine that calls f(); don't wait
As per (https://go.dev/tour/concurrency/2) By default, sends and receives block until the other side is ready. This allows goroutines to synchronize without explicit locks or condition variables.
Being puzzled about channel size (#67) of "100 Go Mistakes and How to Avoid Them" says:
- "Carefully decide on the right channel type to use, given a problem. Only unbuffered channels provide strong synchronization guarantees". Thus they are sometimes called "synchronous channels" as mentioned on pg 226 of "The Go Programming Language"
- "You should have a good reason to specify a channel size other than one for buffered channels"
"Like maps, channels are reference types. When you pass a channel to a function, you are really passing a pointer to the channel"
8.4.2. Pipelines of "The Go Programming Language" is a pattern/technique which has a goroutine sending to a channel, that another goroutine receives. Pg 229: If the first goroutine closes the channel, after the receiving end of the channel has drained all messages, all subsequent receive operations will proceed without blocking but will yield a zero value, unless the receive operation checks the 2nd return value which is a boolean value, conventionally called ok
, which is true for a successful receive and false for a receive on a closed and drained channel. Using a range
loop provides the same behaviour in a more succinct manner.
Also discussed on pg 208, Closing a Channel of "Learning Go an Idiomatic approach...".
Pg 230 "You needn’t close every channel when you’ve finished with it. It’s only necessary to close a channel when it is important to tell the receiving goroutines that all data have been sent."
"A channel that the garbage collector determines to be unreachable will have its resources reclaimed whether or not it is closed.". This is also mentioned on pg 208, Closing a Channel of "Learning Go an Idiomatic approach...".
Pg 233, 8.4.4. Buffered Channels of "The Go Programming Language" "Novices are sometimes tempted to use buffered channels within a single goroutine as a queue, lured by their pleasingly simple syntax, but this is a mistake. Channels are deeply connected to goroutine scheduling, and without another goroutine receiving from the channel, a sender—and perhaps the whole program—risks becoming blocked forever. If all you need is a simple queue, make one using a slice.".
"Unlike garbage variables, leaked goroutines are not automatically collected, so it is important to make sure that goroutines terminate themselves when no longer needed.". The sub-section "Always Clean Up Your Goroutines" below covers this.
The last paragraph of this page (Pg 207) discusses when to use unbuffered vs buffered channels.
Pg 207, Reading, Writing, and Buffering of "Learning Go an Idiomatic approach..." discusses using the built-in cap
and len
functions to find out how many elements the channel can hold, and how many elements are currently in the channel respectively.
Pg 209, How Channels Behave of "Learning Go an Idiomatic approach..." discusses the synchronisation of multiple writing channels using sync.WaitGroup
, then refers to pg 220 (Using WaitGroups). Pg 227, 8.4.1. Unbuffered Channels of "The Go Programming Language" discusses using the "done" channel pattern. "Learning Go an Idiomatic approach..." also has a section on The Done Channel Pattern. "The Go Programming Language" also discusses and has examples for sync.WaitGroup
.
Pg 226, 8.4.1. Unbuffered Channels of "The Go Programming Language" mentions "When a value is sent on an unbuffered channel, the receipt of the value happens before the reawakening of the sending goroutine."
Pg 211 of "Learning Go an Idiomatic approach..." mentions using "The Done Channel Pattern", and https://go.dev/tour/concurrency/5 actually uses it (quit <- 0
). Also seen on pg 227 of "The Go Programming Language" (done <- struct{}{}
).
Pg 212 of "Learning Go an Idiomatic approach..." mentions: "If you want to implement a nonblocking read or write on a channel, use a select
with a default
". There is also a warning that states: "Having a default
case inside a for
-select
loop is almost always the wrong thing to do. It will be triggered every time through the loop when there’s nothing to read or write for any of the cases. This makes your for loop run constantly, which uses a great deal of CPU.". https://go.dev/tour/concurrency/5 states: "A select
blocks until one of its cases can run, then it executes that case. It chooses one at random if multiple are ready (Pg 210 of "Learning Go an Idiomatic approach..." also mentions this)."... Unless of course there is a default
. Pg 245 of "The Go Programming Language" states: "the other communications do not happen." within the select
block.
Pg 245 of "The Go Programming Language" states "A select with no cases, select{}
, waits forever.".
The following example is from pg 245 of "The Go Programming Language". It's behaviour is deterministic.
package main
import "fmt"
func main() {
ch := make(chan int, 1)
for i := 0; i < 10; i++ {
fmt.Printf("Before select, iteration %d, ch now holds %d elements\n", i, len(ch)) // Added for debugging.
select {
case x := <-ch:
fmt.Println(x) // "0" "2" "4" "6" "8"
case ch <- i:
fmt.Printf("ch now holds the value %d\n", i) // Added for debugging.
}
fmt.Printf("After select, iteration %d, ch now holds %d elements\n", i, len(ch)) // Added for debugging.
}
}
"Increasing the buffer size of the previous example makes its output nondeterministic, because when the buffer is neither full nor empty, the select statement figuratively tosses a coin."
The following is the same example as above, but with the channel (ch
) being unbuffered. This deadlocks, why?
Pg 207 of "Learning Go an Idiomatic approach..." says:
"Every write to an open, unbuffered channel causes the writing goroutine to pause until another goroutine reads from the same channel. Likewise, a read from an open, unbuffered channel causes the reading goroutine to pause until another goroutine writes to the same channel. This means you cannot write to or read from an unbuffered channel without at least two concurrently running goroutines."
package main
import "fmt"
func main() {
ch := make(chan int)
for i := 0; i < 10; i++ {
fmt.Printf("Before select, iteration %d, ch now holds %d elements\n", i, len(ch)) // Added for debugging.
select {
case x := <-ch:
fmt.Println(x) // "0" "2" "4" "6" "8"
case ch <- i:
fmt.Printf("ch now holds the value %d\n", i) // Added for debugging.
}
fmt.Printf("After select, iteration %d, ch now holds %d elements\n", i, len(ch)) // Added for debugging.
}
}
Expecting a deterministic behavior using select and channels (#64) of "100 Go Mistakes and How to Avoid Them" is mostly about buffered channels with a size greater than 1.
The following example is made up from bits and pieces from 8.7. Multiplexing with select
of "The Go Programming Language":
package main
import (
"fmt"
"os"
"time"
)
func launch() {}
func main() {
abort := make(chan struct{})
fmt.Println("Commencing countdown. Press return to abort.")
go func() {
os.Stdin.Read(make([]byte, 1)) // read a single byte
abort <- struct{}{}
}()
ticker := time.NewTicker(1 * time.Second)
for countdown := 10; countdown > 0; countdown-- {
fmt.Println(countdown)
select {
case <-ticker.C:
// Do nothing.
case <-abort:
fmt.Println("Launch aborted!")
return
}
}
launch()
}
Any time a goroutine depends on a variable in an outer scope whose value might change, you must pass the value into the goroutine.
I think the reason this example works is because the shadowing v
in the for range
loop's block is part of the goroutine's closure.
for _, v := range a {
v := v
go func() {
ch <- v * 2
}()
}
This is easier to read and makes more initial sense:
for _, v := range a {
go func(val int) {
ch <- val * 2
}(v)
}
Go 1.22 fixes this. "For Go 1.22, we plan to change for loops to make these variables have per-iteration scope instead of per-loop scope."
Whenever you launch a goroutine, you must make sure that it will eventually exit. Unlike variables, the Go runtime can’t detect that a goroutine will never be used again. If a goroutine doesn’t exit, the scheduler will still periodically give it time to do nothing, which slows down your program. This is called a goroutine leak.
func countTo(max int) <-chan int {
ch := make(chan int)
go func() {
for i := 0; i < max; i++ {
ch <- i // Goroutine blocks forever waiting for a listener.
}
close(ch)
}()
return ch
}
func main() {
for i := range countTo(10) {
if i > 5 { // If we exit the loop early.
break
}
fmt.Println(i)
}
}
Pg 219, 8.1. Goroutines of "The Go Programming Language" says that if there are goroutines still running when the main
function returns, "all goroutines are abruptly terminated".
I thought the example provided was incorrect as the case <-done:
didn't provide a return
, but it doesn't need to. select
will block until either searcher
(function) or done
(channel) provides a value, and because all but the fastest search's do not provide values from the searcher
, the done
channel is closed, so the zero value of the done
channel is read by the select
, and the goroutine finishes.
8.9. Cancellation of "The Go Programming Language" covers the same topic.
A function that contains a goroutine that returns it's goroutine's sending channel, along side a cancel function (closure containing code which closes a done channel).
I'm not sure this section answers this question very well, at least it seems possibly a little naive? This is what it says:
"Buffered channels are useful when you know how many goroutines you have launched, want to limit the number of goroutines you will launch, or want to limit the amount of work that is queued up."
See the Channels subsection for quite a few comments around this decision.
If the number (concurrent requests) of tokens (a token being a struct{}
) exceeds a specific number, the request is dropped and an error logged. I can see how this could keep resources within an acceptable/agreed limit.
If you're using a select
statement, and one or more of it's cases reads from a channel that's been closed, that isn't being iterated on by a for
range
loop, then the case
will continue to be successful (return the zero value) and waste time. Instead set the channel's variable to nil
, this will stop the case from being run because a nil
channel doesn't return a value.
Use a select
statement with a case
that reads a done channel (work has finished within time), and a case
that reads a Time
channel returned by time.After(time.Duration)
.
Pg 247, 8.7. Multiplexing with select
of "The Go Programming Language" has an exercise for the reader to pretty much do the example just mentioned in Pg 220 of "Learning Go an Idiomatic approach...".
There is another timeout strategy on Pg 130 of 5.4.1. Error-Handling Strategies of "The Go Programming Language".
Sometimes one goroutine needs to wait for multiple goroutines (or more specifically, the writing channels within the multiple goroutines) to complete their work, and their writing channels closed. If you are waiting for a single goroutine, you can use the done channel pattern that we saw earlier. But if you are waiting on several goroutines, you need to use a WaitGroup.
Also covered in:
- Pg 237-239, 8.5. Looping in Parallel of "The Go Programming Language". The code for this can be found here, as well as a copy of it here. I also used this pattern as a start for the Gitlab interview I did. You can see the pattern in the files: bytes.go, checksum.go, which the find.go file consumes
- Pg 250, 8.8. Example: Concurrent Directory Traversal of "The Go Programming Language" also has an example
- Pg 274, 9.7. Example: Concurrent Non-Blocking Cache of "The Go Programming Language" also has an example
- "100 Go Mistakes and How to Avoid Them" Misusing sync.WaitGroup (#71)
"A sync.WaitGroup
doesn’t need to be initialized, just declared, as its zero value is" where we start from.
Basically we just use the following statements in different places:
var wg sync.WaitGroup
wg.Add(3)
defer wg.Done() // Within a goroutine
defer wg.Done() // Within another goroutine
defer wg.Done() // Within another goroutine
wg.Wait()
We don't pass the sync.WaitGroup
instance to each goroutine, because unless you pass a pointer to it, the copy is modified, which doesn't decrement the original. "By using a closure to capture the sync.WaitGroup
, we are assured that every goroutine is referring to the same instance.".
"While WaitGroups are handy, they shouldn’t be your first choice when coordinating goroutines. Use them only when you have something to clean up (like closing a channel they all write to) after all of your worker goroutines exit."
The golang.org/x/sync/errgroup type builds on top of WaitGroup
"to create a set of goroutines that stop processing when one of them returns an error". Also covered in "100 Go Mistakes and How to Avoid Them" Not using errgroup (#73).
"100 Go Mistakes and How to Avoid Them" Forgetting about sync.Cond (#72) allows you to "send repeated notifications to multiple goroutines"
sync.Once
allows for initialisation (or any) code to run exactly once. Like sync.WaitGroup
, we declare a variable, no need to initialise it, because we want it to have its zero value.
"Also like sync.WaitGroup
, we must make sure not to make a copy of an instance of sync.Once
, because each copy has its own state to indicate whether or not it has already been used. Declaring a sync.Once
instance inside a function is usually the wrong thing to do, as a new instance will be created on every function call and there will be no memory of previous invocations." It's usually best to declare the sync.Once
instance at the package level.
Also covered in:
- Pg 268, 9.5. Lazy Initialization:
sync.Once
of "The Go Programming Language"
Covers the example of 3 web services being called, waiting for the result of the first 2 services, then sending them to the third service. The entire process must complete in 50 milliseconds.
A mutual exclusion limits the concurrent execution of some code or access to a shared (critical section) piece of data.
Being puzzled about when to use channels or mutexes (#57) of "100 Go Mistakes and How to Avoid Them" also discusses this. "Being aware of goroutine interactions can also be helpful when deciding between channels and mutexes. In general, parallel goroutines require synchronization and hence mutexes. Conversely, concurrent goroutines generally require coordination and orchestration and hence channels."
Effective Go: Concurrency - Share by communicating is along similar lines.
I have an example code for this. The easiest way to see the differences is to put the files side by side.
"Like sync.WaitGroup and sync.Once, mutexs must never be copied. If they are passed to a function or accessed as a field on a struct, it must be via a pointer. If a mutex is copied, its lock won’t be shared."
"Never try to access a variable from multiple goroutines unless you acquire a mutex for that variable first."
sync.Map
is only appropriate in situations:
- When you have a shared map where key/value pairs are inserted once and read many times
- When goroutines share the map, but don’t access each other’s keys and values
"sync.Map
uses interface{} as the type for its keys and values; the compiler cannot help you ensure that the right data types are used."
"Given these limitations, in the rare situations where you need to share a map across multiple goroutines, use a built-in map protected by a sync.RWMutex
."
The Go wiki CodeReviewComments#contexts also touches on this.
Misunderstanding Go contexts (#60) of "100 Go Mistakes and How to Avoid Them" also discusses this.
"In general, a function that users wait for should take a context, as doing so allows upstream callers to decide when calling this function should be aborted."
The Go wiki CodeReviewComments#useful-test-failures has some thoughts around what test failures should look like.
A data race is a type of race condition.
Pg 257, 9.1. Race Conditions of "The Go Programming Language"
"100 Go Mistakes and How to Avoid Them" Not understanding race problems (data races vs. race conditions and the Go memory model) (#58)