Cartoon Golang Gopher on a table at a Job Interview

Top Golang Interview Questions [2024]

· Lane Wagner

If you’re preparing for a Golang interview, these 25 questions will give you the practice you need to hit the ground running on the technical portion. You need to have already learned the language and (hopefully) have built projects with it, but these questions are designed refresh your memory and expand your knowledge on some of the more esoteric parts of the language.

Cramming practice interview questions is not a substitwute for spending the months it takes to learn it.

As someone who has hired several Go developers, I handpicked these questions (and give you my answers) because I think they are the kinds of questions that are most likely to come up in your interviews. You’ll notice that the questions are open-ended. This is intentional. It’s not about memorizing answers, it’s about understanding the concepts and being able to explain them in your own words.

Questions

Section 1: Basic Concepts

  1. What is Go, and what kinds of projects do you think it’s best suited for?
  2. Explain Go’s type system, and how it differs from other popular languages.
  3. How does Go handle errors? What do you like and dislike about this approach?
  4. Explain how constants and variables work in Go, and how they differ.
  5. Is Go Object-Oriented? Purely Functional? Somewhere in between?

Section 2: Intermediate Concepts

  1. Explain the basics of Go’s memory management. Compare it to other languages.
  2. How does Go’s interface differ from other languages?
  3. How would you send a deeply nested JSON object in an HTTP request?
  4. Discuss Go’s slice and how it differs from an array.
  5. Let’s say you have 5 valid “color” values: red, green, blue, yellow, and orange. How would you represent this in Go’s type system?

Section 3: Advanced Concepts

  1. What is reflection in Go and how is it used?
  2. What are channels in Go and how do they facilitate concurrency?
  3. When might you use a mutex instead of a channel?
  4. What are some common race conditions in Go and how can they be avoided?
  5. How would you represent a “set” data structure in Go?

Section 4: Tooling

  1. What are some of the unique characteristics of the Go compiler?
  2. How do you manage third-party dependencies in Go?
  3. Explain cross-compilation in Go.
  4. What are some effective tools for debugging memory and CPU performance issues in Go?
  5. How does one write unit tests in Go?

Section 5: Patterns and Best Practices

  1. What are common design patterns used in Go?
  2. What are some common mistakes made by developers new to Go?
  3. How do you write documentation for a Go package?
  4. Can you explain the role of ‘defer’ statements in Go and provide best practice examples for their use in error handling and resource management?
  5. How does Go handle immutability, and what are the best practices for using immutable data structures in Go?

Answers

Section 1: Basic Concepts

1. What is Go, and what kinds of projects do you think it’s best suited for?

Go (or Golang) is a statically typed, compiled programming language known for its simplicity, efficiency, and strong support for concurrency and parallelism, making it a great choice for high-performance applications. Go is particularly well-suited for:

Go’s type system is statically typed, which means the type of a variable is known at compile time. This makes Go more predictable and less prone to runtime errors compared to dynamically typed languages. Key differences include:

No class-based inheritance: Instead of a traditional OOP model, Go uses interfaces and composition. This approach is simpler and more flexible.

type Reader interface {
    Read(p []byte) (n int, err error)
}

Strong typing: Unlike languages like JavaScript, where type coercion is common, Go is more rigid, requiring explicit conversion between types. Go doesn’t have support for more advanced type features like sum types, but generics were added recently in 1.18.

3. How does Go handle errors?

Error handling in Go is explicit and type-safe. It does not use exceptions like many other languages but instead uses multiple return values, one of which (copnventionally the last one) can be an error.

Explicit error checking: After calling a function that can fail, you check if the error is nil before proceeding.

data, err := ioutil.ReadFile("filename.txt")
if err != nil {
    // handle the error
}
// process data

Custom error types: You can create your own error types by implementing the error interface, providing more control over error handling.

4. Explain how constants and variables work in Go, and how they differ.

Constants and variables in Go are basic building blocks of the language, but they serve different purposes:

Constants: Defined using the const keyword, they are immutable values determined at compile time. Constants are used for values that do not change, like mathematical constants or configuration values.

const Pi = 3.14

Variables: Declared using the var keyword, variables are mutable and can change their value over the lifetime of a program. Go also supports type inference for variables using the := syntax, which is frankly the much more popular syntax.

var a int = 10
b := 5 // inferred type

5. Is Go Object-Oriented? Purely Functional? Somewhere in between?

Go is neither purely object-oriented nor purely functional; it takes a pragmatic approach that incorporates features from both paradigms:

