Jim Cheung

An Introduction to Programming in Go.

reading note of the golang book

Chapter 1, Getting Started

(no notes)

Chapter 2, Your First Program

There are two types of Go programs: executables and libraries.

Chapter 3, Types

Numbers

Integers

Go's integer types are: uint8, uint16, uint32, uint64, int8, int16, int32 and int64.

uint means unsigned integer while int means signed integer

In addition there two alias types:

Floating Point Numbers

  1. Floating point numbers are inexact.
  2. Like integers floating point numbers have a certain size (32 bit or 64 bit). Using a larger sized floating point number increases it's precision.
  3. In addition to numbers there are several other values which can be represented: "not a number" (NAN) and positive and negative infinity.

Go has two floating point types:

Strings

Booleans

Chapter 4, Variabl es

Variables in Go are created by first using the var keyword, then specifying the variable name (x), the type (string) and finally assigning a value to the variables.

var x string = "Hello"

Since creating a new variable with a sharting value is so common Go also supports a shorter statement:

x := "Hello"

The type is not necessary because the Go compiler is able to infer the type based on the literal value you assign the value.

How to Name a Variable

Names must start with a letter.

Scope

Go is lexically scoped using blocks. means that the variable exists within the nearest curly braces { } (a block) including any nested curly braces (blocks), but not outside of them.

Constants

Constants are basically variables whose values cannot be changed later. they are created in the same way you create variables but instead of using the var keyword we use the const keyword.

Defining Multiple Variables

var (
    a = 5
    b = 10
    c = 15
)

use the key
word var (or const) followed by parentheses with each variable on his own line.

Chapter 5, Control Structures

For

i := 1
for i <= 10 {
    fmt.Println(i)
    i+=1
}

Other programmi
ng languages have a lot of different types of loops (while, do, until, foreach, ...) but Go only has one that can be used in a variety of different ways:

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

If

if cond {
    // ... 
} else if cond {
    // ...
} else {
    // ...
}
           

Switch

switch i {
    case 0: fmt.Println("zero")
    case 1: fmt.Println("one")
    default: fmt.Println("unknown")
}

Chapter 6, Arrays, Slices and Maps

Arrays

var x [5]int
for _, value := range x {
    total += value
}

A single _ (underscore) is used to tell the compiler that we don't need this.

Go also provides a shorter syntax for creating arrays:

x := [5]float64{ 98, 93, 77, 82, 83 }

Slices

A slice is a segment of an array.

Like arrays slices are indexable and have a length. Unlike arrays this length is allowed to change.

var x []float64

the length is missing between the brackets, x has been created with a length of 0.

but you should create a slice using the built-in make function:


x := make([]float64, 5)

This creates a slices that is associated with an underlying float64 array of length 5.

x := make([]float64, 5, 10)

10 represents the capacity of the underlying array which the slice points to.

another way to create slices is to use the [low : high] expression:

arr := []float64{1,2,3,4,5}
x := arr[0:5]

low is the index of where to start the slice high is the inde x where to end it (but not including the index itself)

for convenience we are allowed to omit low, high or even both.

Slice Functions

Go includes two built-in functions to assist with slices: append and copy

func main() {
    slice1 := []int{1,2,3}
    slide2 := append(slice1, 4, 5)
    fmt.Println(slice1, slice2)
}

append creates a new slice by taking an existing slice and appending all the following arguments to it.

func main() {
    slice1 := []int{1,2,3}
    slide2 := make([]int, 2)
    copy(slice2, slice1)
    fmt.Println(slice1, slice2)
}

the content of slice1 are copied to slice2 (slice2 has room for only two elements, only the first two elements are copied)

Maps

A map is an unordered collection of key-value pairs.

var x map[string]int

key type in brackets and then value type

to initialize a map:

x := make(map[string]int)
x["key"] = 10
fmt.Println(x["key"])

delete items from a map:

delete(x, "key")

accessing an element of a map can return two values: first is the result of the lookup, second is whether or not the lookup was successful

if name, ok := ele                       ments["un"]; ok {
    fmt.Println(name, ok)
}

first we try to get the value from the map, then if it's successful we run the code inside of the block.

another way to create map

elements := map[string]string {
    "H": "Hydrogen",
    ...
    "Ne": "Neon",
}                 

