Introduction to Go


Types

Go is statically typed - variables always have specific type and type cannot change during the program run time.

Types help us reason about what our program is doing and help us catch many errors.

Types are similar to sets in mathematics. They classify data into groups and determine:

See also Basic types, Zero values and Type conversions.

Numbers

Computers use base-2 binary system to store and work with the numbers. So computers count like this: 0, 1, 10, 11, 100, 110, 111, …

Integer types

Floating-point types

// we use .0 to tell Go it's a floating-point number
fmt.Prinln("1 + 1 =", 1.0 + 1.0)

Strings

String literals are created with:

Common operations on strings:

Booleans

Variables

package main

import (
    "fmt"
)

func main() {
    fmt.Print("Enter distance in feet: ")
    var feet float64 // one way to define var
    fmt.Scanf("%f", &feet)
    meters := feet * 0.3048 // another way to define var
    fmt.Printf("%.2f ft = %.2f m\n", feet, meters)
}

See also Type inference.

Control Structures

The for Statement

Other programming languages have various types of loops (while, until, foreach, …). Go only has for loop that can be used in various ways, e.g.:

// traditional c-style
for i := 1; i <= 10; i++ {
    fmt.Println(i)
}

i := 1              // declaration + initialization
for i <= 10 {       // condition
    fmt.Println(i)
    i++             // increment
}

// loop over array/slice
for i, value := range x {
    ...
}

The if and switch Statements

If the if statement becomes too verbose use the switch statement.

for i := 1; i <= 10; i++ {
    switch i { // expression after switch can be omitted
    case 1: fmt.Println("one")
    case 5: fmt.Println("five")
    case 6: fmt.Println("six")
    case 10: fmt.Println("ten")
    default: fmt.Println(i) // similar to else
    }
}

The value of the expressions (in this example i) is compared to the expression following each case keyword. If they are equivalent the statements following : are executed. The first one to succeed is chosen.

More built-in types

Arrays

Array is a numbered sequence of elements of a single type with a fixed length.

var a1 [3]int // array of three integers
a1[0] = 10
a1[1] = 20
a1[3] = 30

// shorter syntax for creating arrays
a2 := [3]int{ 10, 20, 30, }

Now, you rarely see arrays used directly in Go code :-).

Slices

Slice is a segment of an array. Like arrays, they are indexable and have a length. Unlike arrays, the length is allowed to change.

    var s1 []float64            // []
    s2 := make([]float64, 3)    // [0 0 0]

    // define length (3) and capacity (5)
    s3 := make([]float64, 3, 5) // [0 0 0]

    // create slice from array
    a := [5]float64{1,2,3,4,5}
    s4 := a[1:3]                // [2 3]
    s5 := a[:]                  // [1 2 3 4 5]

Slices are always associated with some array. The are like references to arrays.

See also Slice literals.

append operator

s1 := []int{1,2,3}
s2 := append(s1, 4, 5)

See also Appending to a slice.

copy operator

s1 := []int{1,2,3}
s2 := make([]int, 2)
// func copy(dst, src []Type) int
copy(s2, s1)
// s1: [1,2,3], s2: [1,2]

See also copy.

Maps

// x is a map of strings into ints

// WRONG: this will yield a run time error
var x map[string]int
x["key"] = 10 // panic: assignment to entry in nil map

// maps have to be initialized before they can be used
var x = make(map[string]int)
x["key"] = 10

// delete an item from a map
delete(x, "key")
elements := map[string]string{
    "H":  "Hydrogen",
    "He": "Helium",
    "Li": "Lithium",
}

if name, ok := elements["He"]; ok {
    fmt.Printf("He is %s\n", name)
}

Functions

A function (aka a procedure, or a subroutine) is an independent section of code that maps zero or more input parameters to zero or more output parameters:

Inputs -> [ func f(i, j int) int {} ] -> Outputs

Functions form call stacks:

func main() {
    fmt.Println(f1())
}

func f1() int {
    return f2()
}

func f2() int {
    return 1
}
                            +----+
                            | f2 | return
                            +----+
              +----+        +----+        +----+
              | f1 |   f2   | f1 |        | f1 | return
              +----+        +----+        +----+
+----+        +----+        +----+        +----+        +----+
|main|   f1   |main|        |main|        |main|        |main|
+----+        +----+        +----+        +----+        +----+

Return types can have names:

