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.

Packages

  • Go programs are made up of packages.
  • Programs start running in package main.
  • The filename is the package 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.
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)
	}
}
  • Switch without a condition is the same as switch true.
switch {
case t.Hour() < 12:
    fmt.Println("Good morning!")
case t.Hour() < 17:
    fmt.Println("Good afternoon.")
default:
    fmt.Println("Good 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
  • 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

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 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.

  • 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.

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(p \*person, 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