(the last , is required)

multi-dimention map

var elements = map[string]map[string]string

Chapter 7, Functions

              func name(x type, y type, ..) return_type { }

the parameters and the return type are known as the function's signature.

we can also name the return type:

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

Returning Multiple Values

func f() (int, int) {
    return 5, 6
}
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

func add(args ...int) int {
    total := 0
    f                or _, v := range args {
        total += v
    }
    return total
}
func main() {
    fmt.Println(add(1,2,3))
}

by using ... before the type name of the last parameter you can indicate that it takes zero of more of those parameters.

we can also pass a slice of ints by following the slice with ...:

func main() {
    xs := []in       
     t{1,2,3}
    fmt.Println(add(xs...))
}

Closure

create functions inside of functions

func main() {
    add := func(x, y int) int {
        return x + y
    }
    fmt.Println(add(1,1))
}

when you create a local function like this it also has access to other local variables .

func main() { 
    x := 0
    increment := func() int {
        x++
        return x
    }
    fmt.Println(increment())
    fmt.Println(increment())
}

A function like this together with the non-local variables it references is known as a closure.

Recursion

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

Defer, Panic & Recover

Go has a special statement called defer which schedules a function call to be run after the function completes.

defer is often used when resources need to be freed in some way.

f, _ := os.Open(filename)
defer f.Close()

this has 3 advantages:

  1. it keeps our Close call near our Open call so its easier to understand.
  2. if our function had multiple return statements (perhaps one in an if and one in an else) Close will happen before both of them.
  3. deferred functions are run even if a run-time panic occurs.

call the panic function to cause a run time error. and handle a run-time panic with the built-in recover function.

recover stops the panic and returns the value that was passed to the call to panic.

