Skip to content

Cookies on Jobverse

We use cookies that are needed to sign you in and keep the site working. With your permission we’d also use an analytics cookie to count job views, and Google advertising cookies to show ads. The site works the same whatever you choose. Read the Cookie Policy.

Jobverse
Sign in

Go Backend Developer Top 100 Interview Questions

100 Go backend developer interview questions with structured answers, comparison tables, flow diagrams, worked calculations and runnable code. Covers the language core, goroutines and channels, context, the standard HTTP and database packages, testing, profiling, performance and system design, from the basics to production incidents.

100 questions

Questions 1-20 of 100

  1. Q1.What does a Go backend developer own beyond writing HTTP handlers?

    A Go backend developer owns services end to end: their design, correctness, performance and behavior in production. Handlers are a small part; most of the work is in how the service handles concurrency, data, failure and operations.

    Typical responsibilities:

    • API design: clear contracts, validation, versioning and consistent error responses.
    • Concurrency: goroutines, channels and shared state that are correct under load and never leak.
    • Data access: database queries, transactions, connection pools and caching.
    • Reliability: timeouts, cancellation through context, retries and graceful shutdown.
    • Performance: profiling, reducing allocations and sizing the service for its traffic.
    • Testing: unit, integration and race-detector runs in continuous integration.
    • Observability: structured logs, metrics, traces and health checks.
    • Delivery: small, secure binaries and images, built reproducibly.

    Go rewards simple, explicit code, so a good Go developer also keeps the codebase small, readable and free of unnecessary abstraction.

  2. Q2.Why is Go a popular choice for backend services?

    Go combines fast, compiled programs with a simple language, built-in concurrency and a strong standard library, which suits network services that must be reliable and easy to operate.

    Main strengths:

    • Single static binaries: a service compiles to one executable with no runtime to install, which makes deployment and containers simple.
    • Lightweight concurrency: goroutines and channels make it practical to handle many thousands of concurrent connections.
    • Fast compilation: quick builds keep development and continuous integration fast.
    • Standard library: HTTP servers and clients, JSON, cryptography, testing and profiling are built in.
    • Predictable performance: compiled code with a low-latency garbage collector.
    • Simplicity: a small language with one standard formatter, so codebases look consistent across teams.

    Trade-offs:

    • Fewer abstractions mean more explicit, sometimes repetitive code, such as error checks.
    • Generics are deliberately limited compared with some languages.
    • Garbage collection still needs attention for very latency-sensitive or memory-heavy services.
  3. Q3.How do zero values work in Go, and why do they matter?

    Every variable in Go starts with a zero value when it is declared without an explicit value: 0 for numbers, "" for strings, false for booleans and nil for pointers, slices, maps, channels, functions and interfaces. Structs get the zero value of each field.

    Why they matter:

    • No uninitialized memory: a declared variable is always in a defined state.
    • Useful zero values: well-designed types work without a constructor, such as sync.Mutex, bytes.Buffer and strings.Builder.
    • Simpler code: appending to a nil slice and reading from a nil map both work.

    Traps to know:

    1. Writing to a nil map panics; maps must be created with make or a literal before writes.
    2. A nil pointer field causes a panic when dereferenced.
    3. You cannot tell an explicit 0 from "not set"; use a pointer or a separate flag when the difference matters, such as in partial updates.

    Designing your own types so their zero value is ready to use is considered idiomatic Go.

    Example

    A zero-value struct is safe to read and append to, but writing to its nil map panics.

    package main
    
    import "fmt"
    
    type Order struct {
    	ID     int
    	Status string
    	Paid   bool
    	Items  []string
    	Meta   map[string]string
    }
    
    func main() {
    	var o Order
    	fmt.Printf("%d %q %t %v %v\n", o.ID, o.Status, o.Paid, o.Items == nil, o.Meta == nil)
    	o.Items = append(o.Items, "book")
    	fmt.Println(len(o.Items))
    	fmt.Println(o.Meta["missing"] == "")
    
    	defer func() { fmt.Println("recovered:", recover()) }()
    	o.Meta["key"] = "value"
    }
    // Output:
    // 0 "" false true true
    // 1
    // true
    // recovered: assignment to entry in nil map
  4. Q4.How do slices work internally, and what are the common pitfalls with `append`?

    A slice is a small descriptor with three parts: a pointer to an underlying array, a length and a capacity. Several slices can share the same array, so changes through one can appear in another.

    How append behaves:

    • If there is spare capacity, append writes into the existing array and returns a slice with a longer length.
    • If capacity is exhausted, it allocates a larger array, copies the elements and returns a slice pointing to the new array.
    • Always use the returned slice: s = append(s, x).

    Common pitfalls:

    1. Shared arrays: two append calls on the same base slice with spare capacity overwrite each other's element.
    2. Sub-slices keep the whole array alive: a small slice of a huge array prevents the array from being garbage collected; copy the data if you keep it long term.
    3. Modifying a sub-slice changes the original.

    Fixes: limit capacity with a full slice expression, s[low:high:max], so the next append must copy, or use copy or slices.Clone when independence matters. Preallocate with make([]T, 0, n) when the final size is known.

    Example

    Appending to a shared base slice overwrites data; limiting capacity forces a copy.

    package main
    
    import "fmt"
    
    func main() {
    	base := make([]int, 3, 10)
    	a := append(base, 1)
    	b := append(base, 2)
    	fmt.Println(a[3], b[3]) // 2 2
    
    	c := append(base[:3:3], 1)
    	d := append(base[:3:3], 2)
    	fmt.Println(c[3], d[3]) // 1 2
    
    	s := []int{1, 2, 3, 4, 5}
    	t := s[1:3]
    	t[0] = 99
    	fmt.Println(s, len(t), cap(t)) // [1 99 3 4 5] 2 4
    }
  5. Q5.How do arrays, slices and maps differ in Go?

    Arrays have a fixed size that is part of their type, slices are flexible views over arrays and maps are hash tables from keys to values. Most Go code uses slices and maps; arrays appear mainly as fixed-size values, such as hashes.

    ArraySliceMap
    Fixed length, part of the type: [4]intVariable length view over an array: []intUnordered key-value table: map[string]int
    Copied in full on assignment or when passed to a functionCopying shares the underlying arrayCopying shares the same table
    Comparable with == if its elements areNot comparable, except with nilNot comparable, except with nil
    Best for fixed-size data, such as a 32-byte hashBest for ordered collections of any sizeBest for lookups by key

    Practical notes:

    • Pass large arrays by pointer, or use slices, to avoid copying.
    • Maps are not safe for concurrent writes; protect them with a mutex or use sync.Map for specific read-heavy cases.
    • Map iteration order is deliberately randomized, so never depend on it; sort the keys when order matters.
    • Arrays can be map keys because they are comparable, which is useful for composite keys.
  6. Q6.How do maps work in Go, and why is their iteration order random?

    A Go map is a hash table: each key is hashed to find the slot that holds its value, giving fast lookups, inserts and deletes on average. The runtime grows the table as it fills, moving entries to new slots.

    Key behaviors:

    • Lookup with presence check: v, ok := m[key] distinguishes a missing key from a zero value.
    • Deleting with delete(m, key) is safe even if the key is absent.
    • Keys must be comparable: strings, numbers, pointers, arrays and structs of comparable fields; not slices, maps or functions.
    • Not safe for concurrent use: concurrent writes, or a write during a read, can crash the program with a fatal error.

    Why iteration order is random: the runtime deliberately starts iteration at a random position, so programs cannot accidentally depend on an order that would change between versions or as the map grows. Code that needs order sorts the keys first, for example with slices.Sorted(maps.Keys(m)).

    Performance tips:

    1. Give make a size hint when the number of entries is known, to avoid repeated growth.
    2. Use struct keys instead of concatenated strings for composite keys.
    3. Remember that deleting entries does not shrink the table; rebuild very large maps that have shrunk a lot.
  7. Q7.What is the difference between a value receiver and a pointer receiver?

    A method with a value receiver gets a copy of the value, so changes inside the method are lost. A method with a pointer receiver gets the address, so it can modify the original and avoids copying large structs.

    Value receiverPointer receiver
    func (c Counter) Get() intfunc (c *Counter) Inc()
    Works on a copy; cannot change the caller's valueCan modify the caller's value
    Both T and *T satisfy interfaces using itOnly *T satisfies interfaces using it
    Best for small, immutable valuesBest for mutation, large structs and types containing locks

    Guidelines:

    1. If any method needs a pointer receiver, use pointer receivers for all methods of that type, for consistency.
    2. Never copy types that contain a sync.Mutex; pointer receivers prevent accidental copies.
    3. Small value types, such as a Point or a Money amount, are fine with value receivers.

    Go automatically takes the address when you call a pointer method on an addressable value, so c.Inc() works on a variable c, but a value stored in a map is not addressable, so the same call fails to compile on m[k].Inc().

    Example

    The value method changes only a copy, and only the pointer type satisfies the interface.

    package main
    
    import "fmt"
    
    type Counter struct{ n int }
    
    func (c Counter) IncByValue()    { c.n++ }
    func (c *Counter) IncByPointer() { c.n++ }
    
    type Incrementer interface{ IncByPointer() }
    
    func main() {
    	c := Counter{}
    	c.IncByValue()
    	c.IncByPointer()
    	fmt.Println(c.n) // 1
    
    	var inc Incrementer = &c
    	inc.IncByPointer()
    	fmt.Println(c.n) // 2
    }

    Assigning c instead of &c to inc would not compile, because Counter itself does not have the pointer method.

  8. Q8.How do interfaces work in Go, and what does implicit implementation mean?

    An interface is a set of method signatures. Any type that has those methods satisfies the interface automatically; there is no implements keyword. This is implicit implementation, and it lets consumers define the small interfaces they need without changing the types that satisfy them.

    Idiomatic use:

    • Define interfaces where they are used, in the consuming package, not next to the implementation.
    • Keep them small: one or two methods, like io.Reader and io.Writer, which compose into larger ones.
    • Accept interfaces, return concrete types: functions take the minimal behavior they need and return structs, so callers keep full access.

    What an interface value holds: a pair of a dynamic type and a dynamic value. Calling a method uses the dynamic type to find the right implementation.

    Benefits:

    1. Easy testing, since a fake only needs the few methods the consumer calls.
    2. Loose coupling between packages.
    3. Standard library interfaces, such as io.Reader, fmt.Stringer and error, let your types plug into existing code.

    Caution: do not create interfaces with a single implementation "just in case"; add them when a second implementation or a test needs one.

  9. Q9.Why can an interface holding a nil pointer be non-nil?

    An interface value is nil only when both its dynamic type and its value are nil. If you store a typed nil pointer in an interface, the interface has a type, so it is not equal to nil, even though the pointer inside is.

    Where it bites: a function returns a custom error type through the error interface:

    • Declaring var err *MyError and returning it, even when no error happened, produces a non-nil error.
    • The caller's if err != nil check succeeds, and the program treats success as failure, or calls methods on a nil pointer.

    How to avoid it:

    1. Return the literal nil for the success case, never a typed nil variable.
    2. Declare error variables with the interface type, var err error, rather than a concrete pointer type.
    3. Return concrete error types only through the error interface at the point where an error actually occurs.

    Linters catch many of these cases, but understanding the two-part interface value is what makes the bug obvious.

    Example

    The same "no error" path returns a non-nil error when it returns a typed nil pointer.

    package main
    
    import "fmt"
    
    type NotFoundError struct{ ID string }
    
    func (e *NotFoundError) Error() string { return "not found: " + e.ID }
    
    func findBad(id string) error {
    	var err *NotFoundError
    	if id == "missing" {
    		err = &NotFoundError{ID: id}
    	}
    	return err
    }
    
    func findGood(id string) error {
    	if id == "missing" {
    		return &NotFoundError{ID: id}
    	}
    	return nil
    }
    
    func main() {
    	fmt.Println(findBad("42") == nil)  // false
    	fmt.Println(findGood("42") == nil) // true
    }
  10. Q10.How do struct embedding and composition replace inheritance in Go?

    Go has no classes or inheritance. Instead, a struct can embed another type, and the embedded type's fields and methods are promoted, so they can be used as if they belonged to the outer struct. This is composition: the outer type has the inner one, rather than being a subtype of it.

    How embedding behaves:

    • Promoted methods let the outer type satisfy interfaces that the embedded type satisfies.
    • The outer type can define a method with the same name, which takes priority; the embedded version stays reachable as outer.Inner.Method().
    • There is no polymorphism through embedding: a method of the embedded type, calling another method, always calls its own version, not the outer type's override.

    Good uses:

    1. Reusing small behaviors, such as embedding a mutex or a logger.
    2. Wrapping a type to add or change one method, such as embedding an http.ResponseWriter to record the status code.
    3. Composing interfaces, such as io.ReadWriter embedding io.Reader and io.Writer.

    Caution: embedding exposes all promoted methods publicly; use a named field instead when the inner type is an implementation detail.

  11. Q11.How do errors work in Go, and how do you wrap and inspect them?

    Errors are ordinary values returned as the last result of a function, implementing the error interface with a single Error() string method. Callers check them explicitly, which makes failure paths visible in the code.

    Adding context with wrapping:

    • fmt.Errorf("load user %d: %w", id, err) adds context while keeping the original error inside.
    • Each layer adds what it knows, producing messages such as "handle request: load user 7: not found".

    Inspecting wrapped errors:

    • errors.Is(err, ErrNotFound) checks whether any error in the chain equals a sentinel value.
    • errors.As(err, &target) finds the first error in the chain of a given type and assigns it, giving access to its fields.
    • errors.Join combines several errors, such as validation failures, into one.

    Good practices:

    1. Handle each error once: either handle it or return it with context, but do not log and return it at every layer.
    2. Define sentinel errors for conditions callers need to detect, and custom types when they need details.
    3. Wrap with %w only when callers should be able to inspect the cause; use %v to hide implementation details.

    Example

    A sentinel error and a typed error are both found through the wrapping chain.

    package main
    
    import (
    	"errors"
    	"fmt"
    )
    
    var ErrNotFound = errors.New("not found")
    
    type ValidationError struct{ Field string }
    
    func (e *ValidationError) Error() string { return "invalid " + e.Field }
    
    func loadUser(id int) error {
    	if id == 0 {
    		return fmt.Errorf("load user: %w", &ValidationError{Field: "id"})
    	}
    	return fmt.Errorf("load user %d: %w", id, ErrNotFound)
    }
    
    func main() {
    	err := loadUser(7)
    	fmt.Println(err)                         // load user 7: not found
    	fmt.Println(errors.Is(err, ErrNotFound)) // true
    
    	var ve *ValidationError
    	fmt.Println(errors.As(loadUser(0), &ve), ve.Field) // true id
    }
  12. Q12.When should you use `panic` and `recover` in Go?

    Use panic only for programming errors and truly unrecoverable situations, such as an impossible state or a failed invariant at startup. Expected failures, such as invalid input, missing records or network errors, should be returned as error values.

    Reasonable uses of panic:

    • Startup failures where continuing makes no sense, such as an invalid required configuration, often through functions named Must... like regexp.MustCompile.
    • Internal invariants that indicate a bug if broken.

    How recover works: recover stops a panic only when called directly inside a deferred function in the panicking goroutine. It returns the panic value and lets the function return normally.

    Where recover belongs:

    1. At goroutine and request boundaries, such as HTTP middleware that turns a panic in one handler into a 500 response and a log entry, instead of crashing the whole server.
    2. Every goroutine you start needs its own recovery if a panic must not crash the process, since a panic in any goroutine ends the program.

    Avoid: using panic and recover as a general exception mechanism for control flow; it hides failures and makes code hard to follow.

  13. Q13.How does `defer` work, and what are its common pitfalls?

    A defer statement schedules a function call to run when the surrounding function returns, whether it returns normally or by panicking. Deferred calls run in last-in, first-out order, which makes them ideal for cleanup, such as closing files and unlocking mutexes.

    Rules to remember:

    • Arguments are evaluated immediately, when the defer statement runs, not when the deferred call executes.
    • Deferred closures see later changes to the variables they capture.
    • Named results can be modified by deferred functions, which is how some code adds context to returned errors.

    Common pitfalls:

    1. Defer in a loop: deferring file.Close() inside a loop over thousands of files keeps every file open until the function returns; move the loop body into its own function.
    2. Ignoring errors from deferred calls: closing a written file can fail; check the error when it matters, for example through a named result.
    3. Deferring before checking errors: defer f.Close() must come after confirming the open succeeded.

    Example

    Arguments are captured when defer runs, calls execute in reverse order and a deferred closure can change a named result.

    package main
    
    import "fmt"
    
    func tripled(n int) (result int) {
    	defer func() { result *= 3 }()
    	return n
    }
    
    func main() {
    	for i := 0; i < 3; i++ {
    		defer fmt.Println("deferred", i)
    	}
    	x := 1
    	defer fmt.Println("x at defer time:", x)
    	x = 2
    	fmt.Println("x now:", x)
    	fmt.Println("result:", tripled(5))
    }
    // Output:
    // x now: 2
    // result: 15
    // x at defer time: 1
    // deferred 2
    // deferred 1
    // deferred 0
  14. Q14.How do generics work in Go, and when should you use them?

    Generics let functions and types take type parameters, constrained by interfaces that describe which types are allowed. They remove duplicated code for algorithms and data structures that work the same way for many types.

    Key pieces:

    • Type parameters: func Map[T, U any](xs []T, f func(T) U) []U.
    • Constraints: interfaces listing allowed types, such as ~int | ~float64, where ~ includes named types based on them.
    • Built-in constraints: any and comparable, plus cmp.Ordered in the standard library for types supporting <.
    • Type inference: callers usually do not need to write type arguments.

    Good uses:

    1. Collection helpers, such as the standard slices and maps packages.
    2. Data structures, such as typed caches, sets and queues.
    3. Functions that would otherwise take any and use type assertions.

    When not to use them: when an ordinary interface describing behavior is clearer, or when only one type is ever used. Generic code is harder to read, so use it where it removes real duplication.

    Example

    One Sum works for any integer or floating type, including named types, and Map converts between types.

    package main
    
    import (
    	"fmt"
    	"strings"
    )
    
    type Number interface {
    	~int | ~int64 | ~float64
    }
    
    func Sum[T Number](xs []T) T {
    	var total T
    	for _, x := range xs {
    		total += x
    	}
    	return total
    }
    
    func Map[T, U any](xs []T, f func(T) U) []U {
    	out := make([]U, 0, len(xs))
    	for _, x := range xs {
    		out = append(out, f(x))
    	}
    	return out
    }
    
    type Rupees int64
    
    func main() {
    	fmt.Println(Sum([]int{1, 2, 3}))       // 6
    	fmt.Println(Sum([]Rupees{1500, 250})) // 1750
    	fmt.Println(Map([]int{1, 2, 3}, func(n int) string { return strings.Repeat("*", n) })) // [* ** ***]
    }
  15. Q15.How do strings, bytes and runes differ in Go?

    A Go string is an immutable sequence of bytes, usually holding UTF-8 text. A byte is one 8-bit unit, and a rune is one Unicode code point. Characters outside basic English take more than one byte, so byte positions and character positions differ.

    string[]byte[]rune
    Immutable bytes, usually UTF-8 textMutable bytesMutable sequence of Unicode code points
    len counts byteslen counts byteslen counts code points
    Best for keys, messages and text valuesBest for building or modifying data, and for input and outputBest for character-level processing, such as reversing text

    Practical rules:

    1. for i, r := range s iterates by rune, with i as the byte offset of each rune.
    2. Indexing s[i] returns a byte, not a character.
    3. Converting between string and []byte usually copies the data, so avoid repeated conversions in hot loops.
    4. Use unicode/utf8 to count characters or validate text, and the strings and bytes packages for searching and splitting.
    5. For building strings piece by piece, use strings.Builder.

    Example

    A short string with two non-English characters has 10 bytes but 7 runes.

    package main
    
    import (
    	"fmt"
    	"unicode/utf8"
    )
    
    func main() {
    	s := "naïve ₹"
    	fmt.Println(len(s), utf8.RuneCountInString(s))
    	for i, r := range "ï₹" {
    		fmt.Println(i, string(r))
    	}
    	fmt.Println(s[2], string([]rune(s)[2]))
    }
    // Output:
    // 10 7
    // 0 ï
    // 2 ₹
    // 195 ï
  16. Q16.How do type assertions and type switches work in Go?

    A type assertion extracts the concrete value from an interface: v, ok := x.(T) returns the value and true if x holds a T, or the zero value and false otherwise. A type switch tests several types in turn and binds a variable of the matching type in each case.

    Rules:

    • The single-result form, v := x.(T), panics if the type is wrong; use the two-result form unless a mismatch is a bug.
    • Cases can name interfaces, such as error or fmt.Stringer, matching any type that implements them.
    • Cases are checked in order, so put more specific types first.
    • A nil case matches a nil interface value.

    Conversions are different: float64(n) or []byte(s) change a value between compatible types at compile time, while assertions inspect an interface's dynamic type at run time.

    When to use them: decoding loosely typed data, handling special errors, or implementing optional behavior, such as checking whether a writer also supports flushing. Frequent type switches over your own types can signal that an interface method would be a cleaner design.

    Example

    The switch picks the first matching case, including interface cases such as error and fmt.Stringer.

    package main
    
    import (
    	"errors"
    	"fmt"
    	"time"
    )
    
    func describe(v any) string {
    	switch x := v.(type) {
    	case nil:
    		return "nil"
    	case int:
    		return fmt.Sprintf("int %d", x)
    	case string:
    		return fmt.Sprintf("string of %d bytes", len(x))
    	case error:
    		return "error: " + x.Error()
    	case fmt.Stringer:
    		return "stringer: " + x.String()
    	default:
    		return fmt.Sprintf("other %T", x)
    	}
    }
    
    func main() {
    	for _, v := range []any{42, "hi", errors.New("boom"), time.Second, []int{1}, nil} {
    		fmt.Println(describe(v))
    	}
    	n, ok := any("x").(int)
    	fmt.Println(n, ok)
    }
    // Output:
    // int 42
    // string of 2 bytes
    // error: boom
    // stringer: 1s
    // other []int
    // nil
    // 0 false
  17. Q17.How do Go modules manage dependencies and versions?

    A module is a collection of packages versioned together, defined by a go.mod file that names the module and lists its required dependencies with minimum versions. A go.sum file records cryptographic hashes of every dependency, so builds detect tampered or changed downloads.

    How versions are chosen: Go uses minimal version selection: for each dependency, it picks the highest of the minimum versions required anywhere in the build, not the newest available. Builds are therefore reproducible without a separate lock file, and upgrading is always an explicit action.

    Everyday commands:

    • go get example.com/lib@v1.4.2 adds or changes a requirement.
    • go mod tidy adds missing and removes unused requirements.
    • go list -m -u all shows available upgrades.
    • go mod why explains why a dependency is needed.

    Major versions: a breaking release changes the import path, such as example.com/lib/v2, so two major versions can coexist in one build.

    Supply-chain practices:

    1. Review new dependencies before adding them; the standard library often suffices.
    2. Use the checksum database and a module proxy, or vendoring for strict environments.
    3. Scan dependencies for known vulnerabilities in continuous integration.
  18. Q18.How do packages, visibility and `internal` directories organize Go code?

    A package is a directory of Go files compiled together. Visibility is controlled by capitalization: identifiers starting with an uppercase letter are exported and usable from other packages; lowercase ones are private to the package.

    Organizing a service:

    • Packages by responsibility, such as orders, payments and storage, rather than by technical layer names such as models or utils.
    • internal directories: packages under internal/ can be imported only by code rooted at the parent directory, which keeps implementation details out of other modules' reach.
    • cmd/ directories for each executable, with a small main package that wires dependencies together.

    Naming conventions:

    1. Short, lowercase package names without underscores, such as orders.
    2. Avoid repetition: orders.Service, not orders.OrderService.
    3. Avoid catch-all packages such as common or helpers, which attract unrelated code.

    Dependency direction: packages must not import each other in a cycle, which the compiler rejects. Breaking a cycle usually means moving shared types into a smaller package or introducing an interface in the consumer.

  19. Q19.How does escape analysis decide whether a value lives on the stack or the heap?

    The compiler analyzes each value to see whether it can outlive the function that created it. If it cannot, the value stays on the goroutine's stack, which is cheap to allocate and needs no garbage collection. If it might, such as when a pointer to it is returned or stored somewhere longer-lived, it escapes to the heap.

    Common reasons values escape:

    • Returning a pointer to a local variable.
    • Storing a pointer in a global, a struct on the heap or a channel.
    • Assigning a value to an interface, which often requires a heap copy.
    • Closures that capture variables and outlive the function.
    • Slices whose size is not known at compile time, or that are too large for the stack.

    How to inspect it: build with go build -gcflags=-m to print the compiler's escape decisions for each line.

    Why it matters: heap allocations add garbage collection work, which costs processor time and can raise latency. In hot paths, returning values instead of pointers, reusing buffers and avoiding unnecessary interfaces can remove allocations.

    Optimize only after profiling shows allocations matter; most code does not need this attention.

  20. Q20.How does Go's garbage collector work, and how do you tune it?

    Go uses a concurrent, non-moving mark-and-sweep collector. It marks reachable objects while the program keeps running, with only very short pauses, then frees unreachable memory. It is designed for low latency rather than maximum throughput.

    What controls collection frequency:

    • GOGC: the heap growth allowed before the next collection, as a percentage; the default of 100 means a collection when the heap doubles since the last one. Higher values use more memory and less processor time.
    • GOMEMLIMIT: a soft limit on total memory, which makes the collector work harder as memory approaches the limit.

    Tuning in containers:

    1. Set GOMEMLIMIT somewhat below the container's memory limit, such as 90%, so the collector reacts before the container is killed.
    2. With a memory limit set, you can raise GOGC or turn it off to reduce collection work when memory is plentiful.
    3. Watch for a death spiral: if live data nearly fills the limit, the collector runs constantly and processor use soars.

    Reducing pressure is usually better than tuning: fewer allocations, reused buffers and smaller data structures. Measure with the runtime's memory statistics and execution traces before changing settings.