Skip to content

Instantly share code, notes, and snippets.

@bknie1
Last active July 27, 2022 01:03
Show Gist options
  • Select an option

  • Save bknie1/435469ff026f339c0927431e2e741519 to your computer and use it in GitHub Desktop.

Select an option

Save bknie1/435469ff026f339c0927431e2e741519 to your computer and use it in GitHub Desktop.
Let's Go(lang)

Let's Go(lang)

This guide heavily cites A Tour of Go by Google.


Table of Contents

1. About Go

Introduction to Go

2. Basics of Go

Packages

Functions

Variables

Basic Types

3. Flow Control

Loops

Conditions

switch

defer

4. Advanced Types

pointers

structs

Arrays

Slices

Slices: Make

Slices: Append

Maps

Function Values

5. Methods and Interfaces

Methods

Interfaces

6. Concurrency

What is Concurrency?


About Go

Introduction to Go

Todd McLeod on "Why Go?"

  • Created by Google
  • Created by luminaries in Computer Science
    • Rob Pike (Unix, UTF-8)
    • Ken Thompson (B, C, Unix, UTF-8)
    • Robert Greisemer (Hotspot, JVM)

Google looked at all other programming languages and decided they needed to make their own; the others would not do.

It is the fastest growing programming language in America. It is also the highest paying programming language in America. Go is used widely in industry.

In 2005/2006 the first dual core processors became available. Up until then, every programming language was built to only take advantage of a single core. Go natively takes advantage of multiple core systems; it was built for parallel computing and concurrency.

In 2009 the language went open source. In 2012 version 1.0, the first stable version, was released.

Goals of Go
  • Efficient compilation
  • Efficient execution
  • Ease of programming
What is Go good for?
  • Web services at scale
    • Google is rewriting their infrastructure to leverage goal
  • Concurrency & Parallelism
  • Good for system level automation and command line tools
  • Cryptography
  • Image processing

Basics of Go

Packages

Like other languages Go has a number of packages. We can also import specific parts of packages if we know the path to that sub-package:

package main

import (
	"fmt" // fmt is a basic I/O package
	"math/rand" // rand is a sub-package of math
    "time" // Basic time and date package
)

func main() {
	fmt.Println("My favorite number is", rand.Intn(10))
    fmt.Println("The time is ", time.Now())
}

Note: rand.Intn is deterministic; it will always return the same number. rand.Seed offers a more time dependent option.

Importing and Exporting Packages

This procedure is case sensitive. For example, 'math.pi' will not export 'pi' from math. Instead, we must use 'math.Pi'. Notice the uppercase 'P' in 'Pi'. This is a language requirement. Any unexported names are not accessible from outside the package.

Functions

Functions can take 0+ arguments. The variable type comes after the name of the variable:

package main

import "fmt"

// Note: The type comes after the name
// Also, the return type comes after the arguments

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

func main() {
	fmt.Println(add(42, 13))
}

Same Type Parameter Short Hand

If 2+ consecutive named function parameters share a type we can omit the type from all but the last parameter:

package main

import "fmt"

// x and y are both int so we only write 'int' once at the end
func add(x, y int) int {
	return x + y
}

func main() {
	fmt.Println(add(42, 13))
}

Returning Multiple Values from a Function

We can also return multiple results from a function so long as they are assigned:

package main

import "fmt"

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

func main() {
	a, b := swap("hello", "world")
	fmt.Println(a, b)
}

Named return Values and Naked return Values

We can set a default named return value in our function declaration. Furthermore, if we use the 'return' keyword without a return value it will default to this named return value. Below, 'sum' is the named return value.

However, it is important to use naked returns sparingly or only in small functions as it may inhibit readability.

package main

import "fmt"

// Here, the return value is named 'sum'.
func split(sum int) (x, y int) {
	x = sum * 4 / 9
	y = sum - x
	return // This 'naked' return will default to returning the named return value 'sum'
}

func main() {
	fmt.Println(split(17))
}

Variables

