吹拉弹唱


  • Home
  • Archive
  • Categories
  • Tags
  • Books
  •  

© 2022 Kleon

Theme Typography by Makito

Proudly published with Hexo

Tech - Golang

Posted at 2022-04-22Updated at 2022-04-22 interview  interview tech golang 

Interview questions of golang [1].

  • Language basics
    • What is Golang?
    • Why should one use Golang? What are the advantages of Golang over other languages?
    • What are the cons/defects/disadvantages of Golang?
    • What are Golang packages?
    • What do you understand by Golang string literals?
    • What do you understand by the scope of variables in Go?
    • What do you understand by goroutine in Golang?
    • Is it possible to declare variables of different types in a single line of code in Golang?
    • What is “slice” in Go?
    • What are Go Interfaces?
    • Why is Golang fast compared to other languages?
    • What are Go channels and how are channels used in Golang?
    • What do you understand by Type Assertion in Go?
    • How will you check the type of a variable at runtime in Go?
    • Is the usage of Global Variables in programs implementing goroutines recommended?
    • What are the uses of an empty struct?
    • How can we copy a slice and a map in Go?
    • How is GoPATH different from GoROOT variables in Go?
    • In Go, are there any good error handling practices?
    • Which is safer for concurrent data access? Channels or Maps?
    • How can you sort a slice of custom structs with the help of an example?
    • What do you understand by Shadowing in Go?
    • What do you understand by variadic functions in Go?
    • What do you understand by byte and rune data types? How are they represented?
  • Code Reading
    • What do you understand by each of the functions demo_func() as shown in the below code?
  • Dive deep
    • Why is Golang so fast?
    • Describe Golang’s CSP Model?
    • Describe the process of the garbage collection of Golang?
    • Describe the hierarchical memory management strategy?
    • What’s the data structure of map?
    • How is channel implemented?
    • Lock
    • runtime
    • Sentinel-Go

# Language basics

# What is Golang?

Go is a high level, general-purpose programming language that is very strongly and statically typed by providing support for garbage collection and concurrent programming.
In Go, the programs are built by using packages that help in managing the dependencies efficiently. It also uses a compile-link model for generating executable binaries from the source code. Go is a simple language with elegant and easy to understand syntax structures. It has a built-in collection of powerful standard libraries that helps developers in solving problems without the need for third party packages. Go has first-class support for Concurrency having the ability to use multi-core processor architectures to the advantage of the developer and utilize memory efficiently. This helps the applications scale in a simpler way.

# Why should one use Golang? What are the advantages of Golang over other languages?

A:

  • Simple and Understandable: Go is very simple to learn and understand. There are no unnecessary features included. Every single line of the Go code is very easily readable and thereby easily understandable irrespective of the size of the codebase. Go was developed by keeping simplicity, maintainability and readability in mind.
  • Standard Powerful Library: Go supports all standard libraries and packages that help in writing code easily and efficiently.
  • Support for concurrency: Go provides very good support for concurrency using Go Routines or channels. They take advantage of efficient memory management strategies and multi-core processor architecture for implementing concurrency.
  • Static Type Checking: Go is a very strong and statically typed programming language. Statically typed means every variable has types assigned to it. The data type cannot be changed once created and strongly typed means that there are rules and restrictions while performing type conversion. This ensures that the code is type-safe and all type conversions are handled efficiently. This is done for reducing the chances of errors at runtime.
  • Easy to install Binaries: Go provides support for generating binaries for the applications with all required dependencies. These binaries help to install tools or applications written in Go very easily without the need for a Go compiler or package managers or runtimes.
  • Good Testing Support: Go has good support for writing unit test cases along with our code. There are libraries that support checking code coverage and generating code documentation.

# What are the cons/defects/disadvantages of Golang?

A:

  • Lack of Function Overloading and Default Values for Arguments. A lots of code.
  • Lack of Generics. A lots of duplicated code.
  • Error Handling. Lack of exception.
  • Absence of manual memory management. GC is not suit for some time-sensitive application.
  • Runtime safety is not that good. Golang only provides compiled-time safety rather than runtime-safety.

