Thursday, 14 February 2019

Getting Started with Go - Notes on Coursera course

Here are my notes from the course "Getting Started with Go" on Coursera. This is the first course from "Programming with Google Go" Specialization.


Week 1 - Getting Started with Go


  • efficient as C
  • sharing
  • runs fast
  • garbage collection
  • simpler objects
  • essentially OOP; there is a concept of object
  • object is simplified => makes it faster/simple to use
  • concurrency is efficient
  • machine, assembly, high-level language
  • machine language is executed on CPU; very simple add, mul...
  • assembly - almost machine language but is human readable
  • compiled language with some features of interpreted languages, like Garbage Collection
  • weakly OOP
  • go does not use term “class” but “struct”s (containing data) with associated methods
  • no inheritance, no generics, no constructors
  • advantages of Go: its implementation of concurrency
  • Moore’s Law not applies anymore: number of transistors is not increasing over time for temperature/power constraints;
  • number of cores is increasing
  • Go recommends directory hierarchy: common organization is good for sharing;
  • src, pkg (libraries), bin
  • one workspace for many directories
  • GOPATH: workspace directory; defined during installation
  • package: group of related source files; can be imported by other packages;first line in source file names the package

file1.go:

package annpkg


file2.go:

package billpkg

file3.go:

import (
   “annpkg”
   “billpkg”
)

  • There must be one package called main; that’s where execution starts; compiled main package is an executable; other packages compile into packages
  • main package has to have function main

package main
import “fmt” // note: it’s NOT include!!!

func main() {
   fmt.Printf(“Hello, world\n”)
}

  • import - used to access other packages
  • Go standard library includes many packages, like fmt
  • Go searches packages in GOROOT and GOPATH directories first;
  • Go Tool: > go
  • go build - to compile the source code; when compiling main package.exe file has the same name as .go file
  • go doc - prints documentation for package
  • go format - formats source code files (e.g. indentation...)
  • go get - downloads and installs new packages
  • C:\dev\go\src>go get github.com/BojanKomazec/coursera-go-specialization
  • can't load package: package github.com/BojanKomazec/coursera-go-specialization: no Go files in C:\dev\go\src\github.com\BojanKomazec\coursera-go-specialization
  • (but it created a coursera-go-specialization directory regardless, which is good)
  • go list - installs all installed packages
  • go run - compiles and executes
  • go test - runs tests using files ending in “_test.go”
  • names
  • variables: have to have name and type

var x int
var x, y int

  • type defines what type of data and which operations can be performed on the variable
  • floating point
  • strings: unicode
  • to improve clarity/readability alias name for type (with type keyword):

type Celsius float64
type IDnum int

var temp Celsius
var pid IDnum

  • initializing variables
    • in the declaration
    • with explicit type specification
var x int = 100
    • with implicit type inference
var x = 100
    • after the declaration
var x int
x = 100

  • uninitialized variables have value of 0, empty string
  • short variable declaration (:=) declaration & initialization (type is inferred; can be done only inside a function):
x := 100


Week 2 - Basic Data Types


Pointers
  • & (ampersand) - gets address of variable or function
  • * (star operator) - returns data at the address; comes before pointer

var x int = 1
var y int
var ip *int
ip = &x
y = *ip


  • new() - another way to create a variable; it returns a pointer to a variable; variable is initialized to 0 by default

ptr := new(int)
*ptr = 3


  • variable scope - where variable can be accessed
  • in Go - variable scoping is done through blocks - sequence of declarations and statements within matching { }
  • blocks can be hierarchical
  • explicit blocks: when you write { } yourself; e.g. functions - each function has its own block
  • implicit blocks:
    • universe block - all Go source code
    • package block - all source in a package
    • file block - all source in a file (package can contain many files)
    • if, for, switch statements
    • clause in switch or select
  • Lexical scoping - this defines how variable references are resolved; Go is a lexically scoped language using blocks.
  • bi >= bj if bj is defined inside bi ; “defined inside” is transitive
  • variable is looked in the current/local block and then in the next bigger block
  • Variable is accessible from block bj if:
    • variable is declared in bi and
    • bi >= bj
  • variables have to be allocated and deallocated (when is not used anymore so memory space is made available) in memory; this has to be done in a timely fashion - before we run out of memory
  • stack vs heap
    • stack (traditionally, not in Go): dedicated to function calls; local variables for function are allocated in stack; they are deallocated automatically when function returns;
    • heap: persistent memory - you have to explicitly deallocate it; in C: malloc - to allocate; free - to deallocate it; error-prone (if you forget to call free) but fast;
  • Garbage Collection
  • you don’t want to deallocate memory prematurely
  • How to determine when a variable is no longer in use?