The 'var' keyword indicates declaration. If multiple variables are of the same type we can end the var statement with the variable type for all listed variables:

package main

import "fmt"

var c, python, java bool

func main() {
	var i int
	fmt.Println(i, c, python, java)
}

Variables with Initializers

Variables may be initialized. If they are initialized, we do not need to strongly type the variable, as it will inherit the implicit type of the initialized value:

package main

import "fmt"

var i, j int = 1, 2

func main() {
	var c, python, java = true, false, "no!"
	fmt.Println(i, j, c, python, java)
}

Short Variable Declarations

Inside a function the ':=' short assignment statement can be used in place of a var declaration with implicit type. Outside a function every statement must begin with a keyword such as var, func, etc., because the '=:' is only available inside a function.

package main

import "fmt"

func main() {
	var i, j int = 1, 2
	k := 3
	c, python, java := true, false, "no!"

	fmt.Println(i, j, k, c, python, java)
}

Basic Types

These are the basic explicit types we can use in GoLang:

  • 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
package main

import (
	"fmt"
	"math/cmplx"
)

var (
	ToBe   bool       = false
	MaxInt uint64     = 1<<64 - 1
	z      complex128 = cmplx.Sqrt(-5 + 12i)
)

func main() {
	fmt.Printf("Type: %T Value: %v\n", ToBe, ToBe)
	fmt.Printf("Type: %T Value: %v\n", MaxInt, MaxInt)
	fmt.Printf("Type: %T Value: %v\n", z, z)
}

However, unless there is a specific need, we can stick to the most basic types; e.g. using int over int64, etc.

Uninitialized Default Values

If a variable has not been initialized it will default to a specific value depending on its type:

  • 0 for numerics
  • false for booleans
  • "" for strings

Type Conversions

The expression 'T(v)' converts the value 'v' to the type T:

var i int = 42
var f float64 = float64(i)
var u uint = uint(f)

// In function context short hand:

i := 42
f := float64(i)
u := uint(f)

Type Inference

If no explicit type is written, Golang will infer the type using the value on the right most side:

var i int
j := i // j is an int

i := 42           // int
f := 3.142        // float64
g := 0.867 + 0.5i // complex128

Constants

Constants are declared using the 'const' keyword. They can be strings, characters, booleans, or numerics. They cannot be declared using the ':=' short hand syntax as that is only short hand for 'var'; we must explicitly write 'const'.

Numeric Constants

Numeric constants are high precision values. An untyped constant takes the type needed by its context:

package main

import "fmt"

const (
	// Create a huge number by shifting a 1 bit left 100 places.
	// In other words, the binary number that is 1 followed by 100 zeroes.
	Big = 1 << 100
	// Shift it right again 99 places, so we end up with 1<<1, or 2.
	Small = Big >> 99
)

func needInt(x int) int { return x*10 + 1 }
func needFloat(x float64) float64 {
	return x * 0.1
}

func main() {
	fmt.Println(needInt(Small))
	fmt.Println(needFloat(Small))
	fmt.Println(needFloat(Big))
}

Flow Control

Loops

For Loop

No parentheses, only curly braces:

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

The init and post statements are optional:

	sum := 1
	for ; sum < 1000; {
		sum += sum
	}

The for loop serves as a while loop in Go:

	sum := 1
	for sum < 1000 {
		sum += sum
	}

And an empty for loop serves as an infinite loop:

	for {
        // ever and ever and ever ...
	}

Conditions

Conditions omit parentheses but include curly braces:

func sqrt(x float64) string {
	if x < 0 {
		return sqrt(-x) + "i"
	}
	return fmt.Sprint(math.Sqrt(x))
}

We can inject a short statement that executes before the conditional logic, then the condition logic:

func pow(x, n, lim float64) float64 {
    // Assign v, then determine if v is less than the limiter value:
	if v := math.Pow(x, n); v < lim {
		return v
	}
	return lim
}

Here is an 'else' statement with some scope considerations:

func pow(x, n, lim float64) float64 {
	if v := math.Pow(x, n); v < lim {
		return v
	} else {
		fmt.Printf("%g >= %g\n", v, lim)
	}
	// can't use v here, though
	return lim
}

