Get started to GO

Ayush Singh
10 min readAug 21, 2022

--

Installation & Configuration :-

Introduction :-

  • There is a special Keyword in Go called ‘defer‘, which delays the execution of method or function until the nearby function returns.
  • The ‘Goroutines‘ is a special function in Go, which runs concurrently with other functions or methods.
  • Go has got very light weight memory, which saves # of instances and cost.
  • Docker, Kubernetes is written in golang.
  • Companies like Google, Dropbox, Uber, Twitch, etc have switched to go already
  • Go provides it’s own debugging tool support.

Basics of golang :

Variables :-

  • camelCase standard is used generally.

Syntax :-

var <variable_name> <data_type> = <value>

(or)

<variable_name> := <value> //shorthand

Data Types :-

  • bool
  • string
  • int, int8, int16, int32, int64
  • uint, uint8, uint16, uint32, uint64
  • uintptr //pointer value is stored as uintptr
  • byte //alias for uint8, It is equivalent to ‘char’ type in C
  • rune //alias for int32, it is used to represent unicode (eg :- tamil languages, hindi languages)
  • float32, float64
  • complex64, complex 128 // good support for encryptions

Note :- go uses zero value concept , i.e if we only declare a variable and don’t initialize them to a value, default values are inserted.

Type Conversion in go :-

  • Go is very strict about type conversion, it won’t do type conversion implicitly for you.

Code example :-

i, j := 10, 10.5
sum := i + j //int + float64 not allowed

Constants in go :-

  • Constants can be character, variable or numeric values
  • Constants cannot be declared using the := syntax

Code example :-

const Pi = 3.14

  • The value of constant should be known at compile time itself, therefore value returned by a function call should not be stored in constant.

Code example :-

const root = math.Sqrt(16) // not allowed

Note :- constants are untyped if the type is not specified.

If — else in go :-

Code syntax :-

if condition1 {  
...
} else if condition2 {
...
} else {
...
}

Note :- In go the syntax(i.e curly braces) is strict, as compiler will insert semicolon if it doesn't find anything immediately after }. (i.e) make sure to put else in the same line where if block ends

Optimization in if else :- to avoid unnecessary else branch checks, return keyword can be used in if else

Code example :-

num := 10;
if num%2 == 0 {
fmt.Println(num, "is even")
return //return keyword used
}
fmt.Println(num, "is odd")

Loops :-

  • Labels can be used to come out of the outer loop, when inner loop condition fails.

Syntax :-

1)   for initialisation; condition; post {  
}
2) for condition { // alternative to while loop
}
3) for { // infinite loop
}
4) for i,val := range arr { //for array based iteration
}

Switch case :-

Syntax:-

switch optstatement; optexpression{
case expression1: Statement..
case expression2: Statement..
...
default: Statement..
}
  • Both optstatement and optexpression in the expression switch are optional statements. If both are not present compiler assumes that the expression is true.
  • If a case can contain multiple values and these values are separated by comma(,).

Select :-

  • Select is basically meant for receiving channels.
  • So if there are multiple channels and we are not sure which channel receives data and when, we can pass it through select and perform specific actions.
  • So, too much long receive signals like os signals must be put in select to avoid crashing bcz of deadlock.

Eg :-

select {
case v := <-ch:
fmt.Println("received value: ", v)
return
default:
fmt.Println("no value received")
}

Functions all sort of :-

Syntax :-

func function_name(Parameter-list)(Return_type){
// function body.....
}
  • Go allows to return more than 1 values, to achieve this the return types must be specified in the parenthesis “,” separated

Receiver functions(methods) :-

  • When custom type is created, go can link some methods associated to those types

Syntax :-

func(type_name type) function_name(Parameter-list)(Return_type){
// function body.....
}
Note :- (1) "_" is used to receive a value that is not used anywhere in that scope.
(2) Methods with the same name can be defined on different types whereas functions with the same names are not allowed
  • When a function has a value argument, it will accept only a value argument.When a method has a value receiver, it will accept both pointer and value receivers.