func foo() *int {
   x := 1
   return &x
}

func main() {
   var y *int
   y = foo()
   fmt.Printf(”%d”, *y)
}

  • GC is done by interpreter; GC keeps track of pointers/references to variables; only when all references are gone, it deallocates variable; GC keeps track of;
  • compiled languages like C and C++ can’t do this
  • Go is compiled language which has GC built in
  • Go compiler determines whether variable will go on stack or on heap; you don’t need to determine that
  • GC takes some time - that’s the tradeoff
  • comments: //
  • block comments: /* */
  • fmt.Printf():
    • conversion characters %d, %s
    • https://stackoverflow.com/questions/53961617/call-has-possible-formatting-directive
  • fmt.Prtintln()
    • does NOT support conversion/formatting characters
    • adds a new line character at the end
    • in the output, adds a SPACE character after each argument
      • it's not necessary to add a SPACE explicitly at the end of "a = ", "a =" will work ok:
fmt.Println("a =", a)
  • Integers
    • int
    • int8, int16, int32, int64 (2s complement: most significant bit is used for sign)
    • unsigned: uint8...etc
    • int vs int32 vs in64

  • Integers: binary operators
    • arithmetic
    • comparison
    • boolean
  • Type conversions
    • most binary operations (including assignment) require both operands to be of the same type
var x int32 = 1
var y int16 = 2
// [go] cannot use y (type int16) as type int32 in assignment
x = y

    • convert types with T() operation
x = int32(y)
  • Floating point
    • float32 ~6 digits of precision
    • float64 ~ 15 digits of precision
    • decimal representation: x float64 = 123.45
    • scientific representation: x float64 = 1.2345e2 (e2 = 10^2; base 10)
    • complex numbers: var z complex128 = complex(2, 3) // real, imaginary
  • strings - sequences of bytes
  • ASCII and UNICODE
  • each character has to be encoded
    • ASCII: 7 or 8 bits: ‘A’ = 0x41
    • UNICODE: 32-bit long code (2 to 32 characters can be represented)
    • UTF-8: variable length - 8-bit codes same as ASCII; default in Go
      • code point (or “rune” in Go): term for a unicode character
  • string
    • sequence of arbitrary bytes
    • array of runes
    • read-only (immutable)
    • string literal - between “”
  • Unicode package
    • IsDigit(r rune)
    • IsSpace(r rune)
    • IsLetter(r rune)
    • IsLower(r rune)
    • IsPunct(r rune)
    • ToUpper(r rune)
    • ToLower(r rune)
  • Strings package - looks string as a whole
    • Compare(a, b) - lexicographical comparison; returns -1, 0, 1
    • Contains(s, substr) - returns true/false
    • HasPrefix(s, prefix) - returns true/false
    • Index(s, substring) - returns the index of the 1st instance
    • Replace(s, old, new, n) - returns a copy of s where first n instances of old are replaced by new
    • ToLower(s) - returns new string
    • ToUpper(s) - returns new string
    • TrimSpace(s) - returns new string
    • Go Walkthrough: bytes + strings packages
  • Strconv package (string conversion)
    • Atoi(s) - convert string to int
    • Itoa(n) - converts int to string
    • FormatFloat()
    • ParseFloat()
  • Constants
    • expressions whose value is known in compile time
    • values inferred from right hand side
    • iota - function used to generate related but distinct constants (e.g. days of week) - value is not important, we don’t care about value; like an enumeration; each const is assigned to a unique integer

// in the current implementation iota values start from 1 but this is not guaranteed and can change in future
type Grades int

const (
A Grades = iota
B
C
D
E
F
)

fmt.Printf("C = %d", C) // output: C = 2
  • Control flow - determines the order of execution of statements
    • if statement
    • for loops
      • iterates while condition is true; condition is always required
      • may have initialization (executed only once) and update (executed at the end of each loop):
for ;; { }
    • switch 
      • contains tags (labels)
    • tagless switch
    • break - exits the containing loop
    • continue - skips the current iteration

Week 3 - Composite Data Types


  • Composite data types - aggregate other data types

Arrays

  • fixed-length series of elements of a chosen type
  • in compile time is known the length of it - compiler knows how much memory is needed
  • each element is indexed by using subscript notation - index ([ ])
  • indices start from 0
  • elements are initialized to zero value of the type (0 for integers, empty string for strings etc...)
