Monday, 12 April 2021

Channels in Go

 This is a study note for YouTube tutorial: https://www.youtube.com/watch?v=YS4e4q9oBaU&t=20036s


Channel basics

You can only create channels using make function.

ch := make(chan int) // chan int means create a integer channel

Below is an example how to use channel to transfer data between goroutines.

var wg = sync.WaitGroup{}

func main() {
ch := make(chan int)
wg.Add(2)
go func() {
i := <-ch
fmt.Println(i)
wg.Done()
}()
go func() {
ch <- 42
wg.Done()
}()
wg.Wait()
}

The result is:
42


Restricting data flow

Be noted that channel operation will block the goroutine when executing. If you have unequal receiver and sender, then the additional goroutines will be blocked and give runtime errors.

func main() {
ch := make(chan int)
wg.Add(3)
go func() {
i := <-ch
fmt.Println(i)
wg.Done()
}()
go func() {
ch <- 42
wg.Done()
}()
go func() {
ch <- 27
wg.Done()
}()
wg.Wait()
}

27 fatal error: all goroutines are asleep - deadlock!

You can use bidirectional communication using channel in goroutines.

func main() {
ch := make(chan int)
wg.Add(2)
go func() {
i := <-ch
fmt.Println(i)
ch <- 27
wg.Done()
}()
go func() {
ch <- 42
fmt.Println(<-ch)
wg.Done()
}()

wg.Wait()
}

42 27

Goroutines can automatically manage the directions of the communication. However in most cases, we would like to make channel in single directions, which means one goroutine is dedicated to be receiver and another be a sender. To do this, you can put channel as function parameter for goroutine to restrict the data flow. Because function parameter can be used to cast a bi channel to a single direction channel.

func main() {
ch := make(chan int)
wg.Add(2)
go func(ch <-chan int) { // Here actually cast bi channel to receiver channel
i := <-ch
fmt.Println(i)
wg.Done()
}(ch)
go func(ch chan<- int) {
ch <- 42
wg.Done()
}(ch)

wg.Wait()
}

42

Now the first goroutine is restricted to receive-only, and the second is restricted to send-only.

Buffered channels

You can add a buffer for a channel by simply specifying the second parameter of make function with buffer size.

var wg = sync.WaitGroup{}

func main() {
ch := make(chan int, 50)
wg.Add(2)
go func(ch <-chan int) {
i := <-ch
fmt.Println(i)
wg.Done()
}(ch)
go func(ch chan<- int) {
ch <- 42
ch <- 27
wg.Done()
}(ch)

wg.Wait()
}

27 will be buffered instead of throwing a "deadlock" error.



For...range loops with channels

The above code is not a correct way to handle buffered channels. "for...range" loops is preferred in this case.

var wg = sync.WaitGroup{}

func main() {
ch := make(chan int, 50)
wg.Add(2)
go func(ch <-chan int) {
for i := range ch {
fmt.Println(i)
}
wg.Done()
}(ch)
go func(ch chan<- int) {
ch <- 42
ch <- 27
wg.Done()
}(ch)

wg.Wait()
}

The result is:

42 27 fatal error: all goroutines are asleep - deadlock!

You can see there is "deadlock" error again, but this time it is not because of the sender goroutine, it is because of the for range loop in the receiver goroutine. Because the receiver goroutine keeps trying to receive data even there is no data sent anymore.

To solve this problem, you can use "close(ch)" function to close channel in the sender goroutine if there is no data to send anymore

var wg = sync.WaitGroup{}

func main() {
ch := make(chan int, 50)
wg.Add(2)
go func(ch <-chan int) {
for i := range ch {
fmt.Println(i)
}
wg.Done()
}(ch)
go func(ch chan<- int) {
ch <- 42
ch <- 27
close(ch)
wg.Done()
}(ch)

wg.Wait()
}

The result is:

42 27

In fact, channel has a second return value which you can use for checking if the channel is open or closed.

var wg = sync.WaitGroup{}

