Table of contents
Introduction
Go is a modern programming language developed by Google that has gained immense popularity in recent years. It is a statically typed, compiled language that combines aspects of imperative and object-oriented programming. Go aims to provide simplicity, performance, and reliability.
In his book, Jon provides a comprehensive introduction to Go programming. The book focuses on teaching idiomatic Go code by using concrete examples and focusing on how experienced Go developers structure their code. It covers fundamental language features like primitive types, control structures, and composite types. It also dives into more advanced topics like concurrency through goroutines and channels, writing tests, and using reflection.
This summary highlights the key takeaways from each chapter. It aims to provide an overview of the core concepts and best practices for writing clear, readable, robust Go code. It's written from the perspective of a developer who has experience with other programming languages like Python, Java, and TypeScript but practically none in Go.
Setting Up Your Go Environment
- Use
golintto enforce the right coding style of a project. - Use
go versionto check the version of Go installed. - Use
go vetto find errors that may not be detected by the compiler, such as having the wrong number of arguments passed to aPrintfcall. - A common idiom to run multiple commands at once when building a project is to rely on a
Makefilelike this:
.DEFAULT_GOAL := build
fmt:
go fmt ./...
.PHONY:fmt
lint: fmt
golint ./...
.PHONY:lint
vet: fmt
go vet ./...
.PHONY:vet
build: vet
go build hello.go
.PHONY:build
Typing make will run fmt, then vet, then build since the default task is build, which requires vet to have run first, which in turn requires fmt to have run first, which in turn has no dependency, so fmt runs and the chain continues.
- Testing whether a new version of Go works for existing programs compiled on an older version is straightforward:
go get golang.org/dl/go.1.x.y # replace x.y
go1.x.y download
# try out the changes
go1.x.y build
# If all good, remove this secondary version
go1.x.y env GOROOT
/.../.../go1.x.y
rm -rf $(go1.x.y env GOROOT)
rm $(go env GOPATH)/bin/go1.x.y
# Install new version as wanted...
Predeclared Types and Declarations
Predeclared types
- Zero value
- Assigned to a variable that is declared with no initial value (doesn't lead to bugs like in C or C++).
- Literals
- These express different bases, such as
0b(binary),0o(octal) or0x(hexadecimal). As in other languages like Python or Java, underscores can be used to express large numbers by separating digits (e.g.,1_000_000). - Floating point literals look like
6.03e23. - Rune literals are represented with single quotes (no double quotes accepted). The most common ones are
('\n'), tab('\t'), single quote('\''), double quote('\"')and backslash('\\'). Other bases are supported but should be limited to specific contexts (e.g., bit filters for base two). - String literals can be written with double quotes, where everything must be escaped.
- Raw string literals use backticks instead of double quotes and can be used to insert any character except a backtick. They support multiline expressions.
- These express different bases, such as
- Boolean
- The type is
booland the default zero value isfalse.
- The type is
- Numeric types
- Integers
int8(aliasedbyte, which is much more common),int16,int32,int64,uint8,uint16,uint32,uint64.- The zero value is 0.
- Use the minimum size when needed for specific applications.
- Use
int64anduint64for library functions (suggested back when generics weren't available). - Otherwise, just use
int. Other types should be considered a premature optimization until proven otherwise. - Variables can be modified like so:
+=,-=,*=,/=and%=. - Available comparisons are:
==,!=,>,>=,<, and<=. - Bit manipulations
- Shifts:
<<(left),>>(right) - Logical bit masks:
&(AND),|(OR),^(XOR),&^(AND NOT) - These operators can be used to modify a variable as well:
&=,|=,^=,&^=,<<=,>>=.
- Shifts:
- Integers
- Floating point types
float32,float64.- The zero value is 0.
- If using a floating point number, opt for
float64unless a profiler shows significant improvement withfloat32and the precision is good enough (6-7 decimal places). - Strict equality (or inequality) should not be done on floating point numbers: check the variance instead (less than epsilon).
- Complex types
complex64usesfloat32to represent real and imaginary parts, whilecomplex128usesfloat64, using thecomplexbuilt-in function andrealandimagefunctions to extract the relevant parts.- As with floating point numbers, use the epsilon technique to check for equality.
- Strings and runes
- The zero value is an empty string.
- Strings are immutable.
- Strings can be checked for equality or compared for ordering (
>,>=,<, or<=) and can be concatenated with the+operator. - The
runetype represents a single code point, equivalent toint32.
- Explicit type conversion
- All type conversions are explicit.
- There is no concept of "truthiness" (e.g.,
if 2: print("ok")is valid in Python).
var vs. :=
var is more verbose but flexible:
var x int = 1
var x = 1 // because the default type is `int`
var x int // no value => it will be the zero value
var x, y int = 1, 2 // multiple assignments
var x, y int // multiple assignments, zero values
var x, y = 1, "hi" // different default types
// Declaration list
var (
x int
y = 2
z string
)
Type inference can be performed within a function:
// These statements are equivalent
var x = 1
x := 1 // invalid syntax outside a function
Avoid := in the following situations:
- When explicitly initializing a zero value, like
var x int. - To avoid a type conversion, by writing
var x byte = 8instead ofx := byte(8). - To avoid "shadowing" a variable, as
:=can be used to assign to existing variables. Create new variables withvar. - Non-constant package-level variables are a bad idea. If they're unused, they go unnoticed without raising compile-time errors.
const
- Variables cannot be declared as immutable.
- Constants are a way of giving names to literals.
- Inside a function, it is clear when a variable is being modified.
- If a constant is typed (e.g.,
const typedVar int = 1), then it can only be assigned to that type,intin this case. - If a constant is untyped (e.g.,
const untypedVar = 2), then it can be assigned to suitable numerical types.
Unused variables
- Unused declared local variables result in a compile-time error.
Naming Variables and Constants
- Even though many Unicode characters can be used, they should be avoided to maintain clarity.
- Go uses camelCase.
- The less scope a variable has, the shorter its name should be (
kandvare accepted for key/value, just likeiandjto use indices when iterating in loops). - It is common to use the first letter of a type as the variable name (e.g.,
ifor integers,ffor floats,bfor boolean). If the code is hard to understand, it's a sign the function is trying to do too much.
Composite Types
Arrays
- They are rarely used directly.
- They can be compared (
==and!=). - Their length is retrieved with the built-in
lenfunction. - Negative indexing is a compile-time error.
- Out-of-bounds indexing results in a panic at runtime.
- Unless there's a very specific need to use a given size of array (e.g., for a cryptographic library), avoid them.
- They exist basically to provide slices.
- There are a few ways of declaring arrays:
// indicate the size and type
var x [3]int // 3 integers assigned to the zero value
// array literal
var x = [3]int{10, 20, 30} // values specified
var x = [...]int{10, 20, 30} // equivalent
// sparse array:
// indicate few values at specific locations
var x = [12]int{1, 5: 4, 6, 10: 100, 15}
// Get and set values
x[0] = 10
fmt.Println(x[2])
Slices
- The zero value for a slice is
nil, which represents the lack of a value for some type.nilitself has no type. - The size of the array is not specified, making it a slice:
var x = []int{10, 20, 30}. This is a slice literal. - Can be used like a sparse array:
var x = []int{1, 5: 4, 6, 10: 100, 15}. - Multidimensional arrays can be simulated:
var x [][]int. - Reads and assignments are the same as with arrays, using square brackets.
- Slices can be created without assigning initial values:
var x []int. - Slices aren't comparable, except to check if it is nil (
x == nil). - They're useful for sequential data.
len
- A
nilslice returns0(len(x)).
append
It it used to grow slices:
var x []int
x = append(x, 10) // returns a slice
x = append(x, 5, 6, 7) // more than one value
// append to another slice with `...`
// Similar to the spread operator in JavaScript,
// but it goes after the value to spread
y := []int{20, 30, 40}
x = append(x, y...)
Capacity
- It increases automatically as needed. It doubles under 1,024 items, then it increases by at least 25%.
capreturns the current capacity of the slice.- It is better when possible to allocate the needed size upfront to avoid resizing the arrays.
make
- It can be used to create a slice that already has a capacity specified.
x := make([]int, 5): length and capacity of 5 (all zero values). Usingappendhere would add new values to the end of the slice, after the zero values!x := make([]int, 0, 10)creates an empty slice with a capacity of 10 and afterx = append(x, 5,6,7,8), it contains[5 6 7 8].
Emptying a slice
- Available since Go 1.21 with the built-in
clearfunction. - This sets all elements of the slice to their zero value.
x := []int{1, 2, 3, 4}
fmt.Println(x) // [1 2 3 4]
clear(x)
fmt.Println(x) // [0 0 0 0]
y := []string{"a", "b", "c"}
fmt.Println(y) // [a b c]
clear(y)
fmt.Println(y) // [ ] (empty strings)
Declaring a slice
- Slice literals
- An empty slice literal declares a slice that is non-nil:
var x = []int{}. This is useful to convert to JSON. - Useful with some initial values or when the values don't change.
- An empty slice literal declares a slice that is non-nil:
Slicing slices
These work with square brackets:
x := []int{1, 2, 3, 4} // [1 2 3 4]
y := x[:2] // [1 2]
z := x[1:] // [2 3 4]
d := x[1:3] // [2 3]
e := x[:] // [1 2 3 4]
Slices can share data
x := []int{1, 2, 3, 4}
y := x[:2]
z := x[1:]
// These are bidirectional changes!
x[1] = 20 // affects `x`, `y` and `z`
y[0] = 10 // affects `x` and `y`
z[1] = 30 // affects `x` and `z`
// Result:
// x: [10 20 30 4]
// y: [10 20]
// z: [20 30 4]
append can lead to unintuitive results, overwriting existing values:
x := make([]int, 0, 5) // length 0, capacity 5
x = append(x, 1, 2, 3, 4) // x is now [1 2 3 4]
y := x[:2] // [1 2], length 2, capacity 5
z := x[2:] // [3 4], length 2, capacity 3
y = append(y, 30, 40, 50)
// x is now [1 2 30 40], length 4, capacity 5!
// y is now [1 2 30 40 50], length 5, capacity 5
// z is now [30 40], length 2, capacity 3
x = append(x, 60)
// x is now [1 2 30 40 60], length 5, capacity 5
// y is now [1 2 30 40 60], length 5, capacity 5!
// z is still [30 40], length 2, capacity 3
z = append(z, 70)
// x is now [1 2 30 40 70], length 5, capacity 5!
// y is now [1 2 30 40 70], length 5, capacity 5!
// z is now [30 40 70], length 3, capacity 3
One way to avoid this issue is to use full slice expressions to indicate the capacity of the sub-slices:
x := make([]int, 0, 5)
x = append(x, 1, 2, 3, 4)
y := x[:2:2] // take a slice of x, up to index 2, with a capacity of 2
z := x[2:4:4] // take a slice of x, from index 2 to 4, with a capacity of 2
y = append(y, 30, 40, 50)
// x is still [1 2 3 4], length 4, capacity 5
// y is now [1 2 30 40 50], length 5, capacity 6
// z is still [3 4], length 2, capacity 2
x = append(x, 60)
// x is now [1 2 3 4 60], length 5, capacity 5
// y is still [1 2 30 40 50], length 5, capacity 6
// z is still [3 4], length 2, capacity 2
z = append(z, 70)
// x is still [1 2 3 4 60], length 5, capacity 5
// y is still [1 2 30 40 50], length 5, capacity 6
// z is now [3 4 70], length 3, capacity 4
Converting Arrays to Slices
- Arrays can be sliced, though memory will be shared as when slicing a slice.
x := [4]int{5, 6, 7, 8} // [5 6 7 8]
y := x[:2] // [5 6]
z := x[2:] // [7 8]
x[0] = 10 // [10 6 7 8]
// y and z are now [10 6] and [7 8]
Converting Slices to Arrays
- Data in the slice is copied to new memory (slices and arrays remain independent).
- A slice converted into a pointer to an array will share the same underlying data and memory address.
x := []int{1, 2, 3, 4} // [1 2 3 4]
y := x // y is a slice, not a copy
z := make([]int, 4) // [0 0 0 0]
copy(z, x) // copy(dst, src)
x[0] = 10 // [10 2 3 4]
// y is now [10 2 3 4]
// z is now [1 2 3 4]
copy
- It creates a slice that is independent from the original slice.
// It can copy the whole slice if the lengths are the same
x := []int{1, 2, 3, 4} // [1 2 3 4]
y := make([]int, 4) // [0 0 0 0]
num := copy(y, x) // num=4, copy(dst, src)
fmt.Println(y, num) // [1 2 3 4] 4
fmt.Println(x) // [1 2 3 4]
// It can copy a subset of the slice
y := make([]int, 2) // [0 0]
num = copy(y, x) // num=2, copy(dst, src)
fmt.Println(y) // [1 2]
// It can copy from a subset from any position
x := []int{1, 2, 3, 4} // [1 2 3 4]
y := make([]int, 2) // [0 0]
copy(y, x[2:]) // copy(dst, src)
fmt.Println(y) // [3 4]
num = copy(x[:3], x[1:]) // put the last 3 values at the beginning
fmt.Println(x) // [2 3 4 4], overwriting
// It also works with arrays
x := []int{1, 2, 3, 4} // slice, [1 2 3 4]
d := [4]int{5, 6, 7, 8} // array, [5 6 7 8]
y := make([]int, 2) // [0 0]
copy(y, d[:]) // first 2 values of d into y
fmt.Println(y) // [5 6]
copy(d[:], x) // copy x into d
fmt.Println(d) // [1 2 3 4]
Strings and Runes and Bytes
- Strings are arrays of bytes.
- Single characters can be extracted from a string with an index expression:
s := "hello"
c := s[0] // c is a byte, not a rune
fmt.Println(c) // 104
fmt.Printf("%T\n", c) // uint8 (i.e., byte)
// Need to be careful with indexing
var s string = "Hello 😄"
fmt.Println(len(s)) // could have expected 7, but it's 10
fmt.Println(s[:2], s[7:]) // He ���: the emoji is 4 bytes long
Maps
- The built-in map type is a hash map (implemented as an array).
- The zero value for a map is
nil. - Writing to a
nilmap results in a runtime panic. lenon a map returns the number of key/value pairs.- Maps are not comparable (but they can check against
nil). - The key must be comparable: it cannot be a slice, map or function.
- Maps are good when the order of the keys doesn't matter: use a slice when it does.
- All the values must be of the same type, but the keys can be of different types.
- Avoid using them as input parameters to functions (use a struct instead to be self-documenting).
var nilMap map[string]int
// ^ ^
// key type value type
// map literal: length of 0
myMap := map[string]int{} // allows reads and writes
// Non-empty map
reposByOrg := map[string][]string{
"dbeaver": []string{"dbeaver", "cloudbeaver", "team-edition-deploy"},
"slidevjs": []string{"slidev", "slidev-vscode", "themes"},
"ReactiveX": []string{"RxJava", "rxjs", "RxGo"}, // comma at the end here too
}
// With a default size
myValues := make(map[int][]string, 10) // length 0, capacity 10, can grow beyond 10
Reading and writing maps
reposByOrgStars := map[string]int{} // length 0, can grow, string to integer
reposByOrgStars["dbeaver"] = 10 // write
reposByOrgStars["slidevjs"] = 100 // write
reposByOrgStars["ReactiveX"]++ // read, increment, write (0 -> 1)
// reposByOrgStars["slidevjs"] := 100 // invalid syntax
Comma Ok idiom
- One can get the value of a key and a boolean indicating whether the key exists or not:
value, ok := reposByOrgStars["dbeaver"] // 10 true
value, ok = reposByOrgStars["notfound"] // 0 false
Deleting from a map
deleteremoves a key/value pair from a map:
delete(reposByOrgStars, "ReactiveX")
// It is safe to delete a key that doesn't exist
delete(reposByOrgStars, "notfound") // it returns nothing
// It is safe to delete a key from a nil map
var nilMap map[string]int
delete(nilMap, "notfound")
// It is safe to delete a key from an empty map
emptyMap := map[string]int{}
delete(emptyMap, "notfound")
Using maps as sets
- Go doesn't have a built-in set type.
- A map can be used as a set by using the key as the value and the value as a boolean:
mySet := map[string]bool{}
mySet["hello"] = true
mySet["world"] = true
mySet["hello"] = true // no error, but it's still a set
- This works because if the value isn't found, the zero value is returned, which is
falsefor booleans. - To use operations like
union,intersectionanddifference, the most convenient solution is to use a third-party library. - Structs can also be used as sets as they're more memory efficient, but more clumsy to use as they make use of the comma ok idiom.
Structs
- Good when there is related data that needs to be grouped together.
- They are defined with the
typekeyword. - No commas are needed between fields.
- They can be defined inside or outside of a function. If inside a function, they can only be used inside that function.
type user struct {
age int
firstName string
lastName string
}
var u user // zero value for a struct is all zero values for its fields
u.age = 18
u.firstName = "Bob"
u.lastName = "Michigan"
fmt.Println(u) // {18 Bob Michigan}
// Assignments can also be done with a struct literal
bob := user{} // also initializes all fields to zero values
// With initial values
jeremy := user{
49, // must match the order of the fields
"Jeremy", // all fields must be specified
"Stretchy",
} // {49 Jeremy Stretchy}
// With this style, fields can be left out:
otherUser := user{
age: 32, // using the field name...
firstName: "Sweaty", // cannot indicate other fields without the field name
// lastName is zero value, i.e., empty string
}
// Accessing a field:
jeremy.firstName = "Jer"
fmt.Println(jeremy.firstName) // Jer
Anonymous structs
- They are useful when a struct is only used in one place.
- Useful when marshalling and unmarshalling data.
computer := struct {
operatingSystem string
chip string
}{
operatingSystem: "macOS",
chip: "Apple M2 Ultra",
}
Comparing and converting structs
- Contrary to regular structs, they can be compared.
Blocks, Shadows, and Control Structures
Blocks
- A block is a place where declarations are made.
- The top-level block is the package block.
- The import statements are in the file block.
Shadowing Variables
- There is a "global" block, the universe block, which contains the built-in functions and types. Careful: those keywords can be shadowed!
func main() {
x := 10
if x > 5 {
fmt.Println(x)
x := 5 // shadowing
fmt.Println(x)
}
// x is still 10 here (outer block)
fmt.Println(x)
}
func main() {
x := 10
if x > 5 {
// x is shadowed while y is declared!
x, y := 5, 20
fmt.Println(x, y) // 5 20
}
fmt.Println(x) // 10
}
func main() {
x := 10
fmt.Println(x)
fmt := "shadowing fmt package"
fmt.Println(fmt) // undefined!
}
Linters typically won't catch shadowing, but we can install shadow:
go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest
And we can make it part of the Makefile:
lint: fmt
golint ./...
shadow ./...
.PHONY: lint
if statements
- The condition must be a boolean expression.
- The usual flow is
if ... {...} else if ... { ... } else { ... }. - There are no parentheses around the condition.
- Variables can be scoped to the
ifstatement (they'll be available inelse ifandelseblocks as well). Only use that feature to define new variables!
if n := rand.Intn(10); n == 0 {
fmt.Println("That's too low")
} else if n > 5 {
fmt.Println("That's too big:", n)
} else {
fmt.Println("That's a good number:", n)
}
// n is not available here!
for loops
- It is the only looping construct in the language.
- It can be used in four different ways to accomplish all looping needs.
- It uses no parentheses.
Complete for statement
for i := 0; i < 10; i++ {
fmt.Println(i)
}
- The initialization statement is executed once before the loop starts. It must use the
:=operator. It can shadow variables. - The second statement is the condition. It must be a boolean expression. It is checked before each iteration as well as before the loops starts.
- The third statement is the post statement. It is executed after each iteration. It would usually be used to increment a counter.
Condition-only for Statement
i := 0
for i < 10 {
fmt.Println(i)
i++
}
- This is a statement that only has a condition. It is equivalent to a
whileloop in other languages.
Infinite for Statement
for {
fmt.Println("Hello")
}
- This is a statement that has no condition. It is equivalent to a
while trueloop in other languages.
break and continue
- There is no
do... whileconstruct as in other languages like C, Java or JavaScript. do... whileindicates how to stay in the loop whileforindicates how to leave the loop.breakexits the loop.continueskips the rest of the loop and goes to the next iteration. Use it to avoid nesting loops.breakandcontinuecan be used with labels to break out of nested loops.
for-range loop
odds := []int{1, 3, 5, 7, 9}
for i, v := range odds {
fmt.Println(i, v)
}
- Here,
i(0to4) is the index of the iterator construct andv(1, 3 ... 9) is the value. iis usually used for arrays, slices and strings, whilekis used for maps.- As in other languages like Python, use an underscore (
_) to ignore a value. The index or key can be ignored this way. - If the key is needed but not the value, leave the value out:
for k := range myMap { ... }. This is useful when using a map as a set.
Iterating over maps
m := map[string]int{"a": 1, "c": 3, "b": 2}
for i := 0; i < 3; i++ {
fmt.Println("Loop", i)
for k, v := range m {
fmt.Println(k, v) // order will differ
}
}
fmt.Println(m) // will be in ascending order to help with debugging
- For security reasons (hash DoS), the order of the keys is randomized. If the order is important, use a slice of keys instead.
Iterating over strings
- Go iterates over the Unicode code points (runes), not the bytes.
samples := []string{"hello", "worl∂!"}
for _, sample := range samples {
for i, r := range sample {
fmt.Println(i, r, string(r))
}
fmt.Println()
}
/*
Output:
0 104 h // index, rune, string(rune)
1 101 e
2 108 l
3 108 l
4 111 o
// first string is 5 runes long
0 119 w
1 111 o
2 114 r
3 108 l
4 8706 ∂ // this is a single rune: it occupies 3 bytes
7 33 ! // index 7! The second string is 8 runes long
*/
for-range value is a copy
- Modifying the value variable doesn't change the original value.
breakandcontinueare also available in this form.
odds := []int{1, 3, 5, 7, 9}
for _, v := range odds {
v += 2
}
fmt.Println(odds) // [1 3 5 7 9]
for loop labels
- While their use is rare, they can be used to break out of nested loops.
- The label is indented to the same level as the containing block.
samples := []string{"hello", "worl∂!"}
outer:
for _, sample := range samples {
for i, r := range sample {
fmt.Println(i, r, string(r))
if r == 'l' {
continue outer
}
}
fmt.Println()
}
/* Output:
0 104 h
1 101 e
2 108 l
0 119 w
1 111 o
2 114 r
3 108 l
*/
This code will skip the remaining letters of both words once the first l is printed out for each one.
The right for statement
for-rangeis the proper way to iterate over strings to get runes.- A complete
forstatement is good when not iterating through all the items in a collection (except for strings since runes aren't necessarily one byte long). - The condition-only
forstatement is used to replacewhileloops. - Infinite loops can simulate a
do... whileconstruct and can be used to create the iterator pattern.
Expression switch statements
- They don't use parentheses.
- Used to check for equality.
- Scoped variables can be declared (e.g.,
wordis scoped to all cases). - All cases are part of the same block (only the switch statement itself is a block surrounded by braces).
- No need for
breakstatements. It can be used, but may indicate a code smell. breakcan be useful if theswitchstatement is inside a loop.- There is a
fallthroughkeyword to go to the next case (not recommended).
words := []string{"Go", "Ada", "COBOL", "C++", "Python", "Clojure", "WebAssembly"}
for _, word := range words {
switch size := len(word); size {
case 1, 2, 3, 4: // catches multiple matches
fmt.Println(word, "is a short name!") // Go, Ada, C++
case 5:
wordLen := len(word)
fmt.Println(word, "is the right length:", wordLen) // COBOL
case 6, 7, 8, 9: // empty case, nothing happens: Python, Clojure
default:
fmt.Println(word, "is a long name!") // WebAssembly
}
}
// Inside a loop
loop: // label
for i := 0; i < 10; i++ {
switch {
case i%2 == 0:
fmt.Println(i, "is even")
case i%3 == 0:
fmt.Println(i, "is divisible by 3 but not 2")
case i%7 == 0:
fmt.Println("exit the loop!")
break loop // break out of the loop, not just the switch statement
default:
fmt.Println(i, "is boring")
}
}
Blank switch statements
- Instead of checking for equality, they check for any boolean comparison.
func main() {
var x int = 5
switch {
case x%2 == 0:
fmt.Println(x, "is even")
case x > 6:
fmt.Println(x, "is large")
case x <= 6:
fmt.Println("Got it!") // this is the one that will be executed
default:
fmt.Println(x, "Not it...")
}
}
if or switch?
switchshould be used when there is some relationship between comparable elements. It is more concise and makes the comparisons more obvious.
goto
- It should generally be avoided.
- It can be used to replace boolean flags, such as in this non-trivial example from the standard library.
Functions
Declaring and calling them
- Go has no classes, but it has methods (see chapter 7).
- Types are mandatory.
- The
returnkeyword is mandatory (except formain) if the function has a return type. - Nothing goes between the input parameters and the start of the block if there's no return type.
- Go has no named or optional input parameters: you can pass structs instead. In practice, that probably means the function is trying to do too much if you need this feature.
- Go supports variadic parameters (e.g., the
fmt.Printlnfunction) with...right before the type: they are used as a slice inside the function. - Functions can return multiple values. They must all be returned, comma-separated. Unlike Python which uses tuples, Go uses the comma to separate the individual values.
- The error is always the last parameter a function will return. If no error occurred, it will be
nil. - Indicate ignored values (possibly all) with an underscore. A notable exception is
fmt.Printlnwhich returns two values that aren't usually used. - Named values can be returned. They make shadowing possible and should be used sparingly.
- Blank returns can be returned with named values, but they make it harder to understand how data flows.
func div(numerator int, denominator int) int {
// ^ ^ ^ ^
// | input parameter | return type
// Function name parameter type
if denominator == 0 {
return 0
}
return numerator / denominator
}
// The following is equivalent when the types are the same
// func div(numerator, denominator int) int ...
// variadic parameter
func addTo(base int, vals ...int) []int {
out := make([]int, 0, len(vals))
for _, v := range vals {
out = append(out, base+v)
}
return out
}
// fmt.Println(addTo(1, 2, 3, 4))
// a := []int{4, 3}
// fmt.Println(addTo(3, a...))
// fmt.Println(addTo(3, []int{4, 5}...))
// named return values
func divAndRemainder(numerator int, denominator int) (result int, remainder int, err error) { ... }
Functions are values
- Functions can be defined as types, e.g.,
type aFuncType func(int, int) int. - Anonymous functions can be defined inside other functions and called immediately (IIFE). This comes in handy when using
deferand Goroutines.
Closures
- Closures are used to create functions that have access to variables that are outside of their scope.
- Functions can be passed as parameters to other functions.
- Functions can return functions.
- They are useful with
sort.Searchandsort.Slice.
An example of closure is with the sort.Slice function:
import (
"fmt"
"sort"
)
func main() {
type Person struct {
FirstName string
LastName string
Age int
HasPet bool
}
people := []Person{
{"Pat", "Patterson", 37, true},
{"Tracy", "Bobbert", 23, false},
{"Fred", "Fredson", 18, true},
{"Bob", "Tracier", 18, false},
{"Alice", "Anderson", 30, true},
}
fmt.Println(people)
// `people` is captured by the closure
sort.Slice(people, func(i int, j int) bool {
// If one person has a pet and the other doesn't,
// prioritize the one with the pet.
if people[i].HasPet != people[j].HasPet {
return people[i].HasPet
}
// If both have pets, sort by LastName.
if people[i].HasPet && people[j].HasPet {
return people[i].LastName < people[j].LastName
}
// Otherwise, sort by age in ascending order.
return people[i].Age < people[j].Age
})
fmt.Println(people)
}
// This returns:
// - People that have pets first
// - If both have pets, sort by last name in ascending order
// - If neither have pets, sort by age in ascending order
// Unsorted:
[
{Pat Patterson 37 true}
{Tracy Bobbert 23 false}
{Fred Fredson 18 true}
{Bob Tracier 18 false}
{Alice Anderson 30 true}
]
// Sorted:
[
{Alice Anderson 30 true}
{Fred Fredson 18 true}
{Pat Patterson 37 true}
{Bob Tracier 18 false}
{Tracy Bobbert 23 false}
]
A function that returns a function:
func main() {
withTwo := getResult(2)
withThree := getResult(3)
for i := 0; i < 3; i++ {
fmt.Println("i=", i, "withTwo (2 + i):", withTwo(i), "withThree (3 + i):", withThree(i))
}
}
func getResult(initialValue int) func(int) int {
return func(subsequent int) int {
return initialValue + subsequent
}
}
// Output:
// i= 0 withTwo (2 + i): 2 withThree (3 + i): 3
// i= 1 withTwo (2 + i): 3 withThree (3 + i): 4
// i= 2 withTwo (2 + i): 4 withThree (3 + i): 5
defer
- This is used to perform the cleanup code, such as closing a file, after a function has returned. This is similar to the
finallyblock in Java or Python but it executes at the very end of the function, not as part of atry...exceptblock. - It delays the execution of a function until the surrounding function returns.
- They run in LIFO (last in, first out) order.
- The code that runs after
deferis literally the last thing that runs before the function returns, so what is put there is immediately "called" (e.g.,defer close()) but will run until later. - It helps reduce depth of nesting, which, along with "lack of structure", are two of the most important factors that contribute to code complexity (see this paper).
Go is "call by value"
- Go always makes a copy of the value before passing it to a function.
- Maps and slices behave differently because they are implemented with pointers.
Pointers
- A pointer is a variable that holds the address of a value in memory.
- The zero value of a pointer is
nil. &is the address-of operator. It goes before the variable name to get its address.*is the indirection operator (dereference). It goes before a pointer to get the value it points to.- Dereferencing a
nilpointer will result in a runtime panic. - Types with an
*are pointers to that type ("pointer type"). newis a built-in function that allocates memory for a type and returns a pointer to it.- To turn a constant into a pointer, use a helper function that takes a value and returns a pointer to it.
- A pointer is used to indicate that a parameter is mutable, i.e., that the function can modify the original value.
- To update the value of a pointer inside a function, dereference it and assign it a new value.
- Value types should be preferred when returning from functions.
- Use a pointer as a return type when there is a need to return a modified data structure or when the data being passed around is very large (at least 1 MB).
- Slices can be used as buffers when iterating over files to avoid allocating memory in each iteration of the loop.
- The garbage collector will free memory that is no longer used. Go favors low latency over high throughput.
var x int = 10
var y *int = &x
fmt.Println(x, y) // 10 0xc0000b4008
fmt.Println(&x, *y) // 0xc0000b4008 10
// The zero value of a pointer is nil
var z *int
fmt.Println(z) // nil
// Dereferencing a nil pointer will result in a runtime panic
// fmt.Println(*z) // panic: runtime error: invalid memory address or nil pointer dereference
// new allocates memory for a type and returns a pointer to it
var a *int = new(int)
fmt.Println(a) // 0xc0000b4010
fmt.Println(*a) // 0
// To turn a constant into a pointer, use a helper function that takes a value
// and returns a pointer to it
func intPtr(i int) *int {
return &i
}
var b *int = intPtr(10)
// A pointer is used to indicate that a parameter is mutable
func addOne(x *int) {
*x++
}
addOne(b)
fmt.Println(*b) // 11
// To update the value of a pointer inside a function, dereference it and assign it a new value
func updatePointer(x *int) {
*x = 2
}
updatePointer(b)
fmt.Println(*b) // 2
Types, Methods, and Interfaces
- Types can be declared at any level, including at the package level.
Methods
- Method names cannot be overloaded.
type Person struct {
FirstName string
LastName string
Age int
}
func (p Person) String() string {
// ^
// p is a receiver of the String method
return fmt.Sprintf("%s %s, age %d", p.FirstName, p.LastName, p.Age)
}
func main() {
p := Person{"Domi", "Noes", 42}
fmt.Println(p.String()) // Domi Noes, age 42
}
Pointer Receivers and Value Receivers
- Use a pointer receiver when the method needs to modify the receiver.
- Use a pointer receiver when
nilmust be handled. - A value receiver is used when the method doesn't need to modify the receiver.
- If a type has a pointer receiver, all methods should have pointer receivers for consistency.
- Usually, there's no need for getters and setters when using structs: just use the fields directly.
- Pointer receiver methods should check for
nilvalues.
Methods are also functions
- We can use a method value to turn a method into a function.
- A method expression is used to turn a method into a function that takes the receiver as the first parameter.
- They can be use for dependency injection.
// Using a method value
func main() {
p := Person{"Domi", "Noes", 42}
f := p.String // using an instance of the struct
fmt.Println(f()) // Domi Noes, age 42
}
// Using a method expression
func main() {
p := Person{"Domi", "Noes", 42}
f := Person.String // using the type itself
fmt.Println(f(p)) // Domi Noes, age 42
}
Functions vs. methods
- Use a function when there is no need to modify the receiver.
- Use a method with a struct receiver when there is a need to modify data at runtime.
Type declarations are not inheritance
- Declaring a type based on another type is not inheritance: there is no hierarchy.
- Type conversion is used to convert a value from one type to another.
Types serve as executable documentation
- They can be used to make code more readable and self-documenting.
- A
Percentagetype can be used to make it clear what a value is instead of anint.
Use iota for enumeration (sparingly)
- Go does not have an enumeration type.
iotais a built-in constant generator that starts at0and increments by1for each subsequent constant.iotacan be used to create a set of constants that are related to each other.- It should be used for "internal" purposes only, when the constants are referred to by name -- not by value.
- It is useful to differentiate between sets of values, not to rely explicitly on the values themselves.
- If the first value in the constant block (with value
0) is not really initialized or the value0does not make sense, it can be named with an_to skip0.
type Color int
const (
Red Color = iota
Green
Blue
)
func main() {
fmt.Println(Red, Green, Blue) // 0 1 2
}
Embedding for composition
- Types can be embedded to encourage composition.
type Person struct {
FirstName string
LastName string
Age int
}
type Employee struct {
Person // embedded field
EmployeeID int
}
func main() {
e := Employee{
Person: Person{
FirstName: "Domi",
LastName: "Noes",
Age: 42,
},
EmployeeID: 12345,
}
fmt.Println(e.FirstName, e.LastName, e.Age, e.EmployeeID) // Domi Noes 42 12345
}
Interfaces
- Interfaces are declared as a type with the
interfacekeyword. - The methods defined in an interface are the methods that a type must implement to be considered an implementation of that interface: this is referred to as the method set.
- They can be declared in any block.
- They usually end with
er(e.g.,Stringer,Reader,Writer). - They are implemented implicitly.
- Go is a blend of duck typing (e.g., dynamic behavior in Python) and structural typing (Java interfaces).
- Use built-in interfaces from the standard library as much as possible.
- Just like structs, interfaces can be embedded.
- Interfaces are the only abstract type in Go.
// Switching between the logic providers is as simple as changing the type of the L field.
import "fmt"
type LogicProvider struct{}
type LogicProvider2 struct{}
func (lp LogicProvider2) Process(data string) string {
fmt.Println("LogicProvider2: Process")
return data
}
func (lp LogicProvider2) Rework(data string) string {
return data
}
func (lp LogicProvider) Process(data string) string {
return data
}
func (lp LogicProvider) Rework(data string) string {
fmt.Println("LogicProvider1: Rework")
return data
}
type Logic interface {
Process(data string) string
Rework(data string) string
}
type Client struct {
L Logic
}
func (c Client) Program() {
data := "data"
c.L.Process(data)
c.L.Rework(data)
}
func main() {
c1 := Client{
L: LogicProvider{},
}
c1.Program()
c2 := Client{
L: LogicProvider2{},
}
c2.Program()
}
// Output:
// LogicProvider1: Rework
// LogicProvider2: Process
Take interfaces and return structs
- Returning interfaces increases coupling.
- It is better to return structs and take interfaces as parameters.
Empty interfaces
- If an interface is
nil, invoking a method on it will result in a runtime panic. - An empty interface matches any type, because it requires implementing no methods.
- They can be used when receiving data from an external source (e.g., a database or JSON) and the type is unknown.
- Using
interface{}is a code smell: it should be used sparingly.
Type assertions and type switches
- Use the comma ok idiom to avoid a type assertion from panicking.
- Type assertions are checked at runtime.
- Type conversions are checked at compile time.
- A type switch is used to check the type of an interface.
- In the case of errors, use
errors.Isanderrors.Asto test for specific errors. - Add a
defaultcase to switch statements to catch unexpected types. - It can be safer to keep interfaces unexported.
func main() {
var i interface{} = 42
v1, ok1 := i.(int)
fmt.Println(v1, ok1) // 42 true
v2, ok2 := i.(string)
fmt.Println(v2, ok2) // "" false
// type switch
switch v := i.(type) {
case int:
fmt.Println("int", v) // int 42
case string:
fmt.Println("string", v)
case float64, float32: // check both at once
fmt.Println("float", v)
default:
fmt.Println("unknown", v)
}
// type assertion using optional interface
if s, ok := i.(MySpecificInterface); ok {
// myInterface satisfies MySpecificInterface
s.SpecificMethod()
}
}
Function types are a bridge to interfaces
- Functions can implement interfaces.
- Go uses this to implement the
http.Handlerinterface. - Small interfaces are encouraged.
- If a function can depend on many other functions, it is better to use a struct with methods.
type HandlerFunc func(http.ResponseWriter, *http.Request)
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
f(w, r)
}
func main() {
http.Handle("/", HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, world!")
}))
http.ListenAndServe(":8080", nil)
}
An actual, basic HTTP server:
package main
import (
"fmt"
"net/http"
)
type HandlerFunc func(http.ResponseWriter, *http.Request)
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
f(w, r)
}
func main() {
http.Handle("/", HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p, err := fmt.Fprintln(w, "Hello, world!")
if err != nil {
return
}
fmt.Println(p)
}))
err := http.ListenAndServe(":8080", nil)
if err != nil {
return
}
}
Implicit Interfaces Make Dependency Injection Easier
- "[...] code should explicitly specify the functionality it needs to perform its task".
- If a type has methods that match an interface's signature, it implicitly satisfies that interface.
- Go's implicit interfaces make dependency injection easier.
- Interfaces are used to decouple code.
- The client's code should not be responsible for creating the dependencies it needs.
- The client's code defines the interfaces and can customize the method set it needs.
Here is an example of an implicit interface:
package main
import "fmt"
type Talker interface {
Say() string
}
// Dog implicitly implements the Talker interface
type Dog struct{}
func (d Dog) Say() string {
return "Woof!"
}
func Speak(t Talker) {
fmt.Println(t.Say())
}
func main() {
d := Dog{}
Speak(d) // Woof!
}
And an example demonstrating dependency injection:
package main
import (
"fmt"
)
// Logger is an implicit interface with a Log method
type Logger interface {
Log(message string)
}
// SimpleLogger implicitly satisfies Logger by implementing Log
type SimpleLogger struct{}
func (sl SimpleLogger) Log(message string) {
fmt.Println(message)
}
// AdvancedLogger implicitly satisfies Logger by implementing Log
type AdvancedLogger struct{}
func (al AdvancedLogger) Log(message string) {
fmt.Println("ADVANCED: " + message)
}
// CompositeLogger combines SimpleLogger and AdvancedLogger
// Again, it implicitly satisfies Logger by implementing Log
type CompositeLogger struct {
loggers []Logger
}
// Log messages with multiple loggers
func (cl CompositeLogger) Log(message string) {
for _, logger := range cl.loggers {
logger.Log("[CompositeLogger] " + message)
}
}
// Greeter contains logic to greet
type Greeter struct {
logger Logger // Logger is injected
}
// NewGreeter injects dependencies and returns a new Greeter
func NewGreeter(l Logger) Greeter {
return Greeter{logger: l}
}
// Greet uses the Logger to log a greeting
func (g Greeter) Greet() {
g.logger.Log("Hello, dependency injection and implicit interface!")
}
func main() {
logger1 := SimpleLogger{} // Create Logger
greeter1 := NewGreeter(logger1) // Inject Logger into Greeter
greeter1.Greet() // Use Greeter
// Logs:
// Hello, dependency injection and implicit interface!
logger2 := AdvancedLogger{} // Create Logger
greeter2 := NewGreeter(logger2) // Inject Logger into Greeter
greeter2.Greet() // Use Greeter
// Logs:
// ADVANCED: Hello, dependency injection and implicit interface!
logger3 := CompositeLogger{loggers: []Logger{SimpleLogger{}, AdvancedLogger{}}}
greeter3 := NewGreeter(logger3) // Inject CompositeLogger into Greeter
greeter3.Greet() // Use Greeter
// Logs:
// [CompositeLogger] Hello, dependency injection and implicit interface!
// ADVANCED: [CompositeLogger] Hello, dependency injection and implicit interface!
}
Generics
Generics Reduce Repetitive Code and Increase Type Safety
- Generics are "type parameters" that allow one to write functions and data structures that can work with any type.
- They reduce repetitive code and increase type safety.
Generic Functions Abstract Algorithms
- Generic functions can be used to abstract algorithms.
- They can be used to write functions that work with any type.
package main
import "fmt"
// Max returns the maximum of two values
func Max[T any](a, b T) T {
if a > b {
return a
}
return b
}
Generics and interfaces
- Generics can be used with interfaces.
- They can be used to write functions that work with any type that satisfies an interface.
package main
import "fmt"
// Stringer is an interface with a String method
type Stringer interface {
String() string
}
// Print calls the String method on a value that satisfies the Stringer interface
func Print[T Stringer](s T) {
fmt.Println(s.String())
}
Errors
- Go does not have exceptions.
- Errors are values.
- They are the last return value of a function (by convention).
- The
errorinterface is defined in the standard library. - The
errorspackage is used to create errors. - The
fmtpackage is used to print errors. - The
errors.Isfunction is used to check for specific errors. - The
errors.Asfunction is used to check for specific errors and get the underlying error. - The
errors.Unwrapfunction is used to get the underlying error, buterrors.Isanderrors.Asare more commonly used for this. - The
errors.Newfunction is used to create errors. - The
errors.Errorffunction is used to create errors with formatting. - When a function returns an error, it is expected that the caller will check for it.
- If a function does not return an error, its value will be
nil, because it is the zero value for interfaces. - Error messages should not be capitalized or end with punctuation, nor contain newline.
- Because all values must be read, errors cannot be ignored implicitly.
- Because the main code is unindented and the error handling code is indented, the code's purpose is easier to follow.
Sentinel errors
- Sentinel errors are errors that are predefined and can be checked for equality.
- They are defined at the package level.
- They start with
Err(except forio.EOF). - They should be treated as read-only.
- They are used to indicate it is not possible to continue processing (e.g.,
ErrFormatfor ZIP files). - Whenever possible, use existing sentinel errors from the standard library.
Wrapping errors
- To give additional context to an error, wrap it with
fmt.Errorf. - A series of errors can be wrapped with
fmt.Errorfanderrors.Unwrap: these are called error chains. - If context is not required, a brand new error can be created with
errors.Neworerrors.Errorf. - The
%vverb can be used to print the error chain without wrapping the error (e.g.,fmt.Errorf("internal failure: %v", err)).
Is and As
- Use
errors.Isto check for specific errors. - Use
errors.Asto check for specific errors and get the underlying error. errors.Ascan take as the second parameter a pointer to a variable of the type of the error we are looking for, but just as well it can take a pointer to an interface.
Wrapping Errors with defer
- Using named return values and
defercan make error handling easier because the error can be formatted only once at the end of the function.
Panic and recover
- A panic is used to indicate that the program cannot continue (e.g., out of memory error or trying to read beyond the end of a slice).
- When a panic occurs, the program stops executing and the stack is unwound, running all deferred functions until the
mainfunction is reached. panicandrecoverare not intended to be used for error handling.- It is better to explicitly handle errors than to use
panicandrecoverbecause it is not clear when callingrecoverwhat failed exactly.
Getting a Stack Trace from an Error
- Go doesn't provide a stack trace by default outside of a
panicstate. %+vcan be used to print the stack trace withfmt.Printf.- Pass the
-trimpathflag togo buildto remove the absolute path from the stack trace, which otherwise shows full paths to files.
Modules, Packages, and Imports
- The module is the root of the package tree. It can be defined in
go.modor inferred from the directory structure. For a GitHub repository, it is inferred from the URL as inmodule github.com/{USER}/{PROJECT}. - Keep a single module per repository.
go.mod
- Use
go mod init MODULE_PATHto create a new module (go.modfile). - The module path is case-sensitive.
- The minimum version of Go required to build the module can be specified with
go mod init MODULE_PATH GO_VERSIONand it appears in thego.modfile below the module declaration. - There can be a
requiredirective for each dependency. - There are also two optional sections:
replaceandexclude.replaceis used to replace a dependency with a local version.excludeis used to exclude a dependency from the build.
// Example of a go.mod file
module github.com/{USER}/{PROJECT}
go 1.xx
require (
github.com/{OTHER_USER}/{OTHER_PROJECT} v0.0.0-20200921021027-5abc380940ae
github.com/shopspring/decimal v1.2.0
)
Building packages
Imports and exports
- Import statements allow accessing exported constants, variables, functions and types from another package.
- An exported identifier starts with a capital letter. It cannot be accessed from another package without an import statement.
Creating and accessing packages
- The first line of the file should be
package {PACKAGE_NAME}. It's a package clause. - Next is the import section.
- Importing from the standard library doesn't require a path.
- Any other imports require a path, using the module path as a prefix and appending the path to the package.
- Not using any identifier from a path will result in a compiler error. Hence, all code included in the build will be used.
- It is best to always use absolute paths for clarity.
- The name of a package is determined by its package clause, not by the path being imported. In general, the package name should match the last element of the path.
- The
mainpackage cannot be imported as it is the entrypoint of the application.
- Package names are in the file block: the package name is the same for all files in the same directory and must be present.
Naming packages
- The package name should be descriptive.
- Avoid
utilandcommonpackages. Create more packages with fewer functions instead. - Don't include the name of the package in functions, as this will be disambiguated by the package name when importing.
Organizing a module
- There is no official structure.
- The
cmddirectory is used for executables. There can be multiple executables produced by different applications from a module. - When there are a bunch of files at the root to manage deployment and testing, it is a good pattern to put all packages inside a
pkgdirectory.- Inside
pkg, limit dependencies between packages by organizing the code according to the functionality it provides. - A good primer on the topic is GopherCon 2018: Kat Zien - How Do You Structure Your Go Apps.
- Inside
Overriding a package's name
- When names collide, you can use an alias to rename a package.
- In the standard library, both
"crypto/rand"and"math/rand"are imported as"rand", but they can be disambiguated with an alias such ascrand "crypto/rand".
- In the standard library, both
- Using the
.alias is discouraged because it makes it harder to understand where a function is coming from as it will import all exported identifiers from the current namespace (same idea asimport * from ...in Python). - Package names can be shadowed, which renders it inaccessible. Always resolve conflicts by using an alias instead.
Package comments and godoc
- Comments must be placed right above the item to be documented.
- Comments start with
//and can be multiline. - A blank line with
//is used to create paragraphs. - Preformatted text can be inserted with indentation.
- Comments before the package clause create package-level documentation.
- If the package has a lot of documentation, it is better to put it in a separate file called
doc.go.
- If the package has a lot of documentation, it is better to put it in a separate file called
- Comments should start with the name of the item being documented.
Internal package
- Use the
internaldirectory to create packages that are only accessible from the parent and sibling packages. - Trying to access an
internalpackage from outside the module will result in a compiler error.
The init function
- If there is an
initfunction in a package, it runs as soon as the package is referenced by another package. - It has not input or output parameters and can only cause side-effects within the package.
- There can be multiple
initfunctions in a package, even in the same file, although this setup is discouraged. - A blank import can be used to run the
initfunction of a package without using any of its exported identifiers, e.g.,import _ "github.com/lib/pq". Explicit is better than implicit: this is an obsolete pattern.
Circular dependencies
- They are not allowed to keep the code readable and the compiler fast.
- If two packages depend on each other, they should probably be merged into a single package.
- If two packages with circular dependencies are still preferred, it may be possible to move only the culprits into a separate package so that both packages can import them.
API renaming and organizing
- To avoid breaking a function name, a new method can be added with the new name, calling the old one.
- Constants can be re-declared with the same types, but with a different name.
- To rename exported types, we can create an alias such as
type Bar = Foo. In this case, new methods should still be added to the original type to preserve backward compatibility. - Caution: a field name cannot be changed without breaking backward compatibility.
Working with modules
Importing third-party code
go.modis used to manage dependencies.go.modis automatically populated when agocommand runs and a dependency is required (e.g.,go build,go list,go run,go test).go.sumis used to verify the integrity of the dependencies.god.modandgo.sumshould both be committed to version control.
Working with versions
go listis used to list dependencies used by a project.- By default, it lists packages used.
- The
-mflag is used to list modules used. - Appending the
-versionsflag to the previous command lists all versions of the dependencies.
go getcan be used to downgrade or upgrade a dependency (e.g.,go get github.com/{USER}/{PROJECT}@v1.0.0).- It can also be used to add a new dependency.
- It can be used to remove a dependency by adding the
-uflag. - Changes will be reflected in
go.modandgo.sum.
- Dependencies might be shown with
// indirectnext to them. This means that the dependency is not used directly by the project, but by one of its dependencies.- It can be added when the project uses that dependency directly with a newer version than what is declared in the dependency's
go.modfile.
- It can be added when the project uses that dependency directly with a newer version than what is declared in the dependency's
- Go follows semantic versioning (SemVer).
- There is an import compatibility rule: all minor and patch versions should remain compatible and if not, this is considered a bug.
- Instead of importing multiple versions of the same library as with
npm, Go will import the highest version of the library that satisfies the requirements.
- Instead of importing multiple versions of the same library as with
Updating to compatible versions
- Use
go get -u=patch DEPENDENCY_PATHto update to the latest compatible patch version. go get -u DEPENDENCY_PATHwill update to the most recent compatible version.
Updating to incompatible versions
- Go follows the semantic import versioning rule: for all major versions greater than
1, the major version is included in the import path, e.g.,"github.com/{USER}/{PROJECT}/v2". - Once a new major version is used in the project,
go buildwill update thego.modfile to use the new major version. - Older versions may still be present in
go.mod:go mod tidycan be used to remove them.
Vendoring
go mod vendoris used to create avendordirectory with all the dependencies to ensure reproducible builds.- It dramatically increases the size of the project in version control.
pkg.go.dev
- It automatically indexes Go projects.
- It is used to search for packages and their documentation.
- It publishes the godocs, license,
README, the module's dependencies and which other open source projects depend on it.
Publishing modules
- Go favors permissive licenses (e.g., MIT, BSD, Apache).
- There is no need for a central repository, as Go uses the module path to find the module.
Versioning modules
- Minor and patch versions should be compatible and are easy to manage.
- Major versions are slightly more difficult to manage. For instance, let's go from
v1tov2.- Create a directory to put all the old code in, named
v2, includingREADMEandLICENSEfiles. - Create a branch.
- Name the branch
v1if the old code goes in it. - Name the branch
v2if the new code goes in it.
- Name the branch
- Make sure the module path in
go.modends with/v2. - Update all import paths to use the new module path.
- Create a tag for the new version.
- Name the tag
v2.0.0. - Tag the
mainbranch if the new code goes in it. - Otherwise, tag the
v2branch.
- Name the tag
- Create a directory to put all the old code in, named
- If breaking changes might be introduced while on the new version, use a pre-release version, e.g.,
v2.0.0-alpha.1. - The open source project
modcan be used to automate this process. - The Go Blog has a post on the topic.
Module proxy servers
- Google manages a module proxy server that fetches all versions of all publicly available modules.
- Google also maintains a sum database, which stores the checksums of all the modules.
- Modules are only installed from the proxy server if they are not already present in the local cache and if the checksums match.
Specifying a module proxy server
- The
GOPROXYenvironment variable can be used to specify a module proxy server. To use GoCenter, set it toGOPROXY=https://gocenter.io. - If the
GOPROXYenvironment variable is set todirect, the module proxy server will not be used and the module will be downloaded directly from the source. - Projects such as
athenscan be used to create a local module proxy server.
Concurrency in Go
When to use concurrency
- Concurrency is not parallelism.
- Concurrency is useful when there are multiple tasks that can be executed independently.
- Concurrency brings benefits when a process takes a long time to complete.
- Read The Art of Concurrency for more information.
- The book Concurrency in Go is also a great resource.
Goroutines
- "Goroutines are lightweight processes managed by the Go runtime".
- Because Go manages goroutines, they are cheap to create and destroy (no need to create system-level resources).
- They are memory efficient because they are allocated on the stack with small initial sizes.
- Switching between goroutines is fast because it is managed by the Go runtime within a process.
- Go optimizes how work is distributed across goroutines.
- For more details on this, watch GopherCon 2018: Kavya Joshi - The Scheduler Saga.
- A goroutine starts by calling a function with the
gokeyword in front of it.
Channels
- Channels are used to communicate between goroutines.
- They are a built-in type that require the
makefunction to create them, e.g.,ch := make(chan int). - The zero value of a channel is
nil. - They are passed as parameters to functions as a pointer.
Reading, writing, buffering
- The
<-operator is used to send and receive data from a channel. It indicates the direction of the data flow. - A function parameter can specify the direction of the channel, e.g.,
func f(ch <-chan int)will make it so that the channel can only be read from. Likewise,func f(ch chan<- int)will make it so that the channel can only be written to. - By default, channels are unbuffered, meaning that they can only hold one value at a time. They should be used most of the time.
- A channel can be buffered by specifying the buffer size when creating it. E.g.,
ch := make(chan int, 10). len(ch)is used to get the number of elements in the channel.cap(ch)is used to get the capacity of the channel.- The capacity of a channel cannot be changed after it is created.
for-range and channels
for-rangecan be used to read from a channel until it is closed, or until abreakorreturnstatement is encountered.- There is a single variable declared for the channel, which is the value read from the channel.
for v:= range ch {
fmt.Println(v)
}
Closing a channel
- Close a channel with
close(ch). - Writing to a closed channel will result in a runtime panic.
- Attempting to read from a closed unbuffered channel will return the zero value of the channel's type.
- Reading from a closed buffered channel will return the remaining values in the channel until it is empty, then it will return the zero value of the channel's type.
- To know if a channel is closed, use the second return value of the receive operation with the comma ok idiom, e.g.,
v, ok := <-ch.okwill betrueif the channel is open andfalseif it is closed.
How channels behave
- They pause the execution of the goroutine until a value is available to read from the channel.
- They pause the execution of the goroutine until a value can be written to the channel.
- A
panicwill occur if a value is written to a closed channel or when trying to close a closed channel or anilchannel. - Make the writer responsible for closing the channel.
select
- Starvation is when a goroutine is waiting for a resource that is never available.
- It looks very similar to a
switchstatement. - A
casein aselectstatement is executed when the channel is ready to be read from or written to. - If multiple
casestatements are ready, one is chosen at random -- withswitch, the first match is always chosen. This solves the starvation problem since all cases are checked at once. selectalso deals with deadlock issues: if all channels are blocked, it will execute thedefaultcase.selectis often used in a loop to keep reading from a channel until it is closed.- Having a
defaultcase inside a loop for aselectis most certainly not what is intended as it will run constantly.
- Having a
Concurrency practices and patterns
Keep APIs concurrency-free
- Never expose channels or mutexes in an API. If a channel is exposed, the user will have to manage it, know whether it is buffered or not, closed or not or
nil. The user could also trigger deadlocks.
Goroutines, for loops, and varying variables
- Instead of shadowing variables in a
forloop, pass them as parameters to the goroutine.
// Don't do this
for _, v := range a {
v := v // shadowing
go func() {
ch <- v * 2
}()
}
// Do this instead
for _, v := range a {
go func(val int) { // value captured by the closure
ch <- val * 2
}(v)
}
Always clean up your goroutines
- If a goroutine is not cleaned up, it will keep running until the program exits: this is called a "goroutine leak".
"Done channel pattern"
- If multiple goroutines are running, it is useful to have a way to signal them to stop.
- A channel (named
done) can be used to signal the goroutines to stop.
Using a cancel function to terminate a goroutine
- A cancel function can be used to terminate a goroutine.
- It is a function that performs a cleanup and closes a channel.
When to use buffered and unbuffered channels
- Buffered channels are useful when limiting the number of goroutines that can access a resource at the same time, when limiting the amount of work that gets queued up or when the number of goroutines is known.
- Buffered channels are good to gather results from multiple goroutines when a deterministic order is not required.
Backpressure
- Backpressure is a way to signal to a goroutine that it should slow down.
- It is implemented with buffered channels.
Turning off a case in a select
- Setting a variable's channel to
nilwill turn off the case in aselectstatement.
for {
select {
case v, ok := <-in:
if !ok {
in = nil // kills the case
continue
}
fmt.Println(v)
// ... other cases ...
case <-done:
return
}
}
Time out code
- Use
time.Afterto time out code.
select {
// this requires context cancellation to free up resources
// if the function is not finished before the timeout,
// else it will keep processing in the background
case v := <-ch:
fmt.Println(v)
case <-time.After(1 * time.Second):
fmt.Println("timed out")
}
Using WaitGroups
- The done channel pattern works well when waiting for a single goroutine to finish.
- When waiting for multiple goroutines to finish, use a
sync.WaitGroup. Addis used to add a goroutine to the wait group.Doneis used to signal that a goroutine is done.Waitis used to wait for all goroutines to finish.- Use them when some cleanup is required after all goroutines are done.
ErrGroupcan be used to wait for multiple goroutines to finish and return an error if one of them fails.
func main() {
var wg sync.WaitGroup
var a = []int{1, 2, 3, 4, 5}
wg.Add(len(a))
ch := make(chan int, len(a))
for _, v := range a {
// The wait group is passed with a closure
// Otherwise it would need to be passed as a pointer
// So all goroutines share the same instance
go func(val int) {
defer wg.Done() // called even if panic
ch <- val * 2
}(v)
}
wg.Wait()
close(ch)
for v := range ch {
fmt.Println(v)
}
}
Running code exactly once
once.Dois used to run code exactly once.- It can be found in the
syncpackage. - It needn't be initialized, as the zero value is usable.
- It is best to use the minimum amount of concurrency possible.
var once sync.Once
func main() {
once.Do(func() {
fmt.Println("Only once")
})
once.Do(func() {
fmt.Println("Only once") // will not be printed
})
}
When to use mutexes instead of channels
- Mutex stands for mutual exclusion.
- Mutexes limit access to a resource to a single goroutine at a time with a locking mechanism (
LockandUnlock, which must be used carefully to avoid creating deadlocks, especially in functions implemented recursively). - They require to do more bookkeeping than channels.
- They should never be copied, just like
sync.WaitGroupandsync.Once. - Use mutexes when there is a shared resource that needs to be protected, such as a field in a struct.
RWMutexis used when there are multiple readers and a single writer. The critical section is protected by a write lock, while the read lock is used to read the resource by multiple goroutines.- Sometimes, performance issues with channels can be solved by using mutexes instead.
Atomics
- Atomics are used to perform atomic operations on integers and pointers.
- They are more niche than mutexes and channels and as such, they are not covered in this introductory summary on Go.
The Standard Library
- It is battery-included, just like Python.
io and Friends
iois used to read (io.Reader) and write (io.Writer) data.io.Closeris used to close a resource.- Read (
io.ReaderAt) and write (io.WriterAt) data at a specific offset. io.Seekeris used to seek to a specific offset.- There are other combinations to define more explicitly the code's intent (
ReadWriter,ReadCloser,WriteCloser,ReadWriteCloser,ReadSeeker,WriteSeeker,ReadWriteSeeker). - The
iopackage is a great example of the power of interfaces through simple abstractions. Reader,Writer, andScannerfrom thebufiopackage are used to read and write data more efficiently on larger datasets.
time
timeis used to work with dates and times.time.Timeis used to represent a date and time. It includes the usual constants for days of the week, months, etc.time.Durationis used to represent a duration.time.Parseis used to parse a string into atime.Timevalue.time.Formatis used to format atime.Timevalue into a string.time.Nowis used to get the current time.time.Sleepis used to pause the execution of a goroutine for a specified duration.time.Afteris used to create a channel that will receive a value after a specified duration.time.Tickis used to create a channel that will receive a value at regular intervals.time.Timeris used to create a timer that will send a value on a channel after a specified duration.time.Tickeris used to create a ticker that will send a value on a channel at regular intervals.time.NewTickeris used to create a new ticker.time.NewTimeris used to create a new timer.time.Sinceis used to get the time elapsed since a specified time.time.Untilis used to get the time until a specified time.time.AfterFuncis used to execute a function after a specified duration.- Use
Equalto compare twotime.Timevalues.
Monotonic time
- Go uses a monotonic clock to measure time.
- A monotonic clock is a clock that counts up from the start of the computer.
Timers and timeouts
- Use
time.NewTickerto create a ticker that will send a value on a channel at regular intervals. - Use
time.NewTimerinstead oftime.Tick, as it can be stopped and reset.
Encoding and JSON
- "Marshalling" is the process of converting a data structure into a byte stream.
- "Unmarshalling" is the process of converting a byte stream into a data structure.
Use struct tags to add metadata
- Struct tags are strings written inside backticks that can be added to struct fields to add metadata.
- They can span only one line, taking the format
`tagName:"tagValue"`. go vetcan be used to check for struct tags validity.- If no struct tag is specified, the field name will be used instead.
- Use a
-(dash) for a field name to ignore it. ,omitemptycan be added right after the field name to omit a field if it is empty.- While annotations can make the code more declarative and short, they can also make it harder to read and understand. Go for readability first.
Unmarshalling and marshalling
json.Unmarshalis used to unmarshal a JSON byte stream into a data structure.json.Marshalis used to marshal a data structure into a JSON byte stream.
JSON, readers, and writers
json.Decoderis used to decode a JSON byte stream into a data structure.json.Encoderis used to encode a data structure into a JSON byte stream (e.g., encoding a file given an interface:json.NewEncoder(tmpFile).Encode(toFile)).json.NewDecoderis used to create a new decoder (e.g., decoding a file given an interface:json.NewDecoder(tmpFile2).Decode(&fromFile)).json.NewEncoderis used to create a new encoder.
Encoding and Decoding JSON Streams
json.Decoderandjson.Encodercan be used to encode and decode JSON streams.
Custom JSON Parsing
json.Unmarshaleris used to implement custom JSON parsing.json.Marshaleris used to implement custom JSON marshalling.
net/http
- As a modern language, Go has a built-in HTTP client and server.
- Third-party libraries of interest in this space include chi and Gorilla Mux for routing needs and alice to deal with middleware chaining.
Client
- Don't use the default client in production.
- Don't use the functions to make HTTP requests directly in production as they don't have timeouts.
Server
http.ListenAndServeis used to start a server.- The middleware pattern is used to add functionality to a server, so as to check for authentication, logging, etc.
The Context
- Context is not a new feature: it is an instance that meets the
context.Contextinterface and gets passed around as the first argument to functions, which is usually namedctx. context.TODOis used when a context is required but there is no context available. It shouldn't be used in production.
Cancellation
context.WithCancelis used to create a context that can be cancelled.- When a cancellable context is created, a
cancelfunction is returned. It is used to cancel the context and it must be called (at least once) usingdefer.
Timers
A server can do a few things to manage its load:
- It can limit the number of concurrent requests it accepts.
- It can be done by limiting the number of goroutines.
- It can limit the number of requests queued up.
- This can be handled with a buffered channel.
- Limit the amount of time a request can take.
- The context can be used to do this.
context.WithTimeoutis used to create a context that will be cancelled after a specified duration.context.WithDeadlineis used to create a context that will be cancelled at a specified time.
- Limit the resources a request can use (memory, disk space...).
- There is no built-in solution for this in Go.
Values
context.WithValueis used to create a context with a value.
Writing Tests
Basics of testing
- The
testingpackage is used to write tests. go testis used to run tests and generate reports.- Tests are located in files with the suffix
_test.goin the same package as the code being tested, so they have access to unexported identifiers. - Test functions start with
Testand take a*testing.Tparameter. They do not return any value. - The test function name should be descriptive. It can start with
Test_to indicate the function under test is unexported.
Reporting failure
- Use
t.Errorort.Errorfto report a failure and continue the test. Use it to conveniently report as many failures as possible. - Use
t.Fatalort.Fatalfto report a failure and stop the test. Use it when subsequent tests on the same function will fail or trigger apanic. - Use
t.Logto log information about the test.
Setting up and tearing down
TestMainis used to set up and tear down tests.- It is called once before and after all tests, not between each test.
- Can be used to set up and tear down a database, for instance.
- It can be used when package-level variables need to be initialized, although this probably means the code needs refactoring.
- It takes a
*testing.Mparameter. Setup can be done at the beginning, thenexitVal := m.Run()is called to run tests, then teardown can be done at the end, returning the exit value withos.Exit(exitVal).
- Individual test functions receive a
*testing.Tparameter, which has aCleanupmethod that can be used to clean up after a test.- The
Cleanupmethod is similar to thedeferstatement but can be useful is the cleanup actions are performed for multiple tests from a helper function.
- The
Storing sample test data
- A directory named
testdatacan be created to store sample test data for the package under test, as the package directory is used for the currently working directory. - Each package accesses its own
testdatadirectory, so it is possible to have multipletestdatadirectories in a project: up to one per package.
Caching test results
go testcaches test results to speed up subsequent runs.- Tests recompile if the source code or the data in
testdatachanges. go test -count=1can be used to disable caching.
Testing your public API
- Create a directory
packagename_testto test the public API of a package, to be found at the same directory level aspackagename. - Test files in this case will be named
packagename_public_test.go. - Test your public API, not your implementation.
- This is used to test the exported functions and methods, not the unexported ones.
Use go-cmp to compare test results
- It will output the differences between the expected and actual values in a human-readable format.
- It can be used to compare for strict equality as well as with custom comparators.
Table tests
- Table tests are used to test a function with multiple inputs and outputs.
- Table-driven tests are idiomatic in Go.
- A slice of an anonymous struct is used to store the test cases.
- The test function iterates over the test cases and runs the test for each of them.
Given the following:
package adder
import (
"testing"
)
func Add(a, b int) int {
return a + b
}
func TestAdd(t *testing.T) {
// Table of test cases
tests := []struct {
name string
a, b int
expected int
}{
{"Positive integers", 1, 2, 3},
{"Negative integers", -1, -1, -2},
{"Two zeros", 0, 0, 0},
{"Negative and positive", -1, 1, 0},
{"Two large integers", 100, 200, 300},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := Add(test.a, test.b)
if result != test.expected {
t.Errorf("Add(%d, %d) = %d; want %d", test.a, test.b, result, test.expected)
}
})
}
}
The output would look like this:
=== RUN TestAdd
=== RUN TestAdd/Positive_integers
--- PASS: TestAdd/Positive_integers (0.00s)
=== RUN TestAdd/Negative_integers
--- PASS: TestAdd/Negative_integers (0.00s)
=== RUN TestAdd/Two_zeros
--- PASS: TestAdd/Two_zeros (0.00s)
=== RUN TestAdd/Negative_and_positive
--- PASS: TestAdd/Negative_and_positive (0.00s)
=== RUN TestAdd/Two_large_integers
--- PASS: TestAdd/Two_large_integers (0.00s)
--- PASS: TestAdd (0.00s)
PASS
ok github.com/{USER}/{PROJECT}/adder 0.368s
Checking code coverage
- The
-coverflag can be used to check code coverage. go test -coverprofile=coverage.outis used to generate a coverage profile.- Go ships with a very cool tool to visualize the coverage profile:
go tool cover -html=coverage.out.
Benchmarks
- The built-in testing framework can be used to write benchmarks.
- Benchmarks are functions that start with
Benchmarkand take a*testing.Bparameter. - See Profiling Go programs with pprof for more information on profiling Go programs.
Stubs
- We can using function types and interfaces to create stubs.
- A test function can implement an interface and be passed to the function under test.
- A stub can be defined as a struct that implements the interface and has a field for each method of the interface. E.g.,
type MathSolverStub struct {}. - When testing larger interfaces, one can define a stub that implements only the methods required for the test.
- For mocks, use a mocking library such as gomock or testify.
httptest
httptestis used to test HTTP servers without having to start them.- A complete, real-world example is provided in the
test_examplesrepo.
Integration tests and build tags
- Integration tests are used to test the interaction between multiple components.
- A build tag is a comment that starts with
// +buildand is followed by a tag name, found on the first line of a file. - Files with no build tags are included in all builds.
- If a file has a build tag like
// +build integration, then it can be run withgo test -tags=integration -v ./.... - To skip tests that take a long time to run, use
t.Skip.
func TestFileLen(t *testing.T) {
if testing.Short() {
b.Skip("skipping test in short mode.")
}
// ...
}
// Skip it when testing in short mode
// go test -short -v ./...
Finding concurrency problems with the race checker
- A data race is still possible in Go with its built-in concurrency features if a lock hasn't been acquired.
- Go comes with a race checker for just these cases:
go test -race. - Adding "sleep" statements is definitely not the correct approach.
- The race checker can also run after building a binary:
go build -race. This is useful to detect race condition issues for code that is not covered by tests. - Note that the race checker makes the code about 10 times slower, so use it only when needed.
Here There Be Dragons: Reflect, Unsafe, and Cgo
- Theses features are not used that often, but they are useful to know about.
- You cannot make make methods with reflection.
- It should only be used when there is no other way to do it.
- It may increase maintenance cost, because crashes can happen in production due to the lack of type safety (Java cough Script cough).
- This summary will only cover their starting point.
Reflection to work with types at runtime
- This can be used to work with data that didn't exist at compile time.
- Use cases:
- Reading and writing from a database;
- Template engines;
fmtuses it heavily;errorsuses it to implementerrors.Isanderrors.As;sortuses it to sort slices of arbitrary types;- Marshalling/unmarshalling JSON and XML;
- Comparing maps or slices for deep equality with
reflect.DeepEqual.
Types, kinds, and values
- A type is a description of a value's structure and behavior.
- A kind is a description of a type's behavior.
- A value is a representation of a type's behavior.
- A type can have multiple kinds.
- A value can have multiple types.
reflect.TypeOfis used to get the type of a value.reflect.Typeis used to represent a type.
Making new values
reflect.Newis used to create a new value of a type (reflect.Typeas input,reflect.Valueas output).
Use reflection to check if an interface's value is nil
IsValidis used to check if a value is valid (e.g.,iv := reflect.ValueOf(i),iv.IsValid(),iv.IsNil()).
Use reflection to write a data marshaler
reflect.ValueOfis used to get the value of a field.reflect.Typeis used to get the type of a field.reflect.StructFieldis used to get the field's metadata.reflect.StructTagis used to get the struct tag.- There is a complete example of a CSV data marshaler on the Go Playground.
Build functions with reflection to automate repetitive tasks
reflect.MakeFuncis used to create a function.reflect.ValueOfis used to get the value of a function.reflect.TypeOfis used to get the type of a function.- Reflection makes the program slower.
You can build structs with reflection, but don't
reflect.StructOfis used to create a struct.
unsafe... is unsafe
- It allows manipulating memory directly.
Sizeofis used to get the size of a type ("returns how many bytes it uses").Offsetofis used to get the offset of a field ("returns the number of bytes from the start of the struct to the start of the field").Alignofis used to get the alignment of a field ("returns the byte alignment it requires").unsafe.Pointeris used to convert a pointer to a pointer of a different type. Pointer arithmetic is possible just like in C or C++.
Use unsafe to convert external binary data
unsafeis used to convert external binary data.- It can be use to gain performance when interacting with the system.
- It can speed up marshalling and unmarshalling (about twice as fast for simple structs).
unsafe strings and slices
reflect.StringHeaderis used to get the header of a string.
cgo is for integration, not performance
- It is best used to integrate with C libraries.
cgois the FFI (foreign function interface) of Go.- You can call C functions from Go... and even Go functions from C!
- Garbage collection makes it hard to use
cgofor performance. - Only use it when there's no suitable Go library available.
A Look at the Future: Generics in Go
- Go doesn't convert types implicitly.
- For a gentle introduction to the topic, there is Tutorial: Getting started with generics.
Generics reduce repetitive code and increase type safety
- Generics are akin to "type parameters".
- Without generics, Go has to use
interface{}and type assertions. anyis used to represent any type.- Generics allow specifying the type of a generic function's parameters, such as
func Sum[T any](a, b T) T { return a + b }.
Use type lists to specify operators
- A "type list" is a list of types.
type BuiltInOrdered interface {
type string, int, int8, int16, int32, int64, float32, float64,
uint, uint8, uint16, uint32, uint64, uintptr
}
Salient takeaways
- Go is a practical language, valuing clarity of intent and readability (e.g., standard formatting is mandatory). It takes the best of other languages and leaves out the rest.
- Comprehensibility and explicitness is more important than conciseness in idiomatic Go.
- Go is "call by value", meaning it makes copies of function parameters before passing them along.
- Deployment is a breeze: a single binary file.
- Go doesn't have classes nor inheritance, but it has structs and interfaces.
- Professionals use error handling profusely to make their programs more robust.
Conclusion
This book achieves its goal of teaching readers how to write idiomatic Go code that leverages the strengths of the language. It focuses on real-world examples and best practices to structure Go code, rather than just explaining language syntax. The book covers a wide range of topics including primitive types, control structures, composite types like arrays and maps, concurrency, reflection, testing, and more.
We covered the key takeaways from each chapter, providing a broad overview of important concepts. While not a replacement for reading the book in its entirety, this summary may serve as a helpful reference guide on the subject. Whether you are just starting with Go or are looking to improve your skills, Learning Go is an invaluable resource and I highly recommend it!
Resources and references
Articles
- Evaluating code complexity triggers, use of complexity measures and the influence of code complexity on maintenance time, Springer
- Go Modules: v2 and Beyond, go.dev
- Go Release History, go.dev
- Marshalling, Wikipedia
- Profiling Go programs with pprof, Julia Evans
Books
- Concurrency in Go, Katherine Cox-Buday, O'Reilly
- Learning Go, Jon Bodner, O'Reilly
- The Art of Concurrency, Clay Breshears, O'Reilly
- The Go Programming Language, Alan A. A. Donovan and Brian W. Kernighan, Addison-Wesley Professional
Documentation
atof.go, cs.opensource.googlebuiltindocumentation, pkg.go.dev- CodeReviewComments, GitHub
- Effective Go, go.dev
- ErrGroup, pkg.go.dev
- Go By Example, gobyexample.com
- Go Wiki, GitHub
- How to Write Go Code, go.dev
- Standard library documentation, pkg.go.dev
- The Go Programming Language Specification, go.dev
Other open source projects referenced
- Alice, GitHub
- Chi, GitHub
- go-cmp, GitHub
- Gorilla Mux, GitHub
- Learning Go: Code examples, GitHub
- Mod, GitHub
Testing
- gomock, GitHub
- Profiling Go programs with pprof, Julia Evans
test_examplesrepo, GitHubtestingpackage, pkg.go.dev- testify, GitHub