switch

In Go, the selected switch case is the only one that is executed:

func main() {
	fmt.Print("Go runs on ")
	switch os := runtime.GOOS; os {
	case "darwin":
		fmt.Println("OS X.")
	case "linux":
		fmt.Println("Linux.")
	default:
		// freebsd, openbsd,
		// plan9, windows...
		fmt.Printf("%s.\n", os)
	}
}

switch conditions are checked in the order they are written:

func main() {
	fmt.Println("When's Saturday?")
	today := time.Now().Weekday()
	switch time.Saturday {
	case today + 0:
		fmt.Println("Today.")
	case today + 1:
		fmt.Println("Tomorrow.")
	case today + 2:
		fmt.Println("In two days.")
	default:
		fmt.Println("Too far away.")
	}
}

A switch without a condition os the same as 'switch true':

func main() {
	t := time.Now()
	switch {
	case t.Hour() < 12:
		fmt.Println("Good morning!")
	case t.Hour() < 17:
		fmt.Println("Good afternoon.")
	default:
		fmt.Println("Good evening.")
	}
}

defer

'defer' defers the execution of a function until the surrounding function returns. The deferred call's arguments are evaluated immediately but the function is not executed until the function returns. Here, the 'world' print statement is not executed until main() returns:

func main() {
	defer fmt.Println("world")

	fmt.Println("hello")
}

// "hello world"

defer call stacking

Deferred functions are pushed on to a stack in the order which they are called and deferred:

func main() {
	fmt.Println("counting")

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

	fmt.Println("done")
}

The output for this is "done", "9", "8", ... etc. because the final deferred call is the print of 'i' at the end of the loop.


Advanced Types

pointers

Go has pointers. A pointer points to a memory address which contains some value. The type '*T' is a pointer to value 'T'. Unlike other uninitialized values, it has a zero value of 'nil', because 0 could potentially point to an actual location in memory.

var p *int

The & operator generates a pointer to its operand:

i := 42
p = &i

In the above example we instantiate a pointer without a set reference. Then, we instantiate an integer value to i, and point 'p' to the memory location of i using the ampersand.

Below, we retrieve the value of i by way of pointer 'p'. Then, we can use pointer 'p' to set a new value to i.

fmt.Println(*p) // read i through the pointer p
*p = 21         // set i through the pointer p
package main

import "fmt"

func main() {
	i, j := 42, 2701

	p := &i         // point to i
	fmt.Println(*p) // read i through the pointer
	*p = 21         // set i through the pointer
	fmt.Println(i)  // see the new value of i

	p = &j         // point to j
	*p = *p / 37   // divide j through the pointer
	fmt.Println(j) // see the new value of j
}

Go has no pointer arithmetic like its grandfather C.

structs

Structs are a collection of fields. Below, we have a vertex; a point on a graph. This owns two coordinate values: X and Y. once Vertex v is instantiated we can access each field using dot '.' notation.

Struct Literals

A struct literal deonates a newly allocated struct value by listing the values of its fields.

package main

import "fmt"

type Vertex struct {
	X int
	Y int
}

func main() {
	v:= Vertex{1, 2}
    v.X = 4
	fmt.Println(v.X, ",", v.Y)
    p := &v
	p.X = 1e9
    
    var (
	    v1 = Vertex{1, 2}  // has type Vertex
	    v2 = Vertex{X: 1}  // Y:0 is implicit
	    v3 = Vertex{}      // X:0 and Y:0
	    p  = &Vertex{1, 2} // has type *Vertex
    )
}

The above snippet also features of an example of using pointers to reference a struct. Finally, we see that the uninitialized vertices default to '0' integer values, which is consistent with the basic integer initialization rules covered earlier in this guide.

Arrays

The type '[n]T' is an array of n values of type T. An array's length is part of its type so arrays cannot be resized. However, Go has tools to manipulate arrays.

	primes := [6]int{2, 3, 5, 7, 11, 13}

