New Year's Colorful Egg Complete Version Thought Questions and Answers

New Year’s Colorful Egg Complete Version Thought Questions and Answers #

Hello, I am Haolin.

As the Spring Festival approaches in 2019, I have just finished updating all the illustrations and answers to the thought questions in my column. I hope these can be helpful to you. Wishing you a happy new year and a smoother journey in learning Go programming language in the coming year.

Introduction #

1. In what order does Go search for dependency packages in multiple workspaces? #

Answer: The order of searching for dependency packages is determined by the value of the environment variable GOPATH. If you have set multiple workspaces in GOPATH, the search will be performed from left to right in these workspaces.

You can determine the answer to this question through experimentation. For example, import a code package in a source file that does not exist on your machine, then compile this code file. Finally, compare the output compilation error message with the value of GOPATH.

2. Will conflicts arise if the same import path exists in multiple workspaces? #

Answer: No conflicts will arise. The search for code packages is performed one by one in the given order in multiple workspaces.

3. By default, what types of parameter values can be accepted by a command source file? #

Answer: This question can be answered by referring to the documentation of the flag package. In general, it can accept boolean types, integer types, float types, string types, and time.Duration types.

4. Can we use custom data types as parameter value types? If so, how? #

Answer: Strictly speaking, it is not possible, but broadly speaking, it is possible. This requires some customization work, and the given parameter value must be serialized. For specific examples, please refer to the examples in the documentation of the flag package.

5. If you need to import two code packages, and the last level of the import paths of these two packages is the same, such as dep/lib/flag and flag, will conflicts arise? #

Answer: Yes, conflicts will arise. This is because the identifiers representing the two code packages are duplicated, both are flag.

6. If conflicts arise, how can this conflict be resolved? How many ways are there? #

Answer: Continuing from the previous question, there are several simple ways to resolve this conflict. When importing the code package, give it an alias, for example: import libflag "dep/lib/flag". Alternatively, import the code package in a localized way, such as: import . "dep/lib/flag".

7. What does it mean if there is a variable with the same name as the current variable in the outer code block? #

Answer: This means that these two variables become “shadowed variables”. In the code block where the inner variable is located, as well as in deeper code blocks, this variable will “shadow” the outer variable in the code block.

8. If we import a code package using the import . XXX method and the variable in the imported code package has the same name as the variable in the current code package, will Go treat them as “shadowed variables” or will it throw an error? #

Answer: These two variables will become “shadowed variables”. Although the scope of these two variables is the current file of the current code package in this case, they are located in different code blocks.

The variable in the current file is located in the code block represented by that file, while the variable in the imported code package is located in the code block represented by the file declaring it. Of course, we can also say that the code block represented by the imported code package contains this variable.

In the current file, the local variable will “shadow” the imported variable.

9. In addition to what was mentioned in the article “The Things You Need to Know about Program Entities 3,” are there any other noteworthy aspects in the type conversion rules? #

Answer: In simple terms, we need to pay attention to the precedence of various symbols when performing type conversions. For more details, please refer to the conversion section in the Go language specification.

10. Can you give some specific examples of how alias types can play a role in code refactoring? #

Answer: In short, we can achieve code refactoring without affecting the external environment by using alias types. For more details, please refer to the official Go documentation “Proposal: Type Aliases”.

Data Types and Statements #

11. What should we pay attention to if multiple slices point to the same underlying array? #

A: We need to pay special attention to whether operations on one slice will affect other slices that point to the same underlying array.

If so, then ask yourself, is this the result you want? Either completely sever the underlying connection of these slices or immediately lock all related operations.

12. How can we “shrink” a slice using the “expansion” concept? #

A: Regarding the “shrinkage” of a slice, refer to the relevant official wiki. However, if you need frequent “shrinkage,” you may need to consider other data structures, such as the List in the container/list code package.

13. What are the applicable scenarios for the circular linked list in the container/ring package? #

A: For example, storage of reusable resources (such as caches) or the need for flexible organization of resource pools, etc.

14. What are the applicable scenarios for the heap in the container/heap package? #

A: Its most important use is to build priority queues, and the “priority” here can be very flexible. So, there is a lot of room for imagination.

15. Are the values of dictionary types concurrent-safe? If not, are they still unsafe if we only add or delete key-value pairs on the dictionary? #

A: The values of dictionary types are not concurrent-safe, even if we only add or delete key-value pairs. The fundamental reason is that the dictionary values sometimes need to be adjusted for storage purposes.

16. What does the length of a channel represent? When will the length of a channel be equal to its capacity? #

The length of a channel represents the number of element values it currently contains. When the channel is full, its length will be equal to its capacity.

17. When an element value is transmitted through a channel, is the copying shallow or deep? #

