Go language interview questions (2): Implementation principle

Q1 When is the init() function executed?

The init() function is part of Go program initialization. The Go program is initialized before the main function, and each imported package is initialized by the runtime. The initialization sequence is not according to the import sequence from top to bottom, but according to the resolved dependencies. Packages without dependencies are initialized first.

Each package first initializes package-scope constants and variables (constants take precedence over variables), and then executes the package's init() function. The same package, or even the same source file, can have multiple init() functions. The init() function has no input parameters and return value, and cannot be called by other functions. The execution order of multiple init() functions in the same package is not guaranteed.

One sentence summary: import –> const –> var –> init() –> main()

Example:

package main

import "fmt"

func init()  {
    
    
	fmt.Println("init1:", a)
}

func init()  {
    
    
	fmt.Println("init2:", a)
}

var a = 10
const b = 100

func main() {
    
    
	fmt.Println("main:", a)
}
// 执行结果
// init1: 10
// init2: 10
// main: 10

Q2 Are local variables in Go language allocated on the stack or on the heap?

It's up to the compiler to decide. The Go language compiler will automatically decide whether to put a variable on the stack or on the heap. The compiler will perform escape analysis. When it finds that the scope of the variable does not exceed the scope of the function, it can be placed on the stack. Otherwise, it must be allocated on the heap.

func foo() *int {
    
    
	v := 11
	return &v
}

func main() {
    
    
	m := foo()
	println(*m) // 11
}

In the foo() function, if v is allocated on the stack, &v will not exist when the foo function returns, but this function can run normally. The Go compiler finds that the reference to v is out of scope for foo and allocates it on the heap. Therefore, the value can still be accessed normally in the main function.

Q3 Can two interfaces be compared?

In Go language, the internal implementation of interface contains 2 fields, type T and value V, interface can use == or != to compare. Two interfaces are equal in the following two cases

  1. Both interfaces are equal to nil (at this time both V and T are in the unset state)
  2. The types T are the same, and the corresponding values ​​V are equal.
    See the example below:
type Stu struct {
    
    
	Name string
}

type StuInt interface{
    
    }

func main() {
    
    
	var stu1, stu2 StuInt = &Stu{
    
    "Tom"}, &Stu{
    
    "Tom"}
	var stu3, stu4 StuInt = Stu{
    
    "Tom"}, Stu{
    
    "Tom"}
	fmt.Println(stu1 == stu2) // false
	fmt.Println(stu3 == stu4) // true
}

The type corresponding to stu1 and stu2 is *Stu, and the value is the address of the Stu structure. The two addresses are different, so the result is false.
The type corresponding to stu3 and stu4 is Stu, the value is a Stu structure, and all fields are equal, so the result is true.

Q4 Is it possible that two nils are not equal?

possible.

Interface (interface) is the encapsulation of non-interface values ​​(such as pointers, structs, etc.), and the internal implementation contains 2 fields, type T and value V. An interface is equal to nil if and only if T and V are in the unset state (T=nil, V is unset).

  • When comparing two interface values, T will be compared first, and then V will be compared.
  • When an interface value is compared with a non-interface value, the non-interface value is first tried to be converted to an interface value, and then compared.
func main() {
    
    
	var p *int = nil
	var i interface{
    
    } = p
	fmt.Println(i == p) // true
	fmt.Println(p == nil) // true
	fmt.Println(i == nil) // false
}

In the above example, a nil non-interface value p is assigned to interface i. At this time, the internal field of i is (T=*int, V=nil). When comparing i with p, convert p into an interface and then compares, so i == p, p compares to nil, compares values ​​directly, so p == nil.

But when i is compared to nil, nil is converted to interface (T=nil, V=nil), which is not equal to i (T=*int, V=nil), so i != nil. So an interface where V is nil but T is not nil is not equal to nil.

Q5 Briefly describe the working principle of Go language GC (garbage collection)

The most common garbage collection algorithms are Mark-Sweep and Reference Count. The Go language uses the Mark-Sweep algorithm. And on this basis, the three-color marking method and write barrier technology are used to improve the efficiency.

