Moonshake's site

Go Notes

Personal documentation for Go language.

Table of content.

Variable declaration

The var statement declares a list of variables.

A var declaration can include initializers, one per variable. If an initializer is present, the type can be omitted; the variable will take the type of the initializer.

var i, j int = 1, 2

var c, python, java = true, false, "no!"

Variable declarations may be “factored” into blocks.

var (
    ToBe   bool       = false
    MaxInt uint64     = 1<<64 - 1
    z      complex128 = cmplx.Sqrt(-5 + 12i)
)

Variables declared without an explicit initial value are given their zero value. The zero value is:

:= operator is a shortcut for declaring and initializing a variable. for example the following block of code.

message := fmt.Sprintf("Hi, %v. Welcome!", name)

Is equivalent to.

var message string
message = fmt.Sprintf("Hi, %v. Welcome!", name)

Constants are declared like variables, but with the const keyword. Constants cannot be declared using the := syntax.

Types

Type comes after the variable name, for example, x int.

(x int, y int) is equal to (x, y int) (both parameters share the type).

The expression T(v) converts the value v to the type T.

var i int = 42
var f float64 = float64(i)
var u uint = uint(f)

When the right hand side of the declaration is typed, the new variable is of that same type:

    var i int
    j := i // j is an int

Basic types

bool

string

int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr

byte // alias for uint8

rune // alias for int32
     // represents a Unicode code point

float32 float64

complex64 complex128

String

A string is a read-only slice of bytes.

var s string // read-only []byte

Indexing into a string produces the raw byte values at each index:

s := "467..114.."
fmt.Print(s[0]) // outputs 52 (the decimal value of 4 in unicode/utf-8)

To count how many runes are in a string, we can use the utf8 package:

import "unicode/utf8"
// ...
utf8.RuneCountInString("Hello world!")

Array

The type [n]T is an array of n values of type T1:

var a [10]int

Arrays can be initialized as showed beneath:

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

Slice

An array has a fixed size. A slice, on the other hand, is dynamically-sized2.

To learn more about slices, read the Slices: usage and internals article.

The type []T is a slice with elements of type T:

primes := [6]int{2, 3, 5, 7, 11, 13}
var s []int = primes[1:4]

Note that a slice is formed by specifying two indices, a low and high bound (excluded)3, separated by a colon. Also a slice does not store any data, it just describes a section of an underlying array.

Changing the elements of a slice modifies the corresponding elements of its underlying array, therefore other slices that share the same underlying array will see those changes.

Initialize a slice means that it creates an array, then builds a slice that references it:

var s = []bool{true, true, false}
// Is like:
// var a = [3]bool{true, true, false}
// var s = a[0:3]

The length of a slice is the number of elements it contains.

The capacity of a slice is the number of elements in the underlying array, counting from the first element in the slice.

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

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

The make function allocates a zeroed array 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

b = b[:cap(b)] // len(b)=5, cap(b)=5
b = b[1:]      // len(b)=4, cap(b)=4

Slices can contain any type, including other slices:

board := [][]string{
    []string{"_", "_", "_"},
    []string{"_", "_", "_"},
    []string{"_", "_", "_"},
}

You can append elements to a slice using the append function:

func append(s []T, vs ...T) []T

Pointers

Pointers in Go are like pointers in C but without pointer arithmetic.

Pointer of type int:

var p *int

Get a pointer to a variable:

p = &i

Manage pointer underlying value using *:

*p = 21

Struct

A struct is a collection of fields, example:

    type Vertex struct {
        X int
        Y int
    }

Struct fields are accessed using a dot:

v := Vertex{1, 2}
v.X = 4

Struct fields can be accessed through a struct pointer:

v := Vertex{1, 2}
p := &v
p.X = 1e9 // this is the same as (*p).X = 1e9

Different ways of initialize a struct:

var (
    v1 = Vertex{1, 2}  // has type Vertex
    v2 = Vertex{X: 1}  // Y:0 is implicit
    v3 = Vertex{}      // X:0 and Y:0
    p  = &Vertex{1, 2} // has type *Vertex
)

You can initialize a structs on the fly (anonymous struct):

s := struct {
    i int
    b bool
}{
    i: 4,
    b: true,
}

Struct field declaration may be followed by an optional tag, they are made visible through a reflection interface, for example.

import (
    "fmt"
    "reflect"
)

func main() {
    type S struct {
        F string `species:"gopher" color:"blue"`
    }

    s := S{}
    st := reflect.TypeOf(s)
    field := st.Field(0)
    fmt.Println(field.Tag.Get("color"), field.Tag.Get("species"))

}

---

Output:

blue gopher

Functions

A function can return any number of results.

func swap(x, y string) (string, string) {
    return y, x
}

A return statement without arguments returns the named return values. This is known as a “naked” return.

func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return
}

High order functions

Go supports high order functions, for example:

func compute(fn func(float64, float64) float64) float64 {
    return fn(3, 4)
}

Closures

Go supports closures, as an example:

func adder() func(int) int {
    sum := 0
    return func(x int) int {
        sum += x
        return sum
    }
}

Methods

A method is a function with a special receiver argument:

func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    // ...
    v := Vertex{3, 4}
    // ...
}

You can only declare a method with a receiver whose type is defined in the same package as the method. You cannot declare a method with a receiver whose type is defined in another package (which includes the built-in types such as int):

    type MyFloat float64

    func (f MyFloat) Abs() float64 {
        if f < 0 {
            return float64(-f)
        }
        return float64(f)
    }

Methods with pointer receivers can modify the value to which the receiver points. Since methods often need to modify their receiver, pointer receivers are more common than value receivers.

Methods with pointer receivers can be called by a pointer or a value:

func (v *Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func main() {
    // ...
    var v Vertex{3, 4}
    p := &v
    p.Scale(10) // OK
    v.Scale(5)  // OK; Go parse this as (&v).Scale(5)
    // ...
}

The equivalent happens for methods con value receivers:

func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    // ...
        var v Vertex{3,4}
        v.Abs() // OK
        p := &v
        p.Abs() // OK; Go parse this as (*p).Abs()
    // ..
}

In general, all methods on a given type should have either value or pointer receivers, but not a mixture of both.

Defer

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

$ cat main.go

package main

import "fmt"

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

    fmt.Println("hello")
}

$ go run .

hello
world

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

$ cat main.go

package main

import "fmt"

func main() {
    fmt.Println("counting")

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

    fmt.Println("done")
}

$ go run .

counting
done
9
8
7
6
5
4
3
2
1
0

Map

A map maps keys to values.

The make function returns a map of the given type:

m = make(map[string]Vertex)
m["Bell Labs"] = Vertex{
    40.68433, -74.39967,
}

You can initialize them with values on the fly:

var m = map[string]Vertex{
    "Bell Labs": Vertex{
        40.68433, -74.39967,
    },
    "Google": Vertex{
        37.42202, -122.08408,
    },
}

You can omit types in the literal (Vertex in this case).

var m = map[string]Vertex{
    "Bell Labs": {40.68433, -74.39967},
    "Google":    {37.42202, -122.08408},
}

Delete an element from a map:

delete(m, key)

Test that a key is present with a two-value assignment:

elem, ok = m[key]

If key is in m, ok is true. If not, ok is false. If key is not in the map, then elem is the zero value for the map’s element type.

Note that if elem or ok have not yet been declared you could use a short declaration form:

elem, ok := m[key]

Interface

An interface type is defined as a set of method signatures:

type Abser interface {
    Abs() float64
}

Interfaces are implemented implicitly

type I interface {
    M()
}

// This method means type T implements the interface I,
// but we don't need to explicitly declare that it does so.
func (t T) M() {
    fmt.Println(t.S)
}

A value of interface type can hold any value that implements those methods:

func main() {
    var a Abser
    f := MyFloat(3.4)
    v := Vertex{3, 4}

    a = f  // a MyFloat implements Abser
    a = &v // a *Vertex implements Abser

    // In the following line, v is a Vertex (not *Vertex)
    // and does NOT implement Abser.
    // a = v

    fmt.Println(a.Abs())
}

type MyFloat float64

func (f MyFloat) Abs() float64 {
    if f < 0 {
        return float64(-f)
    }
    return float64(f)
}

type Vertex struct {
    X, Y float64
}

func (v *Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

Under the hood, interface values can be thought of as a tuple of a value and a concrete type:

(value, type)

Therefore, calling a method on an interface value is going to execute the method of the specific type the interface is holding.

The interface type that specifies zero methods is known as the empty interface:

interface{}

An empty interface may hold values of any type. (Every type implements at least zero methods).

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

Type assertions

Asserts that the interface value i holds the concrete type T and assigns the underlying T value to the variable t:

t := i.(T)

To test whether an interface value holds a specific type:

t, ok := i.(T)

Control structures

For

Go’s for loop:

sum := 0
for i := 0; i < 10; i++ {
    sum += i
}

C language while is spelled for in Go:

sum := 1
for sum < 1000 {
    sum += sum
}

If you omit the loop condition it loops forever.

for {

}

Iterating slices

The range form of the for loop iterates over a slice or map.

When ranging over a slice, two values are returned for each iteration. The first is the index, and the second is a copy of the element at that index:

for i, v := range some_slice {
    ...
}

If you only want the index, you can omit the second variable:

for i := range some_slice {
    ...
}

If

If statement can start with a short statement to execute before the condition.

func pow(x, n, lim float64) float64 {
    if v := math.Pow(x, n); v < lim {
        return v
    }
    return lim
}

Switch

Switch statement (You don’t need to provide a break in each case like in other C-family languages).

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)
}

A type switch is like a regular switch statement, but the cases in a type switch specify types (not values).

switch v := i.(type) {
case T:
    // here v has type T
case S:
    // here v has type S
default:
    // no match; here v has the same type as i
}

Errors

The error type is a built-in interface:

type error interface {
    Error() string
}

Functions often return an error value, and calling code should handle errors by testing whether the error equals nil:

i, err := strconv.Atoi("42")
if err != nil {
    fmt.Printf("couldn't convert number: %v\n", err)
    return
}
fmt.Println("Converted integer:", i)

A nil error denotes success; a non-nil error denotes failure.

At the moment the steps that I understand that should be taken in consideration when handling errors are the following:

  1. Declare a type, for example:

    type ErrNegativeSqrt float64
    
  2. Declare a method Error with a receiver of the declared type:

    func (e ErrNegativeSqrt) Error() string // this implicitly implements the Error built-in interface.
    

    Note that you can’t call Print’s (like Sprint, or Println) family functions inside this function because it will trigger an infinite loop4

  3. Declare a function that returns an error

    func Sqrt(x float64) (float64, error) {
        // ...
    
        // some logic that raise an error
        return nil, ErrNegativeSqrt
    
        // if no error is detected then error is nil
        return computed_value, nil
    
        // ...
    }
    

Readers

The io.Reader interface has a Read method:

func (T) Read(b []byte) (n int, err error)

The type T calls method Read that populates the given byte slice (b) with and returns the number of bytes (n) populated and an error (err) value. It returns an io.EOF error when the stream ends.

Generics

Generic functions / Type parameters.

The type parameters of a function appear between brackets, before the function’s arguments.

func Index[T comparable](s []T, x T) int

This declaration means that s is a slice of any type T that fulfills the built-in constraint comparable. x is also a value of the same type.

Generic types

To declare a generic type:

type myGenericType[T any] struct {
    generic_value T
    generic_slice []T
}

Goroutines

Goroutines is a lightweight thread5.

func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
}

Note that the output of the above example isn’t going to be always the same (because it depends of how the threats are managed).

Channels

Channels are an abstraction to send / receive data between goroutines.

To create a channel:

ch := make(chan int)

You can send and receive values with the channel operator, <-:

ch <- v    // Send v to channel ch.
v := <-ch  // Receive from ch, and
           // assign value to v.

By default a channel is blocked when data is sent til this data is received, and also is blocked when you are receiving data from a channel but the data hasn’t been sent. However a channel can have a buffer size:

ch := make(chan int, 100)

This means that channel will be blocked if the capacity of the channel in the above example 100 is surpassed with sends. The channel is also blocked when receiving from a channel that is empty.

Sometimes you will need to manage communication between multiple channels, in this context the select statement let a goroutine wait on multiple communication operations.