func main() {
ch := make(chan int, 50)
wg.Add(2)
go func(ch <-chan int) {
for {
if i, ok := <-ch; ok {
fmt.Println(i)
} else {
break
}
}
wg.Done()
}(ch)
go func(ch chan<- int) {
ch <- 42
ch <- 27
close(ch)
wg.Done()
}(ch)

wg.Wait()
}

The result is:

42 27


Select statements

You can use a select statement to manage the workflow in a goroutine. Select statement is similar to switch statement, but select statement is only used for communications. The case statement refers to the communications on different channels.

var logCh = make(chan string)
var doneCh = make(chan struct{})

func main() {
go logger()

logCh <- "test1"
logCh <- "test2"
time.Sleep(100 * time.Millisecond)
doneCh <- struct{}{} // empty struct used as a signal here
}

func logger() {
for {
select {
case log := <-logCh:
fmt.Println(log)
case <-doneCh:
break
}
}
}


References: https://www.youtube.com/watch?v=YS4e4q9oBaU&t=20036s

Goroutines

 This is a study note for YouTube tutorial: https://www.youtube.com/watch?v=YS4e4q9oBaU&t=20036s

Goroutines are green threads. Green threads are created and managed by applications instead of kernel like the native threads. One of the benefits in green thread is that it can be created very fast.

Creating goroutines

Use keyword "go" before the invocation of the function to turn normal functions into goroutines.
func main() {
go sayHello()

}

func sayHello() {
fmt.Println("Hello")
}
In many cases, we may need anonymous functions to run goroutines.
func main() {
var msg = "Hello"
go func() {
fmt.Println(msg)
}()
time.Sleep(100 * time.Millisecond)

}
The result is:
Hello

From above example, you can see that goroutines are able to use the variable in the outer space. This is handled by go runtime. However this is not recommended to use, because later when you change the variable, it could impact the behaviour in goroutine. Instead, you can function parameter to pass value, in this case the variables in upper block will not impact the goroutine, because the value is duplicated in goroutine.
func main() {
var msg = "Hello"
go func(msg string) {
fmt.Println(msg)
}(msg)
msg = "Goodbye"
time.Sleep(100 * time.Millisecond)

}
The result is:
Hello


Synchronization

You may notice the sleep function call in main function is not really good approach, because it really slows down the program. So we can use WaitGroup to solve this problem.

WaitGroups

var wg = sync.WaitGroup{}

func main() {
var msg = "Hello"
wg.Add(1)
go func(msg string) {
fmt.Println(msg)
wg.Done() // decrement the wait group by 1
}(msg)
msg = "Goodbye"
wg.Wait() // wait until wait group be 0

}

To illustrate further, below is slightly more complicated example that uses two goroutine.

var wg = sync.WaitGroup{}
var counter = 0

func main() {
for i := 0; i < 10; i++ {
wg.Add(2)
go sayHello()
go increment()
}
wg.Wait()

}

func sayHello() {
fmt.Printf("Hello #%v\n", counter)
wg.Done()
}

func increment() {
counter++
wg.Done()
}

The result is:
Hello #3 Hello #1 Hello #7 Hello #8 Hello #7 Hello #0 Hello #7 Hello #3 Hello #0 Hello #10

Mutexes

From above result, you see the number is not in sequence. In order to do that, you need to use mutex to lock the shared variables between goroutines.

var wg = sync.WaitGroup{}
var counter = 0
var m = sync.RWMutex{}

func main() {
for i := 0; i < 10; i++ {
wg.Add(2)
m.RLock()
go sayHello()
m.Lock()
go increment()
}
wg.Wait()

}

func sayHello() {
fmt.Printf("Hello #%v\n", counter)
m.RUnlock()
wg.Done()
}

func increment() {
counter++
m.Unlock()
wg.Done()
}

Actually this example has no sense, because it is totally remove the multi threading advantages.

Parallelism

You can use  "runtime.GOMAXPROCS(int)" function to set the number of low-level threads you want to use. If you set the parameter to be -1, the function return the thread number you set or return the number of cores by default if you didn't set any.