and recover usually pair with defer (because panic stops execution, recover won't be able to happen):

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

Chapter 8, Pointers

when we call a fnction that takes an argument, that argument is copied to the function. use pointer if we need to modify the origin variable.

func zero(xPtr *int) {
    *xPtr = 0
}
fun       
               c main() {
    x := 5
    zero(&x)
    fmt.Println(x) // x is 0
}

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

The * and & operators

in Go a pointer is represented using the * (asterisk) followed by the type of the stored value.

* is also used to dereference pointer variables. dereferencing a pointer gives us access to the value the pointer points to.

finally we use the & operator to find the address of a variable.

new

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

func one(xPtr *int) {
    *xPtr = 1
}
func main() {
    xPtr := new(int)
    one(xPtr)
    fmt.Println(*xPtr) // x is 1
} 

new takes a type as an argument, allocates enough memory to fit a value of that type and returns a pointer to it.

pointers are rarely used with Go's built-in types, but they are extremely useful when paired with structs

Chapter 9, Structs and Interfaces

Structs

A struct is a type which contains named fiel ds.

type Circle struct {
    x, y, r float64
}

Initialization

var c Circle

this will create a local Circle variable that is by default set to zero.

zero means e ach of the fields is set to their coresponding zero value (0 for ints, 0.0 for floats, "" for strings, nil for points, ...)

we can also use the new function:

c := new(Circle)

this allocates memory for all the fields, sets each of them to their zero value and return a pointer (*Circle)

to give value on initialization:

c := Circle{x: 0, y: 0, r: 5}

or we can leave off the field names if we know the order they were defined:

c := Circle{0, 0, 5}

*Field

          s*

we can access fields using the . operator

c.x = 10
c.y = 5
fmt.Println(c.x, c.y, c.r)

Methods

method is a special type of function

     func (c *Circle) area() float64 {
    reutrn math.Pi * c.r * c.r
}

in between the keyword func and the name of the function we've added a receiver.

the receiver is like a parameter - it has a name and a type - but by creating the function in this way it allows us to call the function using the . operator:

fmt.Println(c.area())

we no longer need the & operator (Go automatically knows to pass a pointer to the circle for this method)

Embedded Types

A s
truct's field usually represent the has-a relationship.

Embedded type also known as anonymous fields:

type Android struct {
    Person
    Model string
}
a := new(Android)
a.Person.Talk()

we use the type Person and don't give it a name.

but we can also call any Person methods directly on the Android:

a := new(Android)
a.Talk()

Interfaces

type Shape interface {
    area() float64
}

like a struct an interface is created using the type keyword, followed by a name and the keyword interface.

but instead of defining fields, we define a method set. a method set is a list of methods that a type must have in order to implement the interface.

interface can also be used as fields:

type MultiShape struct {
    shapes []Shape
}
                  
                               

Chapter 10, Concurrency

Goroutines

A goroutine is a function that is capable of running concurrently with other functions.

to create a goroutine we use the keyword go followed by a function invocation:

package main
import "fmt"
func f(n int) {
    for i := 0; i < 10; i++ {
        fmt.Println(n, ":", i)
    }
}
func main() {
    go f(0)
    var input string
    fmt.Scanln(&input)
}

This program consists of two goroutines. The first goroutine is implicit and is the main function itself. The second goroutine is created when we call go f(0).

With a goroutine we return immediately to the next line and don't wait for the function to complete.

Goroutines are lightweight and we can easily create thousands of them.

Channels

Channels provide a way for two goroutines to communicate with one another and synchronize their execution.

package main
import (
    "fmt"
    "time"
)
func pinger(c chan string) {
    for i := 0; ; i++ {
        c <- "ping"
    }
}

func printer(c chan string) {
    for {
        msg := <- c
        fmt.Println(msg)
        time.Sleep(time.Second * 1)
    }
}

func main() {
    var c chan string = make(chan string)
    go pinger(c)
    go printer(c)
    
    var input string
    fmt.Scanln(&input)
}

A channel type is represented with the keyword chan followed by the type of the things that are passed on the channel.

The <- (left arrow) operator is used to send and receive messages on the channel.

c <- "ping" means send "ping". msg := <- c means receive a message and store it in msg.

Channel Direction

We can specify a direction on a channel type thus restricting it to either sending or receiving.

func pinger(c chan<- string)

c can only be sent to. Attempting to receive from c will result in a compiler error.

func printer(c <-chan string)

A channel that doesn't have these restrictions is known as bi-directional.

A bi-directional channel can be passed to a function that takes send-only or receive-only channels, but the reverse is not true.

Select

Go has a special statement called select which works like a switch but for channels:

func main() {
    c1 := make(chan string)
    c2 := make(chan string)
    go func() {
        for {
            c1 <- "from 1"
            time.Sleep(time.Second * 2)
        }
    }()
    go func() {
        for {
            c2 <- "from 2"
            time.Sleep(time.Second * 3)
        }
    }()
    go func() {
        for {
            select {
                case msg1 := <- c1:
                    fmt.Println(msg1)
                case msg2 := <- c2:
                    fmt.Println(msg2)
            }
        }
    }()
    
    var input string
    fmt.Scanln(&input)
}

The select statement is often used to implement a timeout:

select {
    case msg1 := <- c1:
        fmt.Println("Message 1", msg1)
    case msg2 := <- c2:
        fmt.Println("Message 2", msg2)
    case <- time.After(time.Second):
        fmt.Println("timeout")
    default:
        fmt.Println("nothing ready")
}

The default case happens immediately if none of the channels are ready.

Buffered Channels

c := make(chan int, 1)

This creates a buffered channel with a capacity of 1.

Normally channels are synchronous; both sides of the channel will wait until the other side is ready.

A buffered channel is asynchronous; sending or receiving a message will not wait unless the channel is already full.

Chapter 11, Packages

Creating Packages

run go install to compile package.

Go packages can be hierarchical.

alias:

import m "foo/bar/math"

In Go if something starts with a capital letter that means other packages (and programs) are able to see it.

Package names match the folders they fall in.

Documentation

godoc foo/bar/math Average

We can improve this documentation by adding a comment before the function:

// Finds the average of a series of numbers
func Average(xs []float64) float64 {}

doc http server

godoc -http=:3000

Chapter 12, Testing

package math
import "testing"
func TestAverage(t *testing.T) {
    var v float64
    v = Average([]float64{1,2})
    if v != 1.5 {
        t.Error("Expected 1.5, got ", v)
    }
}

run go test, it will look for any tests in any of the files in the current folder and run them.

Tests are identified by starting a function with the word Test and taking one argument of type *testing.T.

Chapter 13, The Core Packages

check https://github.com/usrjim/goplay

Chapter 14, Next Steps

(no notes)

[END]