Skip to main content

Book: Go for DevOps

Notes taking when reading the book Go for DevOps.

Basic

All Go files in a directory must belong to the same package.

package main will declare src_go{func main()}, which is the starting point for a binary to run.

All Go files in a directory must have the same package header

If you import a package, you must use it.

You can use var to declare a variable both at the package level (meaning not inside a function) and within a function.

Note that the int and int64 types are distinct. You cannot use an int type as an int64 type, and vice versa.

var (
i int
word = "hello"
)

Within a function (not at the package level), we can do a create and assign by using the := operator.

In Go, declaring a variable without an assignment automatically assigns a value called the zero value.

Looping

Go has only one loop controller: for, but it can simulate others like while, do while. break and continue works as other programming language

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

Also

var i int
for i < 10; i++ {
fmt.Println(i)
}
fmt.Println("i's final value: ", i)

as while

var i int
for i < 10 {
i++
}

b := true
for b { // This will loop forever
fmt.Println("hello")
}

// Creating an infinite loop
for {
fmt.Println("Hello World")
}

Condition

Unlike most languages, Go has the ability to execute a statement within the if scope before the evaluation is made

if err := someFunction(); err != nil {
fmt.Println(err)
}

switch statements are more elegant if/else blocks that are very flexible in their use. They can be used for doing exact matching and multiple true/false evaluations. Unlike some languages, once a match occurs, no other case is considered. switch can also have an init statement, similar to if

switch [value] {
case [match]:
[statement]
case [match], [match]:
[statement]
default:
[statement]
}

Function

Returning multiple values and named results

func divide(num, div int) (res, rem int) {
result = num / div
remainder = num % div
return res, rem // Not necessary because they are named return results
}
result, remainder := divide(3, 2)

Variadic arguments: You can use variadic arguments with other arguments, but it must be the last argument in the function.

func sum(numbers ...int) int {
// Same code
}

Anonymous functions, similar to javascript

func main() {
result := func(word1, word2 string) string {
return word1 + " " + word2
}("hello", "world")
fmt.Println(result)
}

To be public, the constant/variable/function/method must simply start with an uppercase letter. If it starts with a lowercase letter, it is private. There is a third type of visibility that we don't cover here: internally exported. This occurs when a type is public but in a package, located within a directory called internal/. Those packages can only be used by packages within a parent directory.

arrays and slices

The base sequential type in Go is the array (important to know, but rarely used).

Arrays, unlike slices, are not pointer wrapper types. Passing an array as a function argument passes a copy.

Arrays are typed by size – [2]int is distinct from [3]int. You cannot use [3]int where [2]int is required.

Arrays are a set size. If you need more room, you must make a new array.

A slice is not statically sized.

A slice can grow to accommodate new values.

A slice tracks its array, and when it needs more room, it will create a new array that can accommodate the new values and copies the values from the current array into the new array.

Remember that slices are simply views into a trackable array. We can create new limited views of the array. y := x[1:3] creates a view (y) of the array, yielding []int{4, 12} (1 is inclusive and 3 is exclusive in [1:3]). Changing the value at y[0] will change x[1]. Appending a single value to y via y = append(y, 10)will change x[3], yielding []int{8,4,12,10,2}.

for index, val := range someSlice {
fmt.Printf("slice entry %d: %s\n", index, val)
}

// If you don't want the index
for _, val := range someSlice {
fmt.Printf("slice entry: %s\n", val)
}

// Retrieve value from map
if carMake, ok := modelToMake["outback"]; ok {
fmt.Printf("car model \"outback\" has make %q", carMake)
}else{
fmt.Printf("car model \"outback\" has an unknown make")
}

// iterating over map
for key, val := range modelToMake {
fmt.Printf("car model %q has make %q\n", key, val)
}

Ponters

functions always make a copy of the arguments passed.

When declaring a variable, indicates a pointer type, var intPtr int. When used on a variable, means dereference, fmt.Println(intPtr).

var x int = 23
var varPnt *int = &x
fmt.Println(varPnt) // 0xc00012c008
fmt.Println(*varPnt) // dereferencing, print 23
*varPnt = 32
fmt.Println(x) // 32

Structs

A method is similar to a function, but instead of being independent, it is bound to a type.

It is important to remember that a struct is not a reference type. If you pass a variable representing a struct to a function and change a field in the function, it will not change on the outside. For struct types that need to have fields that change, we normally pass in a pointer.

In the same way that a function cannot alter a non-pointer struct, neither can a method. Pass pointer if you want to change property of the object.

func (r *Record) IncrAge() {
r.Age++ // Note that . is a magic operator that works on struct or *struct.
}

