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.
func(s *Stack[T]) Push(newItem T) { s.values = append(s.values, newItem) } func(s *Stack[T]) Pop() (T, bool) { iflen(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 { returntrue } } returnfalse }
funcmain() { 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.
funcwriteToFile(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) }
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.
funcmain() { 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(chanint) a := make(chanbool)
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.
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).
for _, a := range array { channel <- a * 3 } close(channel) }
funcmain() {
ch := make(chanint) 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?
type CreditDetails struct { Principal, Rate float64 Time int }
funccalcSimpleInterest(user CreditDetails, wg *sync.WaitGroup) { defer wg.Done()
result := (user.Principal * user.Rate * float64(user.Time)) / 100.0 fmt.Println("Interest is ", result)
} funcMonthlyEMI(user CreditDetails, wg *sync.WaitGroup) { defer wg.Done()
res := user.Principal + (0.001 * user.Principal) fmt.Println("EMI is ", res) }
funcmain() {
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:
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)
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() <-chanstruct{} 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.