A: Shallow copying. In fact, there is no deep copying in Go, unless we do it ourselves.

18. If a select statement finds that a channel has been closed, how should we mask the branch it is in? #

A: It’s simple, assign nil to the variable representing that channel. In this way, the send and receive operations on this channel (that variable) will be permanently blocked.

19. When select is used in conjunction with for, how can we directly exit the outer for loop? #

A: This is generally done using the goto statement and labels. Please refer to this part of the Go language specification for details.

20. If complexArray1 is passed to a function, will modifications to its parameter value in the function affect its original value? #

A: The declaration of the complexArray1 variable in the text is as follows:

complexArray1 := [3][]string{
    []string{"d", "e", "f"},
    []string{"g", "h", "i"},
    []string{"j", "k", "l"},
}

It depends on how it is modified. Although complexArray1 itself is an array, its elements are all slices. If elements are added or removed from complexArray1, the original value will not be affected. But if you want to modify its existing element values, the original value will change as well.

21. The parameters received by a function are actually just copies of the values. Will the result values returned by the function also be copied? #

A: The result values returned by a function will also be copied. However, in general, we don’t need to pay too much attention to this. But if the function continues to execute after returning the result value and modifies the result value, then we need to be careful.

22. Can we embed a pointer type of a certain type in a struct type? If so, what should we pay attention to? #

A: Of course we can. In this case, we still need to pay attention to various “masking” phenomena. Because a pointer type of a certain type will contain all the associated methods, we need to pay even more attention.

Also, when embedding and referencing such fields, we need to pay attention to some conflicts. Please refer to this part of the Go language specification for details.

23. What does the literal struct{} represent? What is it used for? #

Answer: The literal struct{} represents an empty struct type. Such a type does not contain any fields or methods. The storage space required for a value of this type is almost negligible.

Therefore, we can use such a value as a placeholder. For example, in the same application scenario, a value of type map[int]struct{} will occupy less storage space than a value of type map[int]bool.

24. If we assign a variable of a certain implementation type with a value of nil to an interface variable, can we still call the methods of that interface on this interface variable? If we can, what are the considerations? If we can’t, why not? #

Answer: Yes, we can still call the methods. However, please note that the method being called at this time holds a value of nil for its receiver. Therefore, if this method references a field of its receiver, it will cause a panic!

25. Is the pointer value of a reference type’s value meaningful? If it is not meaningful, why? If it is meaningful, what is the meaning? #

Answer: From the perspective of storage and passing, it is not meaningful. Because a value of a reference type is already equivalent to a pointer to some underlying data structure. However, a value of a reference type is not just a pointer.

26. What methods can be used to limit the number of enabled goroutines? #

Answer: A simple and commonly used method is to use a channel to store tokens. Only if a token is obtained, a goroutine can be enabled. Also, when the go function is about to finish execution, the token needs to be returned to the channel in a timely manner.

More advanced methods require a more complete design. For example, a task dispatcher + task pipeline (a single-level channel) + a fixed number of goroutines. Another example is a dynamic task pool (a multi-level channel) + a dynamic goroutine pool (which can evolve from the aforementioned token scheme). And so on.

Answer: Regarding this question, I believe you will know once you check the documentation. However, it is not enough to just know it, you also need to know how to use it.

28. In a type switch statement, how do we perform the corresponding type conversion on the value being evaluated? #

Answer: In fact, we can let Go automatically do this, for example:

switch t := x.(type) {
// cases
}

When the flow enters a case clause, the value of the variable t has been automatically converted to a value of the corresponding type.

29. In an if statement, what is the scope of a variable declared in the initialization clause? #

Answer: If the variable is a new variable, its scope is the code block represented by the current if statement. Note that subsequent else if clauses and else clauses are also included in the code block represented by the current if statement.

30. Please list three error types that you often use or see, and describe their error type system. Can you draw a tree to describe them? #

Answer: Omitted. You need to do this yourself, I can’t do it for you.

31. Please list three error values that you often use or see, and indicate which error value list they belong to. What kind of errors do these error value lists contain? #

Answer: Omitted. You need to do this yourself, I can’t do it for you.

32. How can a function convert a panic into a value of the error type and return it as the result value of the function to the caller? #

Answer: It can be implemented like this:

func doSomething() (err error) {
    defer func() {
        p := recover()
        err = fmt.Errorf("FATAL ERROR: %s", p)
    }()
    panic("Oops!!")
}

Note the syntax of the result declaration. This is a result declaration with a name.

33. We can recover from a panic in a defer function, but can we panic within it? #

Answer: Of course, we can. This can wrap the original panic and throw it again.

Testing for Go Programs #

