Go language interview advanced 10 questions

1.Golang variable parameters

The parameters of a function method can be any number, which we call variable parameters. For example, our commonly used functions such as fmt.Println() can receive a variable parameter. Parameters can be changed and can be any number. We can also define variable parameters ourselves, and the definition of variable parameters can be done by adding an ellipse before the type...

func main() {
    
    
 print("1","2","3")
}


func print (a ...interface{
    
    }){
    
    
 for _,v:=range a{
    
    
  fmt.Print(v)
 }
 fmt.Println()
}

In the example, we define a function that accepts variable parameters, and the effect is the same as fmt.Println(). The variable parameter is essentially an array, so we use it like an array, such as the for range loop in the example.

2. The underlying implementation of Golang Slice

Slicing is implemented based on arrays. Its underlying layer is an array. It itself is very small and can be understood as an abstraction of the underlying array. Because it is implemented based on arrays, its underlying memory is allocated continuously, which is very efficient. Data can also be obtained through indexes, iteration and garbage collection optimization.
Slices themselves are not dynamic arrays or array pointers. The data structure implemented internally refers to the underlying array through a pointer, and relevant attributes are set to limit data read and write operations to a specified area. The slice itself is a read-only object, and its working mechanism is similar to an encapsulation of an array pointer.
The slice object is very small because it is a data structure with only 3 fields:

  • pointer to underlying array
  • slice length
  • slice capacity

These three fields are the metadata for Go language to operate the underlying array.
Insert image description here

3. What should you pay attention to about the expansion mechanism of Golang Slice?

The strategy for slicing expansion in Go is as follows:
First, it is judged that if the newly requested capacity is greater than 2 times the old capacity, the final capacity will be the newly requested capacity. Otherwise, if the length of the old slice is less than 1024, the final capacity will be twice the old capacity.
Otherwise, if the old slice length is greater than or equal to 1024, the final capacity will be cyclically increased by 1/4 from the old capacity until the final capacity is greater than or equal to the newly requested capacity. If the final capacity calculation value overflows, the final capacity is the newly requested capacity.

Scenario 1: The original array still has capacity that can be expanded (the actual capacity has not been filled). In this case, the expanded array still points to the original array, and the operation on a slice may affect multiple pointers pointing to the Slice at the same address.

Scenario 2: The original array capacity has reached the maximum value, and if you want to expand it, Go will first open a memory area by default, copy the original value over, and then perform the append() operation. This situation does not affect the original array at all.

To copy a Slice, it is best to use the Copy function.

4.Golang Map underlying implementation

The underlying implementation of map in Golang is a hash table, so the process of implementing map is actually the process of implementing hash table.
In this hash table, there are two main structures, one is called hmap (a header for a go map) and the other is called bmap (a bucket for a Go map, usually called its bucket).
hmap looks like this:

Insert image description here
Insert image description here
There are many fields in the picture, but for easy understanding of the map structure, you only need to care about one, which is the field marked in red: the buckets array. The structure used for storage in Golang's map is a bucket array. What is the structure of bucket (ie bmap)? Bucket:
Insert image description here
Compared with hmap, the bucket structure is simpler. The orange fields are still the "core", and the keys and values ​​in the map we use are stored here.
The "high hash value" array records the "index" related to the key in the current bucket, which will be described in detail later. There is also a field that is a pointer to the expanded bucket, so that the bucket will form a linked list structure.
The overall structure should be like this:

Insert image description here
Golang divides the obtained hash value into two parts according to usage: high bit and low bit. The low bits are used to find which bucket in the hmap the current key belongs to, and the high bits are used to find which key in the bucket.
One thing that needs to be pointed out in particular is that the key/value values ​​in the map are all stored in the same array. The advantage of this is that when the lengths of key and value are different, the space waste caused by padding can be eliminated.

Map expansion: When the Go map length grows larger than the map length required by the loading factor, the Go language will generate a new bucket array, and then move the old bucket array to an attribute field oldbucket.
Note: It is not that the elements in the old array are immediately escaped to the new bucket, but only when a specific bucket is accessed, the data in the bucket will be transferred to the new bucket.

5. Does the JSON standard library handle nil slices and empty slices consistently?

First of all, the JSON standard library handles nil slices and empty slices inconsistently.

Usually incorrect usage will report an array out-of-bounds error, because the slice is only declared, but no instantiated object is given.

var slice []int
slice[1] = 0

At this time, the value of slice is nil. This situation can be used for functions that need to return slice. When an exception occurs in the function, it is guaranteed that the function will still have a return value of nil.

Empty slice means that the slice is not nil, but the slice has no value, and the underlying space of the slice is empty. The definition at this time is as follows

slice := make([]int,0
slice := []int{
    
    }

This is very useful when we query or process an empty list, it will tell us that a list is returned, but there are no values ​​in the list. In short, nil slice and empty slice are different things and we need to distinguish them.

6.Golang’s memory model, why too many small objects will cause gc pressure

Usually too many small objects will cause the GC three-color method to consume too much GPU. The optimization idea is to reduce object allocation.

7.How to solve the Data Race problem? Can this problem be solved without locking?

Synchronous access to shared data is an effective way to deal with data contention.
Golang introduced a competition detection mechanism after 1.1. You can use go run -race or go build -race for static detection. Its internal implementation is to start multiple coroutines to execute the same command and record the status of each variable.
The race detector is based on the C/C++ ThreadSanitizer runtime library, which finds many bugs in Google's internal code base and Chromium. This technology was integrated into Go in September 2012, and since then it has detected 42 race conditions in the standard library. Now that it's part of our ongoing build process, it will continue to catch these errors when race conditions arise.
The race detector is fully integrated into the Go toolchain, just add the -race flag to the command line to use the detector.

$ go test -race mypkg    // 测试包
$ go run -race mysrc.go  // 编译和运行程序 $ go build -race mycmd 
// 构建程序 $ go install -race mypkg // 安装程序

To solve the problem of data competition, you can use the mutex sync.Mutex to solve the data race (Data race), or you can use pipelines. The efficiency of using pipelines is higher than that of mutexes.

8. How do you modify the value when iterating the slice in the range?

In range iteration, the value obtained is actually a value copy of the element. Updating the copy will not change the original element, that is, the copied address is not the address of the original element.

func main() {
    
    
 data := []int{
    
    1, 2, 3}
 for _, v := range data {
    
    
  v *= 10  // data 中原有元素是不会被修改的
 }
 fmt.Println("data: ", data) // data:  [1 2 3]
}

If you want to modify the value of the original element, you should use the index to access it directly.

func main() {
    
    
 data := []int{
    
    1, 2, 3}
 for i, v := range data {
    
    
  data[i] = v * 10 
 }
 fmt.Println("data: ", data) // data:  [10 20 30]
}

If your collection holds pointers to values, you need to modify it slightly. You still need to use indexes to access elements, but you can use elements from the range to directly update the original value.

func main() {
    
    
 data := []*struct{
    
     num int }{
    
    {
    
    1}, {
    
    2}, {
    
    3},}
 for _, v := range data {
    
    
  v.num *= 10 // 直接使用指针更新
 }
 fmt.Println(data[0], data[1], data[2]) // &{10} &{20} &{30}
}

9.The difference between nil interface and nil interface

Although interface looks like a pointer type, it is not. A variable of interface type is nil only when both type and value are nil. If the value of your interface variable changes with other variables, be careful when comparing it with nil for equality. If the return value type of your function is interface, be even more careful about this pitfall:

func main() {
    
    
   var data *byte
   var in interface{
    
    }

   fmt.Println(data, data == nil) // <nil> true
   fmt.Println(in, in == nil) // <nil> true

   in = data
   fmt.Println(in, in == nil) // <nil> false // data 值为 nil,但 in 值不为 nil
}

// 正确示例
func main() {
    
    
  doIt := func(arg int) interface{
    
    } {
    
    
  var result *struct{
    
    } = nil

  if arg > 0 {
    
    
  result = &struct{
    
    }{
    
    }
  } else {
    
    
  return nil // 明确指明返回 nil
  }

  return result
  }


  if res := doIt(-1); res != nil {
    
    
  fmt.Println("Good result: ", res)
  } else {
    
    
  fmt.Println("Bad result: ", res) // Bad result: <nil>
  }
}

10.What can select be used for?

Commonly used for perfect exit of goroutine.

Golang's choice is to monitor IO operations. When an IO operation occurs, the corresponding action is triggered. Each case statement must be an IO operation. To be precise, it should be a channel-oriented IO operation.

Guess you like

Origin blog.csdn.net/m0_73728511/article/details/133563888