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
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}
Important: you cannot take the address of a map entry (e.g. like &myMap["Hello"])
The reason for this is the map can change its internal structure and the pointers to entries are dynamic so it is very unsafe to reference a map entry
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)
There is a standard library in Go with lots of useful features. There is also an “extension” to the standard library (e.g. for a package like “golang.org/x/net/html”) that offers less stable packages that might be candidates for the standard library in the future.
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
This is IMPORTANT - the closure gets a reference - watch out for this
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
More on Slices
// The following shows some different slices, with information on them given belowvars[]intt:=[]int{}u:=make([]int,5)v:=make([]int,0,5)w:=[]int{1,2,3,4,5}
Before explaining each slice we define the slice descriptor (an internal thing) as a tuple of (length, capacity, arrAddr)
Length is the amount of elements stored in the slice
Capacity is the size of the underlying array storing the values
arrAddr is a pointer to the underlying array
s is an uninitialised or nil slice
It has 0 length, 0 cap and a nil pointer in arrAddr
t is an initialised but empty slice
It has 0 length and 0 capacity and arrAddr points to a special sentinel struct{} value (again an internal thing that is basically a nothing value but not nil)
This is because it has 0 capacity so it can’t point to a concrete array of 0 length - this sentinel value is an internal language thing that isn’t exposed
u is an initialised slice with 5 length and 5 capacity
It will be storing 5 of the zero value of it’s slice type - e.g. for int it would be [0,0,0,0,0]
This is an important thing to remember - appending to this list will create a list of 6 elements not 1!!
v is an initialised slice with 0 length and 5 capacity
The underlying array will have a size of 5 but won’t be storing anything - attempting to read from this will cause a panic since the length is 0
The Slice Operator
The slice operator allows you to take a view of a slice
It looks like a[0:2] - which will take the 0 and 1 elements of a (it is exclusive for the to side)
The Slice Capacity Issue
The slice operator basically just creates a view into the underlying array of a slice
This means that when slicing a slice of e.g. size 5 to get 0:2, you get back a slice descriptor with length 2 but capacity 5 (since the underlying array is the same and has length 5)
You can then legally slice this slice at e.g. 0:3 and you’ll get back a slice descriptor of length 3 - which will contain the value at index 2 of the original slice!!!
This is an important thing to remember
This design is maybe not ideal but it is what it is. To fix this the slice with capacity operator was introduced
This looks like a[0:2:2] - this will create a slice descriptor of length 2 AND CAPACITY 2
This means if you append to this slice Go will have to allocate a new array with new size and importantly a new memory address so the append works properly and doesn’t touch the underlying array of the original slice
To create an array from an array literal you can do b := [2]string{"Hello", "world"}, and you can do b := [...]string to let Go determine the size of the array for you based on the proceeding literal
Slices are made with the make function (func make([]T, len, cap) []T)
len and cap functions can be used to retrieve the length and capacity of a slice
You can take an array arr and create a slice referencing (or providing a view of) the storage of arr using s := arr[:]
If you slice an array (or slice) with capacity 5, not from the 0th element, then the resulting slice will have a capacity equal to the original capacity minus the length of the specified slice range. This is a variation on the slice capacity issue above
You can grow the slice to the end of the backing array’s length using s = s[:cap(s)]
Growing a slice can be done by making a larger slice and copying the data into it
s:=make([]int,5)// This is basically the internal implementation of slice growing that Go uses when appending to a slice that has reached it's max capacityt:=make([]int,len(s),(cap(s)+1)*2)copy(t,s)s=t
As mentioned a slice will be automatically grown when it’s length reaches its capacity
append(s []T, x ...T) []T
You can append a slice into another slice by using the ... operator to expand the second arg into a list of args
append(s, x...) for s []T and x []T
The zero value of a slice (nil) acts like a zero length slice so you can declare a slice variable (without initialising it) and then append to it in a loop:
One gotcha with slices is re-slicing doesn’t make a copy of the underlying array, so you could accidentally keep the underlying array around when only a small piece of the data is actually needed
To remedy this, make a new slice and copy only the useful data into it and the garbage collector will sort out the rest
Structs and JSON
Structs are an aggregate of multiple types of named fields
You can use the printf (%+v) to pretty print a struct and it’s fields
Maps of Structs
You can store maps of structs (e.g. map[string]MyStruct) however it is really bad practice to do this because a map’s internal structure is dynamic
Instead it is recommended to store a map of pointers to structs (e.g. map[string]*MyStruct)
You also can’t perform mutation operations (e.g. ++) on fields of structs by direct access (e.g. myMap["thing"].IntField++)
This is because the semantics of maps are that they are meant to store values not references, so when you access a value in a map by it’s key, you get a copy of the value meaning you can’t directly mutate it and have the map update
Structure & Name Compatibility of Structs
Anonymous structs with the same field names and types (and tags) are treated as being the same type by the compiler
However when you give a struct a name with type blah struct{...}, that no longer is the case - structs with different names will always be different types even if they have the same field names and types
You can convert structs if they have the same structure:
The zero value of a struct is the zero value of all of it’s fields
This is a core Go concept - make the zero value useful
Structs are copied, so when they are passed in as parameters to functions a copy is made and modifications will only be made on the copy
The dot notation for fields also works on pointers, e.g. for thing *myStruct, thing.field is equivalent to dereferencing (*thing).field
This is different to C/C++ where you’d use -> for accessing or mutating a field in a struct pointer
Structs with no fields are useful - they take up no space
Some uses include creating a set (map[int]struct{}) or creating a chan struct{} to be a “complete” notifier without the need to pass any data if that isn’t needed
The empty struct is a singleton - it is the sentinel value used to indicate an empty slice
JSON with Structs
Struct tags are key value pairs that can be attached to a struct field
They can specify how struct fields should be serialised / deserialised by libraries (done with reflection)
typeResponsestruct{Datastring`json:"data"`// Only exported fields are included in a marshalled JSON stringStatusint`json:"status"`}funcmain(){// Serializingr:=Response{"Some data",200}j,_:=json.Marshal(r)// j will be []byte containing "{"data":"Some data","status":200}"// Deserializingvarr2Response_=json.Unmarshal(j,&r2)}
Reference and Value Semantics
Value semantics (copying) lead to higher integrity, especially in concurrent programs
Pointer semantics tend to be more efficient
Pointers are used when
Some objects can’t be copied (e.g. a mutex)
When we don’t what to copy a large data struct
Some methods needs to mutate the receiver
When decoding protocol data into a DTO
When we need the concept of “null”, e.g. in a tree structure to indicate a node has no children
More on Copying
Any struct that has a mutex cannot be copied - it must be passed via a pointer
Likewise for WaitGroups
Any small struct (under 64 bytes) can be copied since that is smaller than the size of a pointer
Larger structs should be passed by reference
String and slice descriptors are copied - however this is fine since the underlying data isn’t copied - the copied descriptor points to the same underlying data as the original
When you do a range in a for loop, the thing is always a copy:
fori,thing:=rangethings{// thing is always a copy - mutating it doesn't mutate the thing in things}// You have to use an index if you want to mutate the elementfori:=rangethings{things[i].field=value}
If you have a function that mutates a slice that is passed in, you must return a copy - this is because the slice’s backing array may be reallocated when it is grown:
The library also defines a helper method on functions with that signature that makes them conform to the Handler interface:
typeHandlerFuncfunc(ResponseWriter,*Request)// This is a method declaration on a function typefunc(fHandlerFunc)ServeHTTP(wResponseWriter,r*Request){f(w,r)}// Then we can define a function that conforms to that interface without // requiring explicit implementation of ServeHTTPz§funchandler(whttp.ResponseWriter,r*http.Request){fmt.Fprintf(w,"Hello, world")}
Go allows methods to be put on any declared type, including functions as is the case in the above example
http.Template is a package for doing HTTP templating
varform=`
<h1>Todo #</h1>
<div></div>
Above is an example of a template string for the http.Template library to populate. It uses double bracket syntax for templating and has directives like printf to do formatting. It will pull values from the fields specified in the template, e.g. pulling the ID from the .ID field of some struct
More reading / work on HTTP bits will be done in the future
OOP Concepts in Go
An Overview
Go offers OO programming concepts
Encapsulation using packages for visibility control
Abstraction and polymorphism using interface types
Composition (rather than inheritance) to provide structure sharing
Go doesn’t offer inheritance or substitutability based on types
Substitutability is based only on interfaces, a function of abstract behaviour
Go offers more flexibility than OOP since it allows methods to be put on any user defined types rather than only “classes”
Go also allows any object to implement the methods of an interface, not just a subclass
Methods and Interfaces
Interfaces specify abstract behaviour - one or more methods that a concrete implementation must satisfy
Interface satisfaction in Go is implicit - if a type implements the methods of an interface it automatically satisfies that interface, no implements like keyword required
A method is a special type of function that has a receiver parameter before the function name
This receiver parameter is actually just syntactic sugar for an additional argument for the thing the method is being called on, equivalent to self, this etc. in other languages
You can put methods on any user defined type, not just structs (although you can’t put methods directly on inbuilt types)
E.g. you can define type IntSlice []int and attach a method to this named user-declared type, but you can’t attach a method to []int directly
An example interface is the Stringer interface - this defines a method String() that can be used to stringify the receiving thing
typeStringerinterface{String()string}
This interface is used by fmt.Printf - it will check if the thing it needs to print satisfies the Stringer interface (is a stringer), and if so just copies the output of the String() method to its output
Interfaces allow us to define functions in terms of abstract behaviour rather than concrete implementations, e.g. we can create an OutputTo function that accepts any type that implements the Write([]byte) method, meaning we can use any thing that has that method rather than a specific implementation
Methods can have value or pointer receivers, the latter allows you to modify the receiver (the original object)
You can’t have a method with the same signature as both a pointer and a value receiver
You can compose interfaces:
typeReadWriterinterface{ReaderWriter}
Thus a ReadWriter must implement the Read and Write methods
Interface Declarations
All methods for a given type must be declared in the same package where the type is declared
This means the compiler knows all the methods for the type at compile time and ensures safe static typing
However you can extend a type into a new package through embedding:
typeBiggerstruct{otherpackage.Big// Struct composition to be explored later}func(bBigger)SomeMethod(){}
Composition in Go
You can embed a struct into another struct - the fields of the embedded struct are promoted to the level of the embedding struct
typeHoststruct{HostnamestringPortint}typeSimpleURIstruct{HostSchemestringPathstring}funcmain(){s:=SimpleURI{Host:other.Host{Hostname:"google.com",Port:8080},Scheme:"https",Path:"/search",}fmt.Println(s.Hostname,s.Scheme)// See how the Host has been promoted}
The SimpleURI structure would have the fields in the Host struct promoted to it’s level
Importantly the methods on the Host type are also promoted to the SimpleURI type, this is the most powerful part of composition
You can also embed pointers to other types - in this case the methods (both value and pointer receiver) on that embedded pointer are still promoted
typeThingstruct{Fieldstring}func(t*Thing)bruh(){fmt.Println(t.Field)}// Would also be valid with a value receiver method// func (t Thing) bruh() {// fmt.Println(t.Field)// }typeThing2struct{*ThingField2string}funcmain(){t:=Thing2{&Thing{"Hello"},"world"}t.bruh()// Method call here is valid}
Composition with Sorting Example
The standard library sort package uses interfaces to sort things
The main sort interface is:
typeInterfaceinterface{// The length of the collectionLen()int// Says whether the element at index i is less than the element at index jLess(i,jint)bool// Swaps the element at index i with the element at index j in the collectionSwap(i,jint)}
Then the sort.Sort function can take in any Interface conforming collection type and sort it in place
ByName and ByWeight conform to sort.Interface through composition (since Components has the Len and Swap methods defined for it), but they then specialise the Less method to be a specific sorting strategy
The reverse unexported struct in sort is used to sort something in reverse order
typereversestruct{Interface// It just embeds sort.Interface}func(rreverse)Less(i,jint)bool{returnr.Interface.Less(j,i)// Note swapped arguments for reverse sorting}funcReverse(dataInterface)Interface{return&reverse{data}}
See how the Less method on reverse is flipped
Then how the function sort.Reverse is defined that returns a sort.Interface that has the reverse implementation of Less
Making Nil Useful
One of the key concepts in Go is the idea that we can make nil useful
There is nothing stopping you from calling a method on a nil receiver
// The nil / zero value of this struct is ready to use since a nil slice can be appended totypeStringStackstruct{data[]string}func(s*StringStack)Push(xstring){s.data=append(s.data,x)}func(s*StringStack)Pop()string{l:=len(s.data)ifl==0{panic("pop from empty stack")}t:=s.data[l-1]s.data=s.data[:l-1]returnt}
In the above example, the zero value of StringStack is directly usable
This is also a good example of encapsulating the data field inside the StringStack struct so that a client can’t see the implementation details
Another example of making nil useful is a recursive linked list traversal:
See how the base case is elegantly handled by the nil receiver
Exploring Value / Pointer Method Semantics
Go performs some implicit addition of things when calling value / pointer receivers on values / pointers
If you have a value v := T{} you can of course call value receivers on it directly, however you can also call pointer receivers on it. The compiler will implicitly add an (&v).PointerMethod()
Likewise if you have a pointer v := &T{}, you can of course call pointer receiver methods on it directly, however Go will also implicitly add a dereference when you call a value receiver method (*v).PointerMethod()
Although the compiler does this implicitly, the method sets (which are important for interfaces) of a pointer and value type are as follows:
The method set of a value T is all the value receiver methods of T
The method set of a pointer *T is all the value and pointer receiver methods of T
typeThingstruct{}func(tThing)ValMethod(){}func(t*Thing)PointerMethod(){}typeIValinterface{ValMethod()}typeIPtrinterface{PointerMethod()}funcmain(){vartThingvariValIValvariPtrIPtriVal=t// ValidiVal=&t// ValidiPtr=t// Not valid, since the value t doesn't have the pointer method PointerMethod in it's method setiPtr=&t// Valid}
More on Interfaces
Interface variables are nil until initialised
Nil interfaces have a slightly more complex structure than nil structs and values
Nil interfaces have two parts
A value or pointer of some concrete type
A pointer to type information so the correct concrete implementation of the method can be identified
We can picture this as (type, ptr)
An interface is only nil when it’s value is (nil, nil)
varrio.Reader// nil interface herevarb*bytes.Buffer// nil value herer=b// at this point r is no longer nil itself, but it has a nil pointer to a buffer
When we assign r = b the new value of r is (bytes.Buffer, nil)
The Error Interface
typeerrorinterface{funcError()string}
error is an interface that has one method Error()
Therefore we can if err == nil to see if the err return value was assigned - this is the idiomatic error checking mechanism in Go
Because of this nil behaviour, we must NEVER RETURN A CONCRETE ERROR TYPE FROM A FUNCTION OR METHOD:
typesomeErrstruct{errerrorsomeFieldstring}func(esomeErr)Error()string{return"this is some error"}funcsomeFunc(aint)*someErr{// We should NEVER return a concrete error typereturnnil}funcmain(){varerrerror=someFunc(123456)iferr!=nil{// Even though we logically didn't want to throw an error, returning a concrete error type // meant that the err variable was initialised and looks like (*someErr, nil) which in the// semantics of interfaces ISN'T NILfmt.Println("Oops")}else{// If we'd done err := someFunc(123456), the above check would have worked although again we // should never return a concrete error implementation from a function}}
Again we should never return a concrete error type from a function
In the above case, someFunc returns a nil pointer to a concrete value which gets copied into an interface, making the check err != nil return true which is logically incorrect
More on Pointer vs Value Receivers from Matt Holiday Vid
In general if one method of a type takes a pointer receiver, then all it’s methods should take pointer receivers
This isn’t enforced by the compiler but it should always be the case
Having a pointer receiver implies the values of that type aren’t safe to copy, e.g. Buffer which has an embedded []byte which isn’t safe to copy since the underlying array is shared, and any type that embeds any sort of mutex or other synchronisation primitives that should never be copied
Now that I’ve watched the Matt Holiday videos on these concepts I revisited this good conference talk on understanding nil to understand it with added context and knowledge
Zero Values
In Go, all types have a zero value
For bools this is false, numbers is 0 and strings is the empty string
For structs, the zero value is just a struct with all of it’s fields set to their zero values
nil is the zero value for pointers, slices, maps, channels, functions and interfaces
Nil
Nil has no value!!!
nil is not a keyword in Go, it is a predeclared identifier
Understanding the Different Types of nil
Pointers
The nil value of pointers is basically just a pointer that points to nothing
Slices
Internally slices have a pointer to the underlying array, a length and a capacity
The nil value of slices is basically a slice with no backing array; with a length and capacity of 0
Maps, Channels and Functions
These are all pointers under the hood that points to the concrete implementation of these things
Therefore the nil value of these is just a nil pointer
Nil Interfaces
Nil interfaces are the most interesting concept of nil in Go
Interfaces internally are a tuple consisting of (Concrete Type, Value)
The nil value of interfaces is (nil, nil)
The subtlety of this comes in when we assign a nil value to an interface - at that point we have (some concrete type, nil) internally for whatever the assigned type is, and this is no longer ==nil
funcbad1()error{varerr*someConcreteErrorreturnerr// We are returning (*someConcreteError, nil) which !=nil}funcbad2()*someConcreteError{// We are returning a concrete pointer to an error which will pass ==nil, however// it is very bad practice because the second you wrap this pointer in the error// interface you will have the same problem as abovereturnnil}
Basically you should never return concrete error types
How is Nil Useful
Nil Pointers
We can make a nil pointer useful, see the linked list sum example above (also applies to trees and other more complex data structures)
Nil Slices
Nil slices are useful, their length and capacity will be 0 and we can range over one without any issues
The only (expected) exceptional behaviour is indexing a nil slice, which would expectedly panic
Importantly you can also append to a nil slice without issues, this is a useful property
Nil Slices
Nil maps are useful, you can get their length, range over them without issues, and you can check if something is in a map (v, ok := map[i] -> zeroVal(type of map value), false for any key i). That means nil maps are perfectly valid read only empty maps
Again similar to slices the only exceptional behaviour occurs when trying to assign to a nil map
Nil channels are also useful
Nil channels will block indefinitely on send and receive, this is a useful behaviour. For context the behaviour is the opposite on a closed channel - a closed channel will return zero(t), false with that false ok flag indicating the channel is closed
Nil channels can be used to switch off a select case:
Nil channels example
This function merges two channels into one. When a channel closes, ok will be false and we set that channel to nil
This will effectively disable that select case since it will be blocking indefinitely
Of course when both are nil (after both are closed), the function will close the output channel and return
Function Currying
Since functions are first class in Go, you can curry functions as you’d expect
Since methods are just syntactic sugar for a function with an additional receiver parameter, we can close a method over a receiver value:
func(pPoint)Distance(qPoint)float64{returnmath.Hypot(q.X-p.X,q.Y-p.Y)}funcmain(){p:=Point{1,2}q:=Point{4,6}distanceFromP:=p.Distance// Here we close over the receiver value p, returning a curried function}