func f2() (r int) {
    r := 1
    return
}

Multiple values can be returned:

func f() (int, int) {
    return 4, 2
}

func main() {
    x, y := f()
}

Multiple values are often used to return an error value along with the result (x, err := f()), or a boolean to indicate success (x, ok := f()).

Variadic functions

There is a special form available for the last parameter:

// sump up zero or more integers
func sum(args ...int) int {     // prefix ellipsis
    total := 0
    for _, v := range args {
        total += v
    }
    return total
}

func main() {
    fmt.Println(sum())
    fmt.Println(sum(1, 2))

    xs := []int{1,2,3}
    fmt.Println(sum(xs...))     // suffix ellipsis
}

fmt.Println can take any number of values (…) of any type (interface{}):

func Println(a ...interface{}) (n int, err error)

Closures

It’s possible to create functions inside functions. These local functions have access to other local variables:

func main() {
    // local variable accessible by increment
    x := 0

    // local variable of type func() int
    increment := func() int {
        x++
        return x
    }

    fmt.Println(increment()) // 1
    fmt.Println(increment()) // 2
}

One way to use closure is to write a function that returns another function:

func makeEvenGenerator() func() uint {
    i := uint(0) // unlike normal local variable this one persists between calls
    return func() (ret uint) {
        ret = i
        i += 2
        return
    }
}

func main() {
    nextEven := makeEvenGenerator()
    fmt.Println(nextEven()) // 0
    fmt.Println(nextEven()) // 2
    fmt.Println(nextEven()) // 4
}

Recursion

A function is able to call itself:

func factorial(x uint) uint {
    if x == 0 {
        return 1
    }
    return x * factorial(x-1)
}

factorial(2):

  1. Is x == 0? No (x is 2).
  2. Find the factorial of x - 1.
    1. Is x == 0? No (x is 1).
    2. Find the factorial of x - 1.
      1. Is x == 0? Yes, return 1.
    3. Return 1 * 1.
  3. Return 2 * 1.

defer

defer schedules a function call to be run before a function returns. It’s often used when resources need to be freed in some way, e.g.:

func main() {
    f, _ := os.Open(filename)
    defer f.Close()
}

This has three advantages:

panic and recover

WRONG:

func main() {
    panic("PANIC")
    str := recover() // this will never happen!
    fmt.Println(str)
}

CORRECT:

func main() {
    defer func() {
        str := recover()
        fmt.Println(str)
    }()
    panic("PANIC")
}

A panic generally indicates a programmer’s error or an exceptional condition that’s not easy to recover from.

See https://blog.golang.org/defer-panic-and-recover for more.

Pointers

Normally a function’s argument is copied:

func zero(x int) {
    x = 0
}

func main() {
    x := 1
    zero(x)
    // x is still 1
}

If we want to modify the original argument one way to do it is to use a special data type known as a pointer:

func zero(xPtr *int) {
    *xPtr = 0
}

func main() {
    x := 1
    zero(&x)
    // x is 0
}

Pointers reference a location in memory where a value is stored rather than the value itself.

Another way to get a pointer is to use the new built-in function:

xPtr := new(int)

Go is a garbage-collected language. It means memory is cleaned up automatically when nothing refers to it anymore.

Pointers are rarely used with Go’s built-in types but are extremely useful when paired with structs.

Structs and interfaces

At some point it would become tedious and error prone to write programs using only Go’s built-in types.

Structs

A struct is a type that contains named fields:

// Circle represents, well, a circle.
type Circle struct {
    x, y, r float64
}

// Several ways to do initialization.
var c Circle
c := new(Circle) // returns pointer
c := Circle{x: 0, y: 0, r: 5}
c := Circle{0, 0, 5}
c := &Circle{0, 0, 5} // most typical

// Accessing fields.
fmt.Println(c.x)
c.r = 10

Methods

Using structs with functions:

A normal function:

func circleArea(c *Circle) float64 {
    return math.Pi * c.r*c.r // no dereferencing needed... hm
}

c := Circle{0, 0, 5}
fmt.Println(circleArea(&c))

A special function - method:

func (c *Circle) area() float64 { // (c *Circle) is called a receiver
    return math.Pi * c.r*c.r
}

c := Circle{0, 0, 5}
fmt.Println(c.area()) // Go automatically knows to pass a pointer to the circle

Embedded types

