Skip to content

27 Advanced Golang Backend Interview Questions [2026]

Last Updated August 25, 2026 13 min read

Jump to Category

⚡ Concurrency Core Language & Data Structures
Performance & Optimization ️ System Design & Architecture
Modern Go (1.21 to 1.27)

Concurrency

Comparison table of goroutines and OS threads covering stack size, creation, context switching, practical ceiling and syscall blocking
Goroutines versus OS threads. The distinction interviewers probe is the blocking behaviour, not the stack size.

1. What are goroutines and how do they differ from OS threads?

Goroutines are lightweight threads managed by the Go runtime. They are much cheaper than OS threads, starting with only a few kilobytes of stack space that grows and shrinks as needed. Millions of goroutines can run on a small number of OS threads, making concurrent programming in Go highly efficient.
Read more on Go’s official documentation.

2. Explain the Go scheduler and the concepts of G, M, and P.

The Go scheduler uses a model with three main concepts:

  • G (Goroutine): Represents a single goroutine with its stack and instruction pointer.
  • M (Machine): An OS thread that executes the code.
  • P (Processor): A context required for executing Go code. It can be thought of as a CPU core available to the scheduler. There are `GOMAXPROCS` Ps.
The scheduler’s job is to distribute runnable Gs across available Ms and Ps to achieve concurrency.
Explore the scheduler details in the Go source.

Diagram of the Go scheduler showing three logical processors, each with a local run queue and a bound OS thread, above a shared global run queue
The G, M and P model. GOMAXPROCS sets the number of P; work stealing rebalances goroutines when a local queue runs dry.

3. When would you use channels vs. mutexes?

Use **channels** when you need to communicate data or orchestrate execution between goroutines. They are the idiomatic Go way: “Do not communicate by sharing memory; instead, share memory by communicating.” Use **mutexes** (`sync.Mutex`) when you need to protect a shared piece of memory from concurrent access within a single goroutine or when the logic is too complex for a channel-based approach.
See Effective Go’s section on concurrency.

4. How does the `select` statement work?

A `select` statement blocks until one of its cases can run. If multiple cases are ready simultaneously, it chooses one at random to proceed. This prevents starvation and allows a goroutine to wait on multiple communication operations. A `default` case can be added to make the `select` non-blocking.
Try it on the Go Tour.

5. What is the purpose of the `context` package?

The `context` package is used to manage cancellation signals, deadlines, and request-scoped values across API boundaries and between goroutines. It’s essential for controlling long-running operations, such as database queries or HTTP requests, especially for implementing timeouts and graceful shutdowns.
View the context package documentation.

6. What are some common concurrency patterns in Go?

Common patterns include:

  • Generator: A function that returns a channel, pushing a sequence of values to it.
  • Worker Pool: A fixed number of goroutines processing tasks from a shared channel.
  • Fan-in, Fan-out: Distributing work to multiple goroutines (fan-out) and collecting the results into a single channel (fan-in).
  • Rate Limiting: Using a ticker or a token bucket to control the frequency of operations.

Read the Go Blog post on pipelines.

Core Language & Data Structures

7. What is the difference between `new()` and `make()`?

`new(T)` allocates memory for a new item of type `T`, zeroes the memory, and returns a pointer to it (`*T`). `make(T, …)` is only used for creating slices, maps, and channels. It initializes the internal data structures of these types and returns an initialized (not zeroed) value of type `T` (not `*T`).
See Effective Go’s explanation.

8. Explain slices and how they relate to arrays.

An **array** is a fixed-size sequence of elements of a particular type. A **slice** is a flexible, dynamically-sized view into the elements of an array. A slice is a struct containing three fields: a pointer to the underlying array, a length, and a capacity. Slices are much more common and versatile in Go than arrays.
Read the Go Blog post on slices.

9. What are interfaces and why are they powerful in Go?

An interface is a type that specifies a set of method signatures. A type implements an interface by implementing its methods, with no `implements` keyword needed (this is called structural typing). This allows for writing flexible, decoupled code, making it easy to create mocks for testing and build generic systems. The empty interface, `interface{}`, can hold a value of any type.
Learn about interfaces on the Go Tour.

10. How does the `defer` statement work?

A `defer` statement pushes a function call onto a list. The list of saved calls is executed after the surrounding function returns. `defer` is commonly used to simplify functions that perform clean-up operations, such as closing files or unlocking mutexes, ensuring that the cleanup code runs regardless of how the function exits.
Read about defer, panic, and recover.

