Backend Developer (Go) Interview Questions: Complete Guide

Milad Bonakdar
Author
Master Go backend development with essential interview questions covering concurrency, interfaces, error handling, and system design. Perfect preparation for Go developer interviews.
Introduction
Go (Golang) has become a dominant language for building scalable backend systems, microservices, and cloud-native applications. Its simplicity, strong concurrency model, and performance make it a top choice for modern engineering teams.
This guide covers essential interview questions for Backend Developers specializing in Go. We explore core language concepts, concurrency patterns, error handling, and system design to help you ace your next interview.
Core Go Concepts
1. What makes Go different from other languages like Java or Python?
Answer: Go was designed by Google to address the challenges of large-scale software development. Key differences include:
- Simplicity: Go has a small keyword set and lacks complex features like inheritance or method overloading, prioritizing readability.
- Concurrency: First-class support for concurrency via Goroutines and Channels, making it easier to write scalable concurrent programs.
- Compilation: Go compiles directly to machine code (statically linked binaries), offering fast startup and execution speeds without a virtual machine (JVM).
- Garbage Collection: Efficient garbage collection optimized for low latency.
Rarity: Common Difficulty: Easy
2. Explain the difference between Arrays and Slices.
Answer:
- Arrays: Fixed-size sequences of elements of the same type. The size is part of the type (e.g.,
[5]intis different from[10]int). They are value types; assigning one array to another copies all elements. - Slices: Dynamic, flexible views into an underlying array. They consist of a pointer to the array, a length, and a capacity. Slices are reference-like; passing a slice to a function allows modification of the underlying elements without copying the entire data.
Rarity: Common Difficulty: Easy
3. How do Interfaces work in Go? What is implicit implementation?
Answer: Interfaces in Go are collections of method signatures.
- Implicit Implementation: Unlike Java or C#, a type does not explicitly declare that it implements an interface (no
implementskeyword). If a type defines all the methods declared in an interface, it automatically implements that interface. - Duck Typing: "If it walks like a duck and quacks like a duck, it's a duck." This decouples definition from implementation, making code more flexible and easier to mock for testing.
Rarity: Common Difficulty: Medium
4. What is the defer keyword and how does it work?
Answer:
defer schedules a function call to be run immediately before the function returns. It is commonly used for resource cleanup, such as closing files, unlocking mutexes, or closing database connections.
- LIFO Order: Deferred calls are executed in Last-In-First-Out order.
- Arguments Evaluation: Arguments to deferred functions are evaluated when the
deferstatement is executed, not when the call runs.
Example:
func readFile(filename string) {
f, err := os.Open(filename)
if err != nil {
return
}
defer f.Close() // Ensures file is closed when function exits
// ... process file
}Rarity: Common Difficulty: Easy
Concurrency
5. Explain Goroutines and how they differ from OS threads.
Answer:
- Goroutines: Lightweight threads managed by the Go runtime. They start with a small stack (e.g., 2KB) that grows and shrinks dynamically. Thousands of goroutines can run on a single OS thread.
- OS Threads: Managed by the kernel, have fixed large stacks (e.g., 1MB), and context switching is expensive.
- M:N Scheduling: The Go runtime multiplexes M goroutines onto N OS threads, handling scheduling efficiently in user space.
Rarity: Very Common Difficulty: Medium
6. What are Channels? Buffered vs. Unbuffered?
Answer: Channels are typed conduits that allow goroutines to communicate and synchronize execution.
- Unbuffered Channels: Have no capacity. A send operation blocks until a receiver is ready, and vice versa. They provide strong synchronization.
- Buffered Channels: Have a capacity. A send operation only blocks if the buffer is full. A receive operation blocks only if the buffer is empty. They decouple the sender and receiver to some extent.
Rarity: Common Difficulty: Medium
7. How do you handle Race Conditions in Go?
Answer: A race condition occurs when multiple goroutines access shared memory concurrently, and at least one access is a write.
- Detection: Use the built-in race detector:
go run -raceorgo test -race. - Prevention:
- Channels: "Do not communicate by sharing memory; instead, share memory by communicating."
- Sync Package: Use
sync.Mutexorsync.RWMutexto lock critical sections. - Atomic Operations: Use
sync/atomicfor simple counters or flags.
Rarity: Common Difficulty: Hard
8. What is the select statement used for?
Answer:
The select statement lets a goroutine wait on multiple communication operations. It blocks until one of its cases can run, then it executes that case. If multiple are ready, it chooses one at random.
- Timeouts: Can be implemented using
time.After. - Non-blocking Operations: A
defaultcase makes the select non-blocking if no other case is ready.
Example:
select {
case msg := <-ch1:
fmt.Println("Received", msg)
case ch2 <- "hello":
fmt.Println("Sent hello")
case <-time.After(1 * time.Second):
fmt.Println("Timeout")
}Rarity: Medium Difficulty: Medium
Error Handling & Robustness
9. How does Error Handling work in Go?
Answer:
Go treats errors as values. Functions return an error type (usually as the last return value) instead of throwing exceptions.
- Check Errors: Callers must explicitly check if the error is
nil. - Custom Errors: You can create custom error types by implementing the
errorinterface (which has a singleError() stringmethod). - Wrapping: Go 1.13 introduced error wrapping (
fmt.Errorf("%w", err)) to add context while preserving the original error for inspection (usingerrors.Isanderrors.As).
Rarity: Common Difficulty: Easy
10. What are Panic and Recover? When should you use them?
Answer:
- Panic: Stops the ordinary flow of control and begins panicking. It's similar to an exception but should be reserved for unrecoverable errors (e.g., nil pointer dereference, index out of bounds).
- Recover: A built-in function that regains control of a panicking goroutine. It is only useful inside a
deferfunction. - Usage: Generally discouraged for normal control flow. Use
errorvalues for expected error conditions. Panic/recover is mostly used for truly exceptional situations or inside libraries/frameworks to prevent a crash from bringing down the entire server.
Rarity: Medium Difficulty: Medium
System Design & Backend
11. How would you structure a Go web application?
Answer: While Go doesn't enforce a structure, a common standard is the "Standard Go Project Layout":
/cmd: Main applications (entry points)./pkg: Library code that's ok to use by external applications./internal: Private application and library code (enforced by Go compiler)./api: OpenAPI/Swagger specs, protocol definitions./configs: Configuration files.- Clean Architecture: Separating concerns into layers (Delivery/Handler, Usecase/Service, Repository/Data) to make the app testable and maintainable.
Rarity: Common Difficulty: Medium
12. How does the context package work and why is it important?
Answer:
The context package is essential for managing request-scoped values, cancellation signals, and deadlines across API boundaries and goroutines.
- Cancellation: If a user cancels a request, the context is canceled, and all spawned goroutines doing work for that request should stop to save resources.
- Timeouts:
context.WithTimeoutensures database queries or external API calls don't hang forever. - Values: Can carry request-specific data like User ID or Auth tokens (use sparingly).
Rarity: Very Common Difficulty: Hard
13. What is Dependency Injection and how is it done in Go?
Answer: Dependency Injection (DI) is a design pattern where an object receives other objects that it depends on.
- In Go: Usually implemented by passing dependencies (like a database connection or a logger) into a struct's constructor or factory function, often via interfaces.
- Benefits: Makes code more modular and testable (easy to swap real DB with a mock).
- Frameworks: While manual DI is preferred for simplicity, libraries like
google/wireoruber-go/digexist for complex graphs.
Rarity: Medium Difficulty: Medium
Database & Tools
14. How do you handle JSON in Go?
Answer:
Go uses the encoding/json package.
- Struct Tags: Use tags like
`json:"field_name"`to map struct fields to JSON keys. - Marshal: Converts a Go struct to a JSON string (byte slice).
- Unmarshal: Parses JSON data into a Go struct.
- Streaming:
json.Decoderandjson.Encoderare better for large payloads as they process streams of data.
Rarity: Common Difficulty: Easy
15. What are some common Go tools you use?
Answer:
go mod: Dependency management.go fmt: Formats code to standard style.go vet: Examines code for suspicious constructs.go test: Runs tests and benchmarks.pprof: Profiling tool for analyzing CPU and memory usage.delve: Debugger for Go.
Rarity: Common Difficulty: Easy
Advanced Topics & Best Practices
16. What are Generics in Go and when should you use them?
Answer: Generics (introduced in Go 1.18) allow you to write functions and data structures that work with any of a set of types, rather than a specific type.
- Type Parameters: Defined using square brackets
[]. e.g.,func Map[K comparable, V any](m map[K]V) ... - Constraints: Interfaces that define the set of permissible types (e.g.,
any,comparable). - Usage: Use them to reduce code duplication for algorithms that apply to multiple types (like sorting, filtering, or data structures like Sets/Trees). Avoid overuse; if an Interface suffices, use that.
Rarity: Common Difficulty: Medium
17. Explain Table-Driven Tests in Go.
Answer: Table-driven testing is a preferred pattern in Go where test cases are defined as a slice of structs (the "table"). Each struct contains the input arguments and the expected output.
- Benefits:
- Clean separation of test logic and test data.
- Easy to add new test cases (just add a row to the table).
- Clear failure messages showing exactly which input failed.
- Example:
func TestAdd(t *testing.T) {
tests := []struct {
a, b, expected int
}{
{1, 1, 2},
{2, -2, 0},
}
for _, tt := range tests {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d): expected %d, got %d", tt.a, tt.b, tt.expected, result)
}
}
}Rarity: Common Difficulty: Easy
18. What is the Middleware Pattern in Go HTTP servers?
Answer:
Middleware is a function that wraps an http.Handler to perform pre- or post-processing logic before passing control to the next handler.
- Signature:
func(next http.Handler) http.Handler - Use Cases: Logging, Authentication, Panic Recovery, Rate Limiting, CORS.
- Chaining: Middleware can be chained together (e.g.,
Log(Auth(Handler))).
Example:
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println(r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}Rarity: Very Common Difficulty: Medium
19. How do you implement Graceful Shutdown in a Go server?
Answer: Graceful shutdown ensures that a server stops accepting new requests but finishes processing active requests before exiting.
- Mechanism:
- Listen for OS signals (SIGINT, SIGTERM) using
os/signal. - Create a
context.WithTimeoutto allow a cleanup window (e.g., 5-10 seconds). - Call
server.Shutdown(ctx)on thehttp.Server. - Close DB connections and other resources.
- Listen for OS signals (SIGINT, SIGTERM) using
- Importance: Prevents data loss and client errors during deployments.
Rarity: Common Difficulty: Hard
20. When should you use sync.Map instead of a regular map with a Mutex?
Answer:
sync.Map is a concurrent-safe map implementation in the standard library.
- Use Cases:
- Cache Contention: When the entry for a given key is only ever written once but read many times (e.g., lazy-loading caches).
- Disjoint Keys: When multiple goroutines read, write, and overwrite entries for disjoint sets of keys.
- Trade-off: For general use cases (frequent read/write updates), a regular
mapprotected by async.RWMutexis often faster and has better type safety (sincesync.Mapusesany).
Rarity: Uncommon Difficulty: Hard




