Golang

Too close for missles, switching to guns.

For the code I write, Go strikes the best balance between expressiveness and practicality.

Language Structure

Conditionals

if num := 9; num < 0 {
    fmt.Println(num, "is negative")
} else if num < 10 {
    fmt.Println(num, "has 1 digit")
} else {
    fmt.Println(num, "has multiple digits")
}

Types

structs

type person struct {
    name string
    age  int
}

p := person{name: name}

Generics

Go lets you express behavioral polymorphism with interfaces, but until generics land, you essentially can’t express state polymorphism. So don’t try. Seriously. Whatever hacks you have to do with interface{} or reflect or whatever are worse than the disease. The empty interface{} and package reflect are tools of last resort, for when there’s literally no other way to solve your problem. In practice, that means in library code that needs to operate on types that are unknown at compile time. In application code, by definition, you know the complete set of types you’ll ever have to deal with at compile time. So there’s almost never a reason to use either of these hacks. You can always write type-specific versions of everything you need. Do that.

In the above quote, behavioral ≈ subtyping and state ≈ parametric. I think the heap library is a good example of library code that doesn’t know the type it will be operating on.

make vs. new

slice, map and chan are data structures. They need to be initialized, otherwise they won’t be usable.

p := new(chan int)   // p has type: *chan int
c := make(chan int)  // c has type: chan int

// make only inits the top level data structure
cache := make([]map[int]int, len(nums))
for i,_ := range cache {
	cache[i] = make(map[int]int, 0)
}

type assertion

// panic if i is not T, change to T if it is
t := i.(T)

// empty interface may hold values of any type
func describe(i interface{}) {
	fmt.Printf("(%v, %T)\n", i, i)
}

Slices and Arrays

The slice type is an abstraction built on top of Go’s array type

An array’s size is fixed; its length is part of its type ([4]int and [5]int are distinct, incompatible types)

Arrays are not often seen in Go programs because the size of an array is part of its type, which limits its expressive power

// Array literal
b := [2]string{"Penn", "Teller"}

// Have the compiler do the counting
b := [...]string{"Penn", "Teller"}

Arrays have their place, but they’re a bit inflexible, so you don’t see them too often in Go code. Slices, though, are everywhere. They build on arrays to provide great power and convenience. The type specification for a slice is []T, where T is the type of the elements of the slice. Unlike an array type, a slice type has no specified length.

A slice is not an array. A slice describes a piece of an array.

// Slice literal
letters := []string{"a", "b", "c", "d"}

// make function
s := make([]byte, 5)

// append slice to slice
s3 := append(s2, s0...)

// slice a slice
// 0 <= low <= high <= cap(a)
s[inclusive:exclusive]

// pass pointer for recursive append
backtrack(candidates, target, current, &combinations)
...
func backtrack(candidates []int, target int, current []int, combinations *[][]int)
...
*combinations = append(*combinations, tmp)

// copy slice
tmp := make([]int, len(current))
copy(tmp, current)

// delete index
a = append(a[:i], a[i+1:]...)

The zero value of a slice is nil. The len and cap functions will both return 0 for a nil slice.

Compared to an array which is 0

A slice cannot be grown beyond its capacity.

The append function appends the elements x to the end of the slice s, and grows the slice if a greater capacity is needed.

Functions

variadic function

func hello(a int, b ...int) {  
}

// can take 0 or more args

Strings and Runes

Rob Pike’s blog post on strings

a string holds arbitrary bytes. It is not required to hold Unicode text, UTF-8 text, or any other predefined format. As far as the content of a string is concerned, it is exactly equivalent to a slice of bytes.

Characters in golang are called runes, but they are not the characters (aka 2 bytes) of old. They try to cover up some of the ambiguity of utf8 (more bytes).

In Go rune type is not a character type, it is just another name for int32

indexing a string yields its bytes, not its characters: a string is just a bunch of bytes

Most deterministic way to iterate per “character” is to use range to get the runes (plus beginning byte index). ASCII is single byte encoding, so if it can be assumed, can use more general slicing.

// use of rune type
m := make(map[rune]int)

