Notes from learning the fundamentals of the Go programming language from this amazing tutorial. It is a fantastic video tutorial on YouTube that explains Go concepts from the ground up and offers some great insight into the language design
Notes
Variables
Variables are defined with the var keyword or the shorthand := (only inside of functions / methods to simplify parsing!).
varaint// ora:=2// in functions or methods
Fun Printf snippet - %d %[1]v will reuse the first passed in argument (e.g. if we want to print a single variable twice in a Printf, you’d normally do fmt.Printf("%d %d", a, a), but with this you just need to do fmt.Printf("%d %[1]v", a) and that parameter a will be reused)
Only numbers, strings or booleans can be constants
const(a=1b=3*100s="hello")
Strings
byte is a synonym for uint8
rune is a synonym for int32 for characters
string is an immutable sequence of “characters”
Logically a sequence of unicode runes
Physically a sequence of bytes (UTF-8 encoding)
We can do raw strings with backticks (they don’t evaluate escape characters such an \n)
`string with "quotes"`
IMPORTANT: The length of a string is the number of UTF-8 bytes required to encode it, NOT THE NUMBER OF LOGICAL CHARACTERS
Internally strings consist of a length (remember that they are immutable) and a pointer to the memory where the string is stored. Since the descriptor contains a length, no null byte termination is needed (as is the case in C)
Strings are passed by reference
The strings package contains useful string functions
Arrays and Slices
Arrays have fixed size, slices are variable size
Slices have a backing array to store the data; slice descriptors (not an official thing - just used to denote the underlying logic of this data) have a length (how many things in the slice), a capacity (the capacity of the underlying array) and a pointer to the underlying array
Slices are passed by reference, but you can modify them unlike strings
Arrays are passed by value
Arrays are also comparable with ==, slices are obviously not. Arrays can be used as map keys, slices cannot
Slices have copy and append helper operators
Arrays are used almost as pseudo constants
Maps
Maps are implemented using a hash table
When you try to read a key that doesn’t exist, you receive the default value of the map value type (e.g. for an int you’d get 0)
You can also read from nil (uninitialised) maps, again will return the default value for any key
varmmap[string]int// nil map (reading any key will return the default value of the map value type)_m:=make(map[string]int)// empty non-nil map
make creates the underlying hash table and allocates memory etc. It is required to instantiate and write to a map
Maps are passed by reference, and the key type must have == and != defined (so the key cannot be a slice, map or function)
Map literals are a thing:
varm=map[string]int{"hello":1}
Maps can be created with a set capacity for better performance
Maps also have a two-result lookup function:
p:=map[string]int{}// Empty non nil mapa,ok:=p["hello"]// Returns 0, false since the key "hello" doesn't existp["hello"]++b,ok:=p["hello"]// Returns 1, trueifw,ok:=p["the"];ok{// Useful if we want to do something if an entry is / isn't in the map}
nil indicates the absence of something, with part of the Go philosophy being to make the zero value useful
The length of a nil slice is 0, you can read a map that doesn’t exist - any key returns the default value. These features reduce code noise / boilerplate
The nil value has no type; it is defined for the following constructs:
Nil pointer -> the zero value for pointers - points to nothing
Nil slice -> a slice with no backing array (with zero length and zero capacity)
Nil channels, maps and functions -> these are all pointers under the hood so a nil [channel,pointer,function] is just a nil pointer
Nil Interfaces
Nil interfaces -> I still don’t fully understand this but interfaces internally have two things - the type of the value inside and the value itself
varsfmt.Stringer// This is a nil interface with no concrete type and no value (nil, nil)fmt.Println(s==nil)// Will print true since (nil, nil) == nil//---varp*Person// This Person satisfies the person interfacevarsfmt.Stringer=p// Now we have (*Person, nil) - a concrete type (*Person) but still no value. This is now no longer equal to nil//---funcdo()error{// This will return the nil pointer wrapped in the error interface (*doError, nil)varerr*doErrorreturnerr// This is a nil pointer of type *doError}fmt.Println(do()==nil)// Will be FALSE because of the above example - (*doError, nil) != nil!!!// It is good practice to not define or return concrete error variables
Control Statements
If statements require braces
We can have a short declaration in an if statement to simplify logic:
ifx,err:=doSomething();err!=nil{returnerr}
Only for loops exist in Go, no do or while
We can do ranged for loops for arrays and slices:
fori:=rangesomeArr{// i is an index here. Remember this - this mistake can happen often. i is the INDEX NOT THE VALUE. // If you want to range over the values you can use the blank identifier like for _, v := range someArray}fori,v:=rangesomeArr{// i is an index, v is the value at that index// The value v is COPIED - don't modify. If the values are some large struct, it might be better to use the explicit indexing for loop}fork:=rangesomeMap{// Looping over all keys in a map}fork,v:=rangesomeMap{// Getting the keys and values in the loop}
Remember maps in Go have no order since they are based on a hashtable
To run through a maps values in key order, the keys must be extracted, sorted, then looped over to index into the map
An infinite loop can be started with an empty for:
for{// Infinite loop}
Switch statements are syntactic sugar for a series of if-then statements
Cases break automatically in Go - no break statement is needed
There is also a switch on true statement which is used to make arbitrary comparisons:
a:=3switch{casea<=2:casea==8:default:// Do something}
It’s basically just a bunch of if statements, evaluated in the order they are written
Packages
Every standalone program in Go must have a main package
There are two main scopes in Go; package scope and function scope
You can declare anything at package scope but you can’t use the short declaration operator :=
This is to make the program easier to parse since every statement at the top level has a keyword (e.g. const, var, type, func etc.)
Packages break the program down into independent parts
Anything with a capital letter is exported
within a package, everything is visible (even across multiple files - you can have multiple files under the same package)
Imports
Go imports are based on necessity, if an import isn’t used within a file then it is a syntax error
Go understandably doesn’t allow circular imports
There is an init() function for a package, however using this isn’t really recommended
Packages should embed complex behaviour behind a simple API
Variable Declarations
Using the var keyword
varaintvaraint=1varc=1// Type inferencevard=1.0// Declaration block for simplicityvar(x,yintzfloat64sstring)
Short Declaration Operator :=
The short declaration operator := is used to declare and assign to a variable
It can’t be used outside of functions (to allow for faster parsing of a program)
It must declare at least one new variable:
err:=doSomething()err:=doSomethingElse()// This is wrong, you can't re-declare errx,err:=doSomethingOther()// This is fine since you are declaring the new var x, and just reassigning err from the original assignment on the skip line above
The caveat to that final point is that we can redeclare (shadow) to variables in an outer scope
When using a short declaration in a control structure (e.g. if _,err := do(); err != nil), that err declaration is local to the control structure scope (that if block scope).
The mistake here is that the err in the for loop is of an inner scope, it shadows the one defined in the function scope above, and is lost when the for loop exits. Thus returning the err in the last line will always be nil
Typing
Structural and Named Typing
Structural typing is based on the structure of a variable. Some examples of things with the same type:
Arrays with the same base type and size
Slices with the same base type
Maps with the same key and value types
Structs with the same sequence of field names and types
Functions with the same typed parameters and return types
Named typing happens when you introduce a new custom type with the type keyword
Things are only the same type when they have the same declared named type, so declaring type x int means that you can’t assign something with type x to int or vice versa, you would have to use a type conversion like var thing x = x(12)
Integer literals are untyped - they can assign to any size integer without conversion, and can be assigned to floats, complex etc.
The only overloaded operator in Go is the + operator to concatenate strings
Functions
Functions in Go are first class objects
Almost anything can be defined in a function, except (understandably) methods
The signature of a function is the order and type of its parameters and return values. Functions are always typed with structural typing rather than named typing
Parameter Passing
Numbers, bools, arrays and structs are passed by value
This is important to note since structs are the most likely things needing to be modified by a function or method
Things passed by pointer (&x), strings (although they’re immutable) slices, maps and channels are all passed by reference, meaning that their values can be updated inside a function
In actuality the model is similar to Java where it is technically all by value (except the value for those above things passed by reference is the value of the descriptor for that thing)
This means that parameter reassignments for a non-pointer argument won’t change the thing outside of the context of that function (but passing in a pointer to the function does mean we can reassign to the parameter and change the thing outside of the scope of the function). Basically the semantics are similar to Java
Multiple Return Values
Functions can return multiple return values by putting them in parens, e.g. (int, error)
An idiomatic pattern is to return (value, error) where error != nil indicates some error has occurred
Naked Return Values
If you name the return value in the signature of your function, Go will implicitly declare variable(s) with the given names and types
Defer
The defer statement allows you to defer some operation (function call) to run on function exit
Care needs to be taken to make sure the defer makes sense and is valid
Defer operates on a function scope, e.g.:
funcmain(){f:=os.Stdiniflen(os.Args)>1{iff,err:=os.Open(os.Args[1]);err!=nil{...}deferf.close()}// At this point we can do something with the file and only if it is a file passed in the params will it be closed at function exit}
The above example has f.close() running at function exit not block ending
The value of arguments in a deferred function call are copied at the point of the defer call
functhing(){a:=10deferfmt.Println(a)a=11fmt.Println(a)// Will print 11,10}
Closures
Scope is static - based on the structure of the source code
Lifetime depends on the program execution (e.g. returning a reference from a function makes that value live outside of the function scope)
The variable will exist so long as a part of the program keeps a pointer to it
Go will do escape analysis to figure out the lifetime of a thing
A closure is when a function inside another function closes over one or more local variables of the outer function:
funcfib()func()int{a,b:=0,1returnfunc()int{a,b=b,a+breturnb}}funcmain(){f:=fib()forx:=f();x<100;x=f(){fmt.Println(x)// Prints fibonacci numbers less than 100}}
The inner function will get a reference to the outer function’s variables
Those variables may have a longer than expected lifetime so long as there’s a reference to the inner function
The actual closure is the concrete thing returned by calling thing() above - it is a function that returns an int alongside the environment containing references to the values a and b
See this post for information on an important change in Go 1.22 that changes the semantics of for loops that differs from the information shown in the tutorial video