Object-Oriented: Go supports encapsulation through packages and structs. However, it does away with typical OOP features like inheritance, instead favoring composition and interface-based polymorphism.

type Animal struct {
    Name string
}

func (a Animal) Speak() string {
    return "My name is " + a.Name
}

Functional Elements: While Go is not a functional language, it does incorporate some functional concepts like first-class functions and closures, enabling functional programming patterns.

add := func(a, b int) int {
    return a + b
}
result := add(2, 3)

Go’s design philosophy emphasizes simplicity and practicality, making it a unique blend of different programming paradigms.

Section 2: Intermediate Concepts

1. Explain the basics of Go’s memory management. Compare it to other languages.

Go’s memory management is primarily handled through an efficient garbage collector. It’s designed to manage memory allocation and reclamation automatically, reducing the likelihood of memory leaks and segmentation faults common in languages like C and C++.

2. How does Go’s interface differ from other languages?

Go’s interfaces are a form of type abstraction different from those found in languages like Java or C#. They are implicitly implemented, meaning a type implements an interface by implementing its methods, not by explicitly declaring it.

Implicit Implementation: This approach makes Go’s interfaces lightweight and flexible, promoting loose coupling and easier maintenance.

type Reader interface {
    Read(p []byte) (n int, err error)
}

type MyReader struct{}

func (m MyReader) Read(p []byte) (n int, err error) {
    // implementation
}
// MyReader implicitly implements Reader

Duck Typing: Go’s philosophy of “if it quacks like a duck, then it’s a duck” applies to its interfaces. If a type satisfies all the methods of an interface, it is considered to implement that interface.

3. How would you send a deeply nested JSON object in an HTTP request?

Sending a deeply nested JSON object in an HTTP request in Go involves marshalling the object into JSON format and then sending it using the net/http package. It’s usually best to define the shape of the object using a struct.

Marshalling: Convert your nested struct to JSON using json.Marshal.

type RequestBody struct {
    // nested fields
}
data, _ := json.Marshal(RequestBody{/* fields */})

HTTP Request: Use the http package to send the request with the JSON payload.