34. Besides the methods mentioned in this article, do you know or have you used any other methods from the testing.T type and the testing.B type? What are they used for? #

Answer: I’m sorry, but you’ll have to do this yourself. I can’t do it for you.

35. How do we specify the expected output when writing example test functions? #

Answer: The answer to this question can be found in the documentation of the testing package.

36. What is the purpose of the -benchmem flag and the -benchtime flag? #

Answer: The -benchmem flag is used to print memory allocation statistics after completing performance tests. The -benchtime flag is used to set the time limit for executing benchmark functions.

Please refer to the documentation here for more details.

37. How do we enable test coverage analysis during testing? Are there any side effects when enabled? #

Answer: The go test command accepts the -cover flag to enable test coverage analysis. However, since coverage analysis may comment out parts of the source code before the program is compiled, the error report may record line numbers that don’t correspond to the original source code if the program compilation or testing fails.

Usage of standard library #

38. Do you know which interface both the pointer types for mutual exclusion lock and read/write lock implement? #

Answer: They both implement the sync.Locker interface.

39. How can you acquire a read lock from a read/write lock? #

Answer: The sync.RWMutex type has a pointer method called RLocker that can be used to acquire its read lock.

40. Can the value of the type *sync.Cond be passed around? What about the value of type sync.Cond? #

Answer: Once a value of type sync.Cond is used, it should not be passed around anymore as passing typically implies copying. Copying a used sync.Cond value is dangerous because calling any method on this copy will immediately panic. However, its pointer value can be copied.

41. What is the purpose of the public field L in the type sync.Cond? Can we change the value of this field in the process of using the condition variable? #

Answer: This field represents the lock held by the current sync.Cond value. We can change the value of this field in the process of using the condition variable, but we must be clear about the implications before doing so.

42. If we have to choose between atomic value and mutex lock, what would you say are the three most important decision criteria? #

Answer: I think the following questions need to be considered first.

  • What type of data is being protected? Is it a value type or a reference type?
  • What are the ways to manipulate the protected data? Is it simply reading and writing or more complex operations?
  • Is the code that manipulates the protected data centralized or distributed? If it is distributed, can it be made centralized?

After answering these questions (and any other questions that you are concerned about), priority should be given to using atomic values.

43. When using the WaitGroup value to implement a one-to-many goroutine collaboration, how can we get the specific execution result of each subtask in the distributing goroutine? #

Answer: You can consider using locks with a container (array, slice, or map, etc.), or you can consider using channels. Additionally, you may also use the program entities in the golang.org/x/sync/errgroup package, the relevant documentation is found here.

44. When a Context value is used to convey cancellation signals, is it depth-first or breadth-first? What are its advantages and disadvantages? #

Answer: It is depth-first. Its advantages and disadvantages are as follows: children of a direct branch will receive the signal earlier the earlier they are produced. Whether this is an advantage or disadvantage depends on the specific use case.

For example, if the survival time of each subnode is positively correlated with resource consumption, this may be an advantage. However, if each branch has many subnodes and the production order of subnodes in each branch does not follow the branch creation order, then this advantage may turn into a disadvantage. The final conclusion depends on the test results.

45. How can we ensure that a temporary object pool always has an adequate supply of temporary objects? #

Answer: First, we should put enough temporary objects into the temporary object pool in advance. Second, after using a temporary object, we need to return it to the temporary object pool promptly.

Finally, we should ensure that the value represented by its New field is available. Although the temporary object returned by the New function is not placed in the pool, it guarantees that the Get method of the pool will always return a temporary object.

46. Regarding ensuring the correctness of the types of keys and values in a concurrent safe dictionary, can you think of any other solutions? #

Answer: This is an open question that you need to think about on your own. In fact, how to do it depends completely on your application scenario. However, we should try to avoid using reflection because it has a certain impact on program performance.

47. How many ways are there usually to determine whether a Unicode character is a single-byte character? #

Answer: The unicode/utf8 package has several functions that can be used to determine this, such as the RuneLen function, EncodeRune function, etc. We need to choose and use them based on different inputs. For specific details, please refer to the documentation of this package.

48. Which interfaces do strings.Builder and strings.Reader respectively implement? What are the benefits of doing so? #

Answer: The strings.Builder type implements three interfaces: fmt.Stringer, io.Writer, and io.ByteWriter.

The strings.Reader type implements eight interfaces: io.Reader, io.ReaderAt, io.ByteReader, io.RuneReader, io.Seeker, io.ByteScanner, io.RuneScanner, and io.WriterTo.

The benefit is obvious. The more interfaces they implement, the more versatile they are. They will be suitable for situations that require the parameter type to be one of these interface types.

49. Compare the String method of strings.Builder and bytes.Buffer, and determine which one is more efficient. What is the reason? #

