golang

Tags :: Language

Install

See site if not on mac w/ homebrew, otherwise

brew install go

Access all tools with go.

  • compiler: go build
  • code formatter: go fmt
  • dependency manager: go mod
  • test runner: go test
  • mistake scanner: go vet

Hello World

Create a new project (module) with go mod init hello_world. A module contains both source code and an exact specification of the dependencies of the code within the module.

Every module has a go.mod file in its root directory, which declares the nam e of the module, the minimum supported version of Go, and any other modules the module depends on. The tools go get and go mod tidy are used to manage and make changes to the file.

Types

Called predeclared types.

Zero

Assigns a default zero value to any variable that is declared but not assigned a value.

Literals

Explicitly specified number, character or string. There are 4 common and 1 uncommon (complex numbers) literal.

Integer

Base 10 by default, but different prefixs are used to indicate other bases. E.g. 0b for binary, 0o for octal, etc.

Floating-point

decimal point to indicate fractional portion, and can also have an exponent.

Rune

Represents a character and is surrounded by single quotes. Single and double quoutes are not interchangable.

String

Two ways to create. Interpreted string literals are created with double quotes. These contain zero or more rune literals. The other way is with a raw string literal, delimted with backquotes (`) and can contain any character except a backqoute.

Literals are considered untyped.

Booleans

Either true or false, with the zero value being false.

var flag bool // no assignment, set to false
var ff = true

Special integer types

  • byte alias for uint8, with byte being the more commonly used.
  • int declares an signed integer with the size of the “word” for a given archtecture. Operations can’t be preformed with other integer types without an explicit conversion.
  • uint Follows the same rule as int, except its unsigned.
  • rune alias for an int32 type
  • uintptr

Strings

The zero value is an empty string. Strings are immutable, they can be reassigned, but you cannot change the value of the string that is assigned to it.

Type conversion

Go does not support automatic type conversion. All variable types need to be converted to interact.

var x int = 10
var y float64 = 30.2
var sum1 float64 = float64(x) + y
var sum2 int = x + int(y)
fmt.Println(sum1, sum2) // 40.2  40

Slices

A slice created with no value assigned is assigned the zero value of nil.

Slices are not comparable, execpt with == and nil.

var x []int
fmt.Println(x == nil) // prints true

The slices package in the stdlib includes the two methods to compare slices:

  • slices.Equal takes two slices and returns true if the slices are the same length AND all elements are equal. Additionally all of the elements of the slice must be comparable.
  • slices.EqualFunc takes a function to determine equality, and does not require slice elements to be comparable.
x := []int{1, 2, 3, 4, 5}
y := []int{1, 2, 3, 4, 5}
z := []int{1, 2, 3, 4, 5, 6}
//s := []string{"a", "b", "c"}
fmt.Println(slices.Equal(x, y)) // prints true
fmt.Println(slices.Equal(x, z)) // prints false
//fmt.Println(slices.Equal(x, s)) // does not compile

append

built in function for growing slices. Can be used with

  • nil slices
  • slices with existing elements
  • to append multiple elements
  • to append slices into slices with ...
var x[] int
x = append(x, 10)

var x[] int{1, 2, 3}
x = append(x, 4)

x = append(x, 5, 6, 7)

y := []int{20, 30, 40}
x = append(x, y...)

NOTE: it is a compile time error to not assign the value returned from append. This is because golang calls by value. So everytime you pass a parameter to a function, go makes a copy of the value that’s passed in.

Slice capacity and memory

Each element in a slice is a assigned to consecutive memory locations which makes read + write quick. Every slice then has a capacity, the number of consecutive memory locations reserved. This can be larger than the length.

Each time a slice is appended to, one or more values is added to the end of the slice and each value increases the length by one. When length reaches the capacity there’s no more room to put values, and when trying to append new values the following happens:

  • append will use the go runtime to allocate a new backing array with a larger capacity.
  • The values in the original backing array are copied to the new one
  • The new values are added to the end of the new backing array
  • The slice is updated to refer to the new backing array
  • The updated slice is returned

The rules for growing capacity is to double capacity when it is less than 256, while a bigger slice increases by (current capacity + 786)/4 and slowly converges at 25% growth. E.g. a slice with cap of 512 will grow by 63% while a slice with capacity 4,096 will grow by only 30%.

creating an empty slice with length n

The built-in make function is used to declare an empty slice with a specified type, length, and capacity (optional).

x := make([]int, 5)

The initalized values will be set to zero.

The capacity can be set like

x := make([]int, 5, 10)

which creates a slice with length 5, and capacity 10.

A slice with an empty length and a capcity greater than 0 can also be declared.

x := make([]int, 0, 10)
x = append(x, 5,6,7,8)

emptying a slice

go 1.21 adds a clear function that takes in a slice and sets all of the slices elements to their zero value. The length remains unchanged.

s := []string{"first", "second", "third"}
fmt.Println(s, len(s))
clear(s)
fmt.Println(s, len(s))

Declaring

Given the primary goal is to minimize the number of times the slice needs to grow, what is the best way to declare a slice?

If you need a slice that won’t need to grow at all, use a var declaration to create a nil slice (NOT a slice literal). Otherwise, if you have starting values that aren’t going to change, then a slice literal is a good choice.

var data []int // nil slice
data := []int{2, 4, 6, 8} // slice literal

What if you have a good idea of how big it needs to be but don’t know what values will be when writing the program, use make. The then becomes if you should specify a nonzero length in the call to make or specify a zero length and a nonzero capacity. There are three common possibilities.

  • if using slice as a buffer, then specify a nonzero length.
  • if sure you know the exact size you want, specify the legnth and index into the slice to set the values.
    • often done when transforming values in one slice and storing them in a second. However, if you have the size wrong, you’ll end up with zero values at the end of the slice or a panic from trying to access elements that don’t exist.
  • Other situations, use make with a zero length and a specified capacity. Allows you to use append to add items to the slice. If the number of items turns out to be smaller, you won’t have extraneous zero values at the end. and if the number of items is larger, the code will not panic.

Slicing slices

A slice expression gives a slice from a slice. It’s inside brackets and consists of a starting offset and an ending offset, seperated by a colon (:). Think numpy style.

Slicing in Go is exclusive. The starting offset is the first position in the slice that is included in the new slice, and the ending offset is one past the last position to include. E.g. [0:2] will give you index at 0 and 1.

x := []string{"a", "b", "c", "d"}
y := x[:2] // same as x[0:2]
z := x[1:]
d := x[1:3]
e := x[:]
fmt.Println("x:", x)
fmt.Println("y:", y)
fmt.Println("z:", z)
fmt.Println("d:", d)
fmt.Println("e:", e)

When taking a slice, you are not making a copy of the data. Instead you now have two variables that are sharing memory. Thus a change to an element in slice will effect all slices that share that element.

x := []string{"a", "b", "c", "d"}
y := x[:2] // same as x[0:2]
z := x[1:]
x[1] = "y"
y[0] = "x"
z[1] = "z"
fmt.Println("x:", x)
fmt.Println("y:", y)
fmt.Println("z:", z)

This gets particulary confusing with append. Whenever you take a slice from another slice, the subslices capacity is set to the capacity of the original slice, minus the starting offset of the subslice within the original slice. Thus elements of the original subslice beyond the end of the subslice are shared by both slices.

When you make a y slice from x, the length is set to 2, but the capacity is set to 4; the same as x. Thus appending onto the end of y puts the value in the third position of x.

There are a couple ways to prevent from overwriting data with subslices.

  1. don’t use append with subslices.
  2. use a full slice expression to make sure append doesn’t cause an overwrite.
  • Full slice expression

    This includes a third part in the slice which indicates the last position of the parent’s slice capacity that’s available for the subslice.

    x := make([]string, 0, 5)
    x = append(x, "a", "b", "c", "d")
    
    y := x[:2:2]
    yy := x[:2:3]
    z := x[2:4:4]
    
    fmt.Println("x:", x)
    fmt.Println("y:", y, cap(y), len(y))
    fmt.Println("yy:", yy, cap(yy), len(yy))
    fmt.Println("z:", z, cap(z), len(z))
    
  • Copying slices

    Go has a built in copy function to create dependent slices. It takes two params, the destination slice and the source slice. The function copies as many values as it can from the source to destination, limited by whichever slice is smaller and returns the number of elements copied.

    NOTE:The capacity of x and y do not matter, its the length that’s important.

    A subset of a slice can also be copied

    x := []int{1,2,3,4}
    y := make([]int, 2)
    num := copy(y,x) // first two elements are copied
    copy(y, x[2:]) // copies from middle of x
    

    Copy also allows copying between two slices that cover overlapping sections of an underlying slice. E.g. the following copies the last thre values in x on top of the first three values of x.

    x := []int{1,2,3,4}
    num := copy(x[:3],x[1:])
    fmt.Println(x, num)
    

    Copy can also be used to taking a slice of an array, and it can either be the destination or the source of the copy.

    x := []int{1, 2, 3, 4}
    d := [4]int{5, 6, 7, 8} // array
    y := make([]int, 2)
    copy(y, d[:])
    fmt.Println(y)
    copy(d[:], x)
    fmt.Println(d)
    
  • Converting arrays to slices

    An array can have a slice taken using a slice expression, providing a useful way to bridge an array to a function that takes only slices. The [:] syntax is used to convert an entire array to a slice:

    xArray := [4]int{5, 6, 7, 8}
    xSlice := xArray[:]
    

    Or a subset of an array can be sliced

    x := [4]int{5, 6, 7, 8}
    y := x[:2]
    z := x[2:]
    

    NOTE: Taking a slice from an array has the same memory sharing properties as taking a slice from a slice.

  • Converting slices to arrays

    Type conversion is used to make an array from a slice. This cna either be an entire slice to an array of the same type, or an array from a subset. When an array is created from a slice, the data in the slice is copied into new memory.

    xSlice := []int{1, 2, 3, 4}
    xArray := [4]int(xSlice)
    smallArray := [2]int(xSlice)
    xSlice[0] = 10
    fmt.Println(xSlice)
    fmt.Println(xArray)
    fmt.Println(smallArray)
    

    NOTE: The size of the array must be specified at compile time. It is a compile-time error to use [...] in a slice to array type conversion.

    The size of the array can be smaller than the slice, but not bigger. The compiler does not check this and the code will panic at run time.

    • slice to pointer to an array

      A type conversion can be used to convert a slice into a pointer to an array

      xSlice := []int{1, 2, 3, 4}
      xArrPtr := (*[4]int)(xSlice)
      

      This allows the storage between the two to be shared, so a change to one will change the other

Maps

Map type is written as map[keyType]valueType. These can be declared with var to create a map set to its zero value with length zero, and reading it will always return teh zero value for the maps type. Writing to a nil map causes a panic.

// map with string keys and int values
var nilMap map[string]int

The := declaration can create a map variable by assigning it a map literal

totalWins := map[string]int{}

This is not the same as a nil map. Its length is 0, but it can be read and written to.

teams := map[string][]string {
    "Orcas": []string{"Fred", "Ralph", "Bijou"},
    "Lions": []string{"Sarah", "Peter", "Billie"},
    "Kittens": []string{"Waldo", "Raul", "Ze"},
    }

If the number of key value pairs needed are known, but don’t know the exact size make can be used to create a map with a default size

ages := make(map[int][]string, 10)

The length is still zero and they can grow past the initially specified size.

Structs

Typed defined with type keyword, the name of the struct type, the keyword struct and braces.

type person struct {
    name string
    age int
    pet string
}

bob := person{}

julia := person{
    "julia",
    40,
    "cat",
}

beth := person{
    age: 30,
    name: "Beth",
}

Fields are accessed with the dot notation:

type person struct {
    name string
    age int
    pet string
}
bob := person{}
bob.name = "bob"
fmt.Println(bob.name)

anonymous structs

A variable can implement a struct type without giving the name

pet := struct {
    name string
    kind string
} {
    name: "Fido",
    kind: "dog",
}

These are useful for marshaling and unmarshaling data (e.g. Json and protocal buffers)

Runtime

Provides services like memory allocation and garbage collection, concurency support, networking, and implementations of built-in types and functions. It is compiled into every go binary.

This is different from languages which rely on a virtual machine. While a VM must be installed seperatly to allow programs to function, including the runtime in the binary makes it easier to distribute go programs without worrying about compabatility.

The drawback is that the runtime results in a larger binary, approx. 2MB for the simplest go program.

Shadowing

This is a variable that has the same name as a variable in a containing block. For as long as the shadowing variable exists, you cannot access a shadowed variable.

x := 10
if x > 5 {
  fmt.Println(x)
  x := 5
  fmt.Println(x)
}
fmt.Println(x)

if

Like if statements in other languages, but without parenthesis around statements.

n := rand.Intn(10)
if n == 0 {
    fmt.Println("That's too low")
} else if n > 5 {
    fmt.Println("That's too big:", n)
} else {
    fmt.Println("That's a good number:", n)
}

Howver, go adds the ability to declare variables that are scoped to the condition and to both the if and else blocks.

if n := rand.Intn(10); n == 0 {
    fmt.Println("That's too low")
} else if n > 5 {
    fmt.Println("That's too big:", n)
} else {
    fmt.Println("That's a good number:", n)
}

This is handy for variables that only need to be created where they need to be available.

For

There are four ways to use the for keyword:

  • complete, c-style
  • condition only
  • infinite
  • for-range

complete

for i := 0; i < 10; i++ {
    fmt.Printf("%d ", i)
}

NOTE: := must be used to initialize variables, var is not legal. Shadowing is legal.

One or more of the three parts of the statement can be left out. Most common is to leave off the initialization if it is based on a value calculated before the loop, or the increment will be left off becuase of a more complicated increment rule within the loop.

condition only

This leaves off both the initialization and increment and operates like while statement in other languages.

i := 1
for i < 100 {
    fmt.Printf("%d ", i)
    i = i * 2
}

infinite

Leaves off everything

for {
  fmt.Println("Hello")
}

Go includes both the break and continue keywords for dealing with loops.

for-range

Resembles iterators from other languages, and can be used with strings, arrays, slices, and maps.

With a for-range loop you get two variables, the position in the data struct being iterated, and the value at that position.

evenVals := []int{2, 4, 6, 8, 10, 12}
for i, v := range evenVals {
    fmt.Println(i, v)
}

If you don’t need the key/index in, the underscore (_) can be used, telling go to ignore the value.

evenVals := []int{2, 4, 6, 8, 10, 12}
for _, v := range evenVals {
    fmt.Printf("%d ", v)
}

If you just need the key/index, you can leave off the second variable:

uniqueNames := map[string]bool{"Fred": true, "Raul": true, "Wilma": true}
for k := range uniqueNames {
    fmt.Printf("%s ", k)
}

Switch

words := []string{"a", "cow", "smile", "gopher",
    "octopus", "anthropologist"}
for _, word := range words {
    switch size := len(word); size {
    case 1, 2, 3, 4:
        fmt.Println(word, "is a short word!")
    case 5:
        wordLen := len(word)
        fmt.Println(word, "is exactly the right length:", wordLen)
    case 6, 7, 8, 9:
    default:
        fmt.Println(word, "is a long word!")
    }
}

Functions

Does not have named or optional parameters

Variadic parameters

These are indicated with ...type and must be the last|only parameter. The variable thats created is a slice of the specified type.

func addTo(base int, vals ...int) []int {
    out := make([]int, 0, len(vals))
    for _, v := range vals {
        out = append(out, base+v)
    }
    return out
}

Multiple return values

Go allows multiple return values. The return types are listed in parentheses, seperated by comas. If it returns multiple values you must return all of them.

func divAndRemainder(num, denom int) (int, int, error) {
    if denom == 0 {
        return 0, 0, errors.New("cannot divide by zero")
    }
    return num / denom, num % denom, nil
}

Typically this is used to return an error if something goes wrong in a function. By convention, the error is always last (or only) value returned.

The above is used like

result, remainder, err := divAndRemainder(5, 2)
if err != nil {
    fmt.Println(err)
    os.exit(1)
}
fmt.Println(result, remainder)

Each value must be returned from a function, if you try to assign multiple values to one variable, you get a compile time error.

named return values

Functions can have names specified for the return values, which predeclares variables that are used within the function to hold the return values. These values are initialized to their zero values allowing you to return them before any explicit use or assignment.

func divAndRemainder(num, denom int) (result int, remainder int, err error) {
    if denom == 0 {
        err = errors.New("cannot divide by zero")
        return result, remainder, err
    }
    result, remainder = num / denom, num % denom
    return result, remainder, err
}

Closures

These are functions defined within a function, and are able to access and modify variables declared in the outer function. Closures allow you to pass fome function state to another function. These can also be returned from a function.

package main

import "fmt"

func main() {
    a := 20
    f := func() {
        fmt.Println(a)
        a = 30
    }
    f()
    fmt.Println(a)
}

Anonymous function f can read and write a even though a is not passed in to the function. Using := instead of = inside the closure creates a new a that ceases to exist when the closure exits.

Why use closures?

limit function scope

If a function is going to be called from only one other function, but it’s called multiple times, you can use an inner function to “hide” the called function. This reduces the number of declarations at a package level, making it easier to find an unused name.

removing logic repetetion

Given a piece of logic that is repeated multiple times within a function, a closure can be used to remove that repetition. See the following example from a jonbodner/my_lisp:

package scanner

import "github.com/jonbodner/my_lisp/types"

func Scan(s string) ([]types.Token, int) {
    var out []types.Token
    var curTokenTxt []rune
    buildCurToken := func() {
        if len(curTokenTxt) > 0 {
            if len(curTokenTxt) == 1 && curTokenTxt[0] == '.' {
                out = append(out, types.DOT)
            } else {
                out = append(out, types.NAME(curTokenTxt))
            }
            curTokenTxt = make([]rune, 0)
        }
    }
    update := func(t types.Token) {
        buildCurToken()
        out = append(out, t)
    }

    depth := 0
    for _, c := range s {
        switch c {
        case '(':
            update(types.LPAREN)
            depth++
        case ')':
            update(types.RPAREN)
            depth--
        case '.':
            curTokenTxt = append(curTokenTxt, c)
        case '\n', '\r', '\t', ' ':
            buildCurToken()
        case '\'':
            update(types.QUOTE)
        default:
            curTokenTxt = append(curTokenTxt, c)
        }
    }
    buildCurToken()
    return out, depth
}

Here the scanner uses two closures: buildCurToken and update to make the code shorter and easier1 to understand.

passing to funcs/returning from funcs

These become really interesting, allowing you to take variables within your function and use those variables outside of the function

Passing and returning funcs from funcs

Passing

Functions are values and the type of a function is specified using its parameter and return types, thus you can pass functions as parameters into functions.

NOTE: Think about the implications of creating a closure that references local variables and then passing that closure to another function. It means a closure can be used to pass some function state to another function.

An example of this pattern is when sorting slices with the stdlib sort.Slice, which takes in any slice and a function that is used to sort the slice that’s passed in. See the following with a simple type, Person, that is sorted first by last name and then by age:

package main

import (
    "fmt"
    "sort"
)

func main() {
    type Person struct {
        FirstName string
        LastName  string
        Age       int
    }

    people := []Person{			//
        {"Pat", "Patterson", 37},
        {"Tracy", "Bobdaughter", 23},
        {"Fred", "Fredson", 18},
    }
    fmt.Println(people)

    // sort by last name
    sort.Slice(people, func(i int, j int) bool { //
        return people[i].LastName < people[j].LastName
    })
    fmt.Println(people)

    // sort by age
    sort.Slice(people, func(i int, j int) bool { //
        return people[i].Age < people[j].Age
    })
    fmt.Println(people)
}
  • First a slice, people, of type Person is created.
  • people is then sorted by last name and the results printed
    • The closure passed has two parameters i and j, but within the closure people is used, so you can sort it by the LastName field. In CS terms, people is captured by the closure.
  • Another closure is defined to sort by Age following the same practice as before.
  • The original people slice is changed by the call to sort.Slice

Returning

A closure can also be returned from a function. See the following which returns a mutliplier function.

package main

import "fmt"

func makeMult(base int) func(int) int {
    return func(factor int) int {
        return base * factor
    }
}

func main() {
    twoBase := makeMult(2)
    threeBase := makeMult(3)
    for i := 0; i < 3; i++ {
        fmt.Println(twoBase(i), threeBase(i))
    }
}

Returning closures is used for a variaty cases where closures are useful. Such as sorting and and searching slices. For returning closures useful examples include middleware implementations and resource cleanup with defer.

NOTE: Functional languages such as haskell use the terminology higher-order functions to refer to a function that has a function for an input parameter or return value.

defer

Quick notes:

  • can use function, method, or closure
  • can defer multiple functions within a function
    • Runs in LIFO order; the last defer registered runs first.
  • code within defer functions runs after the return statement.
    • input parameters supplied to defer are evaluated immediatly and their values are stored until the function runs.
      package main
      
      import "fmt"
      
      func deferExample() int {
          a := 10
          defer func(val int) {
              fmt.Println("first:", val)
          }(a)
      
          a = 20
          defer func(val int) {
              fmt.Println("second:", val)
          }(a)
      
          a = 30
          fmt.Println("exiting:", a)
          return a
      }
      
      
      func main() {
          _ = deferExample()
      }
      
      

Cleanup code is attached to a the function with the defer keyword. See the following simple version of cat.

package main
import (
    "io"
    "log"
    "os"
)

func main() {
    if len(os.Args) < 2 {
        log.Fatal("no file specified")
    }
    f, err := os.Open(os.Args[1])
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close() //

    data := make([]byte, 2048)

    for {
        count, err := f.Read(data)
        os.Stdout.Write(data[:count])
        if err != nil {
            if err != io.EOF {
                log.Fatal(err)
            }
            break
        }
    }
}

The important thing to see in this example is using defer to close the file handle. Once there is a valid file handle, it needs to be closed after it is used, no matter how the function is exited. defer ensurs that the cleanup code runs. It is used with the defer keyword, followed by a function or method call. Normally a function call will run immediatly but defer delays invocation until the surrounding function exits.

Common pattern to modify values in surrounding function

Deferred functions can examine or modify the return values of the surrounding functio. By using named return values, it can allow your code to take actions based on an error. See the following database example:

func DoSomeInserts(ctx context.Context, db *sql.DB, value1, value2 string) (err error) {
    tx, err := db.BeginTx(ctx, nil)
    if err != nil {
        return err
    }
    defer func() { //
        if err == nil {
            err = tx.Commit()
        }
        if err != nil {
            tx.Rollback()
        }
    }() // compile time error to leave out the braces

    _, err = tx.ExecContex(ctx, "INSERT INTO FOO (val) values $1", value1)
    if err != nil {
        return err
    }
    // use tx to do more database inserts here
    return nil
}

If any of the transactions fail you want to rollback the modifications, and if any succed you want to commit the changes. The closure with defer checks if err has been defined a value. If it hasn’t you run tx.Commit, which could al return an err. If it does the value err is modified. If any database interaction returned tx.Rollback is called.

Common pattern for resource allocation

Take the cat program. It can be rewritten to use a helper function that opens a file and returns a closure.

func getFile(name string) (*os.File, func(), error) {
    file, err := os.Open(name)

    if err != nil {
        return nil, nil, err
    }
    return file, func() {
        file.Close()
    }, nil
}

The helper returns a file, function, and an error. From main we can use the helper like so:

f, closer, err := getFile(os.Args[1])
if err != nil {
    log.Fatal(err)
}
defer closer()

Since go doesn’t allow unused variables, returning the closer from the function means that the program will not compile if the function is not called, reminding the user to use defer.

Pointers

Standard address (&) and indirection/dereferencing (*) operators. Pointer tricks such as pointer arithmetic that you can do in C are not allowed in go.

Before dereferencing a pointer, you must make sure it is non-nil, otherwise the program will panic if you attempt to dereference a nil pointer:

var * int
fmt.Println(x == nil)			// prints true
fmt.Println(*x)					// panics

The pointer type can be based on any type, and the built in function new creates a pointer variable which returns a pointer to a zero-value instance of the type.

var x = new(int)
fmt.Println(x == nil)			// prints false
fmt.Println(*x)					// prints 0

However, the new function is rarely used.

For structs use & before a struct literal to create a pointer instance. When you need a pointer to a primitive type, declare a variable and then point to it.

pointers of constants / literals

Constant literals cannot use & because they don’t have a memory address, e.g.

type person struct {
    FirstName string
    MiddleName *string
    LastName string
}

p := person {
        FirstName: "Pat",
        MiddleName: "Perry", // This line will not compile
        LastName: "Peterson",
    }

There are two ways around the problem. The first is to introduce a variable to hold the constant. The second is to write a generic helper function that takes a parameter of any type and returns a pointer to that type:

func makePointer[T any](t T) *T {
  return &t
}

which allows one to write the previous code as

p := person {
        FirstName: "Pat",
        MiddleName: makePointer("Perry"),
        LastName: "Peterson",
    }

NOTE: The only time you should use pointer parameters to modify a variable is when the function expects an interface, e.g. JSON.

Difference between maps and slices

Within the go runtime, a map is implemented as a pointer to a struct, so passing a map to a function means that you are copying a pointer. Thus changes made to a map within the functions are reflected in the original.

NOTE: maps for input parameters or return values should be carefully considered. On an API design level, they are a bad choice because they say nothing about the values contained within. In general one should prefer a struct for these cases.

A slice is more complicated. Any modification to a slices contents within a function is reflected in the original, but using append to change the length of the array is not, even if the slice has a capacity greater than its length. This is due to slice being implemented a struct with three fields:

  • int for length
  • int for capacity
  • a pointer to a block of memory

Reference


  1. Does it make it easier to track the logic? I guess it depends on experience. Should try to find a writeup discussing this. ↩︎


No notes link to this note