Golang notes from the course : https://www.coursera.org/learn/golang-getting-started/
Golang is NOT an object oriented. The structs in Golang are very simplified, it doesn't have Inheritance, constructors nor generics. Which make the golang code simple and quick to run. It compiles directly to machine code :
constants : const CONSTANT = "constant"
it is possible to declare and initialize a variable like this var a = "a". If we want to declare a variable and attribue a value to it later we need to specify the type of the variable. Golang is a statically typed language
The type casting is the convertion of a data type to another. Unlike Java that has an implicite type conversion, Golang requires an explicite type conversion. We do this like this
var simpleInt int64 = 13
var simpleFloat float64 = float64(simpleInt)
Which a pointer to an address to data in memory it has two operators :
- & operator : returns the address of a variable/function
-
- operator : returns the data at an address (dereferencing) image.png In this piece of code, we have : x as an integer who's value is 1, y is not initialized(gets 0 by default) and we declare a variable ip. The variable ip is a pointer to an integer.
- When we do
ip = &x
, we give ip the value of the address of x, so ip points now to x - When we do
y = *ip
, we give y the data of its address
this function creats a variable and returns a pointer to this variable instead of the data in it.
*ptr = 3
note: the type of space allocated to a variable depends on its type. For example int has a range of values
The scope of a variable is the places in the code where the variable can be accessed the varaible's scope is organized in go with "{}" they constitute a block We can declare package level variables that are accessible to the hole package. this means that all the files within the same package have access to these variables But it is always better to declare variables as local as possible. This is because of the memory deallocation and the readiblity of the code
To summarise we have three types of varible scopes :
- Local variables : Within functions or blocks
- Package variables : Shared between all the files of the same package
- Global variables : Exported variables used between packages. To exporte a variable we start by a capitale letter.
note: Strings are immutable
A package contains multiple files. For example a the package main contains many files. The file that has the main() function is the entrypoint. To run the go script we usually use the command go run main.go, we need to add all the files of the package in command. an alternative is to use
go run .
requires the sort package
sort.Ints(numbers)
it is important to add the "..."
slice = append(slice, slice2...)
image.png For example this function f(), can be run 1M times. And if the variable x doesn't get deallocated it will be created 1M times. In the traditionnal languages, memory is handeled like this : image.png A variable can be stored in two different spaces :
- Stack : affected to variables within functions. The memory is deallocated as soon as the function call ends
- Heap : Affected to other variables, it is persitant and doesn't end as soon as it becomes useless. It needs to be deallocated manually
The control flow is the order in which the statements are exeecuted. But it can change if the developer uses some specific statements such as : *if, for loop, switch case, *
Loops in golang are very simple. There is only one type of loops, for. When used alone like for {}, it is equivalent to a while(true) When used on a slice and array data structure it is used like this
for index, booking := range bookings {
Sometimes we don't need the index variable. And since Go generates an error over non used variables, we can ask Go to ignore it by replacing with '_'.
Similat to all the languages Except :
- it is initialized to 0 value initially (0 of the expected type)
- it has a fixed size but is not immutable (we can change its values)
to get the length of the array len(arrayName)
this is how we declare an array of size 5
here the "..." mean that we dont specify an exact size to the array
to iterate over an array in golang :
with i the index and v the value
It is a window of an array. It has a variable size up to the whole array.
It is declared like this :
var slice []int
Slices are windows on an underlying array
It is different from arrays because we don't specify the length between the brackets
It has three properties :
- Pointer thaat indifcates the start of the slice
- The length of the slice
- The capacity, the maximum number of elements in the slice. (length - beguining of the slice)
Very important
Useful functions
- make() that initialize arrays.
- append() that increases the size of the slice
Super important : In go lang the parameters are passed as call by value. So the parameters passed to the functions are deep copied to its arguments. this will return 2 :
This is great but this call by value solution has a drawback. Making a copy of each argment takes a certain amout of type. For complex structs it can be significative. To avoid this lapse of time we can use call by referance arguments. like this :
important: no need to do this for slices they already are pointers. outside of the main function, declared like this :
func greetUser(theaterName string, remainingTickets uint, theaterTicekets uint) {
fmt.Println("Welcome to ", theaterName, " !")
fmt.Printf("we have total of %v places and %v avaiable\n", theaterTicekets, remainingTickets)
}
to return a value we can use :
func printFirstNames(bookings []string, firstNames []string) []string{
for _, booking := range bookings {
var names = strings.Fields(booking)
firstNames = append(firstNames, names[0])
}
return firstNames
}
It is also possible to return multiple values :
func validateUser(firstName string, lastName string, email string, nbTickets int, remainingTickets uint) (bool, bool, bool){
isValidName := len(firstName) >= 2 && len(lastName) >= 2
isValidEmail := strings.Contains(email, "@")
isValidTicketNumber := nbTickets < int(remainingTickets) && nbTickets > 0
return isValidName, isValidEmail, isValidTicketNumber
}
- Understandability : If someOne is looking for a feature (for example the bluring) he can find it easly
- Give functions a good name
- Function do only one action
- Keep functions short
From functional programming and it means treating a function as a normal variable.
Key value pair just like in other languages. See here https://github.com/ShameGod/HashMaps
A map is the implementation of hashtables in GoLang. A map can be delcared by type like this :
var myMap map[string]string
or creating an empty map directly
myMap := make(map[string]string)
To iterate over a map :
for key,value := rang map{
fmt.Printf("key %v and value %v", key, value)
}
Groups related variables together (like name, address, email) and can have variables of multiple types We define structs on a package level like this :
type UserData struct {
firstName string
lastName string
email string
nbTickets int
}
To instantiate a Struct we do this :
var userData = UserData {
firstName: firstName,
lastName: lastName,
email: email,
nbTickets: nbTickets,
}
Or we can initialise the structure using default field values with the function new()
userData := new(UserData)
Go lang has a way of organising traded data between services. RFC : Request For Comment defines a protocol of data format Examples of RFCs are : Json, Avro, Xml... golang has packages to encode and decode data using these protocols.
Mashalling means converting golang struct to json object. we can do this like this :
barr, err := json.Marshal(userData)
barr for binary array will contain the binaries for a json. Err If something wrong happens if nothing happens it takes a Nil value.
We do an unmarshalling like this
var userData UserData
err := json.Unmarshal(barr, userData)
the Json needs to fit the userData, they need to have the same attributes.
by adding the (mi MyInt) in my function, I am saying that the function Double is only available for the MyInt type.
In the picture above there is an implicit parameter, mi. We do not add a parametre before the v.Double() but the real parameter is the " v ".
func (user UserData) sendEmail() {
time.Sleep(1 * time.Second)
fileName := fmt.Sprintf("bill of %v %v", user.firstName, user.lastName)
err := ioutil.WriteFile(fileName, []byte(user.email), 0777)
fmt.Print(err)
fmt.Println("#####################################")
fmt.Printf("Sending the email to the user %v %v", user.firstName, user.lastName)
fmt.Println("#####################################")
}
Many go function return an error as a second argument. It is necessary to handle them like this :
f, err := os.Open("file.txt")
if err != nil {
fmt.print(err)
return
}
It is possible to do encapsulation by exporting functions giving access to variables, by setting a capital letter.
A polymorphique function is a function that does two different operations according to the input argument. For example a function area() that calculates the area of a form. It is going to do different operations according to its input (a triangle, a square, a circle ...) This aspect that is very present in OOP requires inheritance and overriding. In order to get Polymorphisme we use interfaces
they have the same definition as the Java interfaces. Used to express conceptual similarity between types They define a list of methods. All the types that have ALL these methods are concidered of the type of the interface. If a define an interface called shape2D with two methods, area and perimeter. if the types triangle and circle implemente these two methods they are concidered of type shape2D
type shape2D interface {
Area() float64
Perimeter() float64
}
type Triangle{....}
func (t Triangle) Area() float 64 {....}
func (t Triangle) Perimeter() float 64 {....}
That is it, the class Triangle satisfies the shape2D interface. This is not declared explicitly golang just understands that you meant to set Triangle as a shape2D
An interface has two unknown thigs, its exact type and its value. For example shape2D has an unknown type and an unknown value. For example :
type shape2D interface {
Area() float64
Perimeter() float64
}
var s shaped2D
t := new(Triangle)
s = t
the interface s has now a type but No value. it is still possible to call the speak() function
- a function which takes multiple types of parameter
func IsTooBig(s Shape2D) bool{
if(s.Area()>100 && s.Perimeter() > 100) {
return true
}
return false
}
This function takes an interface as an argument and uses its functions.
- Empty interface is an interface that specifies no methods and that every type satisfies for example rare syntax :
func PrintMe(val interface{}){
fmt.PringIn(val)
}
Sometimes we need to know the exact type of an interface in a function not like the IsTooBig() function. Like I want to take any shape2D but I will call type specific functions
func DrawShape(s Shape2D){
rect, ok := s.(Rectangle)
if ok{
DrawRectangle()
}
tri, ok := s.(Triangle)
if ok{
DrawTriangle()
}
}
But there are eventually too much strcuts that satisfy the interface. We can use the switch method to make it easier
func DrawShape(s Shape2D){
swich := sch := s.(type){
case Rectangle :
DrawRectangle()
case Triangle :
DrawTriangle()
}
}
Many functions built in go packages, return an Error interface defined like this :
type error interface {
Error() string
}
The motivation of concurrency is the need of speed. Concurrency is here to over come the performance limitation. Moure's law says : Number of transistors doubles every 18months. But that's not the case anymore. We arrived to the limits of hardware optimisation (except for the case of quantum computers). In order to go faster with the same hardware we have two solutions :
- parallelism : Depends on the hardware, 4 cores => 4 parallel tasks. It is very difficult to program (conflicts, sharing resources between threads ..)
- Concurrency : management of many tasks in the same time. It enables parallelism. It is exactly like parallelism but without the drawbacks. So concurrency would have : synchronization and communication between tasks
ti charge a new thread to do something we do this
go concurrentFunction()
if the main thread finishes executing its lines of code it exits and the goroutine we started for the concurrentFunction never gets to finish being executed. to do that we do :
var wg = sync.WaitGroup{}
main {
..... code .....
wg.Add(1)
go concurrentFunction()
..... code .....
wg.Wait()
}
func concurrentFunction(){
..... logic code .....
wg.Done()
}
the difference between golang and other languages with concurrency, is that Go lang uses an abstraction of real OS threads called go routines. If the Computer has 4 cores it has 4 threads, but we can create thousands of gorroutines
a process : is an instance of a running program. Every process has its own memory, some code, registers(super fast memories), program counter (tell the process what's the next action he is going to execute )
Operating System : on of the main goal of an OS is to allow many processes to execute concurrently. To do that the OS switches quickly between process. For example linux allows a process to acces the CPU for an average of 20ms it switches so quickly that we feel like it is parallel
scheduling : it is the order in which the processes are run, there many scheduling algrithmes. The basic one is the round and robin. Other algorithms take priority in count for the scheduling. Scheduling is the main task in OS
context switch : it is the action of changing the process being executed. When we stop a process A to start a process B, we save its state(memory, code, register..) where we arrived that is called a process Context in the memory. The OS does the context switch.
Threads : Before we only had prosses and the problem with them is that swiching the context takes too much time (reading from memory). A process contains multiple threads. the threads share some of their context, which makes thread switching quicker than process switching. The yellow part is shared between threads and the green part is specific to the thread
Goroutines : we can have multiple goroutines in the same thread. We put many goroutines inside the main thread(when we only have one thread in a process). It is the Go Runtime Scheduler that switches between goroutines.
The scheduling of goroutines : We have a main thread (1 thread in the processor). So the OS schedules the main threads. Every main thread has a Logical Processor. The the GRS(Go Runtime Scheduler) runs on the Logical processor to schedules the Goroutines. In order to do parallelism + concurrency, we add more logical processors according to the number of Cores that we have. The programmer can change the number of Logical Processor (by default 1)
Goroutines are better than threads for the following reasons :
(source : geeks4geeks)
When something crashes in the app, we can refer to the control flow to know where it crashed exactly and the lines that were runned before that. But when we use concurrency it gets complicated. We can't predict the order of multiple concurrent operations. This problem is called interleaving. A program has to not depend on the order of concurency, otherwise it will be in race condition. The code below is a race condition
This happens because the tasks don't communicate. Goroutines communicate easier than threads.
There is a package called sync that provides functions to synchronize goroutines. sync.WaitGroup forces a goroutine to wait for other goroutines to finish. It is possible to wait on more than one goroutine.
func createFile(wg *sync.WaitGroup){
err := ioutil.WriteFile(fileName, []byte(user.email), 0777)
fmt.Print(err)
wg.Done()
}
func main(){
var wg Synch.WaitGroup
wg.Add(1)
go createFile(&wg)
wg.Wait()
fmt.Println("the file was created")
}
So far we have noticed that goroutines can only receive data in the beguining (passed as arguments). But thanks to channels, it is possible for goroutines to send and receive data during their execution. channels are typed.
func prod(a int, b int, c chan int){
c <- a * b
}
func main(){
c := make(chan int)
go prod(1,2,c)
go prod(4,5,c)
a := <- c
b := <- c
fmt.Println(a * b)
}
When channels are executing, a synchronization is done implicitly. when a goroutine sends somethink to a channel, it is blocked until an other goroutine receives data from that channel.
func prod(a int, b int, c chan int){
c <- a * b
//This goroutine will be blocked untill another goroutine receives the int sent into the channel
fmt.Printf("the result of the product is %v", a*b)
}
func main(){
c := make(chan int)
go prod(1,2,c)
a := <- c // the main goroutine will be blocked untill it receives some data
fmt.Println(a)
}
It it possible to make goroutines communicate without synchronization. By fixing a buffer size, the goroutine sending values to channels will continue to run. It will be blocked if the number of values it sent has reached the buffer's capacity. The capacity of the buffer is precised when calling the make() function
abort
for{
select{
case a <- c:
fmt.Print(a)
case <-abort:
return
}
}
default pattern
for{
select{
case a <- c1:
fmt.Print(a)
case b <- c2:
fmt.Print(b)
default:
fmt.Print("still waiting for a result")
}
}
Sharing a variable between two goroutines can cause problems if they interfere with each other. For example
i :=0
var wg sync.WaitGroup
func increase(){
i = i + 1
wg.Done()
}
func main{
wg.Add(2)
go increase()
go increase()
wg.Wait()
fmt.Println(i)
}
The result should normally be equal to 2. Because even with the interleaving of the go instructions the result is always 2. plot twist: it is not true The interleaving happens at the machine code level ! The instruction i = i + 1 is translated to 3 machine instructions:
- read i
- increment i
- write i example of interleaving the second task had the old version of i, so i end up with the total value of 1.
In order stop multiple goroutines from writing to a shared variable in the same time, we use mutual exclusion
example of locks
var mut sync.Mutex
func increase(){
mut.Lock()
i = i + 1
mut.Unlock()
}
Since we don't know which goroutine is goin to execute first, where do we place the initialization ? The initialization have to be done once and in the beginning. it is recommanded to initialize before starting goroutines, but some time we don't have the choice.
We use sync.Once to do initialization or to do a function only once (send a heartbeat to a kafka controller)
var wg sync.WaitGroup
var on sync.Once
func main()
{
wg.Add(2)
go consumeMessage()
go consumeMessage()
}
func consumeMessage(){
on.Do(openConnectionWithKafka)
on.Do(sendHeartBeat)
sendAck()
processmessage()
}
This happens when the main goroutine finishes executing but the other goroutines didn't finish. It happened to me because I forgor to add the & to the wg variable of type WaitGroup
When a goroutine G1 waits for G2 and G2 is waiting for G1 can happen by waiting on channels.
I will be explain the development process of a booking app with Golang :
When we run a go lang application with many files, go looks for a file where it will start running (entrypoint) .
We recognize this file with the function func main()
Every go application can only have one main function, one entrypoint
Thanks to this function we can write print code like this : fmt.Printf("we have total of %v and we have %v avaiable", theaterTicekets, remainingTickets) the 'v' in %v stands for variable there are other ways to display values such as %s, with s for string .. another usefull tip is to use %T to get the Type of the variable passed as an argument
The fmt package handles input and output
to read the input of the user we use the function scan like this. fmt.Scan(&input) The scan function takes what the user types and puts in a place in memory. The Scan function then takes the address of this input and put it in the pointer of the variable entered as an argument. very important : the scan function can only take one word, as soon as the user uses a white space the scan stops it is recommanded to use another method to read the user input
br := bufio.NewReader(os.Stdin)
input,_,_ := br.ReadLine()
we can either write a variable like : var a = 4 and golang will imply that it is an int. or we can specify the type for more precision. For example var a int32 = 4
We can also declare the variable a like thuis : a := 4