# What are Golang packages?

Go Packages (in short pkg) are nothing but directories in the Go workspace that contains Go source files or other Go packages themselves. Every single piece of code starting from variables to functions are written in the source files are in turn stored in a linked package. Every source file should belong to a package.
From the image below, we can see that a Go Package is represented as a box where we can store multiple Go source files of the .go extension. We can also store Go packages as well within a package.

What are Golang pointers?
Go Pointers are those variables that hold the address of any variables. Due to this, they are called special variables. Pointers support two operators:

  • * operator: This operator is called a dereferencing operator and is used for accessing the value in the address stored by the pointer.
  • & operator: This operator is called the address operator and is used for returning the address of the variable stored in the pointer.
    Pointers are used for the following purposes:
  • Allowing function to directly mutate value passed to it. That is achieving pass by reference functionality.
  • For increasing the performance in the edge cases in the presence of a large data structure. Using pointers help to copy large data efficiently.
  • Helps in signifying the lack of values. For instance, while unmarshalling JSON data into a struct, it is useful to know if the key is present or absent then the key is present with 0 value.

# What do you understand by Golang string literals?

String literals are those variables storing string constants that can be a single character or that can be obtained as a result of the concatenation of a sequence of characters. Go provides two types of string literals. They are:

  • Raw string literals: Here, the values are uninterrupted character sequences between backquotes. For example:
1
`interviewbit`
  • Interpreted string literals: Here, the character sequences are enclosed in double quotes. The value may or may not have new lines. For example:
1
2
"Interviewbit
Website"

# What do you understand by the scope of variables in Go?

The variable scope is defined as the part of the program where the variable can be accessed. Every variable is statically scoped (meaning a variable scope can be identified at compile time) in Go which means that the scope is declared at the time of compilation itself. There are two scopes in Go, they are:

  • Local variables - These are declared inside a function or a block and is accessible only within these entities.
  • Global variables - These are declared outside function or block and is accessible by the whole source file.

# What do you understand by goroutine in Golang?

A goroutine is nothing but a function in Golang that usually runs concurrently or parallelly with other functions. They can be imagined as a lightweight thread that has independent execution and can run concurrently with other routines. Goroutines are entirely managed by Go Runtime. Goroutines help Golang achieve concurrency.

  • In Golang, the main function of the main package is considered the main goroutine. It is the starting point of all other goroutines. These goroutines have the power to start their goroutines. Once the execution of the main goroutine is complete, it means that the program has been completed.
  • We can start a goroutine by just specifying the go keyword before the method call. The method will now be called and run as a goroutine.

# Is it possible to declare variables of different types in a single line of code in Golang?

A:

1
var a,b,c= 9, 7.1, "interviewbit"

# What is “slice” in Go?

Slice in Go is a lightweight data structure of variable length sequence for storing homogeneous data. It is more convenient, powerful and flexible than an array in Go. Slice has 3 components:

  • Pointer: This is used for pointing to the first element of the array accessible via slice. The element doesn’t need to be the first element of the array.
  • Length: This is used for representing the total elements count present in the slice.
  • Capacity: This represents the capacity up to which the slice can expand.

# What are Go Interfaces?

Go interfaces are those that have a defined set of method signatures. It is a custom type who can take values that has these methods implementation. The interfaces are abstract which is why we cannot create its instance. But we can create a variable of type interface and that variable can then be assigned to a concrete value that has methods required by the interface. Due to these reasons, an interface can act as two things:

  • Collection of method signatures
  • Custom types

# Why is Golang fast compared to other languages?

Golang is faster than other programming languages because of its simple and efficient memory management and concurrency model. The compilation process to machine code is very fast and efficient. Additionally, the dependencies are linked to a single binary file thereby putting off dependencies on servers.

# What are Go channels and how are channels used in Golang?

