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 locksync.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.
|
|
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.
And slices are sequences that can be dynamically grown and contracted, and it has the following data structure.
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.
- nil, uninitialized, only declared, or manually assigned to nil
- active, the normal state, readable or writable
- closed, closed, don’t mistake the value of the channel as nil when the channel is closed
Points to note.
- Read a closed Channel, will read to zero value.
- Reading a nil Channel will block.
- write a closed Channel, it will Panic
- Writing a nil Channel will block.
- 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).
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).
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
- The order of execution of multiple defers is “last-in-first-out”, and the defers are stacked sequentially.
- the execution logic of defer, return and return value is
- return is executed first, and return is responsible for writing the result to the return value.
- if there is a defer, execute the defer to start some finishing work (you can modify the return value)
- finally the function exits with the “current return value”.
The following is an example.
|
|
Look at one more example, this one returns 1, not 0.
For recover, it catches exceptions on grandfathered calls, which must be run in the defer function and are not valid for direct calls.
Fifth, exclusive CPU use causes other Goroutines to starve
Goroutine is collaborative preemption scheduling, and Goroutine itself does not actively give up CPUs.
|
|
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.
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.
Determining whether an interface is of a certain type by ’type assertion'.
|
|
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.
|
|
In the first example, an exception occurs: cannot use names (type []string) as type []interface {} in argument to PrintAll
.
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.
|
|
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.