If you are brushing up on your Go skills, or learning Go, the following are things that you need to know about it.
Links
The Basics
Do not communicate by sharing memory; instead, share memory by communicating.
One of the underlying design principles behind Go is the idea that instead of managing access to shared memory between threads that you share the values of said memory between different threads.
https://go.dev/blog/codelab-share
Pass by Reference or Value
Go always passes a copy of the value. In some cases it may not seem like it because not all values are the actual object. Some values are actually addresses. For instance a slice value is a pointer to the address of the slice. The same is true with maps.
package main
import "fmt"
func mySlice(s []int) {
fmt.Printf("%7s: %p\n\n", "mySlice", s)
}
func myMap(m map[string]int) {
fmt.Printf("%7s: %p\n\n", "myMap", m)
}
func myInt(i int) {
fmt.Printf("%7s: %v\n\n", "myInt", &i)
}
func main() {
// Slice references are pointers, thus the pointer is copied
s := []int{1, 2, 3}
fmt.Printf("%7s: %p\n", "s", s)
mySlice(s)
// Same with maps
m := map[string]int{
"foo": 1,
"blah": 2,
}
fmt.Printf("%7s: %p\n", "m", m)
myMap(m)
// A scalar variable not defined as a pointer has its value
// copied, thus the address printed in the function is different
// than the address outside the function.
i := 647
fmt.Printf("%7s: %v\n", "i", &i)
myInt(i)
}
Outputs the following:
s: 0xc00001a018
mySlice: 0xc00001a018
m: 0xc0000161b0
myMap: 0xc0000161b0
i: 0xc00001c038
myInt: 0xc00001c050
Further, the following items, along with anything that you use make, new, or & to create can be passed by reference, as opposed to being copied. That means the following are references and passed by reference:
- slices
- maps
- channels
- pointers
- functions
String Formatting
Go provides string formatting inline with printf formatting rules
| Verb | Type |
| %v | value of a struct |
| %+v | value of struct plus field names |
| %#v | Go syntax representation of the source code that would produce the value |
| %T | the type of the value |
| %t | boolean |
| %d | base-10 formatting of integers |
| %f | base-10 decimal formatting of floats |
| %e or %E | +/-e N scientific notation for floats |
| %b | binary representation |
| %c | char |
| %x | hex encoded representation will also render integers as base 16 |
| %s | string |
| %q | Escape double-quoted strings |
| %p | representation of a pointer |
| %n | control the width and precision of a integer |
| %n.n | control the width and precision of a float |
| %ns | control width of a string |
| %-ns | right-padding for a string |
Strings, bytes, runes and characters in Go
Slices
Slices are ubiquitous in Go and you can get by with a cursory understanding of them by treating them as arrays. This is not entirely incorrect and understanding the details of how they work is important for a competent go programmer.
Key Points
A slice created from another slice will point to and reference the same underlying array. Changes to the contents of the array via either slice will update the data in both slices.
s := []byte{'i', 'a', 'm', 'a', 's', 'l', 'i', 'c', 'e'}
// s1 slice will point the same underlying array
s1 := s[1:3]
fmt.Println(string(s1)) // am
s[1] = 'z'
fmt.Println(string(s1)) // zm
The difference between an array and a slice
package main
import (
"fmt"
"reflect"
)
func sliceAddress(s []string) {
fmt.Printf("type=%+v, address=%p\n", s, s)
}
func arrayAddress(a [2]string) {
fmt.Printf("type=%+v, address=%p\n", a, &a)
}
func main() {
// Declare an array by specifying the number of elements/size of
// the array that you want to allocate.
a := [2]string{"an", "array"}
// Declare a slice of the same type omitting the size which will
// automatically create the underlying array and create a slice
// that will point to the array.
b := []string{"a", "slice"}
fmt.Printf("a type=%+v, b type=%+v\n", reflect.TypeOf(a), reflect.TypeOf(b))
fmt.Printf("address of slice in main=%+p\n", b)
sliceAddress(b)
fmt.Printf("address of array in main=%p\n", &a)
arrayAddress(a)
}
// a type=[2]string, b type=[]string
//
// The slice has the same memory address in main and in the func.
// address of slice in main=+0xc00011e020
// type=[a slice], address=0xc00011e020
//
// The array has a different memory address indicating it was passed
// by value/copied.
// address of array in main=0xc00011e000
// type=[an array], address=0xc00011e040
Links
Arrays
Arrays are different from slices in that a slice is an abstraction over an array. That being said, there may be instances where you want to work with arrays directly.
One thing to keep in mind is that arrays are values. As such, when passing an array to a function you will pass a copy of the array and not a reference or pointer.
Range
When ranging over slices you have access to two values. For a slice: the index of the element in the slice and a copy of the value. The word copy is intentionally highlighted because that is exactly what you get, a copy of the element and not a reference to the element. If it is a pointer, you still get a copy to the pointer, and the behavior in that case is different than the following example where changes made to the copy that you get do not “write through” to the original element in the slice.
package main
import "fmt"
type Person struct {
Name string
Age int
}
func NewPerson(name string, age int) Person {
return Person{name, age}
}
func main() {
// Create a slice of some Person instances
a := []Person{NewPerson("Homer", 50), NewPerson("Marge", 49)}
for _, p := range a {
// p is a *copy* of the element in the underlying slice
fmt.Printf("Before updating copy of element in range, p=%+v\n", p)
p.Name = "Changed"
fmt.Printf("After updating copy of element in range, p=%+v\n", p)
}
fmt.Println("Iterating over same slice after the update exersize")
for _, p := range a {
fmt.Println(p)
}
}
This code will output the following, proving that, in at least this case where the elements are Person instances and not pointers, that range will return a copy of the element in the underlying array.
Before updating copy of element in range, p={Name:Homer Age:50}
After updating copy of element in range, p={Name:Changed Age:50}
Before updating copy of element in range, p={Name:Marge Age:49}
After updating copy of element in range, p={Name:Changed Age:49}
Iterating over same slice after the update exersize
{Homer 50}
{Marge 49}
Pointers and Structs
TBD
Functions, Function Values and Function Closures
Functions are first class objects in go. As a result they can be assigned to variables, passed as arguments to other functions and returned as values by other functions
Functions can be defined as types:
package main
import "fmt"
// Define a function type
type doSomething func(input string) error
// Define a function that accepts the doSomething type as an argument
func exec(arg string, d doSomething) error {
return d(arg)
}
func main() {
// Define a function that matches the doSomething signature
f := func(input string) error {
if input == "hello" {
return nil
}
return fmt.Errorf("input was not 'hello'")
}
err := exec("goodbye", f)
if err != nil {
fmt.Println(err)
}
}
Methods and Receiver Arguments
Value Receiver vs. Pointer Receiver
Interfaces
Interfaces in go are not defined explicitly. If something can do a specific set of things it can be used in a given context. Interfaces define the methods that a given type must implement to be of that interface type.
Embedding
Eli Bendersky has a great three-part series that explains embedding.
There are three basic kinds
- Structs in structs
- Interfaces in interfaces
- Interfaces in structs, which enables us to wrap an interface, reusing the embedded value to gain the base set of functionality with the ability to wrap/decorate said functionality.
Type assertions in switch cases
t, ok := i.(T)
switch v := i.(type) {
case T:
// v is a type T
case U:
// v is a type U
default:
// v is the same type a i
}
type constraint
Stringer Interface
Readers
Generics
Type constraint
type Number interface {
int64 | float64
}
Defer and Recover
defer enables you to specify statements to be executed after the completion of the enclosing function. Similar to finally in other languages. Defer statements are guaranteed to execute in the case of a panic in the function and enable the programmer to execute cleanup code after a function has run.
For instance, it is a good practice to use defer to close file handles, network sockets, or other resources immediately after they are opened in a function to best ensure that they will be closed after the function completes. They are executed in the order in which they are defined in a LIFO, stack-like fashion.
package main
import "fmt"
func main() {
defer fmt.Println("defer 1")
defer fmt.Println("defer 2")
fmt.Println("something")
defer fmt.Println("defer 3")
}
Prints out
something
defer 3
defer 2
defer 1
recover enables you to catch a panic in a function and then continue execution from where the function that caused the panic was called.
package main
import "fmt"
func mightPanic() {
panic("encountered an error")
}
func doSomething() {
defer fmt.Println("doSomething defer 1")
// Ensure that we will not propagate a panic up the call stack
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered an error, r=%s\n", r)
}
}()
defer fmt.Println("doSomething defer 2")
mightPanic()
fmt.Println("Returning normally from doSomething")
}
func main() {
// Add to defer statements that will be executed after main completes
defer fmt.Println("main defer 1")
defer fmt.Println("main defer 2")
// Call a function that then might call a function that panics
fmt.Println("About to doSomething")
doSomething()
fmt.Println("After calling doSomething")
defer fmt.Println("main defer 3")
}
Prints out
// Calling doSomething from main
About to doSomething
// The defers defined in doSomething execute in the reverse order in which
// they were defined. The recover catches the panic.
doSomething defer 2
Recovered an error, r=encountered an error
doSomething defer 1
// doSomething DOES NOT continue executing after the mightPanic() function
// panics, and that function call is pulled off the call stack. Control
// is returned to the main function.
After calling doSomething
main defer 3
main defer 2
main defer 1
Returning a value from a function that panics
Once a function panics and invokes the recover function, execution resumes AFTER the call to the panicking function. In most practical use-cases the function that might panic is doing something that is returning a value or at the very least somehow letting you know whether or not it succeeded. In order to return a value from a recover function we must define a named return for the function that defines the recover implementation. We can then set the named return value in the recover function
package main
import (
"fmt"
"os"
"strconv"
)
func mightPanic(i int) (retval error) {
defer func() {
if r := recover(); r != nil {
// Set the value to be returned from INSIDE the recover func
retval = fmt.Errorf("recovered; r=%v", r)
}
}()
if i%2 == 0 {
panic("encountered an error")
}
return nil
}
func doSomething(i int) error {
defer fmt.Println("doSomething defer 1")
// Try to do something with data provided. Whether or not mightPanic()
// panics we will get back a value from the function call and resume
// execution here.
err := mightPanic(i)
if err != nil {
fmt.Printf("mightPanic returned an error; err=%+v\n", err)
} else {
fmt.Printf("mightPanic returned nil\n")
}
fmt.Println("After call to mightPanic")
return nil
}
func main() {
iStr := os.Args[1]
i, err := strconv.Atoi(iStr)
if err != nil {
panic(err)
}
fmt.Printf("Input is i=%d\n", i)
defer fmt.Println("main defer 1")
defer fmt.Println("main defer 2")
fmt.Println("About to doSomething")
err = doSomething(i)
fmt.Printf("After calling doSomething; err=%+v\n", err)
defer fmt.Println("main defer 3")
}
Prints out
Input is i=2
About to doSomething
mightPanic returned an error; err=recovered; r=encountered an error
After call to mightPanic
doSomething defer 1
After calling doSomething; err=<nil>
main defer 3
main defer 2
main defer 1
Wait Groups
WaitGroups enable us to block while asynchronous tasks/go routines finish processing. Akin to Thread.join() in Java.
A few general points to keep in mind:
- Always create a pointer to a WaitGroup and pass a pointer to it to ensure that your code is actually decrementing the WaitGroup instance.
- Always call
func (wg *WaitGroup) Add(delta int)outside of and before calling your go routine. Otherwise, the current thread might exit before the go routine actually runs. - It is generally a good idea to call
func (wg *WaitGroup) Done()in a defer early in your go routine in case it encounters an error. This ensures that you will decrement the WaitGroup and not deadock
Sync Pools
What are sync pools? https://medium.com/swlh/processing-16gb-file-in-seconds-go-lang-3982c235dfa2
Contexts and Cancel Functions
The key points of a context is that it carries (optional) deadlines, cancellation signals, and (optional) request scoped values across API boundaries when spinning up go routines that may then spin up other go routines to handle a request or complete a task. It enables the “grouping” of tasks together in a way that they can all be cancelled if the parent task signals completion or cancellation.
Some general guidelines (summarized from the API docs):
- Do not pass Contexts inside structs, pass them explicitly to functions that require them as the first parameter, with the conventionally agreed upon name
ctx.
Channels
- Creating channels:
ch := make(chan int) - Buffered channels:
ch := make(chan int, 100) - Closing channels: A sender can
closea channel to indicate that no more data will be written to it. Receivers can check whether a channel has been closed to checking for a boolean when reading from the channel;v, ok := <- ch. When a channel has been closed AND there are no more values to read from the channelokwill be false. - Channel directions
- Read Only Channels:
<-chan int - Write Only Channels:
chan<- int - R/W Channels:
chan int
- Read Only Channels:
- Checking if a channel is empty:
if len(chan) == 0
Timers and Tickers
TBD
Select Loops
Mutexes and Semaphores
Strings, chars, bytes, and runes
There is no char type in go. An ASCII character is 8-bits, or a byte which is a uint8. A rune is an int32 data type and meant for storing UTF-8 characters comprised of more than one byte.
To iterate over all of the characters in a string simply range over the string. Each element is a rune.