Go Interview Questions

Master your next Go interview with our comprehensive collection of questions and expert-crafted answers. Get prepared with real scenarios that top companies ask.

Find mentors at
Airbnb
Amazon
Meta
Microsoft
Spotify
Uber

Master Go interviews with expert guidance

Prepare for your Go interview with proven strategies, practice questions, and personalized feedback from industry experts who've been in your shoes.

Thousands of mentors available

Flexible program structures

Free trial

Personal chats

1-on-1 calls

97% satisfaction rate

Study Mode

Choose your preferred way to study these interview questions

1. What is the difference between `:=` and `=` in Go?

In Go, := is used for declaring and initializing new variables in one step, and it's usually used inside functions. For instance, x := 10 both declares a new variable x and assigns it the value of 10. On the other hand, = is used for assigning a new value to an already declared variable. For example, if you have var y int, you can later use y = 5 to assign a value to y. So, := is for declaration and assignment, while = is for assignment only.

2. How can you check if a key exists in a Go map?

You can check if a key exists in a Go map by using the value, ok idiom. When you access a value from a map, you can also take a second return value which is a boolean indicating whether the key exists. Here's a quick example:

go value, ok := myMap["someKey"] if ok { // Key exists fmt.Println("Value:", value) } else { // Key does not exist fmt.Println("Key not found") }

This idiom is a concise and common way to handle key existence checks in Go.

3. How do you convert a string to an integer in Go?

To convert a string to an integer in Go, you can use the strconv package, specifically the Atoi function. The Atoi function takes a string and returns an integer and an error. It's as simple as:

```go import ( "strconv" )

str := "123" num, err := strconv.Atoi(str) if err != nil { // Handle the error fmt.Println("Error converting string to int:", err) } else { fmt.Println("Converted number:", num) } ```

If you need to handle different bases or larger integers, the ParseInt function from the same package might be more suitable.

No strings attached, free trial, fully vetted.

Try your first call for free with every mentor you're meeting. Cancel anytime, no questions asked.

Nightfall illustration

4. What is a struct and how do you define it in Go?

A struct in Go is a composite data type that groups together zero or more fields with different types into a single entity. It's a way to create more complex data structures by combining simpler types. You define a struct using the type and struct keywords.

Here's a basic example:

go type Person struct { Name string Age int }

In this example, Person is a struct with two fields: Name of type string and Age of type int. You can then create instances of this struct and use them in your code to represent a person.

5. What are the advantages of using interfaces in Go?

I’d answer this by hitting 3 things: flexibility, decoupling, and testability. Then I’d ground it with a simple example.

For me, the main advantages of interfaces in Go are:

  • Flexibility
    You can write code against behavior, not a concrete type.
    If two types implement the same methods, they can be used interchangeably without changing the caller.

  • Loose coupling
    Interfaces help keep packages and components from knowing too much about each other.
    That makes code easier to change, extend, or swap out later.

  • Better testability
    They make unit testing much easier because you can replace a real dependency with a fake or mock implementation.

  • Cleaner design
    In Go, small interfaces usually work best.
    They let you express exactly what a piece of code needs, nothing more.

  • Easier extensibility
    You can add new implementations without rewriting the code that already depends on the interface.

A simple example would be a service that sends notifications.

  • Instead of depending directly on an EmailSender
  • You depend on a Sender interface with something like Send()
  • Then you can plug in email, SMS, or push notification implementations

That keeps the service focused on its job, not on how delivery happens. It also makes testing easy, because in a test you can pass in a fake sender and verify the behavior without calling a real external system.

6. What are channels in Go and how are they used?

Channels are Go’s built-in way for goroutines to talk to each other and coordinate work.

The simple idea is:

  • one goroutine sends a value
  • another goroutine receives it
  • that send/receive can also act as synchronization

A basic channel looks like make(chan int).

You use <- for both directions:

  • send: ch <- 42
  • receive: v := <-ch

Why they matter:

  • They let you pass data safely between goroutines
  • They help avoid a lot of manual shared-state locking
  • They make it easier to model pipelines, worker pools, and task coordination

Two common types:

  1. Unbuffered channels
  2. Send blocks until another goroutine receives
  3. Good when you want strict handoff and synchronization

  4. Buffered channels

  5. Created like make(chan int, 10)
  6. Can hold a limited number of values before blocking
  7. Useful when producer and consumer run at different speeds

A few practical uses:

  • fan out work to workers
  • collect results from multiple goroutines
  • signal that something is done
  • stream data through stages of processing

One important point, channels are not a replacement for every mutex. If you are protecting shared mutable state, a mutex can still be the simpler choice. Channels are best when you want to communicate ownership or pass work between concurrent parts of the program.

So in an interview, I’d frame it like this: channels are for communication and coordination between goroutines, using <- to send and receive values, with blocking behavior that helps synchronize concurrent code.

7. Explain the `panic` and `recover` mechanisms in Go.

panic is a built-in function that stops the ordinary flow of control and begins panicking. When a function panics, it immediately halts its execution, and any deferred functions are executed in reverse order. Panics are typically used for unrecoverable errors, like array out-of-bounds access or using a nil pointer.

recover is another built-in function that regains control of a panicking goroutine. It only works if called within a deferred function, allowing the program to continue executing after handling the panic. Essentially, recover can stop a panic and return the value passed to panic, enabling graceful error handling and recovery in exceptional scenarios.

8. What is shadowing in Go, and how can it affect your code?

Shadowing in Go occurs when a variable declared in an inner scope has the same name as a variable declared in an outer scope. This inner variable "shadows" the outer one, meaning that within the inner scope, any reference to that name will access the inner variable, not the outer one.

This can affect your code by causing unexpected behavior if you're not careful. For instance, if you intended to use the outer variable but instead end up using the inner one, you might encounter bugs that are hard to trace. It's usually best to avoid shadowing by using distinct, descriptive variable names, especially in nested scopes.

User Check

Find your perfect mentor match

Get personalized mentor recommendations based on your goals and experience level

Start matching

9. Describe the visibility rules for different entities in a Go program (e.g., variables, functions).

In Go, visibility is determined mainly by the capitalization of the first letter of the entity's name. If a variable, function, or type name is capitalized, it's exported and accessible from other packages. If it starts with a lowercase letter, it's unexported and only accessible within the same package. This rule is really straightforward compared to some other languages where you have to use keywords like 'public', 'private', or 'protected'.

Within a function, variables declared are only accessible within that function, following standard block scoping rules. For nested blocks like if, for, or switch, variables declared inside those blocks are accessible only within the respective block.

10. Explain the purpose of the `defer` statement.

The defer statement in Go is used to ensure that a function call is performed later in a program’s execution, usually for purposes of cleanup. It is commonly used to release resources such as file handles, network connections, or to unlock a mutex. The deferred function will run after the surrounding function completes, regardless of whether it exits normally or through a panic. One useful aspect of defer is that it helps to keep resource cleanup code close to the resource allocation, making the code easier to read and maintain.

11. What happens when you read from a closed channel?

When you read from a closed channel in Go, you receive the zero value for the channel's type along with a boolean value that indicates whether the read was successful. The boolean will be false when the channel is closed. So, for example, if you're reading from a channel of type chan int, you'd get 0 and false if the channel is closed. This allows for safe detection of closed channels without causing a panic or runtime error.

12. How can you share a variable safely between multiple goroutines?

To safely share a variable between multiple goroutines, you can use channels or sync mechanisms from the sync package. Channels are a great way to communicate between goroutines and ensure that the data is accessed in a thread-safe manner. If you need mutable state, you can use a sync.Mutex to lock and unlock access to the variable, preventing race conditions. Essentially, channels are perfect for passing ownership of data, and sync.Mutex is best for safeguarding shared memory access.

13. What are the different types of loops in Go?

In Go, the primary type of loop is the for loop, which is quite versatile. You can use it in several forms. The most straightforward one is the traditional for loop with a counter, like for i := 0; i < 10; i++ {}, which is similar to loops in languages like C or Java.

Go also supports a simplified form of the for loop which acts as a while loop: for condition {}. This continues to loop as long as the condition is true. If you omit the condition entirely, for {}, you get an infinite loop which you can break out of using a break statement.

Another common variant is the range-based for loop, which is used for iterating over slices, maps, channels, or strings: for index, value := range slice {}. This allows you to easily access both the index and the value when looping through elements of a collection.

14. How do you implement the singleton pattern in Go?

In Go, the singleton pattern can be implemented by ensuring only one instance of a struct is created and accessed globally. You typically use the sync.Once construct to make this thread-safe. Here’s a simple way to do it:

```go package main

import ( "fmt" "sync" )

type singleton struct { // Add your fields here }

var instance *singleton var once sync.Once

func GetInstance() *singleton { once.Do(func() { instance = &singleton{} }) return instance }

func main() { s1 := GetInstance() s2 := GetInstance() fmt.Println(s1 == s2) // This will print: true } ```

This ensures that instance is only initialized once, even in a concurrent environment. You call GetInstance to get the single global instance of the struct.

15. What is the purpose of the `copy` function in Go?

The copy function in Go is used to copy elements from one slice to another. It takes two arguments: the destination slice and the source slice. The function copies elements from the source to the destination up to the minimum of the lengths of the two slices. This is useful for efficiently transferring data between slices without needing to manually iterate over elements or allocate additional memory.

16. What is the difference between a method and a function in Go?

In Go, a function is a standalone piece of code that can be called to perform tasks. It's not associated with any data structure. A method, on the other hand, is a function that is defined on a specific type, also known as a receiver. So, in Go, if you define a function with a receiver, it becomes a method associated with that type. For example, if you have a struct named Rectangle, you can define a method on it to calculate the area. This relationship allows for more organized and modular code, especially when dealing with more complex data structures.

17. Explain how Go implements inheritance.

Go doesn't have classical inheritance like in some other languages such as Java or C++. Instead, Go uses composition to achieve similar behavior. You compose types by embedding one struct within another, allowing the embedded struct's fields and methods to be accessed directly from the outer struct. This encourages code reuse and keeps the relationships between types explicit and clear.

For example, you can embed a struct Person inside another struct Employee, and Employee will have access to Person's methods and fields, but you can also add additional fields and methods specific to Employee. This way, Go emphasizes composition over inheritance, following the principle of "prefer composition over inheritance."

18. How do you test your Go code?

In Go, I generally use the built-in testing framework provided by the testing package, which makes writing tests straightforward. I write unit tests by creating a file ending in _test.go alongside the code I'm testing. Within these files, I define functions starting with Test that take a *testing.T parameter. Using methods like t.Error or t.Fatal, I can signal test failures.

For more complex test cases or to check behavior in different scenarios, I use table-driven tests. This involves creating a slice of test cases and looping over them to apply the same logic. I also use the go test command to run my tests and often include flags for code coverage and verbose output. If I need to mock external dependencies, I consider using interfaces and implementing mock types to isolate the code under test.

19. Explain how to use the `context` package in Go.

I’d explain context as Go’s way to carry cancellation, timeouts, and request-scoped metadata through a call chain.

A clean way to structure the answer is:

  1. Start with what context is for.
  2. Mention the core APIs you actually use.
  3. Show how it flows through real code, like HTTP handlers, DB calls, or goroutines.
  4. Call out a couple of best practices.

Here’s how I’d answer it:

context is mainly for controlling the lifetime of work.

I use it for three things:

  • Cancellation, when the caller goes away or no longer needs the work
  • Deadlines and timeouts, so operations do not run forever
  • Request-scoped values, like trace IDs or auth metadata, used sparingly

The usual pattern is to start with a parent context, often context.Background(), or from an incoming request with r.Context(). Then you derive a child context with something like:

  • context.WithCancel
  • context.WithTimeout
  • context.WithDeadline
  • context.WithValue, only when it really is request-scoped metadata

A few practical rules I follow:

  • Pass ctx context.Context as the first parameter
  • Propagate it downward through every call that can block or do I/O
  • If you create a cancelable context, call cancel(), usually with defer cancel()
  • Do not store contexts in structs
  • Do not use WithValue for optional parameters or general dependency passing

A real example would be an HTTP request that triggers a DB query.

  • The handler gets ctx := r.Context()
  • If I want a stricter timeout for the DB call, I create ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
  • I defer cancel()
  • Then I pass that ctx into QueryContext, an RPC client, or another downstream function

If the client disconnects, or the timeout expires, everything using that context can stop early.

Inside the called function, I typically rely on context-aware APIs first, like db.QueryContext(...) or http.NewRequestWithContext(...). If I have my own long-running loop or goroutine, I watch for cancellation with select on ctx.Done() and return ctx.Err().

That is important because it lets goroutines shut down cleanly instead of leaking.

One thing I’d emphasize in an interview, context is not just about timeouts. It is about making cancellation a first-class part of your program, especially in servers, background workers, and distributed systems.

So in short, I use context to make work bounded, cancellable, and traceable across service boundaries.

20. How can you achieve polymorphism in Go?

In Go, polymorphism is mostly done with interfaces.

The nice part is that Go uses implicit interface implementation. You do not have to declare that a type "implements" an interface. If it has the right methods, it fits.

A simple way to explain it:

  • define behavior with an interface
  • let different types implement that behavior
  • write code against the interface, not the concrete type

Example:

  • Shape has Area() float64
  • Circle implements Area()
  • Rectangle implements Area()

Then a function like printArea(s Shape) can accept either one. That is polymorphism in Go, different types, same interface, same calling code.

A few practical notes:

  • Interfaces should usually be small, often just 1 or 2 methods
  • This keeps code flexible and easy to test
  • Polymorphism in Go is more about behavior than inheritance

You can also get a form of polymorphism with generics, but that is different. Generics help when you want reusable code over multiple types. Interfaces are still the go-to tool when you want different types to share behavior.

21. What is a rune in Go?

I’d answer it like this:

A rune in Go is Go’s way of representing a Unicode code point.

A few practical points:

  • rune is just an alias for int32
  • it represents a character value, not the raw UTF-8 bytes
  • it matters because Go strings are stored as bytes, and some characters take more than one byte

Why that’s useful:

  • if you index a string like s[0], you get a byte
  • if you iterate with for _, r := range s, you get runes
  • that makes it much safer when working with non-ASCII text like accented characters, emojis, or other languages

Example:

  • "A" is 1 byte, 1 rune
  • "é" might be 2 bytes, 1 rune
  • "世" is multiple bytes, 1 rune

So in short, use rune when you care about characters as Unicode values, not just raw bytes.

22. Describe how you implement method receivers in Go.

Method receivers in Go are how you define methods on types, enabling you to call methods with syntax like instance.Method(). You can use either value receivers or pointer receivers.

Value receivers operate on a copy of the original value, meaning any modifications within the method don’t affect the original variable. They’re often used when you don’t need to alter the receiver or for small types where the cost of copying is negligible. Pointer receivers, on the other hand, allow you to modify the original value since the method receives a pointer to the instance. This is useful for large structs or when you need to change the receiver's state.

Here's a quick example to illustrate both:

```go type MyType struct { Name string }

func (v MyType) ValueReceiver() { v.Name = "Value" }

func (p *MyType) PointerReceiver() { p.Name = "Pointer" } ```

In the example, calling ValueReceiver() on an instance of MyType won’t change the actual instance’s Name field, while calling PointerReceiver() will.

23. Can you explain the purpose of type assertions in Go?

Type assertions in Go are used to extract the concrete type and value from an interface. If you have a variable of an interface type and you know it actually holds a more specific type, you can use a type assertion to access the underlying type directly. It's really handy when you need to access specific methods or properties that aren't part of the interface but are available on the concrete type.

For example, if you have an interface var i interface{} = "hello", you can assert its string value with s := i.(string). If the type assertion fails (meaning the underlying type isn't what you expected), it will cause a panic unless you use the "comma, ok" idiom: s, ok := i.(string), which allows you to handle the case where the assertion fails more gracefully.

24. How do you cleanly exit a goroutine?

Exiting a goroutine cleanly can be managed through various means, but a common and effective method is by using channels for cancellation signals. You can provide a channel that, when closed or sent a specific value, signals the goroutine to stop its work. Inside the goroutine, you'll typically use a select statement to listen for this signal alongside other tasks. Another approach can be using the context package, particularly with context.WithCancel or context.WithTimeout, which helps propagate cancellation signals via a context. By doing this, you ensure your goroutine can exit gracefully without leaving resources hanging.

25. How do you handle JSON encoding and decoding in Go?

In Go, JSON encoding and decoding is handled using the encoding/json package. To encode a Go object into JSON, you use the json.Marshal function, which returns the JSON encoding of an object as a byte slice. To decode JSON data into a Go object, use the json.Unmarshal function. You'll need to define your Go structs with field tags that specify the JSON key they correspond to. For example, json:"name" tells the decoder to map the JSON key "name" to that field in the struct.

Here's a quick example: ``go type Person struct { Name stringjson:"name"Age intjson:"age"Emails []stringjson:"emails"` }

// Encoding p := Person{"Alice", 30, []string{"[email protected]"}} jsonData, err := json.Marshal(p) if err != nil { log.Fatal(err) }

// Decoding var p2 Person err = json.Unmarshal(jsonData, &p2) if err != nil { log.Fatal(err) } `` This example converts aPersonstruct to JSON and then back to aPerson` struct.

26. What is the `zero value` concept in Go?

The zero value concept in Go refers to the default value that a variable holds if it is declared without an explicit initialization. Essentially, it's the value a type defaults to when it hasn't been assigned a specific value. For instance, the zero value for an int is 0, for a string it's an empty string "", and for pointers, slices, maps, channels, and functions, it’s nil. This ensures that variables always have a well-defined value, which helps to avoid bugs related to uninitialized data.

27. What is the significance of the `main` package and function in Go?

The main package and function in Go are essential for creating executable programs. The main package is a special package that tells the Go compiler that the program is an executable rather than a library. The main function within this package is the entry point of the program; it's where execution begins. If either the package is not named main or the function is missing or improperly defined, the Go compiler will not produce an executable binary.

28. How do you perform benchmarking in Go?

In Go, I use the built-in testing package for benchmarking.

The basic pattern is:

  • Write a function named BenchmarkXxx
  • Give it the signature func(b *testing.B)
  • Put the code under test inside a loop that runs b.N times

Example shape:

  • func BenchmarkMyFunction(b *testing.B) {
  • loop from 0 to b.N
  • call MyFunction()
  • }

A few practical things I pay attention to:

  • Keep setup work outside the timed loop when possible
  • Use b.ResetTimer() if setup has to happen in the benchmark function
  • Use b.StopTimer() and b.StartTimer() if I need to exclude some work
  • Make sure the result is actually used, so the compiler does not optimize it away
  • Benchmark realistic inputs, not just toy cases

To run benchmarks:

  • go test -bench=.
  • Or target one benchmark with something like go test -bench=MyFunction

If I want better numbers, I’ll usually also run:

  • go test -bench=. -benchmem

That gives allocation stats too, which is often just as useful as raw timing when tuning Go code.

29. How do you perform concurrent programming in Go?

In Go, I usually think about concurrency in three layers:

  1. Start work with goroutines
  2. Coordinate with channels or sync
  3. Control lifetime with context

A goroutine is just a lightweight concurrent function. If I want something to run in parallel with the current flow, I start it with go, like go fetchData().

For communication, channels are the Go-native way to pass data between goroutines safely. One goroutine can send with ch <- value, another can receive with v := <-ch. That works really well for pipelines, worker pools, fan-out/fan-in, and signaling completion.

That said, I do not force channels everywhere. I use the right tool for the job:

  • channels for passing work, results, or signals
  • sync.WaitGroup for waiting on a group of goroutines
  • mutexes when I need to protect shared state
  • context.Context for cancellation, timeouts, and request-scoped lifecycle
  • errgroup when I want concurrent tasks with shared cancellation and error handling

A practical example would be calling multiple external APIs at once:

  • spin up a goroutine per API call
  • collect results through a channel, or use errgroup
  • cancel everything if one call fails or the request times out
  • wait for all goroutines to finish cleanly

The big thing in Go is not just starting concurrency, it is managing it safely:

  • avoid goroutine leaks
  • close channels only from the sender side
  • be careful with shared memory
  • always think about cancellation and backpressure

So the short version is, Go concurrency is mostly goroutines plus coordination primitives, with channels as a core pattern, and context to keep everything under control.

30. What is a goroutine and how does it differ from a thread?

A goroutine is Go’s unit of concurrent work.

The simplest way to explain it is:

  • a thread is managed by the operating system
  • a goroutine is managed by the Go runtime
  • many goroutines can run on a much smaller number of OS threads

Key differences:

  • Lightweight
  • Goroutines are much cheaper to create than threads.
  • They start with a very small stack and grow as needed.
  • Because of that, you can often run thousands or even millions of goroutines in one process.

  • Scheduling

  • Threads are scheduled by the OS.
  • Goroutines are scheduled by Go’s runtime scheduler, which maps them onto OS threads.

  • Cost

  • Threads are heavier in terms of memory and context switching.
  • Goroutines have lower overhead, so they’re better for highly concurrent workloads.

  • Communication style

  • With threads, people often think in terms of shared memory and locks.
  • In Go, goroutines are often paired with channels, so the model is more like “share memory by communicating.”

A practical way to say it in an interview:

“A goroutine is a lightweight concurrent task managed by the Go runtime, not the OS directly. Compared to a thread, it uses less memory, has lower scheduling overhead, and can be multiplexed across a smaller set of OS threads. That’s why Go can handle very large numbers of concurrent tasks efficiently.”

31. How do you manage dependencies in a Go project?

I manage dependencies with Go modules, that is the standard approach now.

A clean way to answer this is:

  1. Start with the tool, go.mod and go.sum
  2. Mention the common commands you actually use
  3. Add a bit about version control and keeping things reproducible
  4. If relevant, mention team practices like pinning versions and tidying regularly

In practice, my workflow looks like this:

  • Initialize the module with go mod init
  • Add packages by importing them and running go mod tidy, or use go get when I want a specific version
  • Track direct and transitive dependencies in go.mod
  • Rely on go.sum for checksum verification and reproducible builds

A few commands I use a lot:

  • go get package@version to add, upgrade, or downgrade a dependency
  • go mod tidy to remove unused deps and make sure the module file stays clean
  • go mod verify to check downloaded modules against expected checksums
  • go list -m all when I want to inspect the full module graph

From an engineering standpoint, I try to keep dependencies boring and controlled:

  • Pin versions intentionally, especially for production services
  • Avoid pulling in large libraries for small needs
  • Review transitive dependencies if a package brings in too much
  • Update dependencies in small, testable batches instead of doing massive jumps

If I am working in a company environment, I also care about:

  • Reproducible CI builds
  • Vulnerability scanning
  • Internal proxies or private modules when needed
  • Keeping go.mod tidy so diffs stay readable

So overall, I would say I use Go modules as the source of truth, keep versions explicit, and regularly tidy and verify dependencies so the project stays stable and easy to maintain.

32. Can you explain the difference between a slice and an array in Go?

I’d explain it like this:

  • An array has a fixed size.
  • A slice is a lightweight, flexible view over an underlying array.

A few practical differences:

  • Array size is part of the type.
  • [3]int and [4]int are different types.
  • Slices don’t include the length in the type.
  • []int is just a slice of ints.

Arrays: - Fixed length, decided at declaration. - Values get copied when you assign them or pass them to a function. - Useful when the size is truly known and constant.

Slices: - Much more common in real Go code. - Have a len and cap. - Can grow with append, which may allocate a new underlying array. - Passing a slice passes a small descriptor, not all the elements.

The big mental model: - Array = the actual storage. - Slice = a window into that storage.

So if you change elements through a slice, you’re usually modifying the underlying array data that slice points to.

Example idea: - If you create an array with 5 items, then take arr[1:4], you get a slice of 3 items. - That slice references part of arr, it doesn’t copy those elements by default.

In interviews, I’d usually add this: - Arrays are rare in day-to-day Go. - Slices are the default choice unless you specifically need a fixed-size collection.

33. How do you handle errors in Go?

In Go, I keep error handling explicit and boring, on purpose. That is usually the right tradeoff.

A solid way to answer this is:

  1. Start with Go’s philosophy, explicit errors, not exceptions.
  2. Explain the common pattern, return value, err, check err immediately.
  3. Mention what you actually do with the error, wrap it, return it, log it, or recover if it is truly fatal.
  4. Add a few best practices, like context, sentinel errors, and avoiding noisy logs.

My answer would be:

Go’s error handling is very direct. If something can fail, the function usually returns an error as the last value, and I check it right away.

So the normal flow is:

  • call the function
  • check if err != nil
  • decide whether to:
  • handle it locally
  • return it up the stack
  • wrap it with more context
  • log it, only if this layer owns the logging

I like that because it makes failure paths obvious. There’s no hidden control flow like exceptions flying around.

In practice, I usually follow a few rules:

  • Check errors immediately, don’t let them sit around.
  • Add context when returning them, for example with fmt.Errorf("fetch user: %w", err).
  • Only log an error once, usually near the edge of the system, so we don’t get duplicate logs.
  • Use errors.Is and errors.As when I need to detect specific cases.
  • Reserve panic for truly unrecoverable programmer or system issues, not normal business errors.

A simple example would be:

  • If I call a database method and it fails, I’ll return something like fmt.Errorf("load order %s: %w", id, err).
  • At the HTTP handler layer, I’ll inspect the error and map it to the right response, like 404 for not found or 500 for an unexpected failure.

So overall, I handle errors early, return them with context, and keep the responsibility clear at each layer.

34. How does Go achieve garbage collection?

Go uses a concurrent, mostly non-blocking garbage collector.

At a high level, it works like this:

  • Go tracks objects that are still reachable from roots, things like stack variables, globals, and registers.
  • It uses a tri-color mark-and-sweep approach.
  • Live objects get marked, unreachable ones get reclaimed.

What makes Go nice in practice is that GC work happens alongside your application, not only when the program fully stops.

A simple way to explain it:

  1. Mark phase
  2. The GC starts from root references.
  3. It walks the object graph and marks everything that is still in use.

  4. Sweep phase

  5. Anything not marked is considered garbage.
  6. That memory gets recycled and can be reused.

A key detail is concurrency:

  • Go’s GC runs concurrently with goroutines.
  • It keeps stop-the-world pauses very short.
  • That makes it a good fit for services that care about latency.

There is also a write barrier involved, which helps the GC stay correct even while your program is still updating pointers during the marking phase.

If I were answering this in an interview, I’d keep it practical:

  • Go has a concurrent tri-color mark-and-sweep GC.
  • It minimizes pause times by doing most of the work in the background.
  • It automatically reclaims unreachable heap memory.
  • You can tune behavior with settings like GOGC if needed.

35. Explain the use of pointers in Go.

Pointers in Go are just references to a value in memory.

I usually explain them in 3 simple use cases:

  1. Change the original value
    If you pass a normal variable into a function, Go passes a copy.
    If you pass a pointer, the function can update the original.

Example: - &x gets the address of x - *p reads or writes the value at that address

  1. Avoid copying large data
    For big structs, passing a pointer can be more efficient than copying the whole value around.

  2. Represent optional or shared state
    A pointer can be nil, so it is useful when you need to distinguish between "zero value" and "not set".

A quick mental model: - x := 10, actual value - p := &x, pointer to x - *p = 20, now x is also 20

A couple of practical notes: - Go does not allow pointer arithmetic like C, which keeps things safer. - You will see pointer receivers on methods when a method needs to modify a struct or avoid copying it. - Slices, maps, and channels already behave like reference-type descriptors, so you do not always need pointers with them.

So in practice, pointers in Go are mostly about mutation, efficiency, and expressing intent clearly.

36. How do you implement dependency injection in Go?

In Go, dependency injection is usually pretty simple. Most of the time, I use plain constructors and pass dependencies in explicitly.

The common pattern is:

  1. Define a struct that needs something, like a repo, logger, or client.
  2. Accept those dependencies in a New... constructor.
  3. Store them on the struct.
  4. In main, wire everything together.

Example shape:

  • UserService depends on a Notifier
  • NewUserService(notifier Notifier) takes that dependency
  • main creates EmailNotifier and passes it into UserService

I usually prefer this style because it is:

  • Idiomatic in Go
  • Easy to read
  • Easy to test
  • Explicit, you can see the object graph clearly

One thing I try to be careful about is interfaces. In Go, I do not create an interface just for the sake of DI. I usually define an interface only when:

  • I genuinely need multiple implementations
  • I want to mock a dependency in tests
  • I want to decouple from an external client or package

So instead of making every dependency an interface up front, I keep concrete types where possible and introduce small interfaces at the point of use.

A simple example would be:

  • A Notifier interface with Notify(message string) error
  • An EmailNotifier that implements it
  • A UserService struct with a notifier Notifier field
  • A constructor like NewUserService(n Notifier) *UserService

Then in main, you do the wiring:

  • create emailNotifier
  • pass it to NewUserService
  • use the returned service

For testing, that becomes really clean. I can inject a fake notifier and verify that Notify was called, without touching real email infrastructure.

If the application gets bigger, I still start with manual wiring. For larger systems, you can use tools like Google Wire, but I see that as an optimization for complex dependency graphs, not the default approach.

So my short answer is, in Go I implement dependency injection with explicit constructor injection, minimal interfaces, and manual composition in main. That tends to be the simplest and most idiomatic approach.

37. What is the purpose of the `select` statement in Go?

I’d answer this by doing two things:

  1. Define what select is in one line.
  2. Mention the main use cases, like waiting on multiple channels, timeouts, and cancellation.

Then give a quick practical example.

select is how Go lets a goroutine wait on multiple channel operations at once.

Think of it like channel-aware control flow:

  • It listens to multiple send and receive cases
  • It runs the one that’s ready first
  • If several are ready, Go picks one
  • If none are ready, it blocks, unless you use default

Why it matters:

  • You can coordinate multiple concurrent tasks cleanly
  • You can add timeouts with time.After
  • You can handle cancellation with a done channel or context.Done()
  • You avoid manually polling channels

A simple example would be:

  • wait for a result from results
  • or an error from errs
  • or a timeout signal

Whichever channel becomes ready first is the branch that runs.

So in practice, select is one of the core tools for writing responsive concurrent Go code.

38. What is the purpose of the `init` function in a Go package?

The init function is for package-level startup work.

A clean way to answer this is:

  1. Say when it runs.
  2. Say what it’s used for.
  3. Mention the important constraints.

Example answer:

init runs automatically when a package is loaded, before the package is used, and before main() runs.

It’s typically used for one-time setup like:

  • initializing package state
  • registering handlers, drivers, or metrics
  • loading configuration defaults
  • validating required environment setup

A few important details:

  • You don’t call init yourself
  • It takes no arguments
  • It returns no values
  • A package can have more than one init function
  • It runs after package-level variables are initialized

I usually think of it as a hook for package bootstrap logic. Useful, but best kept small and predictable, so imports don’t trigger surprising behavior.

39. Explain the use of build tags in Go.

I’d explain build tags as Go’s way of selecting which files are part of a build.

A simple way to frame it:

  • They let you compile different code for different environments
  • Common use cases are:
  • OS-specific code, like Linux vs Windows
  • CPU architecture-specific code
  • Optional features, like debug, integration, or enterprise
  • Swapping implementations, like a real dependency vs a mock

In practice, you put a constraint at the top of a Go file, and Go only includes that file when the condition matches.

Example ideas:

  • A file tagged for Linux only gets built on Linux
  • A debug file might add extra logging, but only when you build with a debug tag
  • You can have separate files for amd64 and arm64 implementations

A good real-world example is platform-specific behavior:

  • file_linux.go for Linux logic
  • file_windows.go for Windows logic

Go can often pick those up just from the filename, but build tags give you more control when naming alone is not enough.

One thing I’d mention in an interview is that there are two forms people may see:

  • Modern syntax, //go:build
  • Older syntax, // +build

The modern form is the current standard, but you may still run into the older one in existing codebases.

So overall, build tags are useful for keeping one codebase clean while supporting multiple targets or build modes, without stuffing the code full of runtime if checks.

40. How do you prevent a race condition in Go?

I’d answer this by keeping it practical:

  1. Start with the core idea, race conditions happen when multiple goroutines read and write shared state at the same time.
  2. Then explain the main ways to avoid it in Go.
  3. If you want to sound strong in an interview, mention both prevention and detection, not just locking.

My answer:

In Go, I prevent race conditions by avoiding shared mutable state whenever I can.

The first option is to use channels and pass data between goroutines, instead of having multiple goroutines touch the same variable. That fits Go’s concurrency model really well.

If goroutines do need to share data, I protect the critical section with synchronization primitives like:

  • sync.Mutex for simple exclusive access
  • sync.RWMutex when I have lots of readers and fewer writers
  • sync/atomic for simple counters or flags

I also try to keep the locked section small, so I’m not creating unnecessary contention.

On top of that, I verify it with the race detector during development and testing using go test -race. That catches a lot of accidental concurrent access issues early.

So my rule of thumb is:

  • Prefer message passing with channels
  • Use mutexes when state must be shared
  • Use atomics only for very simple cases
  • Run the race detector to validate it

41. What tools and practices do you use for debugging Go programs?

I usually think about debugging in layers. Start with the fastest signal, then go deeper only if needed.

My usual stack looks like this:

  • Reproduce it reliably
  • First step is making the bug happen on demand
  • If I can turn it into a failing test, even better
  • In Go, table-driven tests are great for locking down edge cases quickly

  • Use the built-in tooling first

  • go test is my default starting point
  • I’ll use targeted test runs, verbose output, and race detection with go test -race
  • For quick experiments, go run is useful, but I prefer tests when possible because they’re repeatable

  • Debug with Delve when I need to inspect execution

  • dlv is my go-to debugger
  • I use it for breakpoints, stepping through code, checking variable state, and understanding goroutine behavior
  • It’s especially helpful when the issue is around control flow or concurrency

  • Add focused logging and instrumentation

  • I use structured, context-rich logs rather than sprinkling random print statements everywhere
  • For request-driven systems, I like including IDs, timings, and key state transitions so it’s easy to trace what happened

  • Check performance and runtime behavior

  • If the problem looks like a leak, slowdown, or CPU spike, I’ll use pprof
  • That helps with heap usage, goroutine counts, contention, and hot paths

  • Lean on Go-specific safety nets

  • Race detector for concurrency bugs
  • Benchmarks when performance regressions are suspected
  • Linters and static analysis to catch obvious issues before runtime

In practice, a typical flow is:

  1. Reproduce the bug.
  2. Write or isolate a failing test.
  3. Run with go test, often with -race if concurrency is involved.
  4. Add a small amount of logging if the path isn’t obvious.
  5. Use dlv if I need to inspect state step by step.
  6. Use pprof if it smells like a performance or memory issue.

That combination usually gives me a pretty fast path from “something’s wrong” to “here’s the root cause.”

Get Interview Coaching from Go Experts

Knowing the questions is just the start. Work with experienced professionals who can help you perfect your answers, improve your presentation, and boost your confidence.

Complete your Go interview preparation

Comprehensive support to help you succeed at every stage of your interview journey

Still not convinced? Don't just take our word for it

We've already delivered 1-on-1 mentorship to thousands of students, professionals, managers and executives. Even better, they've left an average rating of 4.9 out of 5 for our mentors.

Find Go Interview Coaches