learning-go

The Basics: Part I

Variables

Simple declarations

var x int = 42
var (
	y = 2
	z = 2.01
)

The short variable declaration operator

x := 42

This is designed to allow for concise declaration and initialization of variables within the scope of a function.

Debugging method: printing out a variable’s type and value:

x := 42.0
fmt.Printf("%T %[1]v", x)

The output would be float64 42. In Go’s fmt package, the notation [] is for specifying argument indexes. In this case, the [1] in the formatting verb %v makes it use the first argument provided to %T.

Initialization

In Go, all variables are initialized upon declaration, and, by default, “zero”. Numerical types get 0, string gets "", an empty string with the length of 0. bool gets false, and everything else gets nil.

Constants

Only numbers, strings and booleans can be constants and they are immutable.

const (
    a = 1
    b float64 = 2.0
    c = "c"
    d = a == b // false
)

Program: calculating averages

var sum float64
var n int

for {
	var val float64
	if _, err := fmt.Scanln(&val); err != nil {
		break
	}

	sum += val
	n++
}

fmt.Printf("The average is %v", sum/float64(n))

To end the for-loop and get the result, simply hit control+D(EOF) to cause an error.

Strings

In Go, these types are related to strings: byte(synonym for uint8), rune(synonym for int32 for characters, 4 bytes) and string, which is immutable.

Strings in Go are all Unicode. They are,

Bytes vs. Runes & String length

To compare, we can test with the code below.

s := "Hello, 世界"

fmt.Printf("%T %[1]v %d\n", []rune(s), len([]rune(s)))
fmt.Printf("%T %[1]v %d\n", []byte(s), len([]byte(s)))

fmt.Println(len(s))

// Output
[]int32 [72 101 108 108 111 44 32 19990 30028] 9
[]uint8 [72 101 108 108 111 44 32 228 184 150 231 149 140] 13
13

We are casting the string to slice of runes and bytes, then printing out their values.

The string cast to runes is a slice of the Unicode code points for each character in the string, and the string cast to bytes is a slice of the byte values for each character, UTF-8 encoded.

And, based on the output, the length of a string is the length of a slice of bytes that’s necessary to encode the string in UTF-8.

String structure

In Go, the internal string representation is a pointer and a length(a descriptor).

Strings can share underlying storages. For example:

s := "hello, world"

hello := s[:5]
world := s[7:]

hello and world are substrings in string s. When creating these strings, part of the string s is indexed, and they point to the same characters in memory and can be reused.

When slicing a string using s[start:end], The start index is inclusive, while the end index is exclusive.

Also, strings are passed by reference, thus they are not copied.

func main() {
	s := "the quick brown fox"
	pluralize(s)

	fmt.Println(s)
}

func pluralize(str string) {
	str += "es"
}

However, this does NOT work and will print the quick brown fox. Although strings are passed by reference in Go, they are still immutable. str += "es" creates a new string in memory and points str to it, while s still points to the original value.

Program: Replacing substring

if len(os.Args) < 3 {
	_, _ = fmt.Fprintln(os.Stderr, "Invalid syntax.")
	os.Exit(-1)
}

var s string
old, new := os.Args[1], os.Args[2]
scan := bufio.NewScanner(os.Stdin)

for scan.Scan() {
	s += scan.Text()
}

fmt.Println(strings.ReplaceAll(s, old, new))

The program is fairly simple.

Running the program, instead of typing the input, we could also supply a text file like this:

go run . <search> <replace> < textfile 

or, like this:

cat textfile | go run . <search> <replace>

Composite Types

Arrays

Arrays are typed by size, and passed by value.

var a [3]int{0, 0, 0}

func do (a [3]int) {
    a[0] = 1
}

do(a)
fmt.Println(a) // {0, 0, 0} (unchanged)

Slices

Much like strings, slices have descriptors(pointer and length), and they point to somewhere in the memory. Slices are passed by reference.

A slice has an array behind it(a pointer that points to a backing array), and is copied when it outgrows the array. Similarly, when it’s modified, another chunk of memory is allocated, then assigned back to the original slice with a modified descriptor.

var a = []int{1, 2}
a = append(a, 3)

b := a // {1, 2, 3}

By b := a, b becomes an alias of a. The a’s descriptor(not value) is copied to b, and they point to the same address in memory and share storage. And, also because slices are mutable, we have:

func do(a []int) {
	a[0]--
}

do(b) // a: {0, 2, 3}

b[0]-- // a: {-1, 2, 3}
a[0] == -1 // true
“The off-by-one bug”

Slices are indexed like [8:11], but the end index is exclusive(written [8, 11) in math). I found the figure below help a lot.

 

 

Slices vs. Arrays

Most Go APIs takes slices as inputs, not arrays.

SlicesArrays
Variable lengthFixed length
Passed by referenceCopied and passed by value
Not comparableComparable (==)
Cannot be used as map keyCan be used as map key

Maps

In Go, the type used for a map key must be comparable, that’s not slices or functions, and also not maps, maps cannot be compared.

Maps are passed by reference(descriptor).

var m map[string]int // nil

fmt.Println(m["the"]) // 0

m["the"] = 1 // PANIC

This declares a map variable without creating a map. m is a nil map. Nil maps can be read from, but cannot be assigned any values, or it will panic.

p := make(map[string]int) // or: p := map[string]int{}
m = p

m["the"] = 1
fmt.Println(p["the"]) // 1

n = map[string]int{
    "the": 1
}

m == n // Syntax Error

Maps have a special two-result lookup function, the second return value indicates if the key is defined.

if w, ok := m["the"]; ok {
    // w is not the default value
}

Nil pointers

nil is a predeclared identifier which represents the concept of absence. nil is generally safe for built-in functions in Go.

Link: GopherCon 2016: Francesc Campoy - Understanding nil

Program: word count

scan := bufio.NewScanner(os.Stdin)
scan.Split(bufio.ScanWords)

words := map[string]int{}

for scan.Scan() {
	words[scan.Text()]++
}

type kv struct {
	key string
	val int
}

var ss []kv
for word, count := range words {
	ss = append(ss, kv{word, count})
}

sort.Slice(ss, func(i, j int) bool {
	return ss[i].val > ss[j].val
})

for _, kv := range ss {
	fmt.Println(kv.key, kv.val)
}

fmt.Println(len(ss), "unique words.")

#types

Next: The Basics: Part II