can treat strings like slices to get substrings

s = s[:len(s)-1]

// have to convert byte to string in order to append
s + string(c)

// type byte
'1'
// type string
"1"

Iterations

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

for i := 0; i < 10; i++ {
    sum += i
}
// range returns index and value of a slice
for i, v := range pow {
    fmt.Printf("2**%d = %d\n", i, v)
}

// can use on a string to iterate runes
for i, c := range "abc" {
    fmt.Println(i, " => ", string(c))
}

For is Go’s “while”

for sum < 1000 {
    sum += sum
}
// must use index to update the struct of an array of structs
for i := range unfilteredSpan {
    count := 0
    for _, n := range unfilteredSpan[i].neighbors {
        if nodes[n].distance > request.MinNeighborDistance {
            count++
        }
    }

    unfilteredSpan[i].distantNeigbors = count
}

Maps

// with make
m = make(map[string]int)
// literal
m = map[string]int{}

// check existence
i, ok := m["route"]
// more compressed
if val, ok := dict["foo"]; ok {
    //do something here
}

// maps can be used for sets
m := make(map[rune]bool)

// iterate over key values
for k, v := range m { 
    fmt.Printf("key[%s] value[%s]\n", k, v)
}
// or just keys
for k := range m { 
    fmt.Printf("key[%s]\n", k)
}

Maps are always pointers

A map value is a pointer to a runtime.hmap structure.

This causes non-obvious issues when trying to assign a value to a struct in a map of structs. Easiest work around seems to be to use a map of pointers to structs.

Pointers

The type *T is a pointer to a T value. Its zero value is nil.

The & operator generates a pointer to its operand.

The * operator denotes the pointer’s underlying value.

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

// pointers to builtins (e.g. slices) need to be de-referenced before using syntax (unlike normal structs)
(*traversal)[level] = append((*traversal)[level], root.Val)

Use var keyword when you need to define a variable without any initialization, so the zero value will be used on it.

var a *T // zero value of pointer is nil

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.

Methods and Receivers

a method is a function with a receiver argument

type Vertex struct {
	X, Y float64
}

// can access values of v, but can't change them cause passed by value
func Abs(v Vertex) float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

Methods with pointer receivers can modify the value to which the receiver points (as Scale does here). Since methods often need to modify their receiver, pointer receivers are more common than value receivers.

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}
// simple stack implementation for byte type
type stack []byte

func (s *stack) push(v byte) {
    *s = append(*s, v)
}

func (s *stack) pop() byte {
    v := (*s)[len(*s)-1]
    *s = (*s)[:len(*s)-1]
    return v
}

Bitwise Operators

 &   bitwise AND
 |   bitwise OR
 ^   bitwise XOR
&^   AND NOT
<<   left shift
>>   right shift
// hammingWeight is the number of `1` bits in an integer
func hammingWeight(num uint32) int {
    bits := 0
    mask := uint32(1)
    
    for num != 0 {
        if num & mask == 1 {
            bits = bits + 1
        }
        num = num >> 1
    }
    
    return bits
}

Concurrency

The world is made up of concurrent agents, but our minds are sequential. Language concurrency features try to bridge this gap, not performance (e.g. parallelism).

Channels are a typed conduit through which you can send and receive values with the channel operator, <-

c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // receive from c

// buffered channels provide backpressure
ch := make(chan int, 100)

// blocks until a channel can receive or send (kinda like an event loop)
select {
case i := <-c:
    // use i
default:
    // receiving from c would block
}

// can create a channel which fires an event for a timeout

// range pulls from channel until builtin close is sent

//  nil channels in select cases are ignored

context

makes it easy to pass request-scoped values, cancelation signals, and deadlines across API boundaries to all the goroutines involved in handling a request.

Sorting

// sort the characters of a string

// create a type which implements the sort interface of Less, Swap, and Len
type sortRunes []rune

func (s sortRunes) Less(i, j int) bool {
    return s[i] < s[j]
}

func (s sortRunes) Swap(i, j int) {
    s[i], s[j] = s[j], s[i]
}

func (s sortRunes) Len() int {
    return len(s)
}

func SortString(s string) string {
    r := []rune(s)
    // passing the sort type to Sort
    sort.Sort(sortRunes(r))
    return string(r)
}

func main() {
    w1 := "bcad"
    w2 := SortString(w1)

    fmt.Println(w1)
    fmt.Println(w2)
}

Heap

It also “kindly” asks us to perform some casting on behalf of it

Building

Packages

A package is a collection of source files in the same directory that are compiled together. Functions, types, variables, and constants defined in one source file are visible to all other source files within the same package.

A repository contains one or more modules. A module is a collection of related Go packages that are released together. A Go repository typically contains only one module, located at the root of the repository. A file named go.mod there declares the module path: the import path prefix for all packages within the module. The module contains the packages in the directory containing its go.mod file as well as subdirectories of that directory, up to the next subdirectory containing another go.mod file (if any).

Still leaves room for multi-module repository?

The first statement in a Go source file must be package name. Executable commands must always use package main.

Project layout

A Go package has both a name and a path. The package name is specified in the package statement of its source files; client code uses it as the prefix for the package’s exported names. Client code uses the package path when importing the package. By convention, the last element of the package path is the package name.

I think there are 2 choices:

  1. the top level package is main
    • can bury the library package in a nested directory (e.g. mango/mango)
  2. the top level package is the library’s name (e.g. mango)
    • can bury the main package in the command directory (e.g. cmd/mango) following convention; the app name as the leaf directory is used by the install command to name the executable

Option (1) makes sense for an application, the user will interact with the executable and having it at the top level as interface keeps things clean. Option (2) is much better for a library, since (1) would introduce needless/confusing nesting (mango/mango).

Might try to always use option (2) just for simplicity.

some conventions in the wild

bare minimum

go mod init git.sr.ht/~yonson/sandbox
// main.go
package main

import "fmt"

func main() {
    fmt.Println("Hello, world.")
}
go run main.go

Testing

go test -cover ./...

interfaces

tables

tests := []struct {
    name    string
    graph   *lnrpc.ChannelGraph
    request NodesByDistanceRequest
    want    []Node
}{
    {
        name: "identity",
        graph: &lnrpc.ChannelGraph{
            Nodes: []*lnrpc.LightningNode{
                {
                    PubKey:     rootPubkey,
                    Alias:      rootAlias,
                    LastUpdate: uint32(rootUpdatedTime(t).Unix()),
                },
            },
        },
        request: NodesByDistanceRequest{
            MinUpdated: rootUpdatedTime(t).Add(-time.Hour * 24),
            Limit:      1,
        },
        want: []Node{
            {
                pubkey:  rootPubkey,
                alias:   rootAlias,
                updated: rootUpdatedTime(t),
            },
        },
    },
}

for _, tc := range tests {
    app := App{
        Infoer:  fakeInfoer{info: &lnrpc.GetInfoResponse{IdentityPubkey: rootPubkey}},
        Grapher: fakeGrapher{graph: tc.graph},
        Log:     log.New(ioutil.Discard, "", 0),
        Verbose: false,
    }

    nodes, err := NodesByDistance(app, tc.request)

    if err != nil {
        t.Fatal("error calculating nodes by distance")
    }

    if !reflect.DeepEqual(tc.want, nodes) {
        t.Fatalf("%s nodes by distance are incorrect\nwant: %v\ngot: %v", tc.name, tc.want, nodes)
    }
}

Few things of note in this code block

Advanced patterns

time

The time.RFC3339 format is a case where the format string itself isn’t a valid time. You can’t have a Z and an offset in the time string, but the format string has both because the spec can contain either type of timezone specification.

Linting

go fmt is built-in

Patterns

Enums

// AccessLevel is the level of access for a Dataset
type AccessLevel string

// Access levels
const (
        SharedAccessLevel     AccessLevel = "shared"
        RestrictedAccessLevel AccessLevel = "restricted"
)

CLI

Custom Errors

Make files

Libraries

ff

xdg


Copyright (c) 2021 Nick Johnson