var x[5] int // array of 5 integers
x[0] = 2
fmt.Printf(x[1]) // 0

  • Array literal
    • predefined set of values that make up an array
    • used to initialize an array
    • length of literal must be length of array
var x[5] int = [5] {1, 2, 3, 4, 5}
  • ... (three dots) - keyword; used to express the size of an array literal inferred from the list of values; in the previous example:
x := [...] {1, 2, 3, 4, 5} //  size of the array literal is 5 

  • iterating through array: use a for loop with range keyword; range returns two values: index and value at that index
x := [3] int {1, 2, 3}
for i, v := range x {
   fmt.Printf("index = %d; element value = %d", i, v)
}

  • Only slices, channels and arrays can be used in for-range.


Slices

  • lots of time are used instead of arrays
  • there must be some underlying array which is a basis of the slice
  • slice is a window on the underlying array (larger or of the same size)
  • can change their size; have variable size, up to the size of array
  • has 3 properties:
    • Pointer - indicates the start of the slice; points to the element of the array where slice starts
    • Length - number of element in the slice
    • Capacity - max number of elements; from start of slice to the end of array; e.g. if slice starts at 10th element of the array which has 100 elements => capacity is 90
  • colon (:) is used between m and n
    • m - index of (pointer to) the 1st element in the array which is IN the slice 
    • n - index of (pointer to) the 1st element in the array which is NOT in the slice
arr := [...]{"a", "b", "c", "d", "e", "f", "g", }
slice1 := arr[1:3] // {"b", "c",}
slice2 := arr[2:5] // {"c", "d", "e",}

  • len() - returns the length of the slice
  • cap() - returns the capacity of the slice (number of elements in the underlying array from the beginning of the slice to the end of the array)
a2 := [3]string{"a", "b", "c"}
slice21 := a2[0:1] // { "a" }
fmt.Println(len(slice21), cap(slice21)) // 1, 3
  • writing in the element of slice writes actually to the underlying array
  • overlapping slices can refer to the same element of the array; writing in one slice changes another slice 
  • slice literals
    • can be used to initialize a slice
    • creates the underlying array and creates a slice to reference it
    • slice points to the beginning of the array and length is  capacity
slice := [] int {1, 2, 3}

Variable Slices

  • 3rd way to make slice: make() function
    • 2-argument version: type (elements initialized to 0, empty string etc...), length (equal to capacity): 
slice :=  make([]int, 10)
    • 3-argument version: specify length and capacity separately: e.g. 10 is length and 15 a capacity
slice := make([]int, 10, 15) 
  • size of the slice can be increased by append()
    • adds elements at the end of the slice
    • inserts into underlying array
    • increases the size of the array if necessary
    • we can keep appending as we wish
slice := make([]int, 0, 3) // length slice is 0 but the size of underlying array is 3

slice := append(slice, 100) // 100 is the number (element) we want to add to the slice

Hash Tables

  • contains key-value pairs
  • each value is associated with the unique key
  • hash function
    • used to compute a slot for a key
    • its argument is the key, the output is the slot in the "array"
    • it's never called explicitly, it's part of the hash table implementation
    • e.g. {"Joe", "x" }, {"Jane", "y" }, {"Pat", "z" }. Hash function takes keys (names) and outputs e.g. slots 3, 1, 5 (indexes in the array/slice where values are stored)
    • we never use indexes/slot numbers but only keys e.g. hashTable["Jane"] would return "y"
  • access is in constant time
  • naming is useful as it's easier to use keys (e.g. names) rather than indexes (numbers)
  • advantages: 
    • faster lookup than list: hash table has constant time - it's always the same, no matter how many key-value pairs is in the table; list has linear lookup time - the more elements in list, the more time it takes to find some particular element
    • arbitrary keys, not integers like in slices or arrays; keys have meaning
  • disadvantages:
    • may have collisions: when hash function maps two different keys into the same slot
      • they are rare; hash functions are made in such way to minimize them

Maps

  • Golang's implementation of hash table
var mapId map[string]int // string is the key type and int is the value type
idMap = make(map[string]int) // creating a (empty) map
  • we can define map literal used to initialize a new map:
idMap := map[string]int {"joe": 123}
  • accessing maps
    • referencing a value with [key]: idMap["Joe"]
      • returns 0 if key is not present
    • to add a new key-value pair (or modifying the value of the existing key): idMap["Jane"] = 456
    • to delete a key-value pair: delete(idMap, "Joe")
  • map functions
    • two-value assignment tests for existence of the key; if "Joe" key exists, p (presence of the key) will be true and id will be its value; if we don't care for the value, we can use "_" as the name instead of "id"