package main

import "fmt"

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

Note that at this moment I understand that a case isn’t true till the message sent in to the channel is received, in this case the case c<-x: isn’t ran till the statement in the for loop received it (fmt.Println(<-c))

The default case in a select is run if no other case is ready.

A sender can close a channel to indicate that no more values will be sent.

close(c)

The loop for i := range c receives values from the channel repeatedly until it is closed.

Receivers can test whether a channel has been closed by assigning a second parameter to the receive expression:

v, ok := <-ch

Packages

Go programs are constructed by linking together packages.

A package in turn is constructed from one or more source files that together declare constants, types, variables and functions belonging to the package and which are accessible in all files of the same package. Those elements may be exported and used in another package.

By convention, the package name is the same as the last element of the import path. For instance, the “math/rand” package comprises files that begin with the statement package rand

A package clause begins each source file and defines the package to which the file belongs. for example:

package mycustompackagename

...

// the rest of source code

A set of files sharing the same package name form the implementation of a package. An implementation may require that all source files for a package inhabit the same directory.

In Go, a name is exported if it begins with a capital letter, for example Println function from ftm builtin package.

A complete program is created by linking a single, unimported package called the main package with all the packages it imports, transitively. The main package must have package name main and declare a function main that takes no arguments and returns no value. As an example consider the following main package for a hello world program:

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

Program execution begins by initializing the program and then invoking the function main in package main. When that function invocation returns, the program exits. It does not wait for other (non-main) goroutines to complete.

Use go run command to run a package:

$ go run .

You can get packages using go get <package_repo>, the command stores packages in the module cache this is for default in GOPATH/pkg/mod6

Importing packages

Diferent ways of access exported types from a package:

Import declaration          Local name of Sin

import   "lib/math"         math.Sin
import m "lib/math"         m.Sin
import . "lib/math"         Sin

To import a package solely for its side-effects (initialization), use the blank identifier as explicit package name:

import _ "lib/math"

Modules

In a module you collect one or more related packages.

When your code imports packages contained in other modules, you manage those dependencies through your code’s own module. That module is defined by a go.mod file that tracks the modules that provide those packages. That go.mod file stays with your code, including in your source code repository.

To enable dependency tracking for your code by creating a go.mod file, run the go mod init command, giving it the name of the module your code will be in.

$ go mod init example/hello
go: creating new go.mod: module example/hello

Use go mod tidy to download imported packages.

You can add a local module replacing the module name with the local path where the module is located using the following command.

go mod edit -replace example.com/greetings=../greetings

Note that in this case the module is example.com/greetings and it is located in ../greetings

Multi module workspace

Initialize a workspace.

go work init ./hello

This creates a work.go file and adds the module in ./hello to the workspace.

Then, if you want to add other module in the workspace you use the following command.

go work use ./example/hello

Note that go.work file can be used instead of adding replace directives to work across multiple modules.

Misc

Searching in the API documentation

When inspecting the API of a package (for example in pkg.go.dev) the table of content nest the functions that are related7 to a type for example:

type Engine
    func New(opts ...OptionFunc) *Engine
    func (engine *Engine) HandleContext(c *Context)

Note that New and HandleContext are related to Engine, because New returns a *Engine and HandleContext have a receiver of type *Engine

Features

For what is suggested to use go:

Learning Resources

Recommended order:

  1. Tour
  2. Language specification
  3. Effective Go: tips for writing clear, idiomatic Go code.

  1. Arrays cannot be resized. â†©ď¸Ž

  2. In practice, slices are most common than arrays. â†©ď¸Ž

  3. 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. Therefore these slice expressions are equivalent:

        a[0:10]
        a[:10]
        a[0:]
        a[:]
    
     â†©ď¸Ž
  4. This is because Print’s family functions call the Error method from the declared error type. â†©ď¸Ž

  5. I understand that goroutines are typically used to manage asynchronous tasks. â†©ď¸Ž

  6. You can find the GOPATH running go env GOPATH. By default it should be ~/go. â†©ď¸Ž

  7. This are functions that are methods of a type and functions that returns the type nested inside the type. â†©ď¸Ž