func main() {
fmt.Printf("Threads: %v\n", runtime.GOMAXPROCS(-1))
runtime.GOMAXPROCS(1)
fmt.Printf("Threads: %v\n", runtime.GOMAXPROCS(-1))

}

Threads: 8 Threads: 1

The number is better to use the number of cores, because if you set a big value, you could slow down the performance.

Best practices

  • Don't create goroutines in libraries
    • Let consumer control concurrency
  • When creating a goroutine, know how it will end
    • Avoids subtle memory leaks
  • Check for race conditions at compile time -  go run -race main.go 


References: https://www.youtube.com/watch?v=YS4e4q9oBaU&t=20036s

Sunday, 11 April 2021

Interfaces in Go

 This is a study note from YouTube tutorial: https://www.youtube.com/watch?v=YS4e4q9oBaU&t=12098s

Interface is a collection of function prototypes. You can define interface like below:

type Writer interface {
Write([]byte) (int, error)
}

Interfaces should be implemented as methods.

type ConsoleWriter struct{}

func (cw ConsoleWriter) Write(data []byte) (int, error) {
n, err := fmt.Println(string(data))
return n, err
}

Polymorphism

You can use interface to realise polymorphism like below:

func main() {
var w Writer = ConsoleWriter{} // Here can use different structs
//with different implementation
w.Write([]byte("Hello go!"))

}

Implicit implementation

Except polymorphism, you can use interface to do implicit implementation. What does implicit implementation means? Since you don't need to use "implements" keyword to implement an interface like JAVA and some other languages, interfaces decouple the definitions from their implementations. There are a lot of packages which utilise structs with fields and methods. So you can design your own interfaces which can utilise the methods in the structs.

Use any type to implement interfaces

You don't have to use struct to implement interfaces. In fact, you can use any type to implement interfaces.

func main() {
myInt := IntCounter(0)
var inc Incrementer = &myInt
for i := 0; i < 10; i++ {
fmt.Println(inc.Increment())
}
}

type Incrementer interface { // define interface
Increment() int
}

type IntCounter int // define int type

func (ic *IntCounter) Increment() int { // implement interface
*ic++ // Here is important, it increment the type itself which is int
return int(*ic)
}

Interface embedding

You can embed interface into another interface like structs. Below is a writer example code from the tutorial video.

func main() {
var wc WriterCloser = NewBufferredWriterCloser()
wc.Write([]byte("Hello Go developers, this is a test"))
wc.Close()
}

type Writer interface {
Write([]byte) (int, error)
}

type Closer interface {
Close() error
}

type WriterCloser interface {
Writer // Here two interfaces are
Closer // embedded in composer interface
}

type BufferedWriterCloser struct {
buffer *bytes.Buffer
}

func (bwc *BufferedWriterCloser) Write(data []byte) (int, error) {
n, err := bwc.buffer.Write(data)
if err != nil {
return 0, err
}
v := make([]byte, 8)
for bwc.buffer.Len() > 8 {
_, err := bwc.buffer.Read(v)
if err != nil {
return 0, err
}
_, err = fmt.Println(string(v))
if err != nil {
return 0, err
}

}
return n, nil
}

func (bwc *BufferedWriterCloser) Close() error {
for bwc.buffer.Len() > 0 {
data := bwc.buffer.Next(8)
_, err := fmt.Println(string(data))
if err != nil {
return err
}
}
return nil
}

func NewBufferredWriterCloser() *BufferedWriterCloser {
return &BufferedWriterCloser{
buffer: bytes.NewBuffer([]byte{}),
}
}

Interface type conversion

Interface variable can be converted to the value type (structs or other types) using dot oeprtation.

func main() {
var wc WriterCloser = NewBufferredWriterCloser()
wc.Write([]byte("Hello Go developers, this is a test"))
//wc.Close()

bwc := wc.(*BufferedWriterCloser) // Here wc is converted to
// to struct directly, in
// this way you can access
// the buffer field directly
fmt.Println(bwc.buffer)
}

You can check the if the conversion is successful or failed by checking the second return value from the conversion operation.