id, p := idMap["Joe"]
    • len() returns number of key-value pairs in the map
  • Iterating through map
    • use for loop with the range keyword
    • two-value assignment with range
for key, value := range idMap {
   fmt.Println(key, value)
}

Structs

  • aggregate/composite data type
  • groups data of various types
type Person struct{
   name string
   address string
   phone string
}

var p1 Person
  • each property is called a field
  • to access fields (read/write) - use dot notation:
p1.name = "Joe"
x1 = p1.address

  • initializing structs
    • can use new()
      • initializes fields to 0 
p1 := new (Person)
    • by using a struct literal
p1 := Person { 
   name : "Joe", 
   address : "A street", 
   phone : "123"
}

Week 4 - Protocols and Formats

RFCs

  • definitions of Internet protocols and formats
  • examples:
    • HTML
    • URI 
    • HTTP
  • Go provides protocol packages
    • contain functions that encode/decode protocol format
  • "net/http"
    • web communication protocol
    • e.g. http.Get(www.uci.edu)
  • "net"
    • TCP/IP  and socket programming
    • e.g. net.Dial("tcp", "uci.edu:80")
  • JSON
    • JavaScript Object Notation
    • Format to represent structured information
    • attribute-value pairs
      • naturally correlate to structs/maps
    • basic value types: bool, number, string, array, object
      • can be combined hierarchically
    • Go struct:
p1 :=  Person(name: "Joe", addr: "a street", phone: "123")
    • Equivalent JSON object:
{ "name": "Joe", "addr": "a street", "phone": "123" }

JSON

  • all is UNICODE
  • human readable
  • fairly compact
  • types can be combined recursively
    • arrays of structs, structs of structs...
  • Go package: "encoding/json"
  • JSON marshalling
    • generating JSON representation from an object 

type Person struct{
   name string
   addr string
   phone string
}

p1 := Person(name: "Joe", addr: "a st.", phone: "123")
barr, err := json.Marshal(p1)


  • Marshal() returns JSON representation as []byte
    • err is nil if there was no error
    • barr is byte array that contains JSON representation
  • Unmarshal() converts a JSON []byte into Go object
    • second argument is the address of the object into which JSON will be deserialized; its fields have to match JSON object properties (and the whole structure); object must "fit" JSON 
var p2 Person
err := json.Unmarshal(barr, &p2)


File Access, ioutil

  • File access is liner, not random access
  • files used to be stored on physical tapes
    • mechanical delay when reading as file beginning is on one end of the tape and end on the other
  • physical disks still have linear access
  • RAM (flash memory) has random access
  • basic file operations:
    • Open - get handle for access
    • Read - read bytes into []byte
    • Write - write []byte into file
    • Close - release handle
    • Seek - move read/write head
  • there is more than one package in Go which handles file access, ioutil [1] is one of them
  • ioutil.ReadFile()
    • dat is []byte filled with the content of the entire file
    • explicit open/close are not needed
    • large files can cause a problem: if their size is same or larger than available RAM memory e.g. loading 8GB file to 8GB RAM
dat, e := ioutil.ReadFile("text.txt")
  • ioutil.WriteFile()
    • creates a new file
    • 3rd arg is a read/write permission, 0777 = every user can read/write

dat = "Hello, world"
err := ioutil.WriteFile("outfile.txt", dat, 0777)

File Access, os

  • ioutil provides simple utility
  • for more control over file access, use os package
  • os.Open()
    • opens a file
    • returns file descriptor
  • os.Close()
    • closes a file
  • os.Read()
    • reads from a file into a byte array ([]byte)
    • we can control how much to read
  • os.Write()
    • writes bytes into the file
    • writes as much as byte array is long
  • Opening and reading:
    • os.Read() returns number of bytes read; can be smaller than the size of barr
f, err := os.Open("dt.txt")
barr := make([]byte, 10)
nb, err := f.Read(barr)
f.Close()
  • File Create / Write
    • os.Write() - writes byte array
    • os.WriteString() - writes Unicode sequence
f, err := os.Create("outfile.txt")
barr := []byte {1, 2, 3}
nb, err := f.Write(barr)
nb, err := f.WriteString("Hi")



Bonus 

Project/Code Organization

Naming Conventions

Unit Testing



References:

https://golang.org/doc/






No comments: