golang
Tags :: Language
Install
See site if not on mac w/ homebrew, otherwise
brew install go
Access all tools with go
.
- compiler:
go build
- code formatter:
go fmt
- dependency manager:
go mod
- test runner:
go test
- mistake scanner:
go vet
Hello World
Create a new project (module) with go mod init hello_world
. A module contains both source code and an exact specification of the dependencies of the code within the module.
Every module has a go.mod
file in its root directory, which declares the nam e of the module, the minimum supported version of Go, and any other modules the module depends on. The tools go get
and go mod tidy
are used to manage and make changes to the file.
Types
Called predeclared types.
Zero
Assigns a default zero value to any variable that is declared but not assigned a value.
Literals
Explicitly specified number, character or string. There are 4 common and 1 uncommon (complex numbers) literal.
Integer
Base 10 by default, but different prefixs are used to indicate other bases. E.g. 0b
for binary, 0o
for octal, etc.
Floating-point
decimal point to indicate fractional portion, and can also have an exponent.
Rune
Represents a character and is surrounded by single quotes. Single and double quoutes are not interchangable.
String
Two ways to create. Interpreted string literals are created with double quotes. These contain zero or more rune literals. The other way is with a raw string literal, delimted with backquotes (`
) and can contain any character except a backqoute.
Literals are considered untyped.
Booleans
Either true
or false
, with the zero value being false
.
var flag bool // no assignment, set to false
var ff = true
Special integer types
byte
alias foruint8
, withbyte
being the more commonly used.int
declares an signed integer with the size of the “word” for a given archtecture. Operations can’t be preformed with other integer types without an explicit conversion.uint
Follows the same rule asint
, except its unsigned.rune
alias for anint32
typeuintptr
Strings
The zero value is an empty string. Strings are immutable, they can be reassigned, but you cannot change the value of the string that is assigned to it.
Type conversion
Go does not support automatic type conversion. All variable types need to be converted to interact.
var x int = 10
var y float64 = 30.2
var sum1 float64 = float64(x) + y
var sum2 int = x + int(y)
fmt.Println(sum1, sum2) // 40.2 40
Slices
A slice created with no value assigned is assigned the zero value of nil
.
Slices are not comparable, execpt with ==
and nil
.
var x []int
fmt.Println(x == nil) // prints true
The slices package in the stdlib includes the two methods to compare slices:
slices.Equal
takes two slices and returnstrue
if the slices are the same length AND all elements are equal. Additionally all of the elements of the slice must be comparable.slices.EqualFunc
takes a function to determine equality, and does not require slice elements to be comparable.
x := []int{1, 2, 3, 4, 5}
y := []int{1, 2, 3, 4, 5}
z := []int{1, 2, 3, 4, 5, 6}
//s := []string{"a", "b", "c"}
fmt.Println(slices.Equal(x, y)) // prints true
fmt.Println(slices.Equal(x, z)) // prints false
//fmt.Println(slices.Equal(x, s)) // does not compile
append
built in function for growing slices. Can be used with
nil
slices- slices with existing elements
- to append multiple elements
- to append slices into slices with
...
var x[] int
x = append(x, 10)
var x[] int{1, 2, 3}
x = append(x, 4)
x = append(x, 5, 6, 7)
y := []int{20, 30, 40}
x = append(x, y...)
NOTE: it is a compile time error to not assign the value returned from append. This is because golang calls by value. So everytime you pass a parameter to a function, go makes a copy of the value that’s passed in.
Slice capacity and memory
Each element in a slice is a assigned to consecutive memory locations which makes read + write quick. Every slice then has a capacity, the number of consecutive memory locations reserved. This can be larger than the length.
Each time a slice is appended to, one or more values is added to the end of the slice and each value increases the length by one. When length reaches the capacity there’s no more room to put values, and when trying to append
new values the following happens:
append
will use the go runtime to allocate a new backing array with a larger capacity.- The values in the original backing array are copied to the new one
- The new values are added to the end of the new backing array
- The slice is updated to refer to the new backing array
- The updated slice is returned
The rules for growing capacity is to double capacity when it is less than 256, while a bigger slice increases by (current capacity + 786)/4 and slowly converges at 25% growth. E.g. a slice with cap of 512 will grow by 63% while a slice with capacity 4,096 will grow by only 30%.
creating an empty slice with length n
The built-in make
function is used to declare an empty slice with a specified type, length, and capacity (optional).
x := make([]int, 5)
The initalized values will be set to zero.
The capacity can be set like
x := make([]int, 5, 10)
which creates a slice with length 5, and capacity 10.
A slice with an empty length and a capcity greater than 0 can also be declared.
x := make([]int, 0, 10)
x = append(x, 5,6,7,8)
emptying a slice
go 1.21 adds a clear
function that takes in a slice and sets all of the slices elements to their zero value. The length remains unchanged.
s := []string{"first", "second", "third"}
fmt.Println(s, len(s))
clear(s)
fmt.Println(s, len(s))
Declaring
Given the primary goal is to minimize the number of times the slice needs to grow, what is the best way to declare a slice?
If you need a slice that won’t need to grow at all, use a var
declaration to create a nil
slice (NOT a slice literal). Otherwise, if you have starting values that aren’t going to change, then a slice literal is a good choice.
var data []int // nil slice
data := []int{2, 4, 6, 8} // slice literal
What if you have a good idea of how big it needs to be but don’t know what values will be when writing the program, use make
. The then becomes if you should specify a nonzero length in the call to make or specify a zero length and a nonzero capacity. There are three common possibilities.
- if using slice as a buffer, then specify a nonzero length.
- if sure you know the exact size you want, specify the legnth and index into the slice to set the values.
- often done when transforming values in one slice and storing them in a second. However, if you have the size wrong, you’ll end up with zero values at the end of the slice or a panic from trying to access elements that don’t exist.
- Other situations, use
make
with a zero length and a specified capacity. Allows you to use append to add items to the slice. If the number of items turns out to be smaller, you won’t have extraneous zero values at the end. and if the number of items is larger, the code will not panic.
Slicing slices
A slice expression gives a slice from a slice. It’s inside brackets and consists of a starting offset and an ending offset, seperated by a colon (:). Think numpy style.
Slicing in Go is exclusive. The starting offset is the first position in the slice that is included in the new slice, and the ending offset is one past the last position to include. E.g. [0:2] will give you index at 0 and 1.
x := []string{"a", "b", "c", "d"}
y := x[:2] // same as x[0:2]
z := x[1:]
d := x[1:3]
e := x[:]
fmt.Println("x:", x)
fmt.Println("y:", y)
fmt.Println("z:", z)
fmt.Println("d:", d)
fmt.Println("e:", e)
When taking a slice, you are not making a copy of the data. Instead you now have two variables that are sharing memory. Thus a change to an element in slice will effect all slices that share that element.
x := []string{"a", "b", "c", "d"}
y := x[:2] // same as x[0:2]
z := x[1:]
x[1] = "y"
y[0] = "x"
z[1] = "z"
fmt.Println("x:", x)
fmt.Println("y:", y)
fmt.Println("z:", z)
This gets particulary confusing with append
. Whenever you take a slice from another slice, the subslices capacity is set to the capacity of the original slice, minus the starting offset of the subslice within the original slice. Thus elements of the original subslice beyond the end of the subslice are shared by both slices.
When you make a y
slice from x
, the length is set to 2, but the capacity is set to 4; the same as x
. Thus appending onto the end of y
puts the value in the third position of x
.
There are a couple ways to prevent from overwriting data with subslices.
- don’t use
append
with subslices. - use a full slice expression to make sure
append
doesn’t cause an overwrite.
-
Full slice expression
This includes a third part in the slice which indicates the last position of the parent’s slice capacity that’s available for the subslice.
x := make([]string, 0, 5) x = append(x, "a", "b", "c", "d") y := x[:2:2] yy := x[:2:3] z := x[2:4:4] fmt.Println("x:", x) fmt.Println("y:", y, cap(y), len(y)) fmt.Println("yy:", yy, cap(yy), len(yy)) fmt.Println("z:", z, cap(z), len(z))
-
Copying slices
Go has a built in
copy
function to create dependent slices. It takes two params, the destination slice and the source slice. The function copies as many values as it can from the source to destination, limited by whichever slice is smaller and returns the number of elements copied.NOTE:The capacity of
x
andy
do not matter, its the length that’s important.A subset of a slice can also be copied
x := []int{1,2,3,4} y := make([]int, 2) num := copy(y,x) // first two elements are copied copy(y, x[2:]) // copies from middle of x
Copy also allows copying between two slices that cover overlapping sections of an underlying slice. E.g. the following copies the last thre values in
x
on top of the first three values ofx
.x := []int{1,2,3,4} num := copy(x[:3],x[1:]) fmt.Println(x, num)
Copy can also be used to taking a slice of an array, and it can either be the destination or the source of the copy.
x := []int{1, 2, 3, 4} d := [4]int{5, 6, 7, 8} // array y := make([]int, 2) copy(y, d[:]) fmt.Println(y) copy(d[:], x) fmt.Println(d)
-
Converting arrays to slices
An array can have a slice taken using a slice expression, providing a useful way to bridge an array to a function that takes only slices. The
[:]
syntax is used to convert an entire array to a slice:xArray := [4]int{5, 6, 7, 8} xSlice := xArray[:]
Or a subset of an array can be sliced
x := [4]int{5, 6, 7, 8} y := x[:2] z := x[2:]
NOTE: Taking a slice from an array has the same memory sharing properties as taking a slice from a slice.
-
Converting slices to arrays
Type conversion is used to make an array from a slice. This cna either be an entire slice to an array of the same type, or an array from a subset. When an array is created from a slice, the data in the slice is copied into new memory.
xSlice := []int{1, 2, 3, 4} xArray := [4]int(xSlice) smallArray := [2]int(xSlice) xSlice[0] = 10 fmt.Println(xSlice) fmt.Println(xArray) fmt.Println(smallArray)
NOTE: The size of the array must be specified at compile time. It is a compile-time error to use
[...]
in a slice to array type conversion.The size of the array can be smaller than the slice, but not bigger. The compiler does not check this and the code will panic at run time.
-
slice to pointer to an array
A type conversion can be used to convert a slice into a pointer to an array
xSlice := []int{1, 2, 3, 4} xArrPtr := (*[4]int)(xSlice)
This allows the storage between the two to be shared, so a change to one will change the other
-
Maps
Map type is written as map[keyType]valueType
. These can be declared with var
to create a map set to its zero value with length zero, and reading it will always return teh zero value for the maps type. Writing to a nil
map causes a panic.
// map with string keys and int values
var nilMap map[string]int
The :=
declaration can create a map variable by assigning it a map literal
totalWins := map[string]int{}
This is not the same as a nil
map. Its length is 0, but it can be read and written to.
teams := map[string][]string {
"Orcas": []string{"Fred", "Ralph", "Bijou"},
"Lions": []string{"Sarah", "Peter", "Billie"},
"Kittens": []string{"Waldo", "Raul", "Ze"},
}
If the number of key value pairs needed are known, but don’t know the exact size make
can be used to create a map with a default size
ages := make(map[int][]string, 10)
The length is still zero and they can grow past the initially specified size.
Structs
Typed defined with type
keyword, the name of the struct type, the keyword struct
and braces.
type person struct {
name string
age int
pet string
}
bob := person{}
julia := person{
"julia",
40,
"cat",
}
beth := person{
age: 30,
name: "Beth",
}
Fields are accessed with the dot notation:
type person struct {
name string
age int
pet string
}
bob := person{}
bob.name = "bob"
fmt.Println(bob.name)
anonymous structs
A variable can implement a struct type without giving the name
pet := struct {
name string
kind string
} {
name: "Fido",
kind: "dog",
}
These are useful for marshaling and unmarshaling data (e.g. Json and protocal buffers)
Runtime
Provides services like memory allocation and garbage collection, concurency support, networking, and implementations of built-in types and functions. It is compiled into every go binary.
This is different from languages which rely on a virtual machine. While a VM must be installed seperatly to allow programs to function, including the runtime in the binary makes it easier to distribute go programs without worrying about compabatility.
The drawback is that the runtime results in a larger binary, approx. 2MB for the simplest go program.
Shadowing
This is a variable that has the same name as a variable in a containing block. For as long as the shadowing variable exists, you cannot access a shadowed variable.
x := 10
if x > 5 {
fmt.Println(x)
x := 5
fmt.Println(x)
}
fmt.Println(x)
if
Like if statements in other languages, but without parenthesis around statements.
n := rand.Intn(10)
if 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)
}
Howver, go adds the ability to declare variables that are scoped to the condition and to both the if and else blocks.
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)
}
This is handy for variables that only need to be created where they need to be available.
For
There are four ways to use the for
keyword:
- complete, c-style
- condition only
- infinite
- for-range
complete
for i := 0; i < 10; i++ {
fmt.Printf("%d ", i)
}
NOTE: :=
must be used to initialize variables, var
is not legal. Shadowing is legal.
One or more of the three parts of the statement can be left out. Most common is to leave off the initialization if it is based on a value calculated before the loop, or the increment will be left off becuase of a more complicated increment rule within the loop.
condition only
This leaves off both the initialization and increment and operates like while
statement in other languages.
i := 1
for i < 100 {
fmt.Printf("%d ", i)
i = i * 2
}
infinite
Leaves off everything
for {
fmt.Println("Hello")
}
Go includes both the break
and continue
keywords for dealing with loops.
for-range
Resembles iterators from other languages, and can be used with strings, arrays, slices, and maps.
With a for-range loop you get two variables, the position in the data struct being iterated, and the value at that position.
evenVals := []int{2, 4, 6, 8, 10, 12}
for i, v := range evenVals {
fmt.Println(i, v)
}
If you don’t need the key/index in, the underscore (_) can be used, telling go to ignore the value.
evenVals := []int{2, 4, 6, 8, 10, 12}
for _, v := range evenVals {
fmt.Printf("%d ", v)
}
If you just need the key/index, you can leave off the second variable:
uniqueNames := map[string]bool{"Fred": true, "Raul": true, "Wilma": true}
for k := range uniqueNames {
fmt.Printf("%s ", k)
}
Switch
words := []string{"a", "cow", "smile", "gopher",
"octopus", "anthropologist"}
for _, word := range words {
switch size := len(word); size {
case 1, 2, 3, 4:
fmt.Println(word, "is a short word!")
case 5:
wordLen := len(word)
fmt.Println(word, "is exactly the right length:", wordLen)
case 6, 7, 8, 9:
default:
fmt.Println(word, "is a long word!")
}
}
Functions
Does not have named or optional parameters
Variadic parameters
These are indicated with ...type
and must be the last|only parameter. The variable thats created is a slice of the specified type.
func addTo(base int, vals ...int) []int {
out := make([]int, 0, len(vals))
for _, v := range vals {
out = append(out, base+v)
}
return out
}
Multiple return values
Go allows multiple return values. The return types are listed in parentheses, seperated by comas. If it returns multiple values you must return all of them.
func divAndRemainder(num, denom int) (int, int, error) {
if denom == 0 {
return 0, 0, errors.New("cannot divide by zero")
}
return num / denom, num % denom, nil
}
Typically this is used to return an error if something goes wrong in a function. By convention, the error is always last (or only) value returned.
The above is used like
result, remainder, err := divAndRemainder(5, 2)
if err != nil {
fmt.Println(err)
os.exit(1)
}
fmt.Println(result, remainder)
Each value must be returned from a function, if you try to assign multiple values to one variable, you get a compile time error.
named return values
Functions can have names specified for the return values, which predeclares variables that are used within the function to hold the return values. These values are initialized to their zero values allowing you to return them before any explicit use or assignment.
func divAndRemainder(num, denom int) (result int, remainder int, err error) {
if denom == 0 {
err = errors.New("cannot divide by zero")
return result, remainder, err
}
result, remainder = num / denom, num % denom
return result, remainder, err
}
Closures
These are functions defined within a function, and are able to access and modify variables declared in the outer function. Closures allow you to pass fome function state to another function. These can also be returned from a function.
package main
import "fmt"
func main() {
a := 20
f := func() {
fmt.Println(a)
a = 30
}
f()
fmt.Println(a)
}
Anonymous function f
can read and write a
even though a
is not passed in to the function. Using :=
instead of =
inside the closure creates a new a
that ceases to exist when the closure exits.
Why use closures?
limit function scope
If a function is going to be called from only one other function, but it’s called multiple times, you can use an inner function to “hide” the called function. This reduces the number of declarations at a package level, making it easier to find an unused name.
removing logic repetetion
Given a piece of logic that is repeated multiple times within a function, a closure can be used to remove that repetition. See the following example from a jonbodner/my_lisp:
package scanner
import "github.com/jonbodner/my_lisp/types"
func Scan(s string) ([]types.Token, int) {
var out []types.Token
var curTokenTxt []rune
buildCurToken := func() {
if len(curTokenTxt) > 0 {
if len(curTokenTxt) == 1 && curTokenTxt[0] == '.' {
out = append(out, types.DOT)
} else {
out = append(out, types.NAME(curTokenTxt))
}
curTokenTxt = make([]rune, 0)
}
}
update := func(t types.Token) {
buildCurToken()
out = append(out, t)
}
depth := 0
for _, c := range s {
switch c {
case '(':
update(types.LPAREN)
depth++
case ')':
update(types.RPAREN)
depth--
case '.':
curTokenTxt = append(curTokenTxt, c)
case '\n', '\r', '\t', ' ':
buildCurToken()
case '\'':
update(types.QUOTE)
default:
curTokenTxt = append(curTokenTxt, c)
}
}
buildCurToken()
return out, depth
}
Here the scanner uses two closures: buildCurToken
and update
to make the code shorter and easier1 to understand.
passing to funcs/returning from funcs
These become really interesting, allowing you to take variables within your function and use those variables outside of the function
Passing and returning funcs from funcs
Passing
Functions are values and the type of a function is specified using its parameter and return types, thus you can pass functions as parameters into functions.
NOTE: Think about the implications of creating a closure that references local variables and then passing that closure to another function. It means a closure can be used to pass some function state to another function.
An example of this pattern is when sorting slices with the stdlib sort.Slice
, which takes in any slice and a function that is used to sort the slice that’s passed in. See the following with a simple type, Person
, that is sorted first by last name and then by age:
package main
import (
"fmt"
"sort"
)
func main() {
type Person struct {
FirstName string
LastName string
Age int
}
people := []Person{ //
{"Pat", "Patterson", 37},
{"Tracy", "Bobdaughter", 23},
{"Fred", "Fredson", 18},
}
fmt.Println(people)
// sort by last name
sort.Slice(people, func(i int, j int) bool { //
return people[i].LastName < people[j].LastName
})
fmt.Println(people)
// sort by age
sort.Slice(people, func(i int, j int) bool { //
return people[i].Age < people[j].Age
})
fmt.Println(people)
}
- First a slice, people, of type
Person
is created. - people is then sorted by last name and the results printed
- Another closure is defined to sort by
Age
following the same practice as before. - The original people slice is changed by the call to
sort.Slice
Returning
A closure can also be returned from a function. See the following which returns a mutliplier function.
package main
import "fmt"
func makeMult(base int) func(int) int {
return func(factor int) int {
return base * factor
}
}
func main() {
twoBase := makeMult(2)
threeBase := makeMult(3)
for i := 0; i < 3; i++ {
fmt.Println(twoBase(i), threeBase(i))
}
}
Returning closures is used for a variaty cases where closures are useful. Such as sorting and and searching slices. For returning closures useful examples include middleware implementations and resource cleanup with defer
.
NOTE: Functional languages such as haskell use the terminology higher-order functions to refer to a function that has a function for an input parameter or return value.
defer
Quick notes:
- can use function, method, or closure
- can
defer
multiple functions within a function- Runs in LIFO order; the last
defer
registered runs first.
- Runs in LIFO order; the last
- code within
defer
functions runs after the return statement.- input parameters supplied to
defer
are evaluated immediatly and their values are stored until the function runs.package main import "fmt" func deferExample() int { a := 10 defer func(val int) { fmt.Println("first:", val) }(a) a = 20 defer func(val int) { fmt.Println("second:", val) }(a) a = 30 fmt.Println("exiting:", a) return a } func main() { _ = deferExample() }
- input parameters supplied to
Cleanup code is attached to a the function with the defer
keyword. See the following simple version of cat.
package main
import (
"io"
"log"
"os"
)
func main() {
if len(os.Args) < 2 {
log.Fatal("no file specified")
}
f, err := os.Open(os.Args[1])
if err != nil {
log.Fatal(err)
}
defer f.Close() //
data := make([]byte, 2048)
for {
count, err := f.Read(data)
os.Stdout.Write(data[:count])
if err != nil {
if err != io.EOF {
log.Fatal(err)
}
break
}
}
}
The important thing to see in this example is using defer to close the file handle. Once there is a valid file handle, it needs to be closed after it is used, no matter how the function is exited. defer
ensurs that the cleanup code runs. It is used with the defer
keyword, followed by a function or method call. Normally a function call will run immediatly but defer
delays invocation until the surrounding function exits.
Common pattern to modify values in surrounding function
Deferred functions can examine or modify the return values of the surrounding functio. By using named return values, it can allow your code to take actions based on an error. See the following database example:
func DoSomeInserts(ctx context.Context, db *sql.DB, value1, value2 string) (err error) {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { //
if err == nil {
err = tx.Commit()
}
if err != nil {
tx.Rollback()
}
}() // compile time error to leave out the braces
_, err = tx.ExecContex(ctx, "INSERT INTO FOO (val) values $1", value1)
if err != nil {
return err
}
// use tx to do more database inserts here
return nil
}
If any of the transactions fail you want to rollback the modifications, and if any succed you want to commit the changes. The closure with defer checks if err
has been defined a value. If it hasn’t you run tx.Commit
, which could al return an err. If it does the value err
is modified. If any database interaction returned tx.Rollback
is called.
Common pattern for resource allocation
Take the cat program. It can be rewritten to use a helper function that opens a file and returns a closure.
func getFile(name string) (*os.File, func(), error) {
file, err := os.Open(name)
if err != nil {
return nil, nil, err
}
return file, func() {
file.Close()
}, nil
}
The helper returns a file, function, and an error. From main
we can use the helper like so:
f, closer, err := getFile(os.Args[1])
if err != nil {
log.Fatal(err)
}
defer closer()
Since go doesn’t allow unused variables, returning the closer
from the function means that the program will not compile if the function is not called, reminding the user to use defer.
Pointers
Standard address (&
) and indirection/dereferencing (*
) operators. Pointer tricks such as pointer arithmetic that you can do in C are not allowed in go.
Before dereferencing a pointer, you must make sure it is non-nil, otherwise the program will panic if you attempt to dereference a nil
pointer:
var * int
fmt.Println(x == nil) // prints true
fmt.Println(*x) // panics
The pointer type can be based on any type, and the built in function new
creates a pointer variable which returns a pointer to a zero-value instance of the type.
var x = new(int)
fmt.Println(x == nil) // prints false
fmt.Println(*x) // prints 0
However, the new
function is rarely used.
For structs use &
before a struct literal to create a pointer instance. When you need a pointer to a primitive type, declare a variable and then point to it.
pointers of constants / literals
Constant literals cannot use &
because they don’t have a memory address, e.g.
type person struct {
FirstName string
MiddleName *string
LastName string
}
p := person {
FirstName: "Pat",
MiddleName: "Perry", // This line will not compile
LastName: "Peterson",
}
There are two ways around the problem. The first is to introduce a variable to hold the constant. The second is to write a generic helper function that takes a parameter of any type and returns a pointer to that type:
func makePointer[T any](t T) *T {
return &t
}
which allows one to write the previous code as
p := person {
FirstName: "Pat",
MiddleName: makePointer("Perry"),
LastName: "Peterson",
}
NOTE: The only time you should use pointer parameters to modify a variable is when the function expects an interface, e.g. JSON.
Difference between maps and slices
Within the go runtime, a map is implemented as a pointer to a struct, so passing a map to a function means that you are copying a pointer. Thus changes made to a map within the functions are reflected in the original.
NOTE: maps for input parameters or return values should be carefully considered. On an API design level, they are a bad choice because they say nothing about the values contained within. In general one should prefer a struct for these cases.
A slice is more complicated. Any modification to a slices contents within a function is reflected in the original, but using append to change the length of the array is not, even if the slice has a capacity greater than its length. This is due to slice being implemented a struct with three fields:
int
for lengthint
for capacity- a pointer to a block of memory
Reference
-
Does it make it easier to track the logic? I guess it depends on experience. Should try to find a writeup discussing this. ↩︎