func main() {
r, ok := wc.(io.Reader)
if ok {
fmt.Println(r)
} else {
fmt.Println("Conversion failed")
}
}

The result is:
Conversion failed

Empty interface and type conversion

Anything in Go can be an empty interface. This is useful when you want to get a value and then later want to covert it onto others. Because empty interface has no method in it. So in order to use it, you have to do a type conversion.
func main() {
var myObj interface{} = NewBufferredWriterCloser()
if wc, ok := myObj.(io.WriteCloser); ok { // Here do convertion
wc.Write([]byte("Hello Go developers, this is a test"))
wc.Close()
}

r, ok := myObj.(io.Reader)
if ok {
fmt.Println(r)
} else {
fmt.Println("Conversion failed")
}
}

Type switch

You can use switch to check the type of a variable.
func main() {
var i interface{} = 0
switch i.(type) {
case int:
fmt.Println("i is an integer")
case string:
fmt.Println("i is a string")
default:
fmt.Println("I don't know what it is")

}
}





References: https://www.youtube.com/watch?v=YS4e4q9oBaU&t=12098s

Wednesday, 7 April 2021

Turn off the startup chime permanently using terminal

 The startup chime is the only reason I want to smash my Macbook Pro sometimes. And Apple didn't provide an easy way to turn this thing off. I could not find the setting below in Apple support although it mentioned that it should be there for MacOS Big Sur 11 or later. However I am using the latest MacOS Catalina 10, but I still cannot find it.

But I found a solution that works for me.

You can try the below command to turn it off.

 sudo nvram StartupMute=%01 

If you want to turn it on.

 sudo nvram StartupMute=%00 

For me, I prefer to keeping it mute forever.










Sunday, 4 April 2021

Special handling of functions in Go

This is a study note for YouTube tutorial: https://www.youtube.com/watch?v=YS4e4q9oBaU&t=12079s

This post only talks about some special parts of Go functions.

Function name

Like variables, functions starting with lower case will keep private to the package, however functions starting with upper case will be exposed to the outside of the package.

Function parameters

1, if the function has parameters with same type, you can simply put the type at the last parameter in the function. For example.
func main() {
greet("Hello", "Tiffany")
}

func greet(greeting, name string) {
println(greeting, name)
}

The output is:
Hello Tiffany

2, you can use variadic parameters in Go.

func main() {
sum(1, 2, 3, 4, 5)
}

func sum(values ...int) {
sum := 0
for _, v := range values {
sum += v
}
fmt.Println("The sum is ", sum)
}

The output is:
The sum is 15

You can use variadic parameter with other parameter, but the function can only have one variadic parameter and it must be at the end.

Function return

1, When define function, put the return type after the closing bracket ")".

func main() {
fmt.Println("The result is: ", sum(1, 2, 3, 4, 5))
}

func sum(values ...int) int {
sum := 0
for _, v := range values {
sum += v
}
return sum
}

2, You can return a pointer from a function as well. Unlike in C/C++, when you turn a pointer in call stack, you can get as undefined behaviour because the stack will be destroyed when the function returns. Thus, you cannot control what is in the pointer now. However in Go, when Go detect you are returning a pointer from stack, it will automatically promote this variable from stack to heap, so you don't need to be worry about the value in the pointer is destroyed.

func main() {
fmt.Println("The result is: ", *sum(1, 2, 3, 4, 5))
}

func sum(values ...int) *int {
sum := 0
for _, v := range values {
sum += v
}
return &sum
}

3, You can use multiple returns in Go functions. The most useful cases of multiple returns is to add an error message in a function to indicate if the function execution is successful.

func main() {
result, err := divide(5.0, 0.0)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(result)
}

func divide(a, b float64) (float64, error) {
if b == 0.0 {
return 0.0, fmt.Errorf("cannot divide by zero")
}
return a / b, nil
}

Function as a variable

You can use function as a value to assign a variable.
func main() {
f := func() {
fmt.Println("Hello world")
}
f()
}

You can specify the signature of the function like below.

