title:
style: nestedList # TOC style (nestedList|nestedOrderedList|inlineFirstLevel)
minLevel: 0 # Include headings from the specified level
maxLevel: 0 # Include headings up to the specified level
include:
exclude:
includeLinks: true # Make headings clickable
hideWhenEmpty: false # Hide TOC if no headings are found
debugInConsole: false # Print debug info in Obsidian console
A few points about GoLang 1
- Statically typed language (types can't be changed (not without type conversion))
- Strongly typed language (can't add int with a string)
- It's compiled
- Compilation is fast
- GoRoutines (builtin concurrency)
- Simple syntax
- Garbage Collector
Packages have a bunch of go files Modules have a bunch of packages
go mod init <global url : typically github (github.com/abc/abc)>go.mod has the name of the module, go version, and requirements
./cmd/tutorial/main.go
We'll have a package main line written in the top, signifying the package. Also, main is a special package name, that makes the compiler look for the entry point function (main function)
[!info] "fmt" package It's a standard library used for formatted input and output operations
import "fmt"
func main() {
fmt.Println("Hello World!")
}go build cmd/tutorial/main.go
./main
or
go run cmd/tutorial/main.gopackage main
import "fmt"
func main() {
var intNum int = 32767
// int, int8, int16, int32, int64, uint, ..., float32, float64
// string
// we can't perform any arithmetic operation between any 2 types, like int + float32 won't work. I'll have to do this
// floatNum32 + float32(intNum32)
var myString string = "Hello World" // single line
var myString2 string = `Hello
World`
fmt.Println(len("test")) // 4, but this isn't the number of chars, it's the number of bytes. For ascii, it's 1 byte per char only, but go uses utf8, so, it's slightly variable, for chars outside it
// for actual length,
// import "unicode/utf8"
// fmt.Println(utf8.RuneCountInString("Test"))
// rune type = character in utf8
var myRune rune = 'a'
fmt.Println(myRune) // 97
// bool -> true / false
}we don't need to initialize values btw. Go assigns default values too For int, int8, ..., uint, uint8,..., float32, ..., rune = 0 For string, it's an empty string, and for booleans, it's false
We can also infer the type,
var myString = "hello"
// we can also, remove the var keyword
myString := "hello"
// we can also do this
var var1, var2 int = 1, 2
var var3, var4 = 1, 2
var5, var6 := 1, 2
fmt.Println(var1,var2) // 1 2Instead of var, we can use const as well btw. Const = can't be changed. We have to initialize the value explicitly.
package main
import (
"errors"
"fmt"
)
func main() {
var printValue string = "Hello"
printMe(printValue)
var num int = 11
var den int = 2
var result = intDivsion(num, den)
fmt.Println(result)
var res, rem, err = intDivisionBoth(num, den)
if err != nil {
fmt.Printf(err.Error())
} else if rem == 0 {
fmt.Printf("The result of the int division is %v", res)
} else {
fmt.Printf("The result of the int division is %v with remainder %v", res, rem)
}
}
func printMe(printValue string) {
fmt.Println(printValue)
}
func intDivision(num int, den int) int {
var result int = num/den
return result
}
func intDivisionBoth(num int, den int) (int, int, error) {
var err error // defualt = nil
if den == 0 {
err = errors.New("Can't divide by zero")
return 0, 0, err
}
var result int = num/den
var rem = num%den
return result, rem, err
}package main
import "fmt"
func main() {
var intArr [3]int32
intArr[1] = 123
fmt.Println(intArr[0]) // 0
fmt.Println(intArr[1:3]) // [123 0]
// index 1 and 2
fmt.Println(&intArr[0])
fmt.Println(&intArr[1])
fmt.Println(&intArr[2])
}package main
import "fmt"
func main() {
var intArr [3]int32 = [3]int32{1,2,3}
fmt.Println(intArr)
}package main
import "fmt"
func main() {
intArr := [...]int32{1,2,3}
fmt.Println(intArr)
}They are wrappers around arrays. They are literally the golang equivalent of vectors in c++. They are dynamically sized arrays. However, a slice is a reference into an underlying array, while the std::vector object in c++ owns the underlying storage.
Slice = <pointer, length, capacity>. It's just a descriptor
func main() {
// by ommitting the length value in the array, we'll get a slice
var intSlice []int32 = []int32{4,5,6}
fmt.Println(intSlice) // [4 5 6]
fmt.Printf("length: %v, capacity: %v\n", len(intSlice), cap(intSlice))
// 3 3 : [4 5 6]
intSlice = append(intSlice, 7)
fmt.Println(intSlice) // [4 5 6 7]
// It checks if the array has enough space for more values.
// If not, a new array is created with enough space, and the data is copied
fmt.Printf("length: %v, capacity: %v", len(intSlice), cap(intSlice))
// 4 6 : [4 5 6 7 * *]
// we can also append more values
var intSlice2 []int32 = []int32{8, 9}
intSlice = append(intSlice, intSlice2...) // intSlice2... : spread operator
fmt.Println(intSlice)
// We can use the make function to also create a slice
// type, length, (and optionally, capacity)
var intSlice3 []int32 = make(int32[], 3, 8)
}func main() {
var myMap map[string]uint8 = make(map[string]uint8)
// key: string, value: uint8
var myMap2 = map[string]uint8{"Adam":23, "Sarah":45}
fmt.Println(myMap2["Adam"]) // 23
fmt.Println(myMap2["Jason"])
// doesn't exist : we'll get the default value = 0
var age, ok = myMap2["Adam"] // maps return an optional 2nd value (bool)
// ok = true : the value is in the map, and if it's false, the value isn't
delete(myMap2, "Adam") // if the key doesn't exist, it does nothing : no-op
}func main() {
var myMap2 = map[string]uint8{"Adam":23, "Sarah":45}
for name := range myMap2 {
fmt.Println(name)
}
for name, age := range myMap2 {
fmt.Println(name, age)
}
intArr := [...]int32{1,2,3}
for i, v := range intArr { // i = index, v = value
fmt.Println(i, v)
}
}// while loops are implemented via the for keyword
i := 0
for i<10 {
fmt.Println(i)
i = i + 1
}
// or
i := 0
for {
if i >= 10 {
break
}
fmt.Println(i)
i = i + 1
}
// or use a regular for loop
for i:=0; i<10; i++ {
fmt.Println(i)
}If we aren't using plain ascii characters, using a regular string will end up being complicated, as the length will also change. Hence, it's better to use an array of runes, instead of strings in that case. However, runes are just aliases for int32. So, it does end up consuming more memory.
[!info] Strings are immutable in GoLang
[!info] Printing %T This will print the type of the value
func main() {
// var myString = "resume"
var myString = []rune("résumé")
var indexed = myString[1]
fmt.Printf("%v, %T\n", indexed, indexed) // 233, int32
for i, v := range myString {
fmt.Println(i, v)
}
var myRune = 'a'
var strSlice = []string{"he", "ll", "o"}
var catStr = ""
for i := range strSlice {
// range gives two elements : index, and value
// here we are just using one, so that's the index
catStr += strSlice[i]
}
fmt.Println(catStr)
// however, this is inefficient, as we're
// literally making a new string in every iteration. We should
// instead import strings.
var strBuilder strings.Builder
for i := range strSlice {
strBuilder.WriteString(strSlice[i])
}
var catStr2 = strBuilder.string()
fmt.Println(catStr2)
}Structs = your own type
package main
import "fmt"
type gasEngine struct {
mpg uint8
gallons uint8
ownerInfo owner
// we can also just write "owner" instead of "ownerInfo owner"
// this will add the subfields directly. So, we'll be able to access
// name via, gasEngine.name, instead of gasEngine.ownerInfo.name
// we can also just write "int" here, and that will create a subfield named,
// "int", with the type int.
// however, if a collision is about to occur, like if name was already a
// field, we wouldn't be able to do this.
}
type owner struct {
name string
}
/*
We can also create methods, that are functions directly tied to structs, and can access its values. (e gasEngine) is the part that makes it different from a regular function. Basically the functions within a class
*/
func (e gasEngine) milesLeft() uint8 {
return e.gallons*e.mpg
}
func canMakeIt(e gasEngine, miles uint8) {
if miles <= e.milesLeft() {
fmt.Println("Yes")
} else {
fmt.Println("No")
}
}
func main() {
var myEngine gasEngine = gasEngine{mpg:25, gallons:15, ownerInfo:owner{"A"}}
var myEngine gasEngine2 = gasEngine{25, 15, owner{"A"}}
gasEngine2.mpg = 20
fmt.Println(myEngine.mpg, myEngine.gallons, myEngine.ownerInfo.name)
fmt.Printf("Miles left: %v", myEngine.milesLeft())
var anonymousEngine = struct {
mpg uint8
gallons uint8
}{25,25}
}Now, we'll also add an electric engine, and use an interface
package main
import "fmt"
type gasEngine struct {
mpg uint8
gallons uint8
ownerInfo owner
// we can also just write "owner" instead of "ownerInfo owner"
// this will add the subfields directly. So, we'll be able to access
// name via, gasEngine.name, instead of gasEngine.ownerInfo.name
// we can also just write "int" here, and that will create a subfield named,
// "int", with the type int.
// however, if a collision is about to occur, like if name was already a
// field, we wouldn't be able to do this.
}
type owner struct {
name string
}
type electricEngine struct {
mpkwh uint8
kwh uint8
}
/*
We can also create methods, that are functions directly tied to structs, and can access its values. (e gasEngine) is the part that makes it different from a regular function. Basically the functions within a class
*/
func (e gasEngine) milesLeft() uint8 {
return e.gallons*e.mpg
}
func (e electricEngine) milesLeft() uint8 {
return e.kwh*e.mpkwh
}
type engine interface {
milesLeft() uint8
}
/*
Now, this function can take any engine, as long as it has the milesLeft() method in it. Since both gasEngine, and electricEngine have it, it will accept both
*/
func canMakeIt(e engine, miles uint8) {
if miles <= e.milesLeft() {
fmt.Println("Yes")
} else {
fmt.Println("No")
}
}
func main() {
var myEngine gasEngine = gasEngine{mpg:25, gallons:15, ownerInfo:owner{"A"}}
canMakeIt(myEngine, 50)
var myEngine2 electricEngine = electricEngine{25,15,owner{"A"}}
canMakeIt(myEngine2, 50)
}package main
import "fmt"
func main() {
var p *int32 = new(int32)
var i int32
}A lot of the pointer stuff is just like in C. new() is just like malloc.
[!info] Remember about Slices Slices are basically just the pointers to the array. So, if u try to make a copy of a slice by,
var sliceCopy = slice, both sliceCopy and slice will point to the same memory location
[!info] %p prints the pointer address
package main
import "fmt"
func main() {
var thing1 = [5]float64{1,2,3,4,5}
fmt.Printf("%p", &thing1)
var result = square(&thing1)
fmt.Printf("The result is %v\n", result)
}
func square(thing2 *[5]float64) [5]float64 {
fmt.Printf("%p", &thing2)
for i := range thing2 {
thing2[i] = thing2[i]*thing2[i]
}
return thing2
}[!info] Imp thing about Concurrency Concurrency != Parallelism
Here we use wait groups, (from the sync package). These work just like semaphores, but instead of decrementing the counter, it increases the count when some resource gets added to it. We also have to use the go call to make it run concurrently. There's also the Wait() function, that will wait till the values don't become 0. We can't avoid using the wait groups, as without them, the cpu just adds the concurrency, but doesn't actually wait for the tasks to finish.
Without Wait Groups
package main
import (
"fmt"
"math/rand"
"time"
)
var dbData = []string{"id1","id2","id3","id4","id5"}
func main() {
t0 := time.Now()
for i:=0; i<len(dbData); i++ {
go dbCall(i)
}
fmt.Println(time.Since(t0))
}
func dbCall(i int) {
var delay float32 = rand.Float32()*2000
time.Sleep(time.Duration(delay)*time.Millisecond)
fmt.Println(dbData[i])
}If we run this code, nothing will happen. Our program spawned these tasks in the background, didn't wait for them to finish, and then exited the program. Hence, we need to use wait groups
package main
import (
"fmt"
"math/rand"
"time"
"sync"
)
var wg = sync.WaitGroup{}
var dbData = []string{"id1","id2","id3","id4","id5"}
func main() {
t0 := time.Now()
for i:=0; i<len(dbData); i++ {
wg.Add(1) // increments 1 to the counter
go dbCall(i)
}
wg.Wait() // waits for the counter to go back to 0
fmt.Println(time.Since(t0))
}
func dbCall(i int) {
var delay float32 = rand.Float32()*2000
time.Sleep(time.Duration(delay)*time.Millisecond)
fmt.Println(dbData[i])
wg.Done() // decrements 1 from the counter
}Ight, now that we've implemented wait groups, lets move to another important OS concept, "Mutexes"
package main
import (
"fmt"
"math/rand"
"time"
"sync"
)
var m = sync.Mutex{}
var wg = sync.WaitGroup{}
var dbData = []string{"id1","id2","id3","id4","id5"}
var results = []string{}
func main() {
t0 := time.Now()
for i:=0; i<len(dbData); i++ {
wg.Add(1) // increments 1 to the counter
go dbCall(i)
}
wg.Wait() // waits for the counter to go back to 0
fmt.Println(time.Since(t0))
}
func dbCall(i int) {
var delay float32 = rand.Float32()*2000
time.Sleep(time.Duration(delay)*time.Millisecond)
fmt.Println(dbData[i])
m.Lock()
results = append(results, db) // If used without locks, this can cause a race condition (OS concept)
m.Unlock()
wg.Done() // decrements 1 from the counter
}We also have read write mutexes in golang. sync.RWMutex{}
In this, we have the Lock and Unlock methods too. But along with them, we also have Read Lock, (RLock()), and Read Unlock (RUnlock())
Read locks just exist to check if there's a full lock (someone is writing to it) or not. We can have multiple reads at once, but, we can't read while writing. When Lock() is done, all read locks, and regular locks must be cleared, before it can run. However, when RLock() is done, only the regular locks need to be cleared.
package main
import (
"fmt"
"math/rand"
"time"
"sync"
)
var m = sync.Mutex{}
var wg = sync.WaitGroup{}
var dbData = []string{"id1","id2","id3","id4","id5"}
var results = []string{}
func main() {
t0 := time.Now()
for i:=0; i<len(dbData); i++ {
wg.Add(1) // increments 1 to the counter
go dbCall(i)
}
wg.Wait() // waits for the counter to go back to 0
fmt.Println(time.Since(t0))
}
func dbCall(i int) {
var delay float32 = rand.Float32()*2000
time.Sleep(time.Duration(delay)*time.Millisecond)
fmt.Println(dbData[i])
save(dbData[i])
log()
wg.Done() // decrements 1 from the counter
}
func save(result string) {
m.Lock()
results = append(results, result)
m.Unlock()
}
func log() {
m.RLock()
fmt.Println(results)
m.RUnlock()
}- Channels hold data
- They are thread safe. They avoid data races
- They listen for data, and we can block code execution when data is added or removed from a channel
package main
import "fmt"
func main() {
var c = make(chan int) // this channel can only hold a single int value
c <- 1 // write 1
var i = <- c // read from the channel
fmt.Println(i)
}This will cause a DEADLOCK !!!
As the channel waits for someone to read when it writes something in.
package main
import "fmt"
func main() {
var c = make(chan int)
go process(c)
fmt.Println(<-c)
}
func process(c chan int) {
c <- 123
}For Loops works perfectly with channels too
package main
import "fmt"
func main() {
var c = make(chan int)
go process(c)
for i := range c { // it will wait for something to enter the channel.
// also, it won't close, unless we use the close(c) command in the other func
fmt.Println(<-c)
}
}
func process(c chan int) {
// defer runs the statement just before the function ends
defer close(c) // close causes the channel to end, preventing a deadlock
for i:=0; i<5; i++ {
c <- i
}
}Buffered Channels
package main
import (
"fmt"
"math/rand"
"time"
)
var MAX_BURGER_PRICE float32 = 5
func main() {
var burgerChannel = make(chan string)
var websites = []string{"burgerking.com", "mcdonalds.com"}
for i:=range websites {
go checkBurgerPrices(website[i], burgerChannel)
}
sendMessage(burgerChannel)
}
func checkBurgerPrices(website string, burgerChannel chan string) {
for {
time.Sleep(time.Second*1)
var burgerPrice = rand.Float32()*20
if burgerPrice<=MAX_BURGER_PRICE {
burgerChannel <- website
break
}
}
}
func sendMessage(burgerChannel chan string) {
fmt.Println(<-burgerChannel)
}ight, im really confused. I'll have to understand channels properly later.
Topic left: generics