Re-learning some Basic Go Concepts

2023-03-16

✍️ Go Notes

Golang Pointers

A pointer holds the memory address of a value.

The & is the address operator. It genrates a pointer to an object.

The * is the indirection operator ( also called dereferencing). It denotes the pointer’s underlying value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

func main() {
x := "hello"
pointerToX := &x
fmt.Println(pointerToX)
fmt.Println(*pointerToX)

var p *int
i, j := 42, 2701
p = &i

fmt.Println(p)
fmt.Println(*p)


p = &j
fmt.Println(p)
fmt.Println(*p)

}

Output :

1
2
3
4
5
6
7
0xc000108050
hello
0xc00010c008
42
0xc00010c020
2701

Golang Methods and Interface

Methods

Go does not have classes. However, you can define methods on types.

A method is a function with a special receiver argument.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

type AccountDetails struct {
Principal, Rate float64
Time int
}

func (person AccountDetails) SimpleInterest() float64 {

return (person.Principal * person.Rate * float64(person.Time)) / 100.0
}

func main() {

applicant := AccountDetails{10000.50, 0.4, 3}

output := applicant.SimpleInterest()
fmt.Println(output)

}

Output:

1
120.006
Pointer Receivers

You can declare methods with pointer receivers.

  • If your method modifies the receiver, you must use a pointer receiver. This means the receiver type has the literal syntax *T for some type T.

E.g, if the bank is running a short promo where they are reducing the interests on loans issued from the banks.

1
2
3
4
5
6
7
8

func (person *AccountDetails) RunShortPromo (reducedRates float64){
person.Rate = person.Rate - reducedRates

}
applicant.RunShortPromo(0.04)
newOutput := applicant.SimpleInterest()
fmt.Println(newOutput)

Output:

1
2
108.00540000000001

Interface

Interfaces are named collections of method signatures.
We use interfaces to store a set of methods without implementation.

E.g, we create a simple finance app that serves different needs of individuals or Small/Mid-sized enterprises (SMEs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package main

import (
"fmt"
)

type FinanceApp interface {
MonthlyEMI() float64
CompoundAnnualGrowthRate() float64
}

type Individual struct {
Deposits float64
Time int
}
type SME struct {
Deposits float64
Time , noOfEmployees int
}

func (user Individual) MonthlyEMI() float64 {
emi := user.Deposits + ( 0.001 * user.Deposits)
return emi
}
func (user Individual) CompoundAnnualGrowthRate() float64 {

cagr_rate := user.Deposits + ( 0.001 * user.Deposits) + float64(user.Time /100)
return cagr_rate
}



func (user SME) MonthlyEMI() float64 {
emi := user.Deposits + ( float64(user.noOfEmployees) + 0.001 * user.Deposits)
return emi
}
func (user SME) CompoundAnnualGrowthRate() float64 {
cagr_rate := user.Deposits + ( 0.001 * user.Deposits) + float64(user.Time /100) + 0.005
return cagr_rate
}

func measure(app FinanceApp) {

fmt.Println(app.MonthlyEMI())
fmt.Println(app.CompoundAnnualGrowthRate())
}

func main() {

Mike := Individual{10.00, 6}
measure(Mike)

fmt.Println("=========================")

Pepsico := SME{4e06, 5, 50}
measure(Pepsico)

}

Output:

1
2
3
4
5
6
10.01
10.01
=========================
4.00405e+06
4.004000005e+06

Golang Generics

In programming, a generic type is a type that can be used in conjunction with multiple other types.
Go is a static language meaning parameters or variables are checked before the code has complied.
Sometimes it becomes too verbose to create multiple functions to receive different data types, this is where generics come in.

We’ll create an example to explain this.
A Stack that follows a particular order LIFO(Last In First Out) with some basic operations.

Note: comparable is an interface that is implemented by all comparable types (booleans, numbers, strings, pointers, channels, arrays of comparable types, and structs whose fields are all comparable types). The comparable interface may only be used as a type parameter constraint, not as the type of a variable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

package main

import (
"fmt"
)

type Stack[T comparable] struct {
values []T
}

func (s *Stack[T]) Push(newItem T) {
s.values = append(s.values, newItem)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.values) == 0 {
var zero T
return zero, false
}
top := s.values[len(s.values)-1]
s.values = s.values[:len(s.values)-1]
return top, true
}
func (s Stack[T]) Contains(val T) bool {
for _, v := range s.values {
if v == val {
return true
}
}
return false
}



func main() {
var number Stack[int]
number.Push(10)
number.Push(20)
number.Push(30)
fmt.Println(number)
v, ok := number.Pop()
fmt.Println(v, ok)
fmt.Println(number.Contains(10))
fmt.Println(number.Contains(5))

fmt.Println("===================")

var cars Stack[string]
cars.Push("Volvo")
cars.Push("Mercedes")
cars.Push("Toyota")
fmt.Println(cars)
value, ok := cars.Pop()
fmt.Println(value, ok)
fmt.Println(cars.Contains("Toyota"))
fmt.Println(cars.Contains("Honda"))
}