func main() {

var divide func(float64, float64) (float64, error)
divide = func(f1, f2 float64) (float64, error) {
if f2 == 0.0 {
return 0.0, fmt.Errorf("cannot divide by zero")
}
return f1 / f2, nil
}
result, err := divide(5.0, 0.0)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(result)
}

Method

You can create a function executing in a known context (like struct, int... any type actually, but most of cases are used with structs).

func main() {
g := greeter{
greeting: "Hello",
name: "Tiffany",
}
g.greet()

}

type greeter struct {
greeting string
name string
}

func (g greeter) greet() { // (g greeter) make this function a method
fmt.Println(g.greeting, g.name)
}




Reference:  https://www.youtube.com/watch?v=YS4e4q9oBaU&t=12079s

Defer, panic and recover in Go

 This is a study note for YouTube: https://www.youtube.com/watch?v=YS4e4q9oBaU&t=12079s

Except conditional statement and looping, you can also use defer, panic and recover to manage control flow in Go.

Defer

You can use "defer" keyword to defer the execution of a function when the calling function exits but before it returns. For example:

func main() {
fmt.Println("Start!")
defer fmt.Println("Middle!")
fmt.Println("End!")
}
The result is:
Start! End! Middle!

Deferred functions are executed in LIFO (Last In First Out) order. For exampe:

func main() {
defer fmt.Println("Start!")
defer fmt.Println("Middle!")
defer fmt.Println("End!")
}

The result is:

End! Middle! Start!

Defer function is very useful, one example is that you can use refer function to do some connection closing, resource clean up tasks. For example, in below code, you can put a close function right after you establish the connection before you even start to use the connection. This is because the close function is deferred, it will only execute after the calling function exits.

func main() {
res, err := http.Get("http://www.google.com/robots.txt")
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
robots, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%ss\n", robots)
}

Another important thing to mention is that the deferred functions only take the variable value from the time is deferred, the change after the deferred function call will not impact the input value in the deferred function. For example:

func main() {
a := "start"
defer fmt.Println(a)
a = "end"
}

The result is:

start

You can see that the value is still "start" even though variable "a" was changed to "end" later.

Panic

Panic is kinda like Exception in other programming language like JAVA. When panic function is executed, it will log the the position where causes the panic and then exit.

func main() {

fmt.Println("start")
panic("something bad happened")
fmt.Println("end")
}

The result is:

(base)MyLaptop$ go run src/github.com/mingdos/firstapp/Main.go
start
panic: something bad happened
goroutine 1 [running]:
main.main()
       /Users/caimingda/go/src/github.com/mingdos/firstapp/Main.go:10 +0x95

exit status 2

When works with deferred function, The execution order is like below:

Normal execution -> (defer function calling) -> normal execution -> (panic happens) -> defer function execution -> panic function execution

func main() {

fmt.Println("start")
    defer fmt.Println("this was deferred")
panic("something bad happened")
fmt.Println("end")
}

The result is:

(base)MyLaptop$ go run src/github.com/mingdos/firstapp/Main.go
start
this was deferred
panic: something bad happened

goroutine 1 [running]:
main.main()
       /Users/caimingda/go/src/github.com/mingdos/firstapp/Main.go:11 +0x95

exit status 2



Recover

In defer function, you can call recover() function to check if the current program is panic. The recover() function return nil if the program is not panic, and it will return the error message when it is panic.
func main() {

fmt.Println("start")
defer func() {
if err := recover(); err != nil {
fmt.Println("Error: ", err)
}
}()
panic("something bad happened")
fmt.Println("end")
}

The result is:

start Error: something bad happened

The recover() function does more than this actually. It will recover the execution from where after the caller function who cause the panic inside and continue to execute the rest.

func main() {

fmt.Println("start")
panicer()
fmt.Println("end")
}

func panicer() {
fmt.Println("about to panic")
defer func() {
if err := recover(); err != nil {
fmt.Println("Error: ", err)
}
}()
panic("something bad happened")
fmt.Println("Done panic")
}

The result is:

start about to panic Error: something bad happened end

If the defer function don't know how to handle the panic, it actually can redo a panic when it detect a panic. In this way, the caller function will still have the panic.


Reference: https://www.youtube.com/watch?v=YS4e4q9oBaU&t=12079s

Friday, 2 April 2021

Special handing of conditional statements in Go

This is a study note of YouTube tutorial: https://www.youtube.com/watch?v=YS4e4q9oBaU&t=7285s

Here I will only discuss the special usage of conditional statements in Go compared to other traditional programming languages like Java, C/C++...

Initialiser syntax

Here I give an example directly from the tutorial.

func main() {
statePopulations := map[string]int{
"California": 39250017,
"Texas": 27862596,
"Florida": 20612439,
"Pennsylvania": 12802503,
"Illinois": 12801539,
"Ohio": 11614273,
}
if pop, ok := statePopulations["Florida"]; ok {
fmt.Println(pop)
}
}

The result is:

20612439

There are two parts in above if statement if pop, ok := statePopulations["Florida"]; ok. The first part before the semicolon is the initialiser where you can initialise some variables. The second part after the semicolon is the condition. Be aware that the variables defined in the initialiser part are only alive in the if block. If you try to use the variable ("pop" and "ok") after the if statement, you will get an undefined variable error.

switch statement

Unlike other programming languages where switch statement needs to use "break" to skip the following cases. In Go, by default it only executes the statements in the case block, then exit the switch statement (In fact, in GO you can use "break" as well to exit early where you need it to).

func main() {
switch 2 {
case 1:
fmt.Println("one")
case 2:
fmt.Println("two")
default:
fmt.Println("not one or two")
}
}

The result is:

two

You can also put multiple conditions in one case like below:

func main() {
switch 5 {
case 1, 5, 10:
fmt.Println("one, five or ten")
case 2, 4, 6:
fmt.Println("two, four or fix")
default:
fmt.Println("other number")
}
}

The result is:

one, five or ten

You can also use initialise in switch statement:

func main() {
switch i := 2 + 3; i {
case 1, 5, 10:
fmt.Println("one, five or ten")
case 2, 4, 6:
fmt.Println("two, four or fix")
default:
fmt.Println("other number")
}
}

The result is:

one, five or ten



Reference: https://www.youtube.com/watch?v=YS4e4q9oBaU&t=7285s

Structs in Go

 This is a study note from YouTube tutorial: https://www.youtube.com/watch?v=YS4e4q9oBaU&t=7285s

You can define a struct and initialise it using following example:

type Doctor struct {
Number int
ActorName string
Companions []string
}

func main() {
aDoctor := Doctor{
Number: 3,
ActorName: "Jon Pertwee",
Companions: []string{
"Liz Shaw",
"Jo Grant",
"Sarah Jane Smith",
},
}
fmt.Println(aDoctor)
fmt.Println(aDoctor.ActorName)
}

You can dot operator (".") to access the fields in a struct.

Anonymous struct 

You can create an anonymous struct for informal uses in Go.

func main() {
aDoctor := struct{ name string }{name: "John Pertwee"}
fmt.Println(aDoctor)
}


Struct is a value type

Unlike map is a reference type, struct is a value type. This means if you assign one struct to another, the struct will be made a copy and assign to the other. Any modification in one struct won't impact the others.

func main() {
aDoctor := struct{ name string }{name: "John Pertwee"}
anotherDoctor := aDoctor
anotherDoctor.name = "Tom Baker"
fmt.Println(aDoctor)
fmt.Println(anotherDoctor)
}

The result is:

{John Pertwee} {Tom Baker}

Same like array, if you want two struct refer to the same location, you can use pointer to do so.

func main() {
aDoctor := struct{ name string }{name: "John Pertwee"}
anotherDoctor := &aDoctor // Pass the address of aDoctor to anotherDoctor
anotherDoctor.name = "Tom Baker"
fmt.Println(aDoctor)
fmt.Println(anotherDoctor)
}

The result is:

{Tom Baker} &{Tom Baker}