Answer: The String method of strings.Builder is more efficient. This is because the method only performs a simple type conversion on the underlying value (the byte slice) of the strings.Builder, and directly uses the underlying value (or memory space). The source code is as follows:

// String returns the accumulated string.
func (b *Builder) String() string {
	return *(*string)(unsafe.Pointer(&b.buf))
}
}

The storage methods for array values and string values are actually the same at the underlying level. So the conversion from a slice value to a string value pointer can be straightforward. And because string values are immutable, it is safe to do so.

However, for historical, structural, and functional reasons, the String method of bytes.Buffer cannot do this.

50. How does a synchronous in-memory channel work in the io package? #

A: We have actually made basic explanations in the main text.

The io.Pipe function returns a io.PipeReader value and a io.PipeWriter value, which serve as the two ends of the channel. These two values actually only delegate the functionality of the same *io.pipe type value at the underlying level.

The io.pipe type uses an unbuffered channel to synchronize read operations and write operations, and uses a mutual exclusion lock to serialize write operations. It also uses atomic values to handle errors. All these ensure the concurrent safety of this synchronous in-memory channel.

51. What is the main purpose of the bufio.Scanner type? What are its main features? #

A: The bufio.Scanner type is commonly known as a buffered scanner. Its functionality is quite powerful.

For example, we can customize the segmentation method of the target content before calling its Scan method to scan the target. We can call its Split method and pass in a function to customize the segmentation method.

By default, the scanner scans the target content in lines. The bufio package provides some ready-made segmentation methods. In fact, the scanner uses the bufio.ScanLines function as the default segmentation method.

We can also customize the carrier and the maximum capacity of the buffer before scanning, which requires calling its Buffer method. By default, the scanner’s internally set maximum buffer capacity is 64K bytes.

In other words, each segment of the target content cannot exceed 64K bytes. Otherwise, the scanner will make its Scan method return false and give us an error value representing “token too long” through its Err method. Here, “token” represents a segment of content.

For more features and usage considerations of the bufio.Scanner type, you can refer to its documentation.

52. How to create and manipulate a system process using APIs in the os package? #

A: You can start with the os.FindProcess function and os.StartProcess function. The former is used to find a process by process ID (pid), and the latter is used to start a process based on a program.

Both of them return a value of type *os.Process. This type provides some methods, such as the Kill method to kill the current process, the Signal method to send a system signal to the current process, and the Wait method to wait for the current process to end.

Associated with these are the os.ProcAttr type, os.ProcessState type, os.Signal type, and so on. You can explore more possibilities through active practice.

53. How to correctly set timeout for read and write operations on a net.Conn value? #

A: The net.Conn type has 3 methods available for setting timeouts, namely SetDeadline, SetReadDeadline, and SetWriteDeadline.

These three methods have the same signature, only the names are different. They all accept a parameter of type time.Time and return a result of type error. The SetDeadline method is used to set both the read operation timeout and the write operation timeout at the same time.

One thing to note is that these three methods set timeouts for any current and future corresponding operations.

Therefore, if you want to perform read or write operations in a loop, it is best to set a timeout in each iteration.

Otherwise, later operations may directly fail due to reaching the timeout. In addition, if necessary, you should call them again and pass in a zero value of type time.Time to indicate no time limit.

54. How to gracefully stop a network service program based on the HTTP protocol? #

A: The net/http.Server type has a pointer method named Shutdown that can achieve “graceful stopping”. In other words, it can close the current server smoothly without interrupting any active connections.

It will first close all idle connections and wait. It will only close them when active connections become idle. After all connections are closed smoothly, it will close the current server and return. When an error occurs, it will also return the corresponding error value.

In addition, you can register a function that will be automatically called when the server is about to close by calling the RegisterOnShutdown method of the Server value.

Specifically, the Shutdown method of the current server will asynchronously call all such registered functions. We can use these functions to notify clients with long-lived connections that “the connection is about to be closed”.

55. What is the functionality of the runtime/trace package? #

A: In simple terms, this package helps Go programs implement internal trace operations. The program entities in it can help us record the state of each goroutine, the state of various system calls, various events related to garbage collection, and changes related to memory and CPU, etc.

The generated trace records can be viewed using the go tool trace command. More specific descriptions can be found in the documentation of the runtime/trace package.

With the runtime/trace package, we can install a trace instrument that meets our personalized needs for Go programs. Some packages in the Go standard library have implemented their own functionality using this package, such as the net/http/pprof package.

Okay, all the answers to the discussion questions have been updated. If you have any further questions, feel free to ask. Wish you a happy Lunar New Year and happy learning. Goodbye.

Click here to view the detailed code accompanying the Go language column article.