James's Ramblings

Go

Created: December 19, 2024

Installation

Linux

  • Download the archive from https://golang.org/dl/.
# rm -rf /usr/local/go && tar -C /usr/local -xzf go1.23.3.linux-amd64.tar.gz
  • Add the following to your .bashrc or .zshrc:
export PATH="$PATH:/usr/local/go/bin"

Different versions

go install golang.org/dl/go1.18@latest
go1.18 download

There is now a wrapper script that can be used to install different versions of Go. The script is called go1.18 and is located in the bin directory of the Go installation.

Go has very good backward compatibility so installing an older version should not be necessary.

Packages

  • Go programs are made up of packages.
  • Programs start running in package main.
  • A package is just a directory of .go files that share the same package declaration at the top.
  • File names are not relevant, it’s only the directory name that is relevant for the import. user.go below could be called anything and the import would still work.
myapp/
├── main.go          // package main
├── user/
│   ├── user.go      // package user
│   └── auth.go      // package user
└── chat/
    └── chat.go      // package chat
// main.go package main

import "myapp/user"

func main() { u := user.New("James") }

package user

type User struct { Name string }

func New(name string) User { return User{Name: name} }

Imports

  • Import packages with import "fmt".
  • Multiple packages can be imported.
  • Factored import statement stylistically superior:
import (
    "fmt"
    "math"
)

Exported names

  • A name is exported if it begins with a capital letter.
  • Println in fmt.Println is an exported name.
  • When importing a package, you can refer only to its exported names.
  • Any “unexported” names are not accessible from outside the package.

Functions

package main

import "fmt"

func add(x int, y int) int {
	return x + y
}

func main() {
	fmt.Println(add(42, 13))
}
  • A function can take zero or more arguments.
  • The type comes after the variable name.

Omitted type

  • When two or more consecutive named function parameters share a type, you can omit the type from all but the last.
func add(x, y int) int {
    return x + y
}

Multiple results

  • A function can return any number of results.
func swap(x, y string) (string, string) {
    return y, x
}

Named return values

  • Go’s return values may be named. If so, they are treated as variables defined at the top of the function.
func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return
}
  • These names should be used to document the meaning of the return values.
  • A return statement without arguments returns the named return values. This is known as a “naked” return.
  • Naked return should be used only in short functions, as it can harm readability in longer functions.

Variables

  • The var statement declares a list of variables; as in function argument lists, the type is last.

  • A var statement can be at package or function level.

package main

import "fmt"

var c, python, java bool

func main() {
	var i int
	fmt.Println(i, c, python, java)
}

Initializers

  • A var declaration can include initializers, one per variable.
var i, j int = 1, 2
  • If an initializer is present, the type can be omitted; the variable will take the type of the initializer.
var i, j = 1, 2

Short variable declarations

  • Inside a function, the := short assignment statement can be used in place of a var declaration with implicit type.
k := 3
  • Outside a function, every statement begins with a keyword (var, func, and so on) and so the := construct is not available.

Basic types

  • bool: true or false.
  • string: a sequence of UTF-8 encoded bytes.
  • int: a signed integer type that is 32 or 64 bits in size, depending on the platform. The recommended default for integer types is int.
  • unint: an unsigned integer type that is 32 or 64 bits in size, depending on the platform.
  • int8, int16, int32, int64: signed integers of 8, 16, 32, or 64 bits.
  • uint8, uint16, uint32, uint64: unsigned integers of 8, 16, 32, or 64 bits.
  • float32, float64: 32-bit or 64-bit floating-point numbers.
  • complex64, complex128: complex numbers with float32 or float64 real and imaginary parts.
  • byte: alias for uint8.
  • rune: alias for int32. Represents a Unicode character.
  • uintptr: an unsigned integer large enough to store the uninterpreted bits of a pointer value.

Zero values

  • Variables declared without an explicit initial value are given their zero value.
    • 0 for numeric types.
    • false for the boolean type.
    • "" (the empty string) for strings.

Type conversions

  • The expression T(v) converts the value v to the type T.
var i int = 42
var f float64 = float64(i)

Type inference

  • When declaring a variable without specifying an explicit type (either by using the var keyword or the := operator), the variable’s type is inferred from the value on the right-hand side.
i := 42
f := 3.142
g := 0.867 + 0.5i

If the right-hand side of the declaration is untyped, the new variable is of the same type:

i := 42
j := i

Constants

  • Constants are declared like variables, but with the const keyword.
  • Constants can be character, string, boolean, or numeric values.
  • Constants cannot be declared using the := syntax.
  • An untyped constant takes the type needed by its context.
const Pi = 3.14

For loops

A for loop has three components separated by semicolons:

  • The init statement: executed before the first iteration
  • The condition expression: evaluated before every iteration
  • The post statement: executed at the end of every iteration

The init statement will often be a short variable declaration, and the variables declared there are visible only in the scope of the for statement.

import "fmt"

func main() {
	for i := 0; i < 10; i++ {
		fmt.Println(i)
	}
}

The example prints the numbers 0 to 9.

The init and post statements are optional.

import "fmt"

func main() {
    sum := 1
    for ; sum < 1000; {
        sum += sum
    }
    fmt.Println(sum)
}

You can drop the semicolons in the for loop.

for sum < 1000 {
    sum += sum
}

This is equivalent to a while loop in other languages.

It’s possible to omit the loop condition, creating an infinite loop.

for {
}

Range

The range form of the for loop iterates over an array or slice.

// index and value
for i, v := range slice {}

// index only
for i := range slice {}

// value only
for _, v := range slice {}

You can also iterate over a map.

// key and value
for key, value := range theMap {}

// key only
for key := range theMap {}

// value only
for _, value := range theMap {}

It is also possible to iterate over a channel.

for v := range channel {}

Iterating over a channel is equivalent to receiving from a channel until it is closed.

for {
    v, ok := <-theChan
    if !ok {
        break
    }
}

```go

## If statements

- Go's `if` statements are like `for` loops; the expression need not be
  surrounded by parentheses.

```go
if x < 0 {
    return sqrt(-x) + "i"
}
  • Like for, the if statement can start with a short statement to execute before the condition.

  • Variables declared by the statement are only in scope until the end of the if.

if v := math.Pow(x, n); v < lim {
    return v
} else {
    fmt.Printf("%g >= %g\n", v, lim)
}
// can't use v here, though

Switch statements

  • Switch cases evaluate cases from top to bottom, stopping when a case succeeds.

Style 1

day := "monday"

switch day {
case "monday":
    fmt.Println("Start of week")
case "friday":
    fmt.Println("Almost weekend")
case "saturday", "sunday":
    fmt.Println("Weekend")
}

Style 2

Declare a variable and run it with semi-colons. In this example, the first statement declares an OS variable, and the second statement makes it the switch statement.

package main

import (
	"fmt"
	"runtime"
)

func main() {
	fmt.Print("Go runs on ")
	switch os := runtime.GOOS; os {
	case "darwin":
		fmt.Println("OS X.")
	case "linux":
		fmt.Println("Linux.")
	default:
		// freebsd, openbsd,
		// plan9, windows...
		fmt.Printf("%s.\n", os)
	}
}

Style 3

No condition in the switch and conditions in the case statements.

hour := 14

switch {
case hour < 12:
    fmt.Println("Morning")
case hour < 17:
    fmt.Println("Afternoon")
default:
    fmt.Println("Evening")
}

Defer statements

  • A defer statement defers the execution of a function until the surrounding function returns.

  • Deferred function calls are pushed onto a stack. When a function returns, its deferred calls are executed in last-in-first-out order.

package main

import "fmt"

func main() {
	defer fmt.Println("world")

	fmt.Println("hello")
}

Pointers

  • Go has pointers. A pointer holds the memory address of a value.

  • The & operator generates a pointer to its operand.

i := 42
p = &i
ptr := &MyError{time.Now(), "error"}
  • The * operator denotes the pointer’s underlying value.
    • This is known as dereferencing or indirecting.
fmt.Println(*p) // read i through the pointer p
*p = 21         // set i through the pointer p

* and &

& = “Where is it?” (gives you the address)

* in type = “This holds an address to…”

fmt.Println(*p) // read i through the pointer p
*p = 21         // set i through the pointer p

* in expression = “What’s at this address?” (follows the arrow)

value := *ptr  // get the value at that address

* in a function declaration means the function returns a pointer to that type.

// Returns a POINTER to MyError
func makeError() *MyError {
    return &MyError{
        When: time.Now(),
        What: "something failed",
    }
}

