First, concurrency safety

Golang actually provides a shared memory-based approach in addition to the CSP programming model. For example, there are.

  • sync.Mutex: Mutual exclusion lock
  • sync.WaitGroup: Wait group, wait for all the Goroutines in the group to finish before exiting
  • Atomic operations. For example atomic.AddUint64 (sync/atomic package), thread-safe, no locking required
  • Single instance objects (sync.Once package), which are only initialized once when accessed concurrently by multiple Goroutines

The following example implements a single instance with read/write locks + atomic operations.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
type singleton struct {}

var (
    instance    *singleton
    initialized uint32
    mu          sync.Mutex
)

func Instance() *singleton {
    if atomic.LoadUint32(&initialized) == 1 {
        return instance
    }

    mu.Lock()
    defer mu.Unlock()

    if instance == nil {
        defer atomic.StoreUint32(&initialized, 1)
        instance = &singleton{}
    }
    return instance
}

Second, arrays and slices

An array is a sequence of elements of a specific type of “fixed length” and can consist of 0 or more elements.

The length of an array is a component of the type of the array, so arrays of different lengths or types are of different types. Arrays of different lengths cannot be assigned directly because of their different types, so arrays are rarely used.

The trap of arrays: In function call parameters, arrays are value-passed and cannot return results by modifying the parameters of the array type.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import (
    "fmt"
)

func main() {
    x := [3]int{1, 2, 3}

    func(arr [3]int) {
        arr[0] = 7
        fmt.Println(arr)
    }(x)

    fmt.Println(x)
}

And slices are sequences that can be dynamically grown and contracted, and it has the following data structure.

1
2
3
4
5
type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

Data is a pointer type to the underlying array, Len is the length and Cap is the capacity, which will be automatically expanded when the capacity is not enough.

Slices can be created using make: make([]int, len, cap), or directly []int{1, 2, 3}.

Slicing has several pitfalls to be aware of.

  • Passing a slice into a function and modifying the value of a bit, the data of the external slice is also modified.
  • Slicing on top of an existing slice does not create a new underlying array, so the value of the new slice is needed to affect the value of the original slice Another problem, the original underlying array does not change and memory is occupied until there are no more variables referencing the array. If the original slice consists of a large number of elements, it is possible that although only a small section is used, the underlying array still occupies a large amount of space in memory and is not freed. You can use copy instead of re-slice.
  • If the new slice is append (still assigned to the new slice), the new slice will be copied and the value of the original slice will not be affected by modifying the new slice.

Third, about Channel

Channel has three states.

  1. nil, uninitialized, only declared, or manually assigned to nil
  2. active, the normal state, readable or writable
  3. closed, closed, don’t mistake the value of the channel as nil when the channel is closed

Points to note.

  1. Read a closed Channel, will read to zero value.
  2. Reading a nil Channel will block.
  3. write a closed Channel, it will Panic
  4. Writing a nil Channel will block.
  5. closing a nil or closed Channel will Panic.

Here are the other points.

Use for to read the Channel and exit automatically when the Channel is closed and there is no data (not closed will deadlock).

1
2
3
for x := range ch {
    fmt.Println(x)
}

Use the if _, ok statement to determine if the channel is closed, ok is true for read data, ok is false for no data and closed (if not closed, deadlock).

1
2
3
if v, ok := <- ch; ok {
    fmt.Println(v)
}

Use Select to process multiple Channels.

  • Only the first unblocking Channel found is processed.
  • When a channel is nil, the corresponding case is always blocking, regardless of read or write, which is a special case. In the normal case, a write operation to a nil channel will panic.
  • You can add a timeout or default action to Select.

Fourth, about defer and recover

When a function panic, it will stop executing subsequent normal statements, start executing defer, and return to the caller when it finishes.

Recover can be executed in the defer to capture the arguments that triggered the panic and return to the normal flow of execution.

The logic of a defer is

  1. The order of execution of multiple defers is “last-in-first-out”, and the defers are stacked sequentially.
  2. the execution logic of defer, return and return value is
    1. return is executed first, and return is responsible for writing the result to the return value.
    2. if there is a defer, execute the defer to start some finishing work (you can modify the return value)
    3. finally the function exits with the “current return value”.

The following is an example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
    "fmt"
)

func main() {
    fmt.Println("c return:", *(c())) // 打印结果为 c return: 2
}

func c() *int {
    var i int
    defer func() {
        i++
        fmt.Println("c defer2:", i) // 打印结果为 c defer: 2
    }()

    defer func() {
        i++
        fmt.Println("c defer1:", i) // 打印结果为 c defer: 1
    }()

    return &i
}

Look at one more example, this one returns 1, not 0.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import "fmt"

func main() {
    fmt.Println(test())
}

func test() (result int) {
    defer func() {
        result++
    }()

    return 0
}

For recover, it catches exceptions on grandfathered calls, which must be run in the defer function and are not valid for direct calls.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 无效
func main() {
    recover()
    panic(1)
}

// 无效
func main() {
    defer recover()
    panic(1)
}

// 无效
func main() {
    defer func() {
        func() { recover() }()
    }()
    panic(1)
}

// 有效
func main() {
    defer func() {
        recover()
    }()
    panic(1)
}

Fifth, exclusive CPU use causes other Goroutines to starve

Goroutine is collaborative preemption scheduling, and Goroutine itself does not actively give up CPUs.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 错误
func main() {
    runtime.GOMAXPROCS(1)

    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(i)
        }
    }()

    for {} // 占用 CPU
}

// 正确,主动让出 CPU
func main() {
    runtime.GOMAXPROCS(1)

    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(i)
        }
    }()

    for {
        runtime.Gosched()
    }
}

// 正确,使用 select{} 阻塞
func main() {
    runtime.GOMAXPROCS(1)

    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(i)
        }
        os.Exit(0)
    }()

    select{}
}

Also note that when a Goroutine blocks, Go automatically transfers other Goroutines that are in the same system thread as that Goroutine to another system thread so that those Goroutines do not block.

Sixth, the Sequential Consistency Memory Model Trap

Within the same Goroutine, the sequential consistency memory model is guaranteed, but between different Goroutines, the sequential consistency memory model is not satisfied and needs to be referenced by well-defined synchronization events for synchronization.

Two events are said to be concurrent if they are not sortable. To maximize parallelism, the Go language compiler and processor may reorder execution statements without affecting the above specification (the CPU may also execute some instructions out of order).

Look at the following example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
var a string
var done bool

func setup() {
    a = "hello, world"
    done = true
}

func main() {
    go setup()
    for !done {}
    print(a)
}

This example creates a setup, initializes a, and sets done to true when it’s done. in the main thread where the main function is, it detects when done becomes true by for !done {}, considers the initialization complete, and prints the string.

This seems correct.

However, Golang does not guarantee that the write to done observed in the main function will occur after the write to string a, so it is likely to print an empty string.

Worse, because there is no synchronization event between the two Goroutines, setup’s write to done is not even visible to main, and main may get stuck in a dead loop.

Seventh, about interfaces

An interface defines a collection of methods, and any object that implements these methods can be considered to implement the interface, which is also called Duck Typing.

This is unlike other languages like Java, which require a pre-declaration that the type implements an interface or some interfaces. This makes Golang interfaces and types lightweight, decoupling the hard binding between interfaces and concrete implementations.

Why Duck Typing.

if something looks like a duck, swims like a duck and quacks like a duck then it’s probably a duck.

In Golang, an interface value has two components, a pointer to the specific type of the interface and a pointer to the real data of that specific type.

1
2
3
4
5
6
7
8
9
type iface struct {
    tab  *itab
    data unsafe.Pointer
}

type eface struct {
    _type *_type
    data  unsafe.Pointer
}

Determining whether an interface is of a certain type by ’type assertion'.

1
value, ok := x.(T)

value is the value of x. ok is the Boolean value.

  • If T is a concrete type, check the dynamic type of x. If T is a concrete type, check the dynamic type of x. If T is a concrete type T. If so, return the dynamic value of x whose type is T.
  • If T is an interface type, check if the dynamic type of x satisfies T. If so, the dynamic value of x is not extracted and the return value is an interface value of type T.
  • Regardless of what type T is, the type assertion fails if x is a nil interface value.

Type assertion example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import "fmt"

type Shape interface {
    Area() float64
}

type Object interface {
    Volume() float64
}

type Skin interface {
    Color() float64
}

type Cube struct {
    side float64
}

func (c Cube)Area() float64 {
    return c.side * c.side
}

func (c Cube)Volume() float64 {
    return c.side * c.side * c.side
}

func main() {
    var s Shape = Cube{3.0}
    value1, ok1 := s.(Object)
    fmt.Printf("dynamic value of Shape 's' with value %v implements interface Object? %v\n", value1, ok1)
    value2, ok2 := s.(Skin)
    fmt.Printf("dynamic value of Shape 's' with value %v implements interface Skin? %v\n", value2, ok2)
}

In the first example, an exception occurs: cannot use names (type []string) as type []interface {} in argument to PrintAll.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import (
    "fmt"
)

func PrintAll(vals []interface{}) {
    for _, val := range vals {
        fmt.Println(val)
    }
}

func main() {
    names := []string{"stanley", "david", "oscar"}
    PrintAll(names)
}

In the second example, an exception occurs: cannot use Woman literal (type Woman) as type Human in array or slice literal: Woman does not implement Human (Say method has pointer receiver).

Hint Woman does not implement the Human interface. This is because Woman’s implementation of the Human interface defines a pointer recipient, but what we pass into the main method is a structure of Woman converted to a Human interface value, not a pointer.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

import "fmt"

type Human interface {
    Say()
}

type Man struct {
}

type Woman struct {
}

func (m Man) Say() {
    fmt.Println("I'm a man")
}

func (w *Woman) Say() {
    fmt.Println("I'm a woman")
}

func main() {
    humans := []Human{Man{}, Woman{}}
    for _, human := range humans {
        human.Say()
    }
}

// This way is fine.
// The main method is passed pointers to Man and Woman, but it compiles just fine.
// Man's implementation of the Human interface defines a value recipient, not a pointer recipient.
// The reason is that in Golang everything is value-passing, and even though the pointer is passed in as Man, we can find its corresponding value through that pointer, and Golang implicitly does the type conversion for us.
// Remember that in Golang a pointer type can get any value type it is associated with, but not the other way around.
// Think of it this way: a specific value may have an infinite number of pointers pointing to it, but a pointer will only point to a specific value.
// func main() {
//     humans := []Human{&Man{}, &Woman{}}
//    for _, human := range humans {
//        human.Say()
//    }
// }

Eighth, about inheritance

Golang does not natively support inheritance, but rather implements the ability to inherit by “combining”.

“Combination” is essentially has-a, and “inheritance” is essentially is-a.

Data structures are entities, and interfaces are capabilities that entities have, as well as “interfaces” (capability expressions) that are provided to the outside.

In the design of data structures and interfaces, the principle of “orthogonality” needs to be followed in order to maximize composability.

Combined relationship of A and B