Slices

A slice in Go is a dynamically-sized interface for an array. In practice they are more common than Go Arrays.

The type []T is a slice with elements of type T. a slice is formed by specifying two indices; a low and high bound:

func main() {
	primes := [6]int{2, 3, 5, 7, 11, 13}

	var s []int = primes[1:4]
    // Above: Starts at first element, stops at, but does not include, 4th element:
	fmt.Println(s) // [3 5 7]
}

Slices are like references to arrays. It references a sub array. Changing the elements of a slice modifies the source array:

func main() {
	names := [4]string{
		"John",
		"Paul",
		"George",
		"Ringo",
	}
	fmt.Println(names)

	a := names[0:2]
	b := names[1:3]
	fmt.Println(a, b)

	b[0] = "XXX"
	fmt.Println(a, b)
	fmt.Println(names)
}

Slice Literals

A slice literal is like an array literal without the length. This is an array literal:

[3]bool{true, true, false}

And this creates the same array as above, then builds a slice that references it:

[]bool{true, true, false}

The size is inferred by the initialized data.

func main() {
	q := []int{2, 3, 5, 7, 11, 13}
	fmt.Println(q)

	r := []bool{true, false, true, true, false, true}
	fmt.Println(r)

	s := []struct {
		i int
		b bool
	}{
		{2, true},
		{3, false},
		{5, true},
		{7, true},
		{11, false},
		{13, true},
	}
	fmt.Println(s)
}

Slice Defaults

The default low and high values are the beginning of the array, '0', and end of the array, 'n + 1'. Given an array of 10 elements this is how we can slice it:

a[0:10]
a[:10]
a[0:]
a[:]

Slice Length and Capacity

Slices have length and capacity. Where s is a slice we can use the methods len(s) and cap(s) respectively.

func main() {
	s := []int{2, 3, 5, 7, 11, 13}
	printSlice(s)

	// Slice the slice to give it zero length.
	s = s[:0]
	printSlice(s)

	// Extend its length.
	s = s[:4]
	printSlice(s)

	// Drop its first two values.
	s = s[2:]
	printSlice(s)
}

func printSlice(s []int) {
	fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

The output for this would be:

len=6 cap=6 [2 3 5 7 11 13]
len=0 cap=6 []
len=4 cap=6 [2 3 5 7]
len=2 cap=4 [5 7]

Nil Slices

The zero value of a slice is 'nil':

func main() {
	var s []int
	fmt.Println(s, len(s), cap(s))
	if s == nil {
		fmt.Println("nil!")
	}
}

Make

Slices can be created with make(). This is how we create dynamic arrays in Go. The make() function allocates a zeroed array and returns a slice that refers to that array:

a := make([]int, 5)  // len(a)=5

We may also specify a capacity:

b := make([]int, 0, 5) // len(b)=0, cap(b)=5

b = b[:cap(b)] // len(b)=5, cap(b)=5
b = b[1:]      // len(b)=4, cap(b)=4

Here is a more comprehensive example of make():

func main() {
	a := make([]int, 5)
	printSlice("a", a)

	b := make([]int, 0, 5)
	printSlice("b", b)

	c := b[:2]
	printSlice("c", c)

	d := c[2:5]
	printSlice("d", d)
}

func printSlice(s string, x []int) {
	fmt.Printf("%s len=%d cap=%d %v\n",
		s, len(x), cap(x), x)
}

Slices of Slices

Slices may contain slices:

func main() {
	// Create a tic-tac-toe board.
	board := [][]string{
		[]string{"_", "_", "_"},
		[]string{"_", "_", "_"},
		[]string{"_", "_", "_"},
	}

	// The players take turns.
	board[0][0] = "X"
	board[2][2] = "O"
	board[1][2] = "X"
	board[1][0] = "O"
	board[0][2] = "X"

	for i := 0; i < len(board); i++ {
		fmt.Printf("%s\n", strings.Join(board[i], " "))
	}
}

Append

We can use append(...) to add elements to an existing slice:

func append(s []T, vs ...T) []T

We can expand this into an actual use case:

func main() {
	var s []int
	printSlice(s)

	// append works on nil slices.
	s = append(s, 0)
	printSlice(s)

	// The slice grows as needed.
	s = append(s, 1)
	printSlice(s)

	// We can add more than one element at a time.
	s = append(s, 2, 3, 4)
	printSlice(s)
}

The output for the above is:

len=0 cap=0 []
len=1 cap=1 [0]
len=2 cap=2 [0 1]
len=5 cap=6 [0 1 2 3 4]

Range

We can use the range keyword to iterate over a slice (or a map, discussed in the next section):

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

func main() {
	for i, v := range pow {
		fmt.Printf("2**%d = %d\n", i, v)
	}
}

When ranging over a slice we return two values:

  • The index (i)
  • The copy of the element/value at that index (v)

This yields the output:

2**0 = 1
2**1 = 2
2**2 = 4
2**3 = 8
2**4 = 16
2**5 = 32
2**6 = 64
2**7 = 128

You may also omit index or range by assigning '_':

for i, _ := range pow
for _, value := range pow

And as a shorthand, if you are only interested in the index, just omit the second variable:

for i := range pow

Maps

A Map is a Dictionary of key: value pairs. The uninitialized, zero value of a map is 'nil'. A 'nil' map cannot be added to. We can use make() in conjunction with map to initialize a map with some data:

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,
	}
	fmt.Println(m["Bell Labs"])
}