A struct’s fields usually represent the has-a relationship, e.g. Person has a name:

type Person struct {
    Name string
}

func (p *Person) Talk() {
    fmt.Println("Hi, my name is", p.Name)
}

We use embedded types (anonymous fields) to represent the is-a relationship, e.g. Android is a person (so it can Talk()):

type Android struct {
    Person // embedded type
    Model string
}

a := new(Android) // you cannot do &Android{Name: "R2D2"} here
a.Name = "R2D2"
a.Talk() // could be also: a.Person.Talk()

Interfaces

Interfaces are similar to structs but instead of fields they have a method set. A method set is a list of methods that a type must have in order to implement the interface. We can use interface types as arguments to functions.

package main

import (
    "fmt"
    "math"
)

type Shape interface {
    area() float64 // any type that has area method is a Shape
}
func totalArea(shapes ...Shape) float64 { // interface (not a type) as argument
    var area float64
    for _, s := range shapes {
        area += s.area()
    }
    return area
}

// Circle type with area method.
type Circle struct {
    x, y, r float64
}
func (c *Circle) area() float64 {
    return math.Pi * c.r * c.r
}

// Rectangle type with area method.
type Rectangle struct {
    x1, y1, x2, y2 float64
}
func (r *Rectangle) area() float64 {
    l := distance(r.x1, r.y1, r.x1, r.y2)
    w := distance(r.x1, r.y1, r.x2, r.y1)
    return l * w
}

func distance(x1, y1, x2, y2 float64) float64 {
    a := x2 - x1
    b := y2 - y1
    return math.Sqrt(a*a + b*b)
}

func main() {
    c := &Circle{0, 0, 5}
    r := &Rectangle{0, 0, 10, 10}
    tot := totalArea(c, r)
    fmt.Println(tot)
}

Interfaces can also be used as fields:

type MultiShape struct {
    shapes []Shape
}

multiShape := MultiShape{
    shapes: []Shape{
        Circle{0, 0, 5},
        Rectangle{0, 0, 10, 10},
    },
}

// Turn MultiShape into a Shape by giving it an area method.
func (m *MultiShape) area() float64 {
    var area float64
    for _, s :+ range m.shapes {
        area += s.area()
    }
    return area
}

Now a MultiShape can contain Circles, Rectangles, or even other MultiShapes.

See also John Graham-Cumming: Interfaces.

Packages

The Core Packages

Input/Output

io package consists of a few functions, but mostly interfaces used in other packages. The two main interfaces are Reader and Writer. Readers support reading via the Read method. Writers support writing via the Write method. Many functions in Go take Readers or Writers as arguments. E.g. the io.Copy function copies data from a Reader to a Writer:

func Copy(dst Writer, src Reader) (written int64, err error)

To read/write to a []byte or a string, you can use the Buffer type (struct) from bytes package:

var b bytes.Buffer
b.Write([]byte("Hello "))
fmt.Fprintf(&b, "world!")
b.WriteTo(os.Stdout)

Files and Folders

The easiest way to open a file:

package main

import (
    "fmt"
    "io/ioutil"
)

func main() {
    bs, err := ioutil.ReadFile("/etc/passwd")
    if err != nil {
        // handle error here
        return
    }
    str := string(bs)
    fmt.Println(str)
}

Create a file:

package main

import (
    "os"
)

func main() {
    file, err := os.Create("test.txt") // file is os.File
    if err != nil {
        // handler error here
        return
    }
    defer file.Close()
    file.WriteString("test")
}

Get contents of a directory:

package main

import (
    "fmt"
    "os"
)

func main() {
    dir, err := os.Open(".")
    if err != nil {
        // handle error here
        return
    }
    defer dir.Close()

    fileInfos, err := dir.Readdir(-1) // -1 means return all entries
    if err != nil {
        return
    }
    for _, fi := range fileInfos {
        fmt.Println(fi.Name())
    }
}

Recursively walk a folder (read the folder’s contents, all the subfolders, all the sub-subfolders, etc.):

package main

import (
    "fmt"
    "os"
    "path/filepath"
)

func main() {
    // func() is called for every file and folder in "."
    filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
        fmt.Println(path)
        return nil
    })
}

Errors

Go has a built-in type for errors (the error type). We can also create our own errors:

package main

import "errors"

func main() {
    err := errors.New("error message")
}

Sources

comments powered by Disqus