// Returns a VALUE of MyError
func makeError() MyError {
    return MyError{
        When: time.Now(),
        What: "something failed",
    }
}

Use cases

  • Passing a pointer to a function.
    • For large structs, it’s more efficient to pass a pointer than a copy of the struct.

    • Mutating the value of a function argument.

  • APIs: for consistency.

  • For true absence of a value.
    • nil is the zero value for pointers, interfaces, maps, slices, channels, and functions.

Structs

  • A struct is a collection of fields.
type Vertex struct {
	X int
	Y int
}
  • Struct fields are accessed using a dot.
	v := Vertex{1, 2}
	v.X = 4
	fmt.Println(v.X)
  • Struct fields can be accessed through a struct pointer.
v := Vertex{1, 2}
p := &v
p.X = 1e9
  • To access the field X of a struct when we have the struct pointer p, we could write (*p).X. However, that notation is cumbersome, so the language permits us instead to write just p.X, without the explicit dereference.

Arrays

  • An array’s length is part of its type, so arrays cannot be resized.
// example 1
var a [2]string
a[0] = "Hello"
a[1] = "World"

// example 2
var a [10]int

// example 3
primes := [6]int{2, 3, 5, 7, 11, 13}

Slices

  • A slice is a dynamically-sized, flexible view into the elements of an array. A slice does not store any data, it just describes a section of an underlying array.
// this is an array
primes := [6]int{2, 3, 5, 7, 11, 13}

// this is a slice containing 3, 5, and 7.
var s []int = primes[1:4]
  • Changing the elements of a slice modifies the corresponding elements of its underlying array.

  • Other slices that share the same underlying array will see those changes.

  • Declaring a new slice populated by elements will create a new array and copy the elements into it.

// creates a new array containing these three elements
[]bool{true, true, false}
  • Slices can contain any type, including other slices.

  • Slice indices start at 0 and end at the length of the slice.

Omitted low or high bounds

  • When slicing, you may omit the high or low bounds to use their defaults instead.

  • The default is zero for the low bound and the length of the slice for the high bound.

  • Given an underlying array a of length 10:

a[0:10]
a[:10]
a[0:]
a[:]

Empty slices

// example 1
var myslice []int

// example 2
myslice := []int{}
  • In most instances the two examples above are equivalent, however, the first has nil value and the second has a length of 0. The json.Marshal function will encode the former as null and the latter as [].

  • A nil slice has a length and capacity of 0 and has no underlying array.

Length and capacity

  • A slice has both a length and a capacity.

  • The length of a slice is the number of elements the slice points to. If an underlying array has a length of 10 but the slice range only covers half of the items, the length of the slice will be 5.

  • The capacity of a slice is the number of elements in the underlying array. If the array has a length of 10, the capacity of the slice will be 10.

  • The length and capacity of a slice s can be obtained using the expressions len(s) and cap(s).

  • The length of a slice s can be increased by re-slicing it, provided it has enough capacity.

  • The capacity of a slice s cannot be increased. You must create a new slice with a larger capacity and copying the elements of the original slice into it. In practice, the append function discussed later will do this for you.

Creating a slice with make

  • The make function creates an array full of zeros and returns a slice that refers to that array.
a := make([]int, 5)  // len(a)=5
  • To specify a capacity, pass a third argument to make.
b := make([]int, 0, 5) // len(b)=0, cap(b)=5

Appending to a slice

  • The append function appends elements to a slice.

  • If the slice has enough capacity, the underlying array is reused.

var s []int

// add a single element
// works on empty slices
s = append(s, 0)

// we can add more than one element at a time
s = append(s, 2, 3, 4)

Methods

  • Methods enable you to associate behaviour to a type; methods look a lot like functions but…

  • Methods are similar to functions but specify an additional receiver parameter straight after the func keyword.

  • The receiver can be a value or a pointer.

  • Methods can be defined on any type in the same package, not just structs. That means for types such as int, or string, defined outside the package, you must first create a custom type within the package to use a method.

  • Methods with pointer receivers can modify the value to which the receiver points.

  • Methods enable access to the fields of the receiver.

  • Methods cannot be created for types defined in other packages, including built-in types like int or string.

    • To create a method for a built-in type, you can define a new custom type and create a method for that type.
package main

import "fmt"

type User struct {
    Name string
}

func (u User) Greet() string {
    return "Hello, " + u.Name
}