11. What is struct embedding?

Struct embedding is Go’s approach to composition, allowing you to include one struct type within another. The methods and fields of the embedded struct are “promoted” to the containing struct, making them directly accessible. It’s a way to achieve code reuse and is often preferred over classical inheritance.
See Effective Go on embedding.

12. What is the best practice for error handling?

In Go, errors are values. The idiomatic approach is for functions to return an `error` as their last return value. The caller is expected to check if the error is non-`nil`. Since Go 1.13, the `errors` package supports wrapping errors to add context (`fmt.Errorf` with `%w`) and inspecting error chains with `errors.Is` and `errors.As`.
Learn about the error handling improvements in Go 1.13.

13. What are build tags (or build constraints)?

Build tags are comments placed at the top of a Go source file that control when the file is included in a build. They allow you to compile different code for different operating systems, architectures, or custom build modes (e.g., enabling integration tests). The syntax is `//go:build tag`.
Read the official documentation on build constraints.

14. How do Go modules work?

Go modules are how Go manages dependencies. A module is a collection of Go packages stored in a file tree with a `go.mod` file at its root. The `go.mod` file defines the module’s path and its dependency requirements. The `go` tool uses this file to download dependencies and ensure reproducible builds.
Read the intro to Go Modules.

Performance & Optimization

15. How does Go’s garbage collector (GC) work?

Go uses a concurrent, tri-color mark-and-sweep collector that runs alongside the user program, with stop-the-world pauses typically under a millisecond. Since Go 1.26 the default implementation is the Green Tea collector, which reorganises marking and scanning around memory locality rather than individual objects. The Go team measured a 10 to 40% reduction in GC overhead for programs that lean on the collector, with a further gain of roughly 10% on newer amd64 hardware (Intel Ice Lake, AMD Zen 4 and later) where it uses vector instructions to scan small objects. Green Tea shipped as an experiment in Go 1.25, became the default in 1.26, and the opt-out flag was removed in 1.27. If a candidate describes the GC without mentioning this, they are working from pre-2026 material.
Read the official Go GC Guide.

16. What is escape analysis?

Escape analysis is a compile-time process that determines whether a variable can be allocated on the function’s stack or must be “escaped” to the heap. Stack allocation is much faster and avoids GC overhead. A variable escapes if its lifetime extends beyond the function’s return, such as when its pointer is returned or captured in a closure.
View the Go diagnostics documentation.

17. How would you profile and optimize a Go application?

Go has excellent built-in tooling for profiling. I would use the `pprof` tool to capture and analyze:

  • CPU profiles: To find functions consuming the most CPU time.
  • Heap profiles: To analyze memory allocation and find potential leaks.
  • Goroutine profiles: To debug blocked or leaked goroutines.
Two additions are worth knowing. Profile-guided optimization (PGO), stable since Go 1.21, feeds a production CPU profile back into the compiler so it can inline and devirtualise along the hot path; typical programs see 2 to 14% improvement for the cost of committing a default.pgo file. Go 1.27 added a goroutineleak profile that reports goroutines blocked on a concurrency primitive that can never be unblocked, which turns the hardest class of Go leak into a diagnosable one. Beyond tooling, optimization is still reducing allocations, fixing algorithms, and applying concurrency where it actually helps.
Learn about profiling Go programs.

18. What is the purpose of `sync.Pool`?

A `sync.Pool` is a concurrent-safe pool of temporary objects. Its purpose is to reuse objects to reduce pressure on the garbage collector. It’s particularly useful for managing large numbers of short-lived objects, like buffers for I/O operations, but should be used only after profiling has identified a performance bottleneck due to allocations.
Read the sync.Pool documentation.

System Design & Architecture

19. How would you implement a graceful shutdown in a Go web server?

To implement a graceful shutdown, you listen for an OS interrupt signal (like `SIGINT`). Upon receiving the signal, you call the `http.Server.Shutdown()` method. This method gracefully shuts down the server without interrupting any active connections. It stops accepting new requests and waits for existing ones to complete, using a `context` for a timeout.
See the http.Server.Shutdown documentation.

20. How do you handle database connection pooling?

Go’s standard `database/sql` package handles connection pooling automatically. When you call `sql.Open()`, you get a handle (`*sql.DB`) that represents a pool of connections. You can configure the pool’s behavior with methods like `SetMaxOpenConns`, `SetMaxIdleConns`, and `SetConnMaxLifetime` to optimize for your application’s workload.
Read the database/sql package documentation.

