Reference - Golang

Published on

GO env variables:

These are some go env variables

  • GOROOT
    • Location of GO SDK
export GOROOT=~/Downloads/go
export PATH=$PATH:$GOROOT/bin

  • GOPATH (for go version greater than 1.13) i.e using with GO modules

    • downloaded source code is stored in $GOPATH/pkg/mod
    • compliled binaries are stored in $GOPATH/bin
      • we can change this behaviour with $GOBIN
    • defaults to $HOME/go
  • GOBIN

    • this is where all compiled binaries will be stored
    • if we set this, we can run binaries without specifying full path

Run go env to list all go related environment variables

Go commands

  • go mod init github.com/RahilRehan/banco, Creates a go.mod file
  • go install, compiles and place binary in $GOBIN
  • go build, compiles and places binary in current directory
  • go run main.go, compiles and executes the binary
    • go run cmd/*.go, if main package contains multiple files

Go packages

  • go packages is way to organize code and also helps in code reusability
  • every go file starts with package package_name
    • ex: package main
  • all go files with same package name are placed in same directory

Dependency management in Go

  • collection of packages(nested pacakages may exist) forms a Go module

  • there are go.mod and go.sum files is present in modules root

    • go.mod contains modules import path and list of dependencies

      • if a direct dependency does not have go.mod internally, its dependencies are imported as indirect dependencies in projects go.mod file.
    • go.sum contains

      • list of checksum of all direct and indirect dependencies with versions.
      • this checksum is used to validate none of the dependencies got modified.
    • alias:

      • Import packages with different names: import config demo-app/repo/configuration
      • Blank import is used to just run init function of package, ex: import _ sql/pq
    • init function:

      • Used to initialize global variables of a package.
      • Executed when the package is initialized.
  • go get package_name - downloads a dependency to current project, and adds it to go.mod

  • go mod tidy - makes sure the current projects dependencies reflect go.mod file and vice versa

  • go mod download - downloads dependencies listed in go.mod

  • go mod vendor - bundles all dependencies in vendor folder, reduces downloading dependencies

  • import paths:

    • import pacakge within same module: import module_name/package_name
    • in go.mod file use replace package_name => new_package_name to replace a package.(useful)
  • semantic imports

    • semantic versionning: V.{major}.{minor}.{patch}
    • Go automatically picks up latest from minor and patch versions
    • For using particular major version in imports, append major version to end of dependency , ex: import "github.com/javascript/v8"
  • Notes

    • semi colon is not requried at the end of statements in go

Variables

  • Using var keyword(very explicit:
    • var variable_name type, ex: var name string.
    • All declared variables are assigned zero value of given type.
    • Multiple variable declaration: var one, two, three int64
    • Declaraion + Assignment: var one, two int64 = 1, 2
    • declaration block with multiple types
var (
	name string
	age int64
	vaccinated bool
)
  • Type inferred declaration and initialization:
    • var a = 5, var name = "Alice"
    • type of the variable is automatically inferred from right hand side
  • Short-hand declaration and initialization
    • a := 5, name := "rahil"
    • note that we can only use this type of declaration inside function
    • for reassigning we cannot use :=, = must be used
  • Any unused variables in code will cause compilation error
  • Local variables are one which are defined at function or block level
    • Scope and lifetime of these variables until these blocks are in use
  • Global variables are declared outside functions and blocks
    • they are declared at package level
    • If they start with capital alphabet, they are exported to other packages
    • If they start with small alphabet, they are not exported to other packages

Constants

  • Once declared, we cannot change the value
//single
const a string = "random"
const b = "inferred type"

//multiple
const (
	E = "Exported"
	n = 23
)

  • const values must be known at compile time, so we cannot declare const from a functions return value
  • const are also scoped, i.e if const name start with caps only then its exported out of package and, if declared inside function cannot be accessed in other functions.
  • four types of constants: int, string, character(int32), bool

Datatypes

  • Basic Types
    • Integers
      • ex: var num int64 = 189
      • Int64 (Signed) -> 64 bits => 8 bytes
      • Uint64 (Unsigned) -> 0 to 2^64 - 1 numbers can be stored
      • also, there are int8, int16, int32 and similarly for uint
      • unintptr, can hold pointer of any size
    • Float64 and Float32 for precision based operations
      • ex: var f float64 = 3.56
    • Complex64, Complex128
      • ex: var c complex64 = 9 + 3i
    • Byte
      • ex: var mychar byte := 'r'
      • same as uint8
      • value between 0 - 255
      • can represent ASCII values
    • Rune
      • ex: rPound := '£'
      • same as int32
      • used to represent Unicode points
        • Unicode points are superset of ASCII
        • Unicode points can assign a number to any character in existence
        • UTF-8 stores every Unicode Point either using 1, 2, 3 or 4 bytes
    • String
      • ex: var name string="Alice"
      • Slice of bytes, i.e array of bytes
      • string is usually represented in utf-8 format
      • therefore, according to the character type in string, the size varies between 1-4 bytes.
    • Boolean (true of false)
      • ex: var isGood bool = true

More types

  • Pointer
    • you can declare pointer with new keyword or with &
    • * is the dereferencing pointer, using * we can access and change value pointed by the pointer.
    • lot of different things can be done once we start coupling both * and &
    • default value of pointer is nil
a := new(int)
*a = 10
fmt.Printf("Address of int is %d and value is %d", a, *a)

b := 2
c := &b
*c = 100
fmt.Println("Address of b is %d and value of b is %d", c, *c)
  • Struct
    • is a collectin of data of different types
    • fields can be accessed and set using .
    • empty structs or partial structs can be created
    • while creating new variables from struct, you need not provide field names
    • , is required for seperation of fields while creating struct variables
    • It is possible create pointers to struct variables using & or new keyword
    • we can directly change field values using pointer
    • two ways to print structs, using fmt package or using encoding/json package.
    • struct tags: used to add meta data to struct, can be used while encoding/decoding into different forms
    • Nested structs are possible, mainStruct.nestedStruct.fieldName to access nested structs field
    • Only struct which starts with caps are exported, same for struct fields.
    • Struct equality:
      • struct will be only equal if fields and values are same
      • also, fields needs to compatible for equality to work
      • slice, map and func are not supported/compatible to check equality
    • new copy of struct is created when
      • struct variable assigned to other variable
      • struct variable passed into a function
      • to save memory, use pointer whenever possible
type employee struct {
    name   string
    age    int
    salary int
}

//empty struct variable
a := employee{}

//non empty struct
b := employee{name: "noname", age:90, salary:0}

//without field names
c := employee{"noname", 90, 0}

//access field
fmt.Println("name is %s", b.name)

//set field
b.name = "myname"

sp := &b
(*sp).name = "anothername"
sp.name = "anothername" //also works
  • Array
    • 0 index based arrays
    • Arrays are values in go, that means there is no concept of pointer pointing to first element of array
    • we can iterate array using normal for loop or for range loop.
    • Size is fixed in arrays
arr := [size]{type}{a1, a2... an}
arr2 := [...]int{2, 3} //length is equal to no of elements
arr3 := [4]int{1,2} //rest of values are 0's i.e zero value of type

//access
arr2[0]
//assign
arr[1] = 3

//multiple element access possible, returns a copy in range
arr[index:]
arr[:index]
arr[:] //copy of entire array
  • Slice
    • Unlike array, slice is like a struct which points to an underlying array.
    • Length of slice is no.of elements in slice.
    • Capacity of slice is the max capacity that internal array can hold.
    • make can also be used to create slice
    • new can also be used
//creation
s := []int{1, 2}
fmt.Println(len(s), cap(s))

//creating slice from other slice or array
arr := [4]int{1, 2, 3, 4}
sl := arr[1:3] //slice from array, len=2, cap=3
sll := sl[:] //slice from another slice

numbers := make([]int, 3, 5) //create using make, the later params are length and capacity

multiDim := [][]int{{1, 2, 3}, {3, 4, 5}}
- to append elements to slice: `func append(slice []Type, elems ...Type) []Type` 
- internal size of will increase as slice length exceeds capacity
- unlike structs, for slices copies are not created on assigning slice to other variable. i.e they are referenced
- use `copy` function to perform copy of slice `func copy(dst, src []Type) int`
- default zero value of slice is `nil`
  • Map
    • dictionary / hashmap in go
    • search, insert, retrieve, delete in O(1)
    • Apart from Slice, Map, Function other types can be used as keys in map
    • Any type can be used as values in map
    • declare a map using map[key_type]value_type
    • using make => make(map[string]int)
    • zero value of map is nil
    • map is reference type, so assigning to other variable refers to same map
    • maps are not safe for concurrent use, use locks for concurrent access.
// access
m[key]

//assign/create/modify
m[key] = value

//delete
delete(map, key)

//check if key exists
val, ok := m[key]

len(m) //to get length of map

for k, v in m{ //iterate over map
	fmt.Println(k, v)
}
  • IOTA
    • similar to enums
//without iota
const (
    a = 0
    b = 1
    c = 2
)

//with iota
const (
    a = iota
    b
    c
)

//skip value in iota
const (
	a = iota
	_
	b
	c
)

//iota operations
const (
	a = iota
	b = iota + 4
	c = iota * 4
)

Functions

// signature
func name_of_function(param1 int, param2 string) (int, int){
	a, b := 1, 2
	return a, b
}

// if parameters are of same type, we can club them into one
func name_of_function(param1, param2 string) (int, int){
	a, b := 1, 2
	return a, b
}

//named returns, useful when function returns error
func returnAandB()(a, b int){
	a, b = 1, 2
	return
}
  • Functions are also type in Go and, Higher order functions are supported. So, we can..

    • return functions from other functions.
    • pass functions as parameter to other functions.
    • assign function to a variable(anonymous functions)
    • create function types(declare a type with function signature)
  • Go also supports function closures!

    • Consider, function b which lives inside another function a.
    • From below example, the inner function b can access the counter variable even though it got returned.
    • And the copy of counter vairable is unique to each function returned from a
    • This is called closure
func a() (b func() int){
	counter := 0
	b = func () int{
		counter += 1
		return counter
	} 
	return
}

func main() {
	b := a()
	fmt.Println(b())
	fmt.Println(b())
	
	c := a()
	fmt.Println(c())
	fmt.Println(c())
}

//output
1
2
1
2
  • Immediate invocation is possible, i.e add () at the end of function definition to invoke it.
  • Variadic Parameters
    • func add(numbers ...int)
      • pass any number of int params to add function
      • numbers is an array of ints
      • add(1, 2, 3, 3, 4, 5, 6), this is how we can call it, and params can vary in length

Methods

  • Attaching functions to types
  • func (r receiver_type) func_name(args) returns
  • This will let us emulate object oriented behavior, where types can have functionality attached to them
  • Very similar to calling methods on objects
  • Using pointer receivers will decrease memory usage and we will also be able to change values from method.
    • i.e we can emulate setters
  • Reciever types must be defined in the same package
  • If we have nested struct, we can directly call method attached to nested struct
  • If method names begin with caps then they are exported out of package, but then type also must be public.

Loops

  • there is no while loop in go
  • there are only two types of loops,
    • for loop
    • for range loop
      • can be used to iterate over array, slice, string, map and channels
      • using range keywords return two values => index and value
      • we can ignore second return by using a _ or just using one variable on left hand side
  • break and continue statements work just as in any other programming language
  • labels and goto are also present in golang, to go to particular label from any point in program
// for initialization; condition; post_statement{}
for i:=0; i < 5; i++ {
	fmt.Prinlnt("yello!")
}

// emulate while loop
i := 0
for i<5{
	fmt.Prinlnt("yello!")
	i++
}

// for range loop on array
arr := []int{1, 2, 3, 4} //array
for index, value := range arr{
	fmt.Println(index, value)
}

// on string: we should use range loops on strings as UTF-8 encoded
sample := "a£b"
for idx, letter := range sample {
    fmt.Println(idx, letter)
}

// on map
func main() {
	m := map[string]string{
		"name" : "john" ,
		"vehicle" : "car",
	}
	for key, value := range m{
		fmt.Println(key, value)
		fmt.Println(m[key])
 	}	
}

// on channels, example taken from "golangbyexample.com"
func main() {
    ch := make(chan string)
    go pushToChannel(ch)
    for val := range ch {
        fmt.Println(val)
    }
}
func pushToChannel(ch chan<- string) {
    ch <- "a"
    ch <- "b"
    ch <- "c"
    close(ch)
}

Conditional statements

  • if/else statements
if condition {
   //Do something
} else if condition {
   //Do something
} else {
   //Do something
}
- there are no shorthand if/else in go
- there are no ternary statements in go
- if with initialization statement, you can declare variables in if statement and those variables are scoped to that particular if block. You can even call function in initialization statements.
if a:= 6; a < 10{
	fmt.Println("less than 10")
}

  • switch statements
    • break statements are not required
    • fallthrough to also check more cases after a case match
switch statement(pre); expression {
	case expression1, expresstion2:
	     //Dosomething
	     fallthrough
	case expression2:
	     //Dosomething
	default:
	     //Dosomething
}

defer

  • helps in deferring some activity
  • used for cleanup activities
  • example if you open a file, you must close it. But we often forget it!
  • in Go, you can defer the required things before main logic.
  • if we have multiple defers statements, they will run in reverse order at the end of function.
  • inline defers, anonymous functions which will run at end of function
  • defer itself is implemented internally with stack
  • defer function are executed even if the program panics
func write() error {
	file, err := os.Open("temp.txt")
	if err != nil {
	    return err
	}
	defer file.Close() // this will execute at the end of function, executed second(i.e after below defer)
	defer func(){fmt.Println("closing file..")} // first defer to be executed

	n, err := file.WriteString("text.....")
	if err != nil {
	    return err
	}
	fmt.Printf("Number of bytes written: %d", n)
	return nil
}

Interfaces

  • Interface is a type in Go which is a collection of method signatures
  • With this you can achieve duck typing in Go. i.e we check if some attributes and methods are present.
  • The main reason why we must use interfaces is because we can mock them and hence testing becomes easier.
  • A type imlicitly implements interface if it implements all methods in interface.
type Animal interface {
    walk()
    eat()
}

type dog struct {
    age int
}

func (d dog) eat() {
    fmt.Println("dog eats")
}

func (d dog) walk() {
    fmt.Println("Dog walks")
}

func (l lion) eat() {
	fmt.Println("Lion eats")
}

func (l lion) walk() {
	fmt.Println("Lion walk")
}

func callEat(a Animal){
	a.eat()
}

func main() {
    var a Animal
    fmt.Println(a) //nil interface => zero value
    a = dog{age: 10}
    a.eat()
    a.walk()
    callEat(a)

    a = lion{age:24}
    callEat(a)

    l, ok := a.(lion) //determine type of interface
}

  • In the above example, dog imlements the Animal interface. We can have other types as well which may implment Animal interface.
  • We can assign any variable types which implement an interface to interface variable.
  • We can also pass any variable types which implement an interface to function accepting that interface.
  • Interfaces help in writing more modular and decoupled code. Functions which accept interfaces becomes very generic to use.
  • pointer reciever vs value reciever
    • if we use pointer as reciever then both variable and pointer to the variable of that type can be used while assigning to interface or while passing to a function which accepts an argument as that interface.
    • if we use pointer as reciever then the only pointer to the variable of that type can be used while assigning to that interface or while passing to a function that accepts an argument as that interface.
  • A type can implement multiple interfaces.
  • Zero value of interface is nil.
  • Nested interfaces / embedded interfaces are possible. Then type must implement all methods from main interface and nested interface.
//embedded interface
type ReadWriter interface {
    Reader
    Writer
}

  • It is also possible to have interface in struct. Whenever we create instance of that struct we must pass an object to interface field which implements all methods of that interface.
  • An empty interface has no methods , hence by default all types implement the empty interface.

Goroutines

  • lightweight threads with own execution context which can run concurrently with other goroutines. Concurency is achieved using goroutines in Go.
  • Running goroutine is simple as just adding go keyword before invoking any function
  • Goroutine functions are run asynchronously, so the next statements execute immediately and does not wait for goroutine to complete execution.
  • Main function - is the main goroutine, which spawns other goroutines.
    • when main goroutine exits, program also exits and other goroutines may not even get executed.
  • Read more abour run queues in go here
//example
func threadSpawner(id int) {
    fmt.Println(id)
}

func main() {
    fmt.Println("Start")
    for i := 0; i < 10; i++ {
        go threadSpawner(i)
    }
    time.Sleep(time.Second * 2)
    fmt.Println("Finish")
}

// anonymous goroutines
go func(){
   //body
}(args..)

Channels

  • Channels provide communications and synchrounization between goroutines.
  • Channels internally manage locks as well.
  • They act like pipes, you can perform only two actions. Send to channel, Recieve from channels.
func main() {
    var ch chan int
    ch = make(chan int)

    //send into channel
    ch <- 5

    //recieve from channel
    val := <- ch
}