Technology Sharing

【Interview Question】Golang (Part 4)

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

Table of contents

1. The difference between make and new

2. How to use value passing and address passing (reference passing) in Go language? What is the difference?

3. What is the difference between arrays and slices in Go? What is the difference between arrays and slices in Go? How does Go implement slice expansion?

4.What is Go Convey? What is it usually used for?

5.What are the functions and characteristics of defer?

6. What is the underlying implementation of Go slice? What is the expansion mechanism of Go slice?

7.Are the slices the same before and after expansion?

8.Why is slice not thread-safe?


1. The difference between make and new

Difference: 1. make can only be used to allocate and initialize data of type slice, map, and chan; while new can allocate data of any type.

2. What new allocation returns is a pointer, that is, the type "*Type"; while make returns a reference, that is, Type.

3. The space allocated by new will be cleared to zero; after make allocates space, it will be initialized.

2. How to use value passing and address passing (reference passing) in Go language? What is the difference?

Value passing only copies the value of the parameter and puts it into the corresponding function. The addresses of the two variables are different and cannot be modified mutually.

Address passing (reference passing) will pass the variable itself to the corresponding function, and the value of the variable can be modified in the function.

3. What is the difference between arrays and slices in Go? What is the difference between arrays and slices in Go? How does Go implement slice expansion?

Array:

Arrays have fixed lengths. The length of an array is part of the array type, so [3]int and [4]int are two different array types. Arrays need to have a specified size. If not specified, the size is automatically inferred based on the initialization pair. Immutable arrays are passed by value.

slice:

Slices can change their length. Slices are lightweight data structures with three attributes: pointer, length, and capacity. Slices are passed by address (reference) and can be initialized through arrays or through the built-in function make(). When initialized, len=cap, and then the capacity is expanded.

Arrays are value types, slices are reference types;

Arrays have a fixed length, while slices do not (slices are dynamic arrays)

The underlying layer of a slice is an array

The Go language's slice expansion mechanism is very clever. It achieves expansion by reallocating the underlying array and migrating the data to the new array. Specifically, when a slice needs to be expanded, the Go language creates a new underlying array and copies the data in the original array to the new array. Then, the slice pointer points to the new array, the length is updated to the original length plus the expanded length, and the capacity is updated to the length of the new array.

To achieve slice expansion:

go1.17

  1. // src/runtime/slice.go
  2. func growslice(et *_type, old slice, cap int) slice {
  3.     // ...
  4.     newcap := old.cap
  5.     doublecap := newcap + newcap
  6.     if cap > doublecap {
  7.         newcap = cap
  8.     } else {
  9.         if old.cap < 1024 {
  10.             newcap = doublecap
  11.         } else {
  12.             // Check 0 < newcap to detect overflow
  13.             // and prevent an infinite loop.
  14.             for 0 < newcap && newcap < cap {
  15.                 newcap += newcap / 4
  16.             }
  17.             // Set newcap to the requested cap when
  18.             // the newcap calculation overflowed.
  19.             if newcap <= 0 {
  20.                 newcap = cap
  21.             }
  22.         }
  23.     }
  24.     // ...
  25.     return slice{p, old.len, newcap}
  26. }

Before allocating memory space, you need to determine the new slice capacity. At runtime, different strategies are selected to expand the capacity based on the current capacity of the slice:

If the expected capacity is greater than twice the current capacity, the expected capacity will be used; if the length of the current slice is less than 1024, the capacity will be doubled; if the length of the current slice is greater than or equal to 1024, the capacity will be increased by 25% each time until the new capacity is greater than the expected capacity;

go1.18

  1. // src/runtime/slice.go
  2. func growslice(et *_type, old slice, cap int) slice {
  3.     // ...
  4.     newcap := old.cap
  5.     doublecap := newcap + newcap
  6.     if cap > doublecap {
  7.         newcap = cap
  8.     } else {
  9.         const threshold = 256
  10.         if old.cap < threshold {
  11.             newcap = doublecap
  12.         } else {
  13.             // Check 0 < newcap to detect overflow
  14.             // and prevent an infinite loop.
  15.             for 0 < newcap && newcap < cap {
  16.                 // Transition from growing 2x for small slices
  17.                 // to growing 1.25x for large slices. This formula
  18.                 // gives a smooth-ish transition between the two.
  19.                 newcap += (newcap + 3*threshold) / 4
  20.             }
  21.             // Set newcap to the requested cap when
  22.             // the newcap calculation overflowed.
  23.             if newcap <= 0 {
  24.                 newcap = cap
  25.             }
  26.         }
  27.     }
  28.     // ...
  29.     return slice{p, old.len, newcap}
  30. }

The difference from the previous version is mainly in the expansion threshold and this line of code:newcap += (newcap + 3*threshold) / 4

Before allocating memory space, you need to determine the new slice capacity. At runtime, different strategies are selected to expand the capacity based on the current capacity of the slice:

  • If the expected capacity is greater than twice the current capacity, the expected capacity will be used;

  • If the length of the current slice is less than the threshold (default 256), the capacity will be doubled;

  • If the length of the current slice is greater than or equal to the threshold (default 256), the capacity will be increased by 25% each time, based on newcap + 3*threshold, until the new capacity is greater than the expected capacity;