Error Handling with Panic, Recovery and Defer Statement in Go

Defer

Defer is used to ensure that a function call is performed later in a program’s execution, usually for purposes of cleanup, e.g connecting and disconnecting to a database, writing and closing a file. it ensure resources are gracefully released, irrespective of the occurrence of an error.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package main

import (
"encoding/csv"
"fmt"
"os"
"strconv"
)

type Employee struct {
FullName string
Age int
Role string
}

func openOrCreateFile(fileName string) *os.File {
file, err := os.Create(fileName)
if err != nil {
panic(err)
} else {
return file
}
}

func closeFile(file *os.File) {
err := file.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "Error! %v\n", err)
os.Exit(1)
}
}

func writeToFile(file *os.File, records []Employee) {
w := csv.NewWriter(file)
defer w.Flush()
var data [][]string
for _, record := range records {
row := []string{record.FullName, strconv.Itoa(record.Age), record.Role}
data = append(data, row)
}
w.WriteAll(data)
}

func main() {
records := []Employee{
{"Alex Yvonne", 30, "Executive Director"},
{"Jessica Wils", 45, "Chief Marketing Officer"},
{"John Doe", 32, "Public Relation Officer"},
}
fileName := "records.csv"
file := openOrCreateFile(fileName)
defer closeFile(file)
writeToFile(file, records)

}

Panic and Recover

Go generates a panic whenever there is a situation where the Go runtime is unable to figure out what should happen next e.g Memory errors, attempting to read past a slice.
Panics are for fatal situations and recover is used gracefully handle these situations.

Note
Recover is only useful when called inside deferred functions. If recover is called outside the deferred function, it will not stop a panicking sequence.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"fmt"
)

func recoverInvalidSlicing() {
if r := recover(); r != nil {
fmt.Println("Recovered", r)
}
}

func invalidSlice() {
defer recoverInvalidSlicing()
deck := []string{"Spade", "Club", "Heart", "Diamond"}
fmt.Println(deck[5])

}

func main() {
invalidSlice()
fmt.Println("program returned from deck panic")
fmt.Println("normal program resumed")

}

Output:

1
2
3
Recovered runtime error: index out of range [5] with length 4
program returned from deck panic
normal program resumed

Concurrency in Go

If you want a detailed explanation of Concurrency and parallelism, check out these notes from a Stackoverflow answer

We are going to do a quick brush on concurrency in Go:

  • goroutines
  • channels
  • select keyword
  • Waitgroups
  • Mutexes

The goroutine is the core concept in Go’s concurrency model, they are lightweight processes managed by the Go runtime.
A goroutine is launched by placing the go keyword before a function invocation.

Goroutines communicate using channels, like slices and maps, channels are a built-in type created using the make function

1
2
ch := make(chan int)
a := make(chan bool)

Reading and Writing to a Channel

Use the <- operator to interact with a channel.

1
2
val := <- result // reads a value from result and assigns it to val
result <- vals // write the value in vals to result

By default channels are unbuffered. Every write to an open, unbuffered channel causes the writing goroutine to pause until another goroutine reads from the same channel.

Likewise, a read from an open, unbuffered channel causes the reading goroutine to pause until another goroutine writes to the same channel. This means you cannot write to or read from an unbuffered channel without at least two concurrently running goroutines ( source : Learning Go by Jon Bodner)
If the scenarios defined above does not happen, then the program will panic at runtime with Deadlock.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
"fmt"
)

type CreditDetails struct {
Principal, Rate float64
Time int
}

func calcSimpleInterest(user CreditDetails, interest chan float64) {
result := (user.Principal * user.Rate * float64(user.Time)) / 100.0
interest <- result

}
func MonthlyEMI(user CreditDetails, emi chan float64) {
res := user.Principal + (0.001 * user.Principal)
emi <- res

}

func main() {

applicant := CreditDetails{10000.50, 0.4, 3}

interest := make(chan float64)
emi := make(chan float64)
go calcSimpleInterest(applicant, interest)
go MonthlyEMI(applicant, emi)
applicant_interest, applicant_emi := <-interest, <- emi
fmt.Println("Final output", applicant_interest, applicant_emi)
}

Output:

1
Final output 120.006 10010.5005

Closing a Channel

When you are done writing to a channel, you close it using the built-in close function:

1
close(ch)

Once a channel is closed, any attempts to write to the channel or close the channel again will cause the program to panic.

1
v, ok := <-ch

If ok is set to true, then the channel is open. If it is set to false, the channel is closed.
Closing a channel is only required if there is a goroutine waiting for the channel to close (such as one using a for-range loop to read from the
channel).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"fmt"
)

func multiplication(array []int, channel chan int) {

for _, a := range array {
channel <- a * 3
}
close(channel)
}

func main() {

ch := make(chan int)
numbers := []int{7, 9, 1, 2, 4, 5}
go multiplication(numbers, ch)
for {
v, ok := <-ch
if ok == false {
break
}
fmt.Println("Received ", v, ok)
}
}

