Go

I’ve been meaning to learn Go for a while now. I’ve recently come to renew an interest in simple, no non-sense languages like Python, and what I imagine Go to be. The resources I’ll be using are the go specification, go tour, and effective go. A lot of this is straight from these sources, with my commentary of how I come to understand it.

Packages

Packages can be imported with the import statement. They can also be grouped inside parentheses. The containing package can be specified with the package statement. Names that begin with capitalized letters are exported by the package:

package somepkg

import (
  "fmt"
  "math/rand"
)

Functions

When there are consecutive named function parameters of the same type, we can omit the type of all but the last:

func add(x, y int) int {
  return x + y
}

Functions can return multiple values:

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

Result values can be named and set within the function, in which case return returns them as they are:

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

Functions are also values, and can be used to create closures:

hypot := func(x, y float64) float64 {
  return math.Sqrt(x * x + y * y)
}

Variables

Variables are declared with var. Multiple variables can be initialized in a tuple-assignment fashion, in which case the type can be omitted as it will be inferred from the initializers. Inside functions, := can be used in place of a var declaration with inferred type. Variables can be declared constant with const but not using the := syntax:

var i, j = 1, 2
k := 3
const World = "hello"

Numeric constants have arbitrary precision and don’t overflow.

The following are the available types in Go. Type conversions are possible through T(v).

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

Control Structures

There’s only one looping construct and that’s the for loop. The for loop in Go doesn’t use parentheses around the parameters, but the braces around the code to loop are mandatory.

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

As in C, pre and/or post conditions can be omitted. If both are omitted, it’s just like a while loop:

for sum < 1000 {
  sum += sum
}

Further still, the condition can be omitted to achieve a forever-loop.

if conditions can take a statement to run before the condition, scoped until the end of the if block (i.e. including else blocks):

if v:= math.Pow(x, n); v < lim {
  return v
} else {
  fm.Printf("%g >= %g\n", v, lim)
}

The switch statement case bodies break by default, unless they end in a fallthrough statement:

switch os := runtime.GOOS; os {
case "darwin":
  fmt.Println("OS X")
case "linux":
  fmt.Println("Linux")
default:
  fmt.Println("%s", os)
}

The condition on the switch statement can be omitted, in which case it acts like a long if-then-else chain.

Structures

Structures can be defined with the struct keyword and given a name with type. Fields are accessed using a dot as in other languages. When creating a struct, any non-initialized field takes on that type’s default value:

type Vertex struct {
  X int
  Y int
}

func main() {
  // type Vertex
  v := Vertex{1, 2}
  v.X = 4

  // Y:0 implicit
  var r = Vertex{X: 1}

  // constructs pointer to newly allocated struct
  // new(T) == &T{}
  var q = &Vertex{1, 2} // type *Vertex
}

Pointers

There are pointers but no pointer arithmetic. Indirection through pointers is transparent:

p := Vertex{1, 2}
q := &p
q.X = 1e9

A value of type T can be allocated with the new function:

var t *T = new(T)
t := new(T)

Arrays and Slices

An array of n type T elements is created with the [n]T syntax. A slice points to an array and includes a length and capacity. Slices can be re-sliced with the syntax s[lo:hi] as in Python slices, which essentially points to a region of the same array. Slices can be created with make which takes the type of array, the length and optional capacity. The range function can be used to iterate over a slice or map, yielding an index, value per iteration, or if the value isn’t needed simply omit it:

var a [2]int
a[0] = 1

s := make([]int, 0, 5)
var z []int

if z == nil {
  fmt.Println("nil")
}

p := []int{2, 3, 5}
fmt.Println("p[1:4] == ", p[1:4])

for i, v := range p {
  fmt.Printf("%d: %d\n", i, v)
}

for i := range p {
  // something
}

for _, v := range p {
  // something
}

Maps

Maps take the form map[key]value. A nil map is empty and can’t be assigned to. Map literals accept a trailing comma. When reading a map element, if the key doesn’t exist in the map, the value returned is the default value for the map value type:

type Vertex struct {
  Lat, Long float64
}

var m map[string]Vertex

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

  var l = map[string]Vertex{
    "Google": Vertex{
      37.42202, -122.08408
    },
  }
  
  // top-level type is just a type name, can be omitted
  var s = map[string]Vertex{
    "Bell Labs": {40.68433, -74.39967},
  }

  delete(m, "Bell Labs")

  // ok is true if key exists
  // else val is default value for map val type
  val, ok := m["Bell Labs"]
}

Methods

While Go doesn’t have classes, it has a syntax for defining methods on structures. In the function declaration, the method receiver appears before the function name. This can also be done on any type we create, such as with type, but not on types from other packages or basic types. A pointer receiver can be modified and doesn’t have to copy the entire structure on the method call, just like in C/C++:

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

v := &Vertex{3, 4}
v.Abs()

type MyFloat float64

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

f := MyFloat(-math.Sqrt2)
f.Abs()

Interfaces

Interfaces types are defined by a set of methods. A value of interface type can hold any value that implements those methods. It’s important to note that types implement interfaces implicitly, there’s no explicit declaration of intent as with Haskell typeclasses (class T where) or traditional Java interfaces (class A implements B):

type Abser interface {
  Abs() float64
}

var a Abser
a = MyFloat(-math.Sqrt2)
a = &v
a = v // error: Vertex doesn't implement Abser, *Vertex does

Interfaces can also be composed of other interfaces:

type Reader interface {
  Read(b []byte) (n int, err error)
}

type Writer interface {
  Write(b []byte) (n int, err error)
}

type ReadWriter interface {
  Reader
  Writer
}

Errors

In Go, an error is anything that can be described as a string. Interface type error defines a single method Error that returns a string:

type error interface {
  Error() string
}

type MyError struct {
  When time.Time
  What string
}

func (e *MyError) Error() string {
  return fmt.Sprintf("at %v, %s", e.When, e.What)
}

func run() error {
  return &MyError{
    time.Now(),
    "it didn't work"
  }
}

func main() {
  if err := run(); err != nil {
    fmt.Println(err)
  }
}

Concurrency

A goroutine is a lightweight thread managed by the Go runtime. The evaluation of the function and its arguments occurs in the current goroutine but execution of the function occurs in a new goroutine:

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

  // javascript-like idiom of defining
  // and calling a function
  go func() {
    // something
  }()
}

Channels are typed conduits through which data of that type can be sent, a lot like Haskell channels. The <- operator is used to send and receive values, where the data travels in the direction of the arrow. Sending and receiving blocks until the other side is ready, allowing simple synchronization of the different goroutines:

ch := make(chan T) // create channel of type T
ch <- v            // send v to channel ch
v := <-ch          // receive from channel ch into v

There are also bounded, buffered channels, in which case the buffer doesn’t block on send as long as the buffer isn’t full, nor on receive as long as the buffer isn’t empty:

buffered := make(chan T, 100)

Channels can be closed with the close function. Receivers can test if the channel is closed by accepting the second return value. Alternatively, looping with a range on the channel will read values until the channel is closed:

v, ok := <-ch

for i := range ch {
  fmt.Println(i)
}

The select statement lets a goroutine wait on multiple communication operations, a lot like the POSIX select() system call. It’ll block until one of its cases can run, randomly choosing if multiple are ready. A default case can be specified for when no other case is ready, particularly useful when the select is used within a forever loop, as is usually the case. The default case is usually used to try a send or receive without blocking:

for {
  select {
  case c <- x:
    x, y = y, x + y
  case <-quit:
    fmt.Println("quit")
    return
  default:
    // ...
  }
}
December 20, 2013
329ce08 — May 23, 2024