Map literals

Map literals are similar to structs but keys are not required:

type Vertex struct {
	Lat, Long float64
}

var m = map[string]Vertex{
	"Bell Labs": Vertex{
		40.68433, -74.39967,
	},
	"Google": Vertex{
		37.42202, -122.08408,
	},
}

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

If a top-level type is just a type name we can omit it from the elements of the literal in this shorthand style:

var m = map[string]Vertex{
	"Bell Labs": {40.68433, -74.39967},
	"Google":    {37.42202, -122.08408},
}

Above, we write 'Vertex' once to indicate the value type, but do not need to repeat 'Vertex' for each literal entry.

Mutating Maps

Insert, Update, and Delete

Insert or update an element in map m:

m[key] = elem

Retrieve an element:

elem = m[key]

Delete an element:

delete(m, key)

Test that a key is present with a two-value assignment:

elem, ok = m[key]

If key is in m, ok is true. If not, ok is false.

If key is not in the map, then elem is the zero value for the map's element type.

Note: If elem or ok have not yet been declared you could use a short declaration form:

elem, ok := m[key]

Here is an actual implementation:

func main() {
	m := make(map[string]int)

	m["Answer"] = 42
	fmt.Println("The value:", m["Answer"])

	m["Answer"] = 48
	fmt.Println("The value:", m["Answer"])

	delete(m, "Answer")
	fmt.Println("The value:", m["Answer"])

	v, ok := m["Answer"]
	fmt.Println("The value:", v, "Present?", ok)
}

And the output:

The value: 42
The value: 48
The value: 0
The value: 0 Present? false

Function Values

Functions are values too. They can be passed like other values. Function values may be used as function arguments and return values:

package main

import (
	"fmt"
	"math"
)

func compute(fn func(float64, float64) float64) float64 {
	return fn(3, 4)
}

func main() {
	hypot := func(x, y float64) float64 {
		return math.Sqrt(x*x + y*y)
	}
	fmt.Println(hypot(5, 12))

	fmt.Println(compute(hypot))
	fmt.Println(compute(math.Pow))
}

Go functions support closures. Closures can use data from outside its scope but from outside this scope we cannot reach into the closure:

func adder() func(int) int {
	sum := 0
	return func(x int) int {
		sum += x
		return sum
	}
}

func main() {
	pos, neg := adder(), adder()
	for i := 0; i < 10; i++ {
		fmt.Println(
			pos(i),
			neg(-2*i),
		)
	}
}

Methods and Interfaces

WIP

Methods

Interfaces


Concurrency

WIP

What is Concurrency?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment