Table of Contents
ToggleGolang, also known as Go, has become a powerhouse in the world of programming languages, known for its efficiency and simplicity. Whether you’re a experienced Go developer or just starting your journey with this language, preparing for a Golang interview can be both exciting and challenging.
To help you navigate this path to success, we’ve curated a comprehensive list of Golang interview questions that cover a wide range of topics. In this article, we’ll explore these questions and equip you with the knowledge and confidence needed to ace your next Golang interview.
Let’s dive in and unravel the world of Golang together!
Basic Golang Interview Questions
Q1. What are some key features of the Go programming language?
Some key features of the Go programming language are:
- Strongly typed and compiled language
- Fast compile times
- Garbage collected
- Built-in concurrency using goroutines and channels
- Simplicity, structural typing, focus on productivity
Q2. Explain Go’s type system
Go has a static type system with support for:
- Basic types: numbers, string, bool
- Aggregate types: array, structs
- Reference types: pointers, slices, maps, functions
- Interfaces for polymorphic code
Type inference is used so variable declarations don’t need explicit types.
Q3. What is the GOPATH environment variable, and how is it used?
GOPATH indicates the location of the Go workspace. It contains:
- src folder for source code
- pkg folder for compiled package objects
- bin folder for executable binaries
Go tools use GOPATH to find all dependencies and modules.
Q4. Explain error handling in Go.
Errors are handled via return values instead of exceptions.
- An additional error return variable is used.
- nil value indicates no error.
- Functions handling errors should be checked if not nil.
- You can use the errors package to create and wrap errors
- Here is some sample code showing how errors are handled in Go using return values:
// Error Handling in Go
package main
import (
"errors"
"fmt"
)
// Function that returns an error
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("divide by zero error")
}
return a/b, nil
}
func main() {
// Call divide function
result, err := divide(10, 2)
// Check if error is nil
if err != nil {
fmt.Println(err)
return
}
fmt.Println("Result:", result)
}
In this example:
- The divide function returns an int and error. It checks for divide by zero error case
- Using errors. Now, it creates an error object
- In main, the error return is checked if not nil
- If there is no error, the result is used. Otherwise, the error is printed
Q5. How are packages used in Go?
Related functions and data are grouped into packages. The package declaration on the first line denotes the package name.
- Lowercase packages are private, and uppercase are public.
- Main is a unique package that defines an executable.
- Packages are initialised only once across the entire program.
Intermediate Golang Interview Questions
Q6. Explain goroutines and how they are used.
Goroutines are lightweight threads managed by Go runtime. To create a goroutine, prefix function call with the go keyword.
go doWork()
- Goroutines execute concurrently with other goroutines.
- Channel communication is used between goroutines.
- Goroutines are multiplexed onto a smaller number of OS threads.
Q7. Explain channels in Go and how they are used?
Channels in Go are communication and synchronization primitives used for data transfer between goroutines. They allow one goroutine to send data to another safely and efficiently. Channels ensure synchronization and prevent race conditions in concurrent programs.
They are created with make(chan T)
and can be used for sending and receiving data using the <-
operator. Closing channels and the select
statement are additional features that enhance their utility in managing concurrency.
ch := make(chan int)
- Send values into channels using channel <-
- Receive values from channels using <- channel
- Channel communication blocks until send/receive complete
Q8. How does Go handle concurrency?
Concurrency constructs in Go:
Goroutines
- Goroutines are lightweight threads managed by Go runtime.
- Unlike OS threads, goroutines have low overhead (2-4KB stack).
- Launch goroutines using the go keyword before the function call.
- Goroutines run concurrently, interleaved on OS threads.
- Use channels for communication between goroutines.
Channels
- Channels connect goroutines and synchronise execution.
- Declared using make(chan Type) to create channel object.
- <-channel syntax receives from channel, channel <- sends values.
- Unbuffered channels block until the other side is ready.
- Buffered channels accept sends until the buffer is full.
Synchronisation
- sync.Mutex and sync.RWMutex types provide mutual exclusion locking.
- Lock() before accessing shared resources, and Unlock() after.
- Sync.WaitGroup allows waiting for goroutines to finish.
- Add() sets counter, Done() decrements, Wait() blocks until 0.
- Atomic functions like sync/atomic.AddInt32() manages atomic access.
Proper usage of goroutines, channels and locks is needed to write safe concurrent programs in Go. Let me know if you want me to provide code examples demonstrating these actions. No explicit thread management is needed. Concurrency primitives make parallel code easy.
Q9. How are interfaces used in Go?
Interfaces define method signatures that concrete types must satisfy For Example:
// Interfaces In Go
type Reader interface {
Read(p []byte) (n int, err error)
}
The above code defines an interface in Go called Reader
. An interface specifies a set of methods that any type implementing it must provide.
This interface is commonly used in Go to define a contract that various types (e.g., files, network connections, buffers) can adhere to. Any type that implements the Read
method with the specified signature can be considered a Reader
in Go, allowing for polymorphism and abstraction when working with different types of data sources.
Q10. How do you handle dependencies in Go?
To Handle Dependencies in Go:
- Use Go Modules for Managing Dependencies:
- Utilize Go modules, enabled by running
go mod init
within your project directory. Go modules provide a reliable way to manage and version your project’s dependencies.
- Utilize Go modules, enabled by running
- Import Packages in Your Code:
- Import external packages in your Go code using the
import
keyword. This allows you to access and use functionality from external libraries.
- Import external packages in your Go code using the
- Maintain Dependencies with ‘go mod tidy’:
- Periodically run
go mod tidy
to analyze your project’s dependencies and add any missing or remove unused ones. This ensures that your project stays up-to-date and free from unnecessary dependencies.
- Periodically run
- Vendor Dependencies with ‘go mod vendor’:
- When you want to vendor your project’s dependencies (i.e., keep a local copy of them within your project directory), use the command
go mod vendor
. This helps in isolating your project from external changes in the dependencies.
- When you want to vendor your project’s dependencies (i.e., keep a local copy of them within your project directory), use the command
- Packages Compiled During Build:
- During the build process, Go compiles your source code and any imported packages into executable files. These files can be executed to run your Go applications.
These steps ensure effective dependency management in Go, making your projects more maintainable and portable.
Advanced Golang Interview Questions
Q11. Explain the select statement in Go.
The select
statement in Go is a powerful construct used for handling concurrent communications with multiple channels. It allows you to choose and execute one of several communication operations that are ready to proceed. Let’s understand this with an example code snippet:
package main
import (
"fmt"
"time"
)
func main() {
// Create two channels for communication.
ch1 := make(chan string)
ch2 := make(chan string)
// Goroutine 1: Send "Hello" to ch1 after a delay.
go func() {
time.Sleep(2 * time.Second)
ch1 <- "Hello"
}()
// Goroutine 2: Send "World" to ch2 after a delay.
go func() {
time.Sleep(1 * time.Second)
ch2 <- "World"
}()
// Use select to wait for and print messages from channels.
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println("Received:", msg1)
case msg2 := <-ch2:
fmt.Println("Received:", msg2)
}
}
}
In this example:
- We create two channels
ch1
andch2
to facilitate communication between goroutines. - Two goroutines are started concurrently. One sends “Hello” to
ch1
after a 2-second delay, and the other sends “World” toch2
after a 1-second delay. - In the main goroutine, we use the
select
statement to wait for and handle messages from these channels. - The
select
statement evaluates the cases in the order they appear. It chooses the first case that is ready to proceed. In our example, we have two cases:
- Case 1:
msg1 := <-ch1
– This case waits for a message fromch1
. - Case 2:
msg2 := <-ch2
– This case waits for a message fromch2
.
- Whichever message arrives first will be processed by the corresponding case, and the “Received” message along with the content of the message is printed.
- Since we iterate through the
select
statement twice, we ensure that both messages are received and printed.
The select
statement is particularly useful for handling multiple channels concurrently, allowing your Go programs to respond to events from different sources efficiently. It’s a key feature for building responsive and concurrent applications.
Q12. How does Go handle memory management and garbage collection?
Go uses a concurrent, generational garbage collector. Objects are allocated on the heap and automatically freed when no longer needed.
Generational Scanning
- The heap is split into new and old generations
- Newer objects are scanned more frequently
- Older objects are scanned less often for efficiency
- Objects marked white (not processed), grey (processing), black (processed)
- Objects referred from grey objects get marked grey
- Once the grey set is empty, white objects are unreachable/removed
Concurrent GC
- The collector runs concurrently with other goroutines
- Collector pauses other goroutines only during marking
- Helps minimise GC latency impact on execution
Allocation
- Uses bounded allocation caches to minimise cost
- Pointer bump allocation for small objects
- Best-fit segmentation for large objects
By leveraging techniques like these, Go’s GC reduces pause times and minimises overhead due to memory management. Developers don’t have to worry about manual allocation/freeing. No manual memory management is needed. Reduces the entire class of bugs.
Q13. How are unit tests written in Go?
Unit testing in Go is straightforward and well-supported through the built-in testing package, which is part of the standard library. Here’s a step-by-step guide on how unit tests are typically written in Go:
Test File Naming Convention:
Create a test file for the package or module you want to test. By convention, the test file should have the same name as the source file with _test
appended to it. For example, if you have a math.go
source file, the corresponding test file should be named math_test.go
.
Test Functions:
In the test file, define functions with names that start with Test
. These functions are your test cases. For example:
func TestAdd(t *testing.T) {
// Your test logic here.
}
Testing Package:
Import the testing
package at the top of your test file.
Test Logic:
Write your test logic within the test functions. Use various testing functions provided by the testing
package, such as t.Error
, t.Errorf
, t.Fail
, t.FailNow
, and t.Log
, to report test failures and log information.
Assertions:
Use assertion functions from external testing libraries like github.com/stretchr/testify
if needed, although Go’s built-in testing functions are often sufficient.
Running Tests:
To run your tests, use the go test
command followed by the package or module name:
go test package_name
Go will discover and execute all test functions in files ending with _test.go
.
Output:
The go test
command will display test results. Failures and errors will be reported, along with the names of the test functions.
Benchmarking (Optional):
Go also supports benchmarking using the Benchmark
functions, which follow a similar naming convention (e.g., BenchmarkFunctionName
). Benchmarks help you measure the performance of your code. You can run benchmarks using the go test -bench
command.
Here’s a simple example of a test function for a hypothetical Add
function:
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Expected %d, but got %d", expected, result)
}
}
In this test, Add
is a function being tested, and the test function checks if it returns the expected result. If not, it reports an error using t.Errorf
.
By following these conventions and guidelines, you can write effective unit tests for your Go code, ensuring its correctness and robustness.
Q14. Explain closures in Go.
Closures in Go refer to a function value that references variables from its enclosing function, even if that enclosing function has already returned. This allows Go functions to have access to the scope of their containing function, preserving the state of local variables.
Here’s an example of a closure in Go:
package main
import "fmt"
func main() {
// Outer function that returns a closure.
outer := func() func() int {
count := 0
return func() int {
count++
return count
}
}
// Create an instance of the closure.
counter := outer()
// Call the closure multiple times.
fmt.Println(counter()) // 1
fmt.Println(counter()) // 2
fmt.Println(counter()) // 3
}
In this example:
- We define an outer function (
outer
) that returns a closure. Insideouter
, there’s a local variablecount
initialized to 0. - The closure is defined as an anonymous function within
outer
. It increments thecount
variable and returns its value. - We create an instance of the closure by calling
outer()
, which returns a reference to the inner function. - We call the closure multiple times (
counter()
), and it continues to increment and return the value ofcount
. The closure “remembers” the state of thecount
variable even thoughouter
has already returned.
Closures are a powerful feature in Go and are often used to encapsulate and maintain the state within a function. They are commonly seen in functional programming and are particularly useful for scenarios where you need to maintain and manipulate state within a function that’s returned for later use.
Golang Practical Interview Questions
Q15. Write a program to calculate the factorial of a number in Go.
The Factorial
function takes an integer n
as input and recursively calculates the factorial.
- If
n
is 0, it returns 1 (base case). - Otherwise, it multiplies
n
by the result of callingFactorial
withn-1
.
// factorial of a number in Go
func Factorial(n int) int {
if n == 0 {
return 1
}
return n * Factorial(n-1)
}
func main() {
fmt.Println(Factorial(6)) // 720
}
Q16. How would you implement a LRU cache in Go?
Let’s implement an LRU (Least Recently Used) cache in Go:
Data Structures
We need two primary data structures:
- A map to store key-value pairs
- A doubly linked list to maintain access order
type entry struct {
key string
value interface{}
prev, next *entry
}
var cache = make(map[string]*entry)
var lruList = &entry{nil, nil, nil, nil}
The entry struct contains key, value and prev/next pointers.
Get
On getting in, we look up the key in the map to retrieve the entry. If present, we move the entry to the front of a linked list to mark it as recently used.
func get(key string) interface{} {
if entry, ok := cache[key]; ok {
lruList.moveToFront(entry)
return entry.value
}
// Key not found
return nil
}
Put
On put, we again lookup entry in the map.
- If not present, add a new entry to the map and linked list.
- If present, update the value and move to the front of the list.
Also, check if the cache is full-on puts. If so, remove the last entry of the linked list (oldest entry)
func put(key string, value interface{}) {
if entry, ok := cache[key]; ok {
// Update existing entry
entry.value = value
lruList.moveToFront(entry)
return
}
// Add new entry
entry := &entry{key, value, nil, nil}
cache[key] = entry
lruList.addToFront(entry)
// Check if cache is full
if len(cache) > MAX_SIZE {
last := lruList.back
lruList.remove(last)
delete(cache, last.key)
}
}
This implements an efficient LRU cache in Go using a map and linked list. Let me know if you need any clarification!
Q17. Implement a socket server that echoes back data sent to it in Golang.
- Listen on port with the net.Listen()
- Spawn goroutine per client in loop with the net.Accept()
- Read data from the connection
- Write data back to the client
- Close connection
Golang Scenario-based Interview Questions
Q18. Design a real-time stats aggregation API in Golang.
To Design real time stats aggregation API:
- Expose HTTP POST endpoint to ingest stats events
- Parse and validate request body data
- Increment counters in an in-memory map by metric
- Run aggregation every 5 seconds in a goroutine
- Compute window aggregates like sum, count
- Expose HTTP GET endpoint to fetch live aggregates
- Persist aggregates to database every minute
Q19. Design a load-balanced API gateway in Go.
To Design load-balanced API:
- Create n HTTP servers running identical REST APIs
- Implement load balancer using IP level net.Dial()
- Maintain a list of backend URLs
- On HTTP request, round robin to a backend
- Track each backend load using running counters
- If the backend fails, exclude it from the load balancer
- Collect stats like requests per backend that can be monitored
Q20. Design a distributed job worker system using work queues.
Major components of distributed job worker system using work queues are:
- Job producer inserts job requests into RabbitMQ
- Consumer workers pull requests using RabbitMQ clients
- Use goroutines to process requests concurrently
- Mark jobs as complete/failed using Ack/Nack
- Persist job status after processing
- Use prefetch count to control channel throughput
- Monitor queue depth and job duration at the consumer end
This provides horizontal scale-out using distributed workers.
Additional Golang Interview Questions
- In Go, how are interfaces implemented implicitly rather than explicitly, and what benefits does this design choice offer?
- Concurrency is a significant feature in Go. Can you explain the difference between goroutines and OS threads? How does the Go scheduler manage goroutines?
- Go does not support classical object-oriented programming with inheritance. How can you achieve code reusability and polymorphism in Go?
- How does Go’s type system handle nil values, especially about interfaces? Explain the concept of the “nil interface value” and potential pitfalls.
- Channels are a core part of Go’s concurrency model. What is the difference between buffered and unbuffered channels, and when might you use one over the other?
- Describe Go’s memory model and its significance, especially in concurrent programming.
- Go’s standard library provides several packages for error handling. Explain the significance of the “errors” and “fmt” packages for error generation and how Go 1.13 enhanced error handling.
I hope You liked the post ?. For more such posts, ? subscribe to our newsletter. Check out more terraform interview questions