Go channel is a medium using which goroutines communicate data values with each other. It is a technique that allows data transfer to other goroutines. A channel can transfer data of the same type. The data transfer in the channel is bidirectional meaning the goroutines can use the same channel for sending or receiving the data.

# What do you understand by Type Assertion in Go?

The type assertion takes the interface value and retrieves the value of the specified explicit data type. The syntax of Type Assertion is:

1
t := i.(T)

Here, the statement asserts that the interface value i has the concrete type T and assigns the value of type T to the variable t. In case i does not have concrete type T, then the statement will result in panic.
For testing, if an interface has the concrete type, we can do it by making use of two values returned by type assertion. One value is the underlying value and the other is a bool value that tells if the assertion is completed or not. The syntax would be:

1
t, isSuccess := i.(T)

Here, if the interface value i have T, then the underlying value will be assigned to t and the value of isSuccess becomes true. Else, the isSuccess statement would be false and the value of t would have the zero value corresponding to type T. This ensures there is no panic if the assertion fails.

# How will you check the type of a variable at runtime in Go?

In Go, we can use a special type of switch for checking the variable type at runtime. This switch statement is called a “type switch”.
Consider the following piece of code where we are checking for the type of variable v and performing some set of operations.

1
2
3
4
5
6
7
8
switch v := param.(type) { 
default:
fmt.Printf("Unexpected type %T", v)
case uint64:
fmt.Println("Integer type")
case string:
fmt.Println("String type")
}

In the above code, we are checking for the type of variable v, if the type of variable is uint64, then the code prints “Integer type”. If the type of variable is a string, the code prints “String type”. If the type doesn’t match, the default block is executed and it runs the statements in the default block.

# Is the usage of Global Variables in programs implementing goroutines recommended?

Using global variables in goroutines is not recommended because it can be accessed and modified by multiple goroutines concurrently. This can lead to unexpected and arbitrary results.

# What are the uses of an empty struct?

Empty struct is used when we want to save memories. This is because they do not consume any memory for the values.

  • While implementing a data set: We can use the empty struct to implement a dataset. Consider an example as shown below.
  • In graph traversals in the map of tracking visited vertices. For example, consider the below piece of code where we are initializing the value of vertex visited empty struct.
  • When a channel needs to send a signal of an event without the need for sending any data. From the below piece of code, we can see that we are sending a signal using sending empty struct to the channel which is sent to the workerRoutine.

# How can we copy a slice and a map in Go?

To copy a slice: We can use the built-in method called copy() as shown below:

1
2
3
4
5
slice1 := []int{1, 2}
slice2 := []int{3, 4}
slice3 := slice1
copy(slice1, slice2)
fmt.Println(slice1, slice2, slice3)

To copy a map in Go: We can copy a map by traversing the keys of the map. There is no built-in method to copy the map.

# How is GoPATH different from GoROOT variables in Go?

The GoPATH variable is an environment variable that is used for symbolizing the directories out of $GoROOT which combines the source and the binaries of Go Projects. The GoROOT variable determines where the Go SDK is located. We do not have to modify the variable unless we plan to use multiple Go versions. The GoPATH determines the root of the workspace whereas the GoROOT determines the location of Go SDK.

# In Go, are there any good error handling practices?

In Go, the errors are nothing but an interface type where any type implementing the single Error() method is considered as an error. Go does not have try/catch methods as in other programming languages for handling the errors. They are instead returned as normal values.
We use this whenever we apprehend that there are possibilities where a function can go wrong during type conversions or network calls. The function should return an error as its return variable if things go wrong. The caller has to check this error value and identify the error. Any value other than nil is termed as an error.
As part of good error handling practices, guard classes should be used over if-else statements. They should also be wrapped in a meaningful way as they can be passed up in the call stack. Errors of the same types should not be logged or handled multiple times.

# Which is safer for concurrent data access? Channels or Maps?

Channels are safe for concurrent access because they have blocking/locking mechanisms that do not let goroutines share memory in the presence of multiple threads.
Maps are unsafe because they do not have locking mechanisms. While using maps, we have to use explicit locking mechanisms like mutex for safely sending data through goroutines.

# How can you sort a slice of custom structs with the help of an example?

We can sort slices of custom structs by using sort.Sort and sort.Stable functions. These methods sort any collection that implements sort.Interface interface that has Len(), Less() and Swap() methods as shown in the code below:

1
2
3
4
5
6
7
8
9
10
type Interface interface {
// Find number of elements in collection
Len() int

// Less method is used for identifying which elements among index i and j are lesser and is used for sorting
Less(i, j int) bool

// Swap method is used for swapping elements with indexes i and j
Swap(i, j int)
}

Consider an example of a Human Struct having name and age attributes.

1
2
3
4
type Human struct {
name string
age int
}

Also, consider we have a slice of struct Human of type AgeFactor that needs to be sorted based on age. The AgeFactor implements the methods of the sort.Interface. Then we can call sort.Sort() method on the audience as shown in the below code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// AgeFactor implements sort.Interface that sorts the slice based on age field.
type AgeFactor []Human
func (a AgeFactor) Len() int { return len(a) }
func (a AgeFactor) Less(i, j int) bool { return a[i].age < a[j].age }
func (a AgeFactor) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

func main() {
audience := []Human{
{"Alice", 35},
{"Bob", 45},
{"James", 25},
}
sort.Sort(AgeFactor(audience))
fmt.Println(audience)
}

This code would output:

1
[{James 25} {Alice 35} {Bob 45}]

# What do you understand by Shadowing in Go?

Shadowing is a principle when a variable overrides a variable in a more specific scope. This means that when a variable is declared in an inner scope having the same data type and name in the outer scope, the variable is said to be shadowed. The outer variable is declared before the shadowed variable.
Consider a code snippet as shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var numOfCars = 2    // Line 1
type Car struct{
name string
model string
color string
}
cars:= [{
name:"Toyota",
model:"Corolla",
color:"red"
},
{
name:"Toyota",
model:"Innova",
color:"gray"
}]

func countRedCars(){
for i:=0; i < numOfCars; i++{
if cars[i].color == "red" {
numOfCars +=1 // Line 2
fmt.Println("Inside countRedCars method ", numOfCars) //Line 3
}
}
}

Here, we have a function called countRedCars where we will be counting the red cars. We have the numOfCars variable defined at the beginning indicated by the Line 1 comment. Inside the countRedCars method, we have an if statement that checks whether the colour is red and if red then increments the numOfCars by 1. The interesting point here is that the value of the numCars variable after the end of the if statement will not be affecting the value of the numOfCars variable in the outer scope.

# What do you understand by variadic functions in Go?

The function that takes a variable number of arguments is called a variadic function. We can pass zero or more parameters in the variadic function. The best example of a variadic function is fmt.Printf which requires one fixed argument as the first parameter and it can accept any arguments.

  • The syntax for the variadic function isHere, we see that the type of the last parameter is preceded by the ellipsis symbol (…) which indicates that the function can take any number of parameters if the type is specified.
  • Inside the variadic function, the … type can be visualised as a slice. We can also pass the existing slice (or multiple slices) of the mentioned type to the function as a second parameter. When no values are passed in variadic function, the slice is treated as nil.
  • These functions are generally used for string formatting.
  • Variadic parameter can not be specified as return value, but we can return the variable of type slice from the function.

# What do you understand by byte and rune data types? How are they represented?

byte and rune are two integer types that are aliases for uint8 and int32 types respectively.
The byte represents ASCII characters whereas the rune represents a single Unicode character which is UTF-8 encoded by default.
The characters or rune literals can be represented by enclosing in single quotes like ‘a’,‘b’,’\n’.
Rune is also called a Code point and can also be a numeric value. For example, 0x61 in hexadecimal corresponds to the rune literal a.

# Code Reading