Slice expansion is divided into two stages, before and after go1.18:

1. Before go1.18:

  • If the expected capacity is greater than twice the current capacity, the expected capacity will be used;

  • If the length of the current slice is less than 1024, the capacity will be doubled;

  • If the length of the current slice is greater than 1024, the capacity will be increased by 25% each time until the new capacity is greater than the expected capacity;

2. After go1.18:

  • If the expected capacity is greater than twice the current capacity, the expected capacity will be used;

  • If the length of the current slice is less than the threshold (default 256), the capacity will be doubled;

  • If the length of the current slice is greater than or equal to the threshold (default 256), the capacity will be increased by 25% each time, based on newcap + 3*threshold, until the new capacity is greater than the expected capacity;

4.What is Go Convey? What is it usually used for?

go convey is a unit testing framework that supports golang

go convey can automatically monitor file modifications and start testing, and can output test results to the web interface in real time

go convey provides a rich set of assertions to simplify the writing of test cases

5.What are the functions and characteristics of defer?

The purpose of defer is:

You only need to add the keyword defer before calling a normal function or method to complete the syntax required for defer. When the defer statement is executed, the function following defer will be delayed. The function following defer will not be executed until the function containing the defer statement is executed, regardless of whether the function containing the defer statement ends normally through return or ends abnormally due to panic. You can execute multiple defer statements in a function, and their execution order is opposite to the declaration order.

Common scenarios for defer:

The defer statement is often used to handle paired operations, such as open, close, connect, disconnect, lock, and release.

Through the defer mechanism, no matter how complex the function logic is, it can ensure that resources are released in any execution path.

 

6. What is the underlying implementation of Go slice? What is the expansion mechanism of Go slice?

Slices are implemented based on arrays. The underlying layer is an array. It is very small and can be understood as an abstraction of the underlying array. Because it is implemented based on an array, its underlying memory is continuously allocated, which is very efficient. It can also obtain data through indexes, and can be iterated and optimized for garbage collection. The slice itself is not a dynamic array or array pointer. The data structure implemented internally references the underlying array through a pointer, and sets related properties to limit data read and write operations to the specified area. The slice itself is a read-only object, and its working mechanism is similar to an encapsulation of an array pointer.

Slice objects are very small because they are data structures with only three fields:

Pointer to the underlying array

The length of the slice

Slice capacity

The strategy for slice expansion in Go (1.17) is as follows:

First, if the newly applied capacity is greater than 2 times the old capacity, the final capacity is the newly applied capacity.

Otherwise, if the length of the old slice is less than 1024, the final capacity is twice the old capacity.

Otherwise, if the old slice length is greater than or equal to 1024, the final capacity will be increased by 1/4 of the original capacity in a loop until the final capacity is greater than or equal to the newly applied capacity.

If the final capacity calculation value overflows, the final capacity is the newly applied capacity.

7.Are the slices the same before and after expansion?

Case 1:

The original array still has capacity to be expanded (the actual capacity has not been filled). In this case, the expanded array still points to the original array, and operations on a slice may affect multiple slices pointing to the same address.

Case 2:

If the original array has reached its maximum capacity and you want to expand it, Go will open a memory area by default, copy the original value, and then perform the append() operation. This does not affect the original array at all. To copy a Slice, it is best to use the Copy function.

8.Why is slice not thread-safe?

slice底层结构并没有使用加锁等方式,不支持并发读写,所以并不是线程安全的,
使用多个 goroutine 对类型为 slice 的变量进行操作,每次输出的值大概率都不会一样,与预期值不一致;
slice在并发执行中不会报错,但是数据会丢失
​
如果想实现slice线程安全,有两种方式:
​
方式一:通过加锁实现slice线程安全,适合对性能要求不高的场景。

  1. func TestSliceConcurrencySafeByMutex(t *testing.T) {
  2. var lock sync.Mutex //互斥锁
  3. a := make([]int, 0)
  4. var wg sync.WaitGroup
  5. for i := 0; i < 10000; i++ {
  6.  wg.Add(1)
  7.  go func(i int) {
  8.   defer wg.Done()
  9.   lock.Lock()
  10.   defer lock.Unlock()
  11.   a = append(a, i)
  12. }(i)
  13. }
  14. wg.Wait()
  15. t.Log(len(a))
  16. // equal 10000
  17. }
方式二:通过channel实现slice线程安全,适合对性能要求高的场景。
  1. func TestSliceConcurrencySafeByChanel(t *testing.T) {
  2. buffer := make(chan int)
  3. a := make([]int, 0)
  4. // 消费者
  5. go func() {
  6.  for v := range buffer {
  7.   a = append(a, v)
  8. }
  9. }()
  10. // 生产者
  11. var wg sync.WaitGroup
  12. for i := 0; i < 10000; i++ {
  13.  wg.Add(1)
  14.  go func(i int) {
  15.   defer wg.Done()
  16.   buffer <- i
  17. }(i)
  18. }
  19. wg.Wait()
  20. t.Log(len(a))
  21. // equal 10000
  22. }