Go
[[Go/Code Review]]
[[Templ]]
Learning resources:
- Concurrency in Go
- Ultimate Go Programming, Second Edition
- Learning Go
- Learn Go with Tests - TDD exercises
- 100 Go Mistakes and How to Avoid Them
Setup
$GOROOT
- where your SDK is$GOPATH
- where your gosrc
,pkg
, andbin
live
Modules
got mod init gitlab.com/viktomas/{name}
go mod init
creates a new module, initializing thego.mod
file that describes it.go build
,go test
, and other package-building commands add new dependencies togo.mod
as needed.go list -m all
prints the current module’s dependencies.go get
changes the required version of a dependency (or adds a new dependency).go mod tidy
removes unused dependencies.- read about minimal version selection https://stackoverflow.com/a/70006832/606571
Build
go build -C dir -o output-file
this will build the package indir
and will produce output-file if the package ismain
package. It will follow all the imports and will build those packages as well
Variables
-
Naming conventions
- camel case -
seasonNumber
- acronyms all uppercase -
theURL
- first letter case decides public/private
- longer names for longer lifespan
- camel case -
-
Declaration
var foo int
- declare in different scope than where we assign, or just want to create zero valuevar foo int = 42
- when compiler guesses wrongfoo := 42
- all the time
-
Type conversions
-
var i int = 42 var j float32 j = float32(i)
- for converting numbers to strings, you have to use
strconv
package, otherwise go will use int number as the unicode number (e.g. converting42
to*
) - go doesn’t do automatic type conversion because it might loose information
- It’s different from casting, casting is telling the runtime to trust us that there is a different type, conversion transforms one value in memory to a different representation
- You can do explicit type conversion between compatible structs
-
Primitive types
-
Boolean
- zero value is
false
- zero value is
-
Numeric types
- zero value is always
0
or equvialent -
Integer
int
- we don’t know the size, it will be at least 32, but it might be 64 bits
- also can be unsigned int with
uint
-
Float
f := 3.14
- always initialises tofloat64
- zero value is always
-
String
- collection of UTF-8 characters
- Immutable
- can be converted back and forth with
[]byte
array
Constants
const
keyword- naming is the same as variables
- they need to be set up at compile time, you can assign the result of a function call to constant
- const gets replaced with literal everywhere
iota
- scoped to a constant block
- every time we use it it’s like using
i++
starting with0
-
const ( a = iota // will be 0 b // 1 c // 2 )
- be careful, zero value is
0
so uninitialised variable is going to be equal to the first constant- solution
_ = iota
orerrorState = iota
- solution
iota
should be used only for constants where the numerical representation doesn’t matter and is not stored-
Don’t use iota for defining constants where its values are explicitly defined (elsewhere). For example, when implementing parts of a specification and the specification says which values are assigned to which constants, you should explicitly write the constant values. Use iota for “internal” purposes only. That is, where the constants are referred to by name rather than by value. That way you can optimally enjoy iota by inserting new constants at any moment in time / location in the list without the risk of breaking everything.
- Danny van Heumen
-
Arrays
a := [3]int{97, 85, 93}
- shorthand
a := [...]int{97, 85, 93}
- shorthand
len(a)
to find out the length- arrays are values
- copied during re-assignment
- passed as values to functions
Slices
a := []int{97, 85, 93}
- same initialisation as an array, but without specifying the length
- Slicing operations
-
a := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} b := a[:] // slice of all elements c := a[3:] // slice from 4th element (index 3 inclusive) d := a[:6] // slice first 6 elements (index 6 exclusive) e := a[3:6] // slice the 4th, 5th, and 6th elements
- remove first element
a[1:]
- remove last element
a[:len(a)-1]
- remove first element
-
make([]int, len, cap)
- initialise slice withlen
length andcap
capacityappend(arr, el, el1, el2)
appends element to the slice, it makes the underlying array larger if needs beappend
can use “spread” operator to append another array:a = append(a, []int{1,2,3,4}...)
- all stack operations are manipulating one underlying array
Maps
m := map[string]int{"a": 1, "b": 2}
- key type has to be testable for equality, struct, map and function can’t be a key
val := myMap["key"]
val, ok := myMap["key"]
- we can check if the key exists,ok
is convention- you can use
len()
delete(myMap, "key")
Structs
-
type Doctor struct { number int actorName string companions []string } d := Doctor{ number: 3, actorName: "John Doe", companions: []string{"a", "b"} } //or positional syntax (not recommended) d = Doctor{3, "John Doe", []string{"a", "b"}}
- Anonymous struct:
a := struct{name: string}{name: "Tomas"}
- Structs are passed by value
- Embedding
type Bird struct {Animal, canFly: bool}
- I need to be aware of the embedding when creating an instance:
bird := Bird{Animal: Animal{}, canFly: true}
- Embedding is a good idea when we want consumers of our libraries to embed a complicated structure rather than extend an interface.
- I need to be aware of the embedding when creating an instance:
- Tags
-
type Animal struct { Name string `required max:"100"` Origin string } t := reflect.TypeOf(Animal{}) field, _ := t.FieldByName("Name") field.Tag
-
-
When not working with JSON (or other external protocols), resist the temptation to use a pointer field to indicate no value. While a pointer does provide a handy way to indicate no value, if you are not going to modify the value, you should use a value type instead, paired with a boolean.
- John Bonder - Learning Go
- Internal implementation introduces another two terms: Alignment and Padding
- alignment and padding dictate how is the struct stored in memory, depending on the size of the word. It is a mechanism to avoid storing a value over two words, which would mean doubling the memory access for working with that value
-
Embedding
- You can embed a struct in another struct, then you can access the child fields and methods through the parent:
-
type user struct { name string email string } type admin struct { user accesLevel string }
-
- You can embed a struct in another struct, then you can access the child fields and methods through the parent:
If statement
- Initializer:
-
if val, ok := myMap["key"]; ok { fmt.Println(val) }
- The variables are scoped to the
if
block
- The variables are scoped to the
-
- Short circuiting - when evaluating
||
go stops evaluating after the firsttrue
,&&
stops evaluating after the firstfalse
Switch statement
-
switch <tag> { case 1, 5, 10: fmt.Println("one, five, or ten") default: fmt.Println("not one") }
- You can use initialiser as well
- No braces, any statements between the case and the next case are part of the block
- Tagless syntax:
- each
case
can have a full comparisoncase i < 10:
- each
- Implicit break between cases, you can force immediate execution of the next case (without checking the condition) by using the
fallthrough
keyword - Type switch:
switch i.(type) {}
- Break early with
break
For statement
for i:=0;i<10;i++
for i, j := 0, 0; i<5; i, j =i+1, j+1
- TODO
i++
is a statement, not an expression - investigate more about this design choice - we can omit the initialiser
for ; i<10;i++
or even the continuation statementfor ;i<10;
(which is effectively a while loop)- syntax sugar for while loop:
for i<10
(omitting the semicolons)
- syntax sugar for while loop:
for { break }
for when we don’t know when to finishcontinue
- skip this iteration and start over with the next loop iteration- we can label which statement we want to break out of
-
Label: for i := 0;i<5;i++ { for j := 0;j<5;j++ { if i*j > 10 { break Label } fmt.Println(i*j) } }
-
- range :
for k,v := range s {}
to loop over arrays, slices, and maps
Defer
defer
moves statement after the function is done, but before it returns any results to the calling function-
a := "start" defer fmt.Println(a) a = "end"
- Will print “start” because
defer
evaluates the argument the moment it is called
- Will print “start” because
panic
happens afterdefer
statements have been executed- we can pass anonymous function to the
defer
statement
Panic/Recover
panic(error)
err := recover()
-
defer func(){ err := recover() handleError() }()
-
Pointers
var a *int
- declare pointer&
- “address of” operator -a := &b
*
- “dereferencing” operatora := *b
- lower precedence than the
.
operator =>(*a).val
- lower precedence than the
- pointer arithmetic available in the
unsafe
package new(myStruct)
- we get back a pointer to an empty struct- compiler automatically dereferences before using the
.
operator(*a).val
==a.val
- When function returns pointer to a local variable, the Go runtime has to copy the value on the heap
Functions
- You can pass arguments by reference or by value
- maps and slices are always passed by reference
func sum(values ...int) int {}
Variadic parametervalues
is a slice
- You can return local variable as a pointer
func sum(values ...int) (result int){}
- named return variable
result
- named return variable
func divide(a float64, b float64) (float64, error)
- we can return error object as the second parameter
-
Anonymous functions
- best practice is to pass in the variable from outer scope as arguments
-
func(a){ fmt.Println(a) }()
-
var divide func(float64, float64)(float64, error)
- best practice is to pass in the variable from outer scope as arguments
Methods
- functions that are executed on a know context (structs, primitive types)
func (g greeter) greet() {}
g greeter
is a value receiver- Use value receiver if all methods can use value receivers - that means they don’t need to mutate data and don’t work with synchronisation primitives
g *greeter
is a pointer receiver- Use pointer receiver if even a single method needs to use pointer receiver
- Mixing the two is bad, because the type dictates whether it’s a value or a reference (user is a reference, time is a value) then you should respect the type and use its semantics even if you would otherwise use the other option
Interface
- Interface should represent behaviour,
->Animal
Barker
- Interface value is internally represented as a pair of pointers:
- Go interfaces, the tricky parts from Tim Ruffles explains it really well
-
type Writer interface { Write([]byte) (int, error) }
- Naming convention is that if there’s only one method, we name the interface with the name of the method +
er
e.g.Write
=>Writer
- When I want to add methods to a type, I have to have a control over it in my package.
type Counter int
- Composing same as struct embedding
-
type WriterCloser interface { Writer Closer }
-
t, ok := val.(TypeToConvertTo)
- type conversion for interfacesinterface{}
- empty interface- Receiver trickery:
- if the interface is stored as pointer, it can use both value and pointer receivers
- if the interface is stored as a value, it can only use value receivers
Printing
%v
- default printer for the value%+v
- prints keys as well%#v
- prints the full go syntax (invokes thego.Stringer
interface if the value implements it)%[1]v
- refers to the first argument (1 indexed)
Errors
- Internally, error is implemented with
errorString
in theerrors
package fmt.Errorf("Error happened %w")
-%w
format verb wraps the error so it can be later unwrapped:- When you handle multiple errors with the same treatment (e.g. prepending the same message) you can use
defer
with named return value:- Before:
collapsed:: true
-
func DoSomeThings(val1 int, val2 string) (string, error) { val3, err := doThing1(val1) if err != nil { return "", fmt.Errorf("in DoSomeThings: %w", err) } val4, err := doThing2(val2) if err != nil { return "", fmt.Errorf("in DoSomeThings: %w", err) } result, err := doThing3(val3, val4) if err != nil { return "", fmt.Errorf("in DoSomeThings: %w", err) } return result, nil }
-
- After:
collapsed:: true
-
func DoSomeThings(val1 int, val2 string) (_ string, err error) { defer func() { if err != nil { err = fmt.Errorf("in DoSomeThings: %w", err) } }() val3, err := doThing1(val1) if err != nil { return "", err } val4, err := doThing2(val2) if err != nil { return "", err } return doThing3(val3, val4) }
- `
-
- Before:
collapsed:: true
-
Error values
- You can define a “sentinel” error as
var ErrBadRequest := errors.New("Bad Request")
- This is useful if there is no need for additional context
- You can define a “sentinel” error as
-
Using type to provide more context
- course material
- Type assertion switch:
-
if err != nil { switch e := err.(type) { case *UnmarshalTypeError: fmt.Printf("UnmarshalTypeError: Value[%s] Type[%v]\n", e.Value, e.Type) case *InvalidUnmarshalError: fmt.Printf("InvalidUnmarshalError: Type[%v]\n", e.Type) default: fmt.Println(err) }
-
Performance
-
Garbage collection
-
Performance drains
- Performance is most affected by these categories (in order)
- Latency - IO Latency - network, disc, memory access
- Memory allocations (see garbage collection above)
- data access
- algorithm efficiencies
- Performance is most affected by these categories (in order)
-
Profiling
- add this snippet to your main
-
f, err := os.Create("cpuprofile") if err != nil { fmt.Fprintf(os.Stderr, "could not create CPU profile: %v\n", err) os.Exit(1) } if err := pprof.StartCPUProfile(f); err != nil { fmt.Fprintf(os.Stderr, "could not start CPU profile: %v\n", err) os.Exit(1) } defer pprof.StopCPUProfile()
-
- Then inspect the result with
-
go tool pprof -http=:7777 cpuprofile
-
- add this snippet to your main
Design values
- Write less code, for every 20 lines of code you’ll introduce one bug
Examples of hiding complexity
dotGo 2015 - Rob Pike - Simplicity is Complicated - YouTube
- Garbage collection - not even included in the spec
- Import statement - simple string hides a large complexity
go
const
- the typing ofconst
numbers into arbitrary precision variables