The mark-sweep collector is a tracking garbage collector, and its execution process can be divided into two stages: mark (Mark) and sweep (Sweep):

  • Marking phase - starting from the root object to find and mark all surviving objects in the heap;
  • Clearing phase - traverse all objects in the heap, recycle unmarked garbage objects and add the reclaimed memory to the free list.

A major problem with the mark clearing algorithm is that during the mark period, the program needs to be suspended (Stop the world, STW). After the mark is over, the user program can continue to execute. In order to be able to execute asynchronously and reduce STW time, the Go language uses a three-color notation method.

The three-color labeling algorithm divides the objects in the program into three categories: white, black and gray.

  • White: Uncertain object.
  • Gray: Surviving objects, child objects are pending.
  • Black: surviving objects.

At the beginning of marking, all objects are added to the white set (this step requires STW). First mark the root object as gray and add it to the gray collection. The garbage collector takes out a gray object, marks it black, and marks the object it points to as gray and adds it to the gray collection. Repeat this process until the gray set is empty and the marking phase ends. Then the white object is the object that needs to be cleaned up, and the black object is the root-reachable object and cannot be cleaned up.

Because the three-color marking method has an additional white state to store uncertain objects, the subsequent marking stages can be executed concurrently. Of course, the cost of concurrent execution is that some omissions may be caused, because objects that were marked black earlier may not be reachable at present. So the three-color marking method is a false negative (false negative) algorithm.

There is still a problem in the concurrent execution of the three-color notation method, that is, the object pointer changes during the GC process. Like the following example:

A () -> B () -> C () -> D ()

Normally, D objects would end up being marked black and should not be collected. However, during the concurrent execution of the mark and the user program, the user program deletes C's reference to D, and A obtains the D reference. Marking continues, and D has no chance of being marked black (A has already been dealt with and will not be dealt with again this round).

A () -> B () -> C () 
  ↓
 D ()

To solve this problem, Go uses memory barrier technology, which is a piece of code executed when the user program reads objects, creates new objects, and updates object pointers, similar to a hook. The garbage collector uses the Write Barrier technique, which colors objects gray when they are added or updated. In this way, even if it is executed concurrently with the user program, when the reference of the object changes, the garbage collector can handle it correctly.

A complete GC is divided into four phases:

  1. Mark Setup (Mark Setup, STW required), open Write Barrier (Write Barrier)
  2. Marking with three-color marking (Marking, concurrent)
  3. Mark the end (Mark Termination, STW required), close the write barrier.
  4. Cleanup (Sweeping, concurrent)

Q6 Is it safe for a function to return a pointer to a local variable?

This is safe in Go, the Go compiler will perform escape analysis on every local variable. If the scope of the local variable is found to be outside the function, the memory will not be allocated on the stack, but on the heap.

Q7 Can any non-interface type T() call the method of *T? And vice versa?

  • A value of type T can call methods declared for type T, but only if the value of T is addressable. The compiler will automatically take the address of this T value before calling the pointer owner method. Because not any T value is addressable, not any T value can call a method declared for type T.
  • Conversely, a value of type T can call methods declared for type T, because dereferencing pointers is always legal. In fact, you can think that for every method declared for type T, the compiler automatically and implicitly declares a method for type T with the same name and signature.

Which values ​​are not addressable?

  • bytes in the string;
  • The elements in the map object (the elements in the slice object are addressable, and the bottom layer of the slice is an array);
  • constant;
  • Package-level functions, etc.

To give an example, define type T and declare a method hello() for type *T, variable t1 can call this method, but when constant t2 calls this method, a compilation error will occur.

type T string

func (t *T) hello() {
    
    
	fmt.Println("hello")
}

func main() {
    
    
	var t1 T = "ABC"
	t1.hello() // hello
	const t2 T = "ABC"
	t2.hello() // error: cannot call pointer method on t
}

Guess you like

Origin blog.csdn.net/weixin_44816664/article/details/132159354