Output:

1
2
3
4
5
6
Received  21 true
Received 27 true
Received 3 true
Received 6 true
Received 12 true
Received 15 true

Select

Go’s select lets you wait on multiple channel operations.
Select solves he problem of performing multiple concurrent operaton and which one do you allow first?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package main

import (
"fmt"
)

type CreditDetails struct {
Principal, Rate float64
Time int
}

func calcSimpleInterest(user CreditDetails, interest chan float64) {

result := (user.Principal * user.Rate * float64(user.Time)) / 100.0
interest <- result

}
func MonthlyEMI(user CreditDetails, emi chan float64) {

res := user.Principal + (0.001 * user.Principal)
emi <- res
}

func main() {

applicant := CreditDetails{10000.50, 0.4, 3}

interest := make(chan float64)
emi := make(chan float64)
go calcSimpleInterest(applicant, interest)
go MonthlyEMI(applicant, emi)

for i := 0; i < 2; i++ {
select {
case applicant_interest := <-interest:
fmt.Println("received Interest", applicant_interest)
case applicant_emi := <-emi:
fmt.Println("received Emi", applicant_emi)
}
}

}

Output:

1
2
3
received Interest 10010.5005
received Emi 120.006

Waitgroups

If you are waiting on several goroutines, you need to use a WaitGroup, which is found in the sync package in the standard library.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import (
"fmt"
"sync"
)

type CreditDetails struct {
Principal, Rate float64
Time int
}

func calcSimpleInterest(user CreditDetails, wg *sync.WaitGroup) {
defer wg.Done()

result := (user.Principal * user.Rate * float64(user.Time)) / 100.0
fmt.Println("Interest is ", result)

}
func MonthlyEMI(user CreditDetails, wg *sync.WaitGroup) {
defer wg.Done()

res := user.Principal + (0.001 * user.Principal)
fmt.Println("EMI is ", res)
}

func main() {

applicant := CreditDetails{10000.50, 0.4, 3}

wg := new(sync.WaitGroup)
wg.Add(2)
go calcSimpleInterest(applicant, wg)
go MonthlyEMI(applicant, wg)

wg.Wait()

}

Output:

1
2
Interest is  120.006
EMI is 10010.5005

Mutexes

Go allows us to run code concurrently using goroutines. However, concurrent processes accessing the same data can lead to race conditions.
When you want only one goroutine to access a variable at a time and avoid conflicts, we use the concept called mutual exclusion, and the conventional name for the data structure that provides it is a mutex (Source).
Golang provides mutual exclusion with sync.Mutex and its two methods:

  • Lock
  • Unlock
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package main

import (
"fmt"
"sync"
)

type BankAccount struct {
name string
balance float64
accountType string
}

var user = BankAccount{"Alex", 100.00, "Savings Account"}
var mu sync.Mutex

func MakeDeposit(wg *sync.WaitGroup, amount float64) {

mu.Lock()
user.balance += amount
mu.Unlock()
wg.Done()
}

func MakeWithdrawal(wg *sync.WaitGroup, amount float64) {

mu.Lock()
user.balance -= amount
mu.Unlock()
wg.Done()
}

func main() {

var wg sync.WaitGroup

wg.Add(3)

go MakeWithdrawal(&wg, 30.00)

go MakeDeposit(&wg, 10090.90)
go MakeDeposit(&wg, 1090.90)

wg.Wait()
fmt.Println(user.balance)

credits := []float64{100.00, 56.12, 89.45}

wg.Add(len(credits))

for i := 0; i < len(credits); i++ {
fmt.Println("Adding the value", credits[i])
go MakeDeposit(&wg, credits[i])

}
wg.Wait()
fmt.Println(user.balance)

}

Output:

1
2
3
4
5
6
11251.8
Adding the value 100
Adding the value 56.12
Adding the value 89.45
11497.37

Context

Context is a standard package of Golang that makes it easy to pass request-scoped values, cancelation signals, and deadlines across API boundaries to all the goroutines involved in handling a request (source)

I like this simple explanation from Parikshit Agnihotry

A way to think about context package in go is that it allows you to pass in a “context” to your program. Context like a timeout or deadline or a channel to indicate stop working and return. For instance, if you are doing a web request or running a system command, it is usually a good idea to have a timeout for production-grade systems. Because, if an API you depend on is running slow, you would not want to back up requests on your system, because, it may end up increasing the load and degrading the performance of all the requests you serve. Resulting in a cascading effect. This is where a timeout or deadline context can come in handy.

To derive a context, you can use the following functions :

  • WithCancel
  • WithTimeout
  • WithDeadline
  • WithValue
1
2
3
4
5
6
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}

Rules for using context

  • Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it.
  • The Context should be the first parameter, typically named ctx.
  • Incoming requests to a server should create a Context, and outgoing calls to servers should accept a Context.
  • Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use.
  • Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.

Source : Official documentation