client := &http.Client{}
req, _ := http.NewRequest("POST", "http://example.com", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
client.Do(req)

4. Discuss Go’s slice and how it differs from an array.

Slices in Go are used to work with dynamically sized lists of elements. There are much more common in application code than arrays, and are different from arrays in several key ways:

Dynamic Size: Unlike arrays, slices can be resized. They are essentially references to arrays with a dynamic length, offering more versatility.

arr := [5]int{1, 2, 3, 4, 5} // Array
slc := arr[:3]                // Slice of the first 3 elements

Underlying Array: A slice does not store data itself. It points to an underlying array, making it more efficient for passing around large collections of data without copying them.

5. Let’s say you have 5 valid “color” values: red, green, blue, yellow, and orange. How would you represent this in Go’s type system?

In Go, you can represent a set of predefined values using an enumeration pattern with iota, or simply by defining a custom type.

Using iota for Enumeration:

type Color int

const (
    Red Color = iota
    Green
    Blue
    Yellow
    Orange
)

Custom Type without iota:

type Color string

const (
    Red    Color = "red"
    Green  Color = "green"
    Blue   Color = "blue"
    Yellow Color = "yellow"
    Orange Color = "orange"
)

Both methods define a type Color with a restricted set of valid values, helping with type safety and clarity. Unfortunately, Go does not have a built-in enum or support for sum types, so this is the best we can do. The reason this is suboptimal is because its easy to accidentally pass a value into a function that accepts a Color that is not one of the valid values. Runtime checks can be added to prevent this, but runtime checks are not as good as compile-time checks.

Section 3: Advanced Concepts

1. What is reflection in Go and how is it used?

Reflection in Go, provided by the reflect package, is a powerful tool for inspecting and manipulating objects at runtime. It allows you to examine the type and value of variables, even if their types are unknown in the program’s context. This capability is especially useful in scenarios like serialization and deserialization, creating generic functions, and working with data structures dynamically.

import "reflect"

func inspectType(value interface{}) {
    t := reflect.TypeOf(value)
    fmt.Println("Type:", t.Name())
    fmt.Println("Kind:", t.Kind())
}

// Usage
inspectType(42)

The code snippet demonstrates basic reflection usage. The function inspectType takes an interface{} as input, which means it can accept any type. Inside the function, reflect.TypeOf retrieves the type information of the passed value. Reflection is used with caution in Go due to its impact on performance and type safety.

Reflection is often used to add arbitrary tags to struct fields, for example when serializing to JSON. The encoding/json package uses reflection to determine which fields to include in the JSON output.

2. What are channels in Go and how do they facilitate concurrency?

Channels in Go are conduits through which you can send and receive values between goroutines, which enables some pretty cool concurrency patterns. Channels ensure safe and synchronized communication, preventing common concurrency issues like race conditions.

func worker(ch chan int) {
    // Receive value from channel
    value := <-ch
    fmt.Println("Received:", value)
}

func main() {
    ch := make(chan int)
    go worker(ch)
    // Send value to channel
    ch <- 42
    close(ch)
}

In this example, a channel ch is created for transmitting integers. The worker goroutine waits to receive a value from ch. When the main goroutine sends 42 through ch, the worker goroutine receives it and prints the value. Channels are fairly unique to Go, and are a powerful tool for concurrent programming.

3. When might you use a mutex instead of a channel?

Mutexes (mutual exclusion locks) in Go are used to protect shared resources from concurrent access, making them suitable for scenarios where you need to ensure that only one goroutine accesses a specific resource at a time. Use a mutex when you have shared data that goroutines read and modify, and where the communication pattern doesn’t fit well with channels.

import "sync"

var mu &sync.Mutex
var sharedResource int

func worker() {
    mu.Lock()
    // Modify shared resource
    sharedResource += 1
    mu.Unlock()
}

In this example, mu.Lock() and mu.Unlock() are used to safeguard access to sharedResource. This prevents race conditions when multiple goroutines modify sharedResource. Unlike channels that facilitate communication, mutexes are about controlling access to shared data.

4. What are some common race conditions in Go and how can they be avoided?

Race conditions in Go occur when multiple goroutines access and manipulate the same data concurrently, and the outcome depends on the sequence of accesses. Common scenarios include unsynchronized access to global variables and improper usage of channels. The sync package, including Mutexes, and proper channel operations, are key to avoiding these issues.

// Race condition example
var counter int

func increment() {
    counter++
}

func main() {
    go increment()
    go increment()
}

In this code snippet, two goroutines increment a shared counter variable concurrently, leading to a race condition. Using a mutex, atmoic value, or channel to synchronize access to counter can prevent this.

5. How would you represent a “set” data structure in Go?

Go doesn’t have a built-in set type, but you can represent a set using a map with empty structs as values. This approach is memory efficient and provides quick lookups, insertions, and deletions.

func main() {
    set := make(map[int]struct{})
    elements := []int{1, 2, 3, 3, 2, 1}

    for _, e := range elements {
        set[e] = struct{}{}
    }
    
    // Check if 2 is in the set
    if _, exists := set[2]; exists {
        fmt.Println("2 is in the set")
    }
}

In this example, set is a map that uses integers as keys. The presence of a key represents the existence of an element in the set. The struct{} type is used as it occupies zero bytes of memory, making it an efficient way to represent set elements. Sometimes new Go developers use bool values instead of struct{}, but this is less efficient because bool values occupy memory and can be confusing if the truthiness doesn’t matter.

Section 4: Tooling

1. What are some of the unique characteristics of the Go compiler?

The Go compiler is known for its speed and efficiency. It compiles code directly to machine code, bypassing the need for an intermediate bytecode or virtual machine. This direct compilation contributes to the performance of Go applications. Another characteristic is its simplicity and ease of use, with minimalistic command-line options. The Go compiler also performs strict type checking and incorporates a garbage collector, enhancing memory management.

// Example: Simple Go program
package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

To compile the above program, you would simply run go build. The compiler takes care of dependencies and produces an executable specific to your system’s architecture.

The Go compiler is famous for compiling code very fast, as opposed to languages like C++, Rust, and Java that frequently take minutes to compile with larger projects.

2. How do you manage third-party dependencies in Go?

Dependency management in Go is handled through its package management tool, go mod. Introduced in Go 1.11, it allows you to define, track, and manage dependencies in a Go project. The go.mod file in your project directory declares the module’s dependencies, and Go automatically downloads and manages these dependencies.

// go.mod file example
module mymodule

go 1.16

require (
    github.com/some/dependency v1.2.3
    github.com/another/dependency v0.1.0
)

This go.mod file declares dependencies with specific versions. When you build your project, Go retrieves these dependencies, ensuring version consistency and reproducible builds. The go get command can be used to add new dependencies to your project, and Go is tightly integrated with Git which means there is no centralized repository like NPM, we just download source code from the remote Git repository.

3. Explain cross-compilation in Go.

Cross-compilation in Go refers to building executables for a different platform than the one you are compiling on. This feature is inherent to Go, making it easy to develop applications that can run on multiple architectures and operating systems. You can specify the target OS and architecture using environment variables GOOS and GOARCH.

# Cross-compile for Windows on a Linux or macOS machine
GOOS=windows GOARCH=amd64 go build -o myapp.exe myapp.go

This command compiles a Go program for Windows, regardless of the OS on which the build command is run. This is actually a really powerful feature, there are other ecosystems where compilation for a system that isn’t the same as the host system is a huge pain.

4. What are some effective tools for debugging memory and CPU performance issues in Go?

Go provides several built-in tools for profiling and debugging, such as pprof for CPU and memory profiling. This tool can analyze the runtime performance of a Go application and identify bottlenecks.

import _ "net/http/pprof"

// In your main function
go func() {
    log.Println(http.ListenAndServe("localhost:6060", nil))
}()

By including net/http/pprof, you can start a server that provides profiling data accessible through standard HTTP endpoints. Other tools like trace and benchmarks created with the testing package can also be useful for performance analysis. Many developers use graphviz to visualize the output of pprof as a graph.

5. How does one write unit tests in Go?

In Go, unit tests are written in the same package as the code being tested. The testing is done using the testing package. A test file is named with the _test.go suffix, and each test function starts with Test.

// Example: simple_test.go
package simple

import "testing"

func TestSum(t *testing.T) {
    result := Sum(2, 3)
    if result != 5 {
        t.Errorf("Sum was incorrect, got: %d, want: %d.", result, 5)
    }
}

This snippet shows a basic test for a hypothetical Sum function. To run tests, you use the go test command in the directory of your package. I like to use table-driven tests (see Dave Cheney’s article), which just means that I write my test cases as slices of structs, and then iterate over them in a loop. This makes it easy to add new test cases, and it makes the test code more readable.

Section 5: Patterns and Best Practices

1. What are common design patterns used in Go?

Go developers generally treasure simplicity over all else, so the design patterns in Go projects tend to be simple and straightforward. For example, hastily adding abstraction around a concrete type like a struct is generally frowned upon. Interfaces and generics tend to only be added as needed. They are much more common in library code than application code.

2. What are some common mistakes made by developers new to Go?

Some common mistakes include:

Another mistake is not following idiomatic Go practices like capitalizing names for exported functions, variables, etc., and ignoring the powerful standard library in favor of reinventing the wheel.

// Common mistake: Ignoring error handling
result, _ := someFunctionThatReturnsError()

Ignoring error handling, as shown in the snippet, can lead to undetected issues in your program.

3. How do you write documentation for a Go package?

Documentation in Go is written as comments directly preceding the declarations of functions, types, constants, and variables. The godoc tool parses these comments and creates documentation. Comments should be complete sentences and the first sentence should be a summary that starts with the name being declared.

// Sum calculates the sum of two integers.
func Sum(a, b int) int {
    return a + b
}

In this example, Sum has a descriptive comment that will appear in godoc. If you publish your code to GitHub, you can use pkg.go.dev to automatically generate documentation for your package.

4. Can you explain the role of ‘defer’ statements in Go and provide best practice examples for their use in error handling and resource management?

The defer statement in Go postpones the execution of a function until the surrounding function returns. It’s often used for cleanup tasks like closing files or network connections. In error handling, defer can be used to ensure that necessary cleanup occurs even when an error causes early return from a function.

func readFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close()

    // Process file
    return nil
}

This snippet demonstrates the use of defer to close a file, ensuring the file is closed regardless of how the function returns. Deferred functions are executed in LIFO order, so the last defer statement is executed first, which can be useful for resource management, but also has caused some confusion for new Go developers. Deferring functions inside loops can also be a common source of errors. In those cases, you usually want to break the body of the loop out into a separate function, and then defer the function call.

5. How does Go handle immutability, and what are the best practices for using immutable data structures in Go?

Go doesn’t have built-in support for immutable data structures (other than const primitives) like some other languages. However, immutability can be achieved through design patterns and practices. One common approach is to pass copies of objects rather than references. Another is to use unexported fields in structs, providing exported methods that don’t modify the internal state.

type ImmutableStruct struct {
    field int
}

func NewImmutableStruct(field int) ImmutableStruct {
    return ImmutableStruct{field: field}
}

func (i ImmutableStruct) Field() int {
    return i.field
}

In this example, ImmutableStruct has an unexported field field, making it immutable from outside the package. This kind of struct design enforces immutability in Go.

Best of luck with your upcoming interviews!