Variadic parameters :-

Functions like append in slices are Variadic functions.

Syntax :-

func hello(a int, b ...int) {  
}

Calling hello fn :-
nums := []int{1,2,3,4}
hello(0,1,2,3,4)
hello(nums...) // The call happening in variadic fns is call by reference, when slices are passed with ellipses.
hello() // optional parameter, we didn't pass anything

Note:- Variadic parameters can be used as an optional parameter

Modules, Packages :-

  • Modules :- Hold all related packages together for a project.
  • Packages :- Help in solving a problem in component level(splitting in separate files)

In the root directory of module “mod.go” file has to be created, the file can be updated and created with `go mod init <module_dir_name>`

Note :- to export a variable/ function from one package to the other use Caps as the first letter in name (eg :- Var :=10 //Can be exported)

func init(){

}

  • This function is executed in the beginning when a package is exposed to the compiler, in the order it is introduced to the compiler.
  • Initial tasks like package variables initialization, database connectivity is done in init function.

Arrays & Slices :-

creating slice references using make :-

syntax : func make([]T, len, cap) []T //make basically helps in memory allocation

Note :- In go if you are giving comma related inputs (eg, array inputs) , “,”(comma) is expected even for the last element end.

Eg :-

arr := [3]int{
12,
13, 14, //comma is needed here }

Pointers :-

  • Local pointers can be returned from a function, go allocates memory for it in heap.
  • Go does not support pointer arithmetic. (i.e) if “ptr” is a pointer variable => ptr++ //this address updation using arithmetic is not possible

Struct :-

  • Structs are value types, user defined type.
  • Struct variables are comparable only if all the fields they contain are comparable.
  • Field names can be hidden(avoid export) to other packages if first character of name is not capital letter
  • Structs can be created anonymously also, even the fields within struct can be created anonymously

Eg of struct :-

type Address struct {  
city string
state string
}
type Person struct {
name string
age int
Address
}

String :-

  • Strings are immutable in go, to mutate and use do type conversion to slice and after mutating again do type conversion

Map :-

  • Zero value of a map is nil.

Eg :-

package main     func main() {  
var employeeSalary map[string]int
employeeSalary["steve"] = 12000 // will throw error as employeeSalary is nil, instead use "make" for memory allocation to an empty map
}
  • If the key is not present , the zero value of the val (key-val) is returned.
  • Check if a key is present or not :
value, ok := map_name[key]    //if ok== true, then key is present
  • Maps cant be comapared, == is used only if a map is nil or not
  • Maps are reference types
  • Delete a key-val in a map

delete(map_name, key) // if the key isn't present no runtime error comes

Interface :-

  • Interfaces help different types associate to a same functionality logic, saving repetition of code .
  • Interfaces are basically named collections of methods.

Eg :-

type Shape interface {
area() float64
}
  • Empty interfaces can be implemented by all type methods

Eg :-

func describe(i interface{}) {  
fmt.Printf("Type = %T, value = %v\n", i, i)
}
//In main method :-
s := "Hello World"
describe(s) //Type = string, value = Hello World
i := 55
describe(i). //Type = int, value = 55

How to check the type of the value in interface ??

Eg :-

func assert(i interface{}) {  v, ok := i.(int)        // if type of i is not int false goes in   ok variable, zero value of T goes in v  fmt.Println(v, ok)}
  • type switch can also be used to compare the concrete type of interface

Eg :-

func findType(i interface{}) {  switch i.(type) {  case string:    fmt.Printf("I am a string and my value is %s\n", i.(string))  case int:    fmt.Printf("I am an int and my value is %d\n", i.(int))  default:    fmt.Printf("Unknown type\n")  }
}
  • Embedded interface is also possible

Eg :-

type SalaryCalculator interface {  
DisplaySalary()
}
type LeaveCalculator interface {
CalculateLeavesLeft() int
}
type EmployeeOperations interface {
SalaryCalculator
LeaveCalculator
}
  • Zero value of interface is nil

Concurrency :-

  • Concurrency is the art of doing many tasks at the same time , but dealing with only 1 task at a time.
  • Go has something called as goroutines, which make concurrency possible.
  • goroutines are methods/functions that can run concurrently along with other methods.
  • goroutines are lighter than the actual thread, i.e a single os thread can spawn multiple goroutines.
  • goroutines communicate through channels.
  • Based on the use case, mutex & channels can be used to avoid race conditions.

Eg of mutex :-

mutex.Lock()  
x = x + 1
mutex.Unlock()

Understanding Go Scheduler :-

Go Scheduler runs at the top of kernel. go routines actually run on kernel threads and that is managed by the scheduler.

Goals of Scheduler :-

  1. Use a small no. of kernel threads as kernel threads are expensive to create.
  2. Support high concurrency, Go routines should be able to run lots of go routines.
  3. leverage parallelism. i.e , On N core machine, go programs should be able to run N goroutines in parallel

When does the scheduling happen ?

How Scheduler actually works :-

  • The goal is to reuse threads once spawned as spawning new threads is costly.So, the go routines doesn’t let a thread die after it has finished its job. The no. of CPU cores the machine has, that many threads are actually used from the kernel by the scheduler.
  • Go Scheduler assigns a runqueue to each thread, and the capacity of runqueue is again the no. of cores in the machine.
  • So now suppose a runqueue gets an entry of a goroutine(gmain) in Tmain, Tmain takes it and thats how the main function starts.
  • Now, a goroutine g1 is created and sits in the runqueue of Tmain, but the Tmain is already executing gmain, so it stays in runqueue,it can now use any parked thread, if not, then if another thread can be allocated(i.e No. of threads spawned till now is less than the no. of CPU cores in the machine) a new thread gets spawned and it should have its own runqueue right? Yes, that runqueue shares the load of the Tmain runqueue and thats how g1 executes in the T1 thread.
  • Now, if g1 gives the blocking system call. T1 thread also gets blocked. Now this thread runqueue has to be passed to a background thread runqueue, so that it can run. The option is to either use any parked threads or if required start a new thread.This mechanism is called “handoff”

What if we are not able to access these features, (i.e if a go routine is doing complex computations)?

  • For this go does preemption, it uses a background monitor thread called “sysmon” to detect long running goroutines and unschedules them when possible.
  • These preempted goroutines are put on global runqueues (less priority than other runqueues)

defer :-

We can break up defer ... into three parts.

  1. invoke defer, evaluating the value of function parameter.
  2. execute defer, pushing a function in to stack(the stack is a new stack frame ).
  3. execute functions in stack after return or panic.

Note :- if program crashes or gets a SIGKILL, defer will not execute

Understanding defer in depth (from go ~1.14) :-

  • Something special about go’s defer is that it can be used in conditional statements, no other language has this feature.
  • defer statements are stored in a unique stack slots, and in each defer the generated code(compiler converted form of the go code) updates the bitmask called “defer bits” indicating which defer was activated.
  • At every exit from the function, the compiler generates code to execute each active defer calls(based on defer bits) in reverse order.
  • This method of implementing differ is called “open-coded defers

Note:- If the function has more than 8 defer statements, the previous(<1.13) linkedlist based stack(i.e defer chain) is implemented

(fig) The left side is the go code, right side is the compiler generated code.

deferBits are stored at the same stack as that of the function(eg, in the above fig deferBits,tempF1,tempA are stored in the same stack as that of function G)

What about panic statements, we are not storing any defer records, so how recover is done ?

  • “funcdata” :- all the info about each defer, including stack slots(eg :-location of tempF1,tempA,deferBits & function pointer)
  • funcdata is generated for each function with open coded defers
  • for recovery, an extra stub calls runtime.deferreturn() and returns to the calling function
(fig) Architecture design of executing code stack, compiler converted assembly code and funcdata schema.

Algorithm for Panic processing in go(~1.14) :-

  1. Note :- Avoid using defers in loops as it goes to the heap not the stack

--

--

Ayush Singh
Ayush Singh

No responses yet