Embedded type of struct

You can embed a struct in another struct by simply declaring the embedding struct in the other. For example:

type Animal struct {
Name string
Origin string
}

type Bird struct {
Animal
SpeedKPH float32
CanFly bool
}

func main() {
b := Bird{}
b.Name = "Emu"
b.Origin = "Australia"
b.SpeedKPH = 48
b.CanFly = false

fmt.Println(b)
}

The result is:

{{Emu Australia} 48 false}

You can also initialise the struct in a structured way, however you can still refer the fields in the embedded struct directly.

func main() {
b := Bird{
Animal: Animal{Name: "Emu", Origin: "Australia"},
SpeedKPH: 48,
CanFly: false,
}
fmt.Println(b.Name)
}

The result is:

Emu




Reference: https://www.youtube.com/watch?v=YS4e4q9oBaU&t=7285s

Maps in Go

 This is a study note from YouTube tutorial: https://www.youtube.com/watch?v=YS4e4q9oBaU&t=7285s

Maps can be created in Go using type map[string]int{}.
func main() {

statePopulations := map[string]int{
"California": 39250017,
"Texas": 27862596,
"Florida": 20612439,
"New York": 19745289,
"Pennsylvania": 12802503,
"Illinois": 12801539,
"Ohio": 11614373,
}
fmt.Println(statePopulations)
}

You can also use make function to create map:

func main() {
statePopulations := make(map[string]int) // you can use make(map[string]int, 100)
                                             // to specify length as well
statePopulations = map[string]int{
"California": 39250017,
"Texas": 27862596,
"Florida": 20612439,
"New York": 19745289,
"Pennsylvania": 12802503,
"Illinois": 12801539,
"Ohio": 11614373,
}
fmt.Println(statePopulations)
}

Like dictionary in C#, you can simply access a map value using key - statePopulations["Ohio"].

You can also add key-value pair in map like dictionary in c#:

statePopulations["Georgia"] = 10210371

You can use delete built-in function to delete a key in map:

delete(statePopulations, "Texas")

The first parameter is the map, and second parameter is the key you want to delete.

What if you access a key which does not exist in the map?

Will this give an error or exception? The answer is neither.

func main() {
statePopulations := map[string]int{
"California": 39250017,
"Texas": 27862596,
"Florida": 20612439,
"New York": 19745289,
"Pennsylvania": 12802503,
"Illinois": 12801539,
"Ohio": 11614373,
}
fmt.Println(statePopulations["Georgia"])
}

The result is:

0

From the example above, you can see the value is 0 when key does not exist. This will cause problem if you use the value directly because you don't know "0" means key does's exist or the population is 0. However there is a solution: you can use comma separated expression to get another boolean value from map. 

func main() {
statePopulations := map[string]int{
"California": 39250017,
"Texas": 27862596,
"Florida": 20612439,
"New York": 19745289,
"Pennsylvania": 12802503,
"Illinois": 12801539,
"Ohio": 11614373,
}

population, ok := statePopulations["Georgia"]
fmt.Println(population, ok)
}

The result is:

0 false

If you don't care about the value for now, you can simple use the write-only operator "_" to skip the value and get the true/false result only.

_, ok := statePopulations["Georgia"]
fmt.Println(ok)


Map is also a reference type, so if you have two map variable refer to the same memory, when one is modified, the other is modified as well.

func main() {
statePopulations := map[string]int{
"California": 39250017,
"Texas": 27862596,
"Florida": 20612439,
"New York": 19745289,
"Pennsylvania": 12802503,
"Illinois": 12801539,
"Ohio": 11614373,
}
a := statePopulations
delete(a, "Ohio")
fmt.Println(a)
fmt.Println(statePopulations)
}

In above example, you can see the result that, "Ohio" is removed from both "statePopulations" and "a" referencces.



Reference: https://www.youtube.com/watch?v=YS4e4q9oBaU&t=7285s

Difference between "docker stop" and "docker kill"

 To stop a running container, you can use either "docker stop" or "docker kill" command to do so. Although it seems doin...