# What do you understand by each of the functions demo_func() as shown in the below code?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//DemoStruct definition
type DemoStruct struct {
Val int
}
//A.
func demo_func() DemoStruct {
return DemoStruct{Val: 1}
}
//B.
func demo_func() *DemoStruct {
return &DemoStruct{}
}
//C.
func demo_func(s *DemoStruct) {
s.Val = 1
}

A. Since the function has a return type of the struct, the function returns a copy of the struct by setting the value as 1.
B. Since the function returns *DemoStruct, which is a pointer to the struct, it returns a pointer to the struct value created within the function.
C. Since the function expects the existing struct object as a parameter and in the function, we are setting the value of its attribute, at the end of execution the value of Val variable of the struct object is set to 1.

# Dive deep

# Why is Golang so fast?

  • Because Go is statically typed and compiled to machine code, it will naturally outperform languages that are interpreted or have virtual runtimes.
  • Efficient memory management[2][3]. The golang runtime manages memories in a hierarchical structure. It allocates a large piece of memory at a time to avoid the overhead and the fragmentation of memory allocation. The core idea of TCMalloc (thread cache malloc) is to divide the memory into multiple levels to reduce the granularity of the lock. Inside TCMalloc Memory management is divided into two parts: thread memory and page heap.
    • thread memory Each memory page divided into — Free List of multiple fixed allocatable size-classes, which helps in reducing fragmentation. So each thread will have a cache for small objects without locks, which makes it very efficient to allocate small objects (<=32k) under parallel programs.
    • page heap The heap managed by TCMalloc consists of a collection of pages, where a set of consecutive pages can be represented by span. When allocated Object is larger than 32K, Pages Heap is used for allocation.
      When there is not enough memory to allocate small objects, go to page heap for memory. If there is not enough, page heap will ask more memory from the Operating System.
      As such an allocation model maintains a user-spaced memory pool, it greatly improves the efficiency of memory allocation and release.
  • Efficient concurrency model[4][5]. The Golang runtime manages all goroutines, which avoids the context switch overhead than the thread or the process.
    • Concurrency. Go implements a variant of the CSP model, in which channels are the preferred method for two Goroutines (a user space thread-like, with a few kilobytes in its stack) to share data. This approach is actually the opposite of that frequently used with other languages like Ruby or Python—a global shared data structure, with synchronization primitives for exclusive access (semaphores, locks, queues, etc.). Keeping these global data structures consistent across all units involves a lot of overhead.
      By following the CSP model, Go makes it possible to have concurrent constructions as primitives of the language. By default, Go knows how to deal with multiple tasks at once, and knows how to pass data between them. This, of course, translates to low latency with intercommunicating Goroutines. In Go, in the context of multithreading, you don’t write data to common storage. You create Goroutines to share data via channels. And because there is no need for exclusive access to global data structures, you gain speed.
      It is important to note that you can also use mutex (or lock) mechanisms in Go, but that isn’t the default approach for a concurrent program.
    • Threading model. Go operates under an M:N threading model. In an M:N model, there are units of work under the user space (the Goroutines or G in the scheduler lexicon) which are scheduled to be run by the language runtime on OS threads (or M in the scheduler lexicon) on machine processors (or P in the scheduler lexicon). A Goroutine is defined as a lightweight thread managed by the Go runtime. Different Goroutines (G) can be executed on different OS threads (M), but at any given time, only one OS thread can be run on a CPU §. In the user space, you achieve concurrency as the Goroutines work cooperatively. In the presence of a blocking operation (network, I/O or system call), another Goroutine can be assigned to the OS thread.
      Once the blocking call ends, the runtime will try to reassign the previous Goroutine to an available OS thread. It’s possible to achieve parallelism here, because once the Goroutines are assigned to an OS thread, the OS can decide to distribute its threads’ execution through its multiple cores.
      By having multiple Goroutines assigned to OS threads—thus being run cooperatively (or in parallel if two OS threads are run simultaneously on different cores)—you get an efficient use of your machine’s CPUs, because all cores will be available for running your program’s functions.
    • Goroutines. Goroutines live within the user thread space. In comparison to OS threads, their operations cost less: The overhead for assigning them, suspending them, and resuming them is lower than the overhead required by OS threads. Goroutines and channels are two of the most important primitives Go offers for concurrency. One important aspect of Goroutines is that expressing them in terms of code is fairly easy. You simply put the keyword go before the function you want to schedule to be run outside of the main thread.
      But how do Goroutines help make Go more performant? The minimal stack required for a Goroutine to exist is 2 KB. Goroutines can increase their stack on runtime if they see the need for more space, but overall, they are memory-friendly. This means their management overhead is minimal. In other words, you can have more working units being processed with a decent quantity of memory, and that translates into efficiency and speed.
    • Task Scheduling. Go comes with its own runtime scheduler. The language does not rely on the native OS thread/process scheduler, but it cooperates with it. Because the scheduler is an independent component, it has the flexibility for implementing optimizations. All these optimizations aim for one thing: to avoid too much preemption of the OS Goroutines, which would result in suspending and resuming the functions’ execution, an expensive operation.
      Next, we are going to highlight some specific optimizations done by the scheduler in order to avoid preemption.
    • Work Stealing. Generally, there are two ways to distribute workloads across CPUs. The first one is work sharing, in which busy processors send threads to other, less busy processors with the hope they will be taken and executed. The second method is work stealing, in which an idle processor is constantly looking to steal other processor threads. Go uses work stealing.
      How does the work stealing approach help make Go faster? The migration of threads between processors is expensive, as it involves context switch operations. Under the stealing paradigm, this phenomenon occurs less frequently, resulting in less overhead.
    • Spinning Threads. The scheduler also implements a particular strategy called spinning threads, which tries to fairly distribute as many OS threads across processors as possible. Go runtime not only reduces the frequency of thread migrations between processors, it is also capable of moving an OS thread with no work assigned to another processor. This can balance CPU usage and power.
      When you have all CPUs working with fairly distributed workloads, you are avoiding resource under utilization, which, again, translates to resource efficiency and speed.
    • System Calls. What strategy does the Go scheduler follow for handling system calls? It turns out that it also helps reduce overhead overall. Let’s see how.
      For system calls expected to be slow, the scheduler applies a pessimistic approach. It makes the OS thread release the processor in which it’s been running, just before the system call. Then, after the system call ends, the scheduler tries to reacquire the processor if it’s available. Otherwise, it’s enqueued by the scheduler until it finds a new available processor. The inconvenience of this approach is the overhead required for dropping and reacquiring a processor.
      However, the scheduler uses a second approach for system calls that are known to be fast—an optimistic approach. With this approach, the OS thread running the Goroutine with the system call does not release the processor, but it flags it.
      Then, after a few microseconds (20 to be precise), another independent special Goroutine (the sysmon Goroutine) checks for all flagged processors. If they are still running the heavy Goroutine that involves the system call, the scheduler takes their processors away, so they’re suspended. If the stolen processor is still available once the system call ends, the Goroutine can continue executing. Otherwise, it will need to be scheduled for execution again (until a processor becomes available).

# Describe Golang’s CSP Model?

# Describe the process of the garbage collection of Golang?

# Describe the hierarchical memory management strategy?

# What’s the data structure of map?

A linked hash table.

# How is channel implemented?

A ring linked list with mutex [6].

# Lock

mutex TryLock()

# runtime

runtime.GoSche()

# Sentinel-Go



  1. Golang Interview Questions ↩︎

  2. A visual guide to Go Memory Allocator from scratch (Golang) ↩︎

  3. 详解Go语言的内存模型及堆的分配管理 ↩︎

  4. Deep Dive into Golang Performance ↩︎

  5. 进程线程协程的本质区别 ↩︎

  6. 图解Go的channel底层实现 ↩︎

Share 

 Previous post: Tech - Outline Next post: Tech - Kubernetes 

© 2022 Kleon

Theme Typography by Makito

Proudly published with Hexo