func main() {
    user := User{Name: "James"}

    message := user.Greet()
    fmt.Println(message) // Hello, James
}
  • Receivers can also be pointers, which enables you to modify the underlying value:
type User struct {
    Name string
}

// Value receiver - gets a copy
func (u User) SetNameBroken(name string) {
    u.Name = name // modifies the copy, original unchanged
}

// Pointer receiver - gets the actual struct
func (u *User) SetName(name string) {
    u.Name = name // modifies the original
}

func main() {
    user := User{Name: "James"}

    user.SetNameBroken("Alice")
    fmt.Println(user.Name) // James (unchanged)

    user.SetName("Alice")
    fmt.Println(user.Name) // Alice (changed)
}
  • Pointer receivers can also help avoid large structs being copied around.

Difference between declaring methods and functions

This is a method:

func (p *person) changeName(newName string) {

The receiver is (p *person).

This is a function:

func changeName(newName string) {

Methods are invoked using dot notation .:

a := person{name: "a"} a.changeName("John")

When to use methods

  • Use methods when managing state.
  • Code organization: group related functions together.
  • Interface implementation: methods are used to implement interfaces.

Other differences between methods and functions

Aspect Method Function
Contains a receiver Yes No
Methods with the same name but different types Allowed Not allowed
Usage as first-order objects Cannot be used Can be used

Interfaces

Interfaces allow decoupling code, among other benefits. In a function declaration, your function might take an interface as input rather than a concrete type. Any code that implements the methods of the interface can be passed to the function. The methods of an interface could appear in any package.

Decoupling also makes interfaces very good for testing.

type Greeter interface {
    Greet() string
}

Now any type with a Greet() string method satisfies Greeter:

type User struct {
    Name string
}

func (u User) Greet() string {
    return "Hello, " + u.Name
}

type Dog struct {
    Name string
}

func (d Dog) Greet() string {
    return "Woof! I'm " + d.Name
}

Both User and Dog implement Greeter implicitly. You can use them interchangeably where a Greeter is expected:

func PrintGreeting(g Greeter) {
    fmt.Println(g.Greet())
}

func main() {
    user := User{Name: "James"}
    dog := Dog{Name: "Rex"}

    PrintGreeting(user) // Hello, James
    PrintGreeting(dog)  // Woof! I'm Rex
}

Empty Interfaces

Empty interfaces are used by code that handles values of unknown type. For example, fmt.Print takes any number of arguments of type interface{}.

func describe(i interface{}) {
	fmt.Printf("(%v, %T)\n", i, i)
}

Type Assertions

Let’s say you need to test an empty interface for the underlying type.

t := i.(T)

This statement asserts that the interface value i holds the concrete type T and assigns the underlying T value to the variable t.

If i does not hold a T, the statement will trigger a panic.

To test whether an interface value holds a specific type, a type assertion can return two values: the underlying value and a boolean value that reports whether the assertion succeeded.

t, ok := i.(T)

If i holds a T, then t will be the underlying value and ok will be true.

If not, ok will be false and t will be the zero value of type T, and no panic occurs.

package main

import "fmt"

func main() {
	var i interface{} = "hello"

	s := i.(string)
	fmt.Println(s) // hello

	s, ok := i.(string)
	fmt.Println(s, ok) // hello, true

	f, ok := i.(float64)
	fmt.Println(f, ok) // 0, false

	f = i.(float64) // panic
	fmt.Println(f)
}

Type Switches

There is a 4th switch pattern that can be used to branch off the type of an empty interface:

package main

import "fmt"

func do(i interface{}) {
	switch v := i.(type) {
	case int:
		fmt.Printf("Twice %v is %v\n", v, v*2)
	case string:
		fmt.Printf("%q is %v bytes long\n", v, len(v))
	default:
		fmt.Printf("I don't know about type %T!\n", v)
	}
}

func main() {
	do(21)
	do("hello")
	do(true)
}

The Stringer interface

The Stringer interface is an important interface in the fmt package.

If there is a method with name String it satisfies the Stringer interface and will be used Println and similar functions.

Errors

Errors are implemented as an interface.

type error interface {
    Error() string
}

Errors are always the last returned value.

If an error is nil then the function returned success. If the error was not nil, then an error occurred.

if err != nil {
    fmt.Printf("couldn't convert number: %v\n", err)
    return
}

Channels

Buffered Channels

Buffered channels have a capacity whereas unbuffered channels do not.

ch := make(chan string, 4)

Unbuffered channel:

ch := make(chan string)

Unbuffered channel:

  • Sending blocks until another goroutine receives.
  • Synchronous — sender and receiver must meet at the same time.

Buffered channel:

  • Sending only blocks when the buffer is full.
  • Receiving only blocks when the buffer is empty.
  • Asynchronous — sender can “fire and forget” up to the buffer size.

This buffer size of 4 (matching the number of URLs) means:

  • All 4 goroutines can send their results without blocking.
  • No goroutine has to wait for main to start receiving.
  • Without the buffer, you’d risk a deadlock since wg.Wait() blocks before reading from the channel.

Make keyword

The make keyword can be used to make slices, maps, and channels.

  1. Slices/arrays
// make([]Type, length, capacity)

// Just length (capacity = length)
s1 := make([]int, 5)        // [0 0 0 0 0], len=5, cap=5

// Length and capacity
s2 := make([]int, 5, 10)    // [0 0 0 0 0], len=5, cap=10

// Common pattern: empty slice with pre-allocated capacity
s3 := make([]string, 0, 100) // [], len=0, cap=100
  1. Maps
// make(map[KeyType]ValueType)

// Create empty map
m1 := make(map[string]int)
m1["age"] = 30

// With size hint (optimization, not a limit)
m2 := make(map[string]int, 100)  // Hint: expect ~100 entries
  1. Channels
// make(chan Type)
// make(chan Type, bufferSize)

// Unbuffered channel (blocks until receiver ready)
ch1 := make(chan int)

// Buffered channel (can hold N items before blocking)
ch2 := make(chan int, 10)    // Buffer of 10
ch3 := make(chan string, 1)  // Buffer of 1
  1. Slice of maps
// Slice of maps where each map is string -> int
data := make([]map[string]int, 3)

// But wait! The maps inside are nil, you need to initialize them
data[0] = make(map[string]int)
data[0]["age"] = 30

data[1] = make(map[string]int)
data[1]["age"] = 25

data[2] = make(map[string]int)
data[2]["age"] = 40

fmt.Println(data)  // [map[age:30] map[age:25] map[age:40]]
  1. Array of maps
// Array of 3 maps (fixed size)
var data [3]map[string]int

// Same gotcha - maps are nil and need initialization!
data[0] = make(map[string]int)
data[0]["age"] = 30

data[1] = make(map[string]int)
data[1]["age"] = 25

data[2] = make(map[string]int)
data[2]["age"] = 40

fmt.Println(data)  // [map[age:30] map[age:25] map[age:40]]
  1. Map of slice
// Map where values are slices of strings
teams := make(map[string][]string)

teams["engineering"] = []string{"Alice", "Bob", "Charlie"}
teams["marketing"] = []string{"Diana", "Eve"}
teams["sales"] = []string{"Frank", "Grace", "Henry", "Iris"}

fmt.Println(teams["engineering"])  // [Alice Bob Charlie]
  1. Map of maps
// Map of maps: map[string]map[string]int
users := make(map[string]map[string]int)

// Initialize inner maps before using them!
users["alice"] = make(map[string]int)
users["alice"]["age"] = 30
users["alice"]["score"] = 95

users["bob"] = make(map[string]int)
users["bob"]["age"] = 25
users["bob"]["score"] = 88

fmt.Println(users)
// map[alice:map[age:30 score:95] bob:map[age:25 score:88]]
  1. Maps of structs
type User struct {
    Name  string
    Age   int
    Email string
}

// Map user IDs to User structs
users := make(map[int]User)

users[1] = User{Name: "Alice", Age: 30, Email: "alice@example.com"}
users[2] = User{Name: "Bob", Age: 25, Email: "bob@example.com"}

fmt.Println(users[1].Name)  // Alice
  1. Slice of structs
type Product struct {
    ID    int
    Name  string
    Price float64
}

// Start with empty slice
products := make([]Product, 0)

// Append products
products = append(products, Product{ID: 1, Name: "Laptop", Price: 999.99})
products = append(products, Product{ID: 2, Name: "Mouse", Price: 29.99})
products = append(products, Product{ID: 3, Name: "Keyboard", Price: 89.99})

fmt.Println(len(products))  // 3