!! If the struct type should be a pointer, then make all methods pointer methods. If it shouldn't be, then make them all non-pointers. Don't mix and match.

Constructors

func NewRecord(name string, age int) (*Record, error) {
if name == "" {
return nil, fmt.Errorf("name cannot be the empty string")
}

if age <= 0 {
return nil, fmt.Errorf("age cannot be <= 0")
}

return &Record{Name: name, Age: age}, nil
}

rec, err := NewRecord("John Doak", 100)
if err != nil {
return err
}

Interface

Interfaces allow us to pass values that can do common operations defined by their methods.

type Stringer interface {
String() string
}

The first thing to note about interfaces is that values must implement every method defined in the interface. Your value can have methods not defined for the interface, but it doesn't work the other way.

Another common issue new Go developers encounter is that once the type is stored in an interface, you cannot access its fields, or any methods not defined on the interface.

Of particular interest is the empty interface, which defines exactly zero methods. Because the empty interface has no methods, it is implemented by every type. interface{} is Go's universal value container that can be used to pass any value to a function and then figure out what it is and what to do with it later. Let's put some things in i:

i = 3
i = "hello world"
i = 3.4
i = Person{First: "John"}

// This is actually how fmt.Printf() and fmt.Println() work
func Println(a ...interface{}) (n int, err error)
func Printf(format string, a ...interface{}) (n int, err error)

Go 1.18 has introduced an alias for the blank interface{}, called any. The Go standard library now uses any in place of interface{}.

Type assertion

// i.(string) is asserting that i is a string value
if v, ok := i.(string); ok {
fmt.Println(v)
}

switch v := i.(type) {
case int:
fmt.Printf("i was %d\n", i)
case string:
fmt.Printf("i was %s\n", i)
case float:
fmt.Printf("i was %v\n", i)
case Person, *Person:
fmt.Printf("i was %v\n", i)
default:
// %T will print i's underlying type out
fmt.Printf("i was an unsupported type %T\n", i)
}

Errors

Two ways to create error: fmt.Errorf() or errors.New()

Constants

Constants are declared using the const keyword.

Constants are different from variable types in that they come in two flavors, as follows:

  • Untyped constants: if you don't declare the exact type (as in the third example, num64, where we declared it to be an int64 type), the constant can be used for any type that has the same base type or family of types (such as integers).
  • Typed constants:

Enumeration via constants

Enumeration with iota is great, as long as the values will never be stored on disk or sent to another process that is local or remote. The value of constants is controlled by the order of the constants in the code.

const (
a = iota // 0
b = iota // 1
d = iota // 2
)

const (
a = iota *2 // 0
b // 2
d // 4
)

//go:generate stringer -type=Pill is a special syntax that indicates that when the go generate command is run for this package, it should call the stringer tool and pass it the -type=Pill flag, which indicates to read our package code and generate a method that reverses the constants based on type Pill to a string.

defer

The defer keyword allows you to execute a function when the function that contains defer exits. If there are multiple defer statements, they execute last to first.

panic

The panic keyword is used to cause the execution of the program to stop and exit while displaying some text and a stack trace.

In most circumstances, a user should return an error and not panic.

As a general rule, only use panic in the main package.

recover

If, like the RPC framework, you need to catch a panic that is occurring or protect against potential panics, you can use the recover keyword with the defer keyword.

goroutine

When doing concurrent programming, there is a simple rule: You can read a variable concurrently without synchronization, but a single writer requires synchronization.

  • The channel data type to exchange data between goroutines
  • Mutex and RWMutex from the sync package to lock data access
  • WaitGroup from the sync package to track access

A WaitGroup is a synchronization counter that only has positive values starting at 0. It is most often used to indicate when some set of tasks is finished before executing code that relies on those tasks.

A WaitGroup has a few methods, as outlined here:

.Add(int): Used to add some number to the WaitGroup
.Done(): Subtract 1 from the WaitGroup
.Wait(): Block until WaitGroup is 0

Channels provide a synchronization primitive in which data is inserted into a channel by a goroutine and removed by another goroutine.

Channels are used to pass data from one goroutine to another, where the goroutine that passed the data stops using it. This allows you to pass control from one goroutine to another, giving access to a single goroutine at a time. This provides synchronization.

Channels are typed, so only data of that type can go into the channel. Because channels are a pointer-scoped type such as map and slice, we use make() to create them

for val := range ch { // Acts like a <-ch
fmt.Println(val)
}

Channels can be closed so that no more data will be sent to them. This is done with the close keyword. To close the preceding channel, we could do close(ch). This should always be done by the sender. Closing a channel will cause a for range loop to exit once all values on the channel have been removed.