Go Notes

These are notes on the Go programming language.

Basics

Documentation is at golang.org. It’s also available using the go doc tool.

go doc <module> <function>

Go source is compiled and executed from a GOPATH.

cd ./project
export GOPATH=`pwd`
go install project

Go dependencies are done with Go Modules. Dependencies will be detected within their use in code and then automatically included (but may need to be downloaded).

cd ./project
go mod init project
go get <package>
go mod tidy

Projects with a main package and a main.go file can be directly executed.

go run main.go

Variables

Go has static types with hinting. You can explicitly declare types or you can have Go figure it out.

The following are equivalent:

var message string
message = "Hello"

// or

var message string = "Hello"

// or

message := "Hello"

The assignment operator := will also allow you to redeclare some variables, as long as part of the declaration statement includes new variables.

old_var := 3
new_var, old_var := 3, 4

Outside functions, const blocks declare global constants and var blocks declare global variables. Capitalized constants and variables export them for use by outside modules.

const (
  foo = "bar"
)

var (
  Fizz = "buzz"
)

The _ variable can be used to throw away unneeded information.

var foo, _ = myFunc()

Go will error on unused or over-declared variables.

All variables, when declared, are initialized to their “zero value,” which depends on the type (e.g., booleans get false and integers get 0).

Types

Types can be defined using type functions.

var number = uint(9)  // 9 is an argument to the uint type function.

Type functions can also do some conversions. Each conversion creates a copy.

my_string := "This is a test"
my_byte_slice := []byte(my_string)
back_to_string = string(my_byte_slice)

Some times include:

Defined Types

Structs can be used to define types.

type something struct {
  foo string
  bar int
}

s := &something{foo: "test"}
s.bar = 3

Both examples below will instantiate a struct with zero-values.

s := &something{}
s := new(something)

Types and the type keyword can also be used to alias and extend existing types with methods.

type SuperIntSlice []int
s := &SuperIntSlice{1, 2, 3, 4, 5, 6}

Strings

Strings can be indexed using array brackets and :.

astr := "This is my string.\n"

astr[0:4]
astr[:4]

The range keyword allows k/v iteration.

for _, r := range astr {
  ...
}

Looping over a string rune by rune:

for i, r := range astr {
  fmt.Printf("%d %c\n", i, r)
}

Get string length with len().

Strings in back quotes, `, are interpreted literally. Double quotes, "", will interpret special characters, like \n.

Arrays and Slices

Go handles normal array behavior with arrays and slices.

Declaring arrays can be done bounded or unbounded.

unbounded := [...]string{"foo", "bar"}
bounded := [2]string{"fizz", "buzz"}

Slices are a specific sections of arrays. They are defined using empty [].

my_slice := []string{"the", "quick", "brown", "fox"}

Slices can be formed on the fly from arrays or other slices. The first value is the starting index (inclusive), the second value is the ending index (exclusive).

foo = unbounded_slice[1:2]  // Includes only unbounded_slice[1]
bar = unbounded_slice[:3]   // Includes the first three items.

Arrays are fixed in memory and are only passed between functions by copying, whereas slices are passed by reference. Slices are more performant when passed between functions because it doesn’t copy data, and allows array data to be directly modified outside of its declaration scope.

Dynamic slices can be created using the make function, which allocates memory.

words := make([]string, 4)      // Initializes with four empty strings.
words := make([]string, 0, 4)   // Intiializes with no entries, but a max
                                // capacity of four.

words = append(words, "Added string")

Attempting to access sections of a slice or array that do not exist will cause “out of bounds” runtime errors.

Slices have no equality property, so they cannot be compared, except to nil.

Maps

Maps can be instantiated using the map keyword, either by make or by explicit definition.

my_map := make(map[key_type]value_type)

my_other_map := map[key_type]value_type{
    "foo": "bar",
    ...
}

Accessing an element of the map that doesn’t exist will return the zero type. An error can be explicitly thrown by checking for a return value.

foo, ok := my_map["bar"]  // "bar" doesn't exist.
if !ok {
  // This was an errorneous lookup.
} else {
  // The lookup worked.
}

Iterating over a map uses range. Maps are not sorted by default, so results will come out in arbitrary order.

for month, days := range days_in_months {
  fmt.Printf("There are %d days in %s.\n", days, month)
}

Adding a value to a map just requires an assignment.

my_map["foo"] = "bar"

Items can be deleted from maps. Deletion will silently pass if the key does not exist.

delete(my_map, "foo")
delete(my_map, "foo")  // Does not delete, does not error.

Keys can be any type that has equality.

Conditionals

Go can use if statements for error handling. Variables declared within if statements are only accessible within the conditional block.

if readCount, err = module.Format("Hello.\n"); err != nil {
  // Respond to an error.
} else if readCount == 0 {
  fmt.Printf("Read nothing!")
}else {
  fmt.Printf(readCount)
}

Go also has switch statements.

switch {
case foo == "bar":
  ...
case foo == "bizz":
  ...
default:
  os.Exit(1)
}

Switch statements do not have a necessary break and do not fall-through. Instead, the fallthrough keyword will cause cases to collapse.

case foo == "bar":
  ...
  fallthrough

You can also switch on a particular variable.

r = someRandomCharacter()

switch r {
case "a", "e", "i", "o", "u":
  fmt.Printf("r turned out to be a vowel.")
default:
  fmt.Printf("r is weird!")
}

Loops

The for loop covers all cases.

An infinite loop:

for {
  fmt.Printf("Hello, world.\n")
}

A constrained loop:

counter := 1
for counter < 100 {
  counter += 1
}

// or ...

for counter := 0; counter < 10; counter++ {
  fmt.Printf("Foo bar.\n")
}

Go does have post-fix updating, foo++, but it isn’t a statement and is only valid inside loop clauses.

Loops can simultaneously use assignments and multiple conditions.

Functions

Calling a function takes this form:

<package>.<function>([args ...])

A function definition looks something like this:

func name(param1 type, param2, param3 shared_type) (return_type1, return_type2) {
  ... statements ...

  return foo, bar
}

The defer keyword queues up actions to be taken after a function exits. Statements which are defered execute in stack order. Useful for function clean-up.

func printer(msg []byte) error {
  f, err := os.Create("helloworld.txt")
  defer f.Close()

  if err == nil {
    f.Write(msg)
  }

  return err
}

Return values in Go can also be defined in the function clause for shorthand returns.

func tester(msg []byte) (e error) {
  _, e = fmt.Printf("%s\n", msg)
  return  // Automatically returns `e`.
}

Go supports variadic functions with the ... parameter.

func tester(fmt string, msgs ...string) {
  for _, msg := range msgs {
    fmt.Printf(fmt, msg)
  }
}

In general, pass-by-reference is accomplished by using pointers. Otherwise, Go usually uses pass-by-copy.

Errors

Go has errors which often return from functions and also (the very rarely used) panics to handle extreme events.

func my_printer(msg string) error {
  _, err := fmt.Printf("%s\n", msg)
  return err
}

if err := my_printer("Hello, world."); err != nil {
  // The function returned an error.
  os.Exit(1)
}

By default, errors use the built-in standard library. Defining errors can be done using error formatting, Errorf, or the errors library.

panic and recover in Go can stop execution or return stack traces if something happens that is not recoverable.