21. What are the pros and cons of using Go for microservices?

Pros:

  • Excellent performance and low memory footprint.
  • Built-in concurrency primitives make handling many requests easy.
  • Fast compile times and static binary deployment simplify CI/CD.
  • A strong standard library for networking (`net/http`).
Cons:
  • Less mature ecosystem compared to Java or Python.
  • Verbose error handling can be tedious for some developers.
  • Lack of generics before Go 1.18 made some reusable code harder to write.

Read about Go for Microservices.

22. How do you work with JSON in Go?

The standard library’s encoding/json package handles JSON. json.Marshal converts a Go value into a JSON byte slice, json.Unmarshal parses one back, and struct field tags such as json:"fieldName" map fields to keys. Go 1.27 introduced encoding/json/v2, a revision with stricter defaults, materially faster unmarshaling and variadic options for behaviour that previously required custom marshalers. The v1 package is not going away, so the useful interview answer is knowing that v2 exists, that its defaults differ deliberately, and that mixing the two in one codebase needs care.
Read the Go blog post on JSON.

23. How would you design a rate limiter for a backend API?

A simple in-memory rate limiter can be built using a map to store timestamps for each user/IP and a mutex for safe concurrent access. For a more robust and efficient solution, the `golang.org/x/time/rate` package provides a token bucket algorithm implementation. For a distributed system, an external store like Redis would be used to maintain state across multiple service instances, often using atomic operations.
Explore the rate package.

Modern Go (1.21 to 1.27)

Timeline of Go releases from 1.21 in August 2023 to 1.27 in August 2026, listing the headline change in each
Go 1.27 landed on 19 August 2026. Guides written before 2024 predate the loop-variable fix, iterators and the current collector.

24. How do generics work in Go, and when should you avoid them?

Generics arrived in Go 1.18. A function or type takes type parameters constrained by an interface, so func Map[T, U any](s []T, f func(T) U) []U works across element types without interface{} and without reflection. Constraints are ordinary interfaces, and the comparable constraint covers types usable as map keys. Go 1.24 added generic type aliases, and Go 1.27 added generic methods, so a method can now declare its own type parameters rather than forcing the whole type to carry them. Interface methods still cannot. The judgement half of the answer matters more than the syntax: generics earn their place in container types and algorithms over collections, and cost readability when used to avoid writing two concrete functions.
Work through the official generics tutorial.

25. What changed about loop variables in Go 1.22, and why did it matter?

Before Go 1.22 a for loop declared its variables once and reused them across iterations, so for _, v := range items { go func() { use(v) }() } captured a single shared v and every goroutine typically saw the last element. The idiomatic fix was v := v at the top of the body. Go 1.22 changed the semantics so each iteration creates fresh variables, and the bug class disappeared. Two practical consequences: the v := v line is now redundant, and the behaviour depends on the go directive in your go.mod, so a module still declaring an older version keeps the old semantics. It is a favourite interview question precisely because it separates people who learned Go recently from people repeating advice that is now obsolete.
Read the Go team on fixing for loops.

26. What are range-over-function iterators, and what problem do they solve?

Go 1.23 allows for range over a function of the right shape, so a package can expose a sequence without returning a slice or exposing an index-and-next pair. An iterator is a function taking a yield callback: func(yield func(T) bool), where returning false from yield stops iteration and lets the producer run cleanup. The iter package names these shapes as iter.Seq and iter.Seq2, and slices and maps gained functions that produce and consume them. The problem it solves is composition over large or lazily produced sequences: previously you either materialised the whole collection or invented a bespoke iterator interface per package.
See the iter package documentation.

27. How should a Go service handle structured logging?

log/slog, in the standard library since Go 1.21, is the default answer. It logs key and value attributes rather than formatted strings, ships JSON and text handlers, and supports levels and grouped attributes. A logger carrying request-scoped attributes via slog.With propagates them through a request without threading a custom logger type. The reason it matters in an interview is what it replaced: before 1.21 every team picked zap, logrus or zerolog, and library authors could not log without imposing that choice on consumers. A candidate who reaches for a third-party logger should be able to say why, and performance under heavy allocation is a legitimate reason.
Read the log/slog documentation.

Skip the interview marathon.

Browse engineers already pre-vetted with questions like these. Shortlist and hire on the platform, free to start.

Try for Free
WhatsApp