Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Top 50 - Part 3
GO

Top 50 - Part 3

Practice the final 20 questions from a comprehensive set of 50 important Go programming interview questions.

1. What is string immutability in Go?

Answer: The underlying byte contents of a string cannot be modified in place. s[0] = 'x' is invalid; string data is read-only.

A Go string is an immutable byte sequence. Once created, the bytes behind it can’t change — the language enforces this by making individual element assignment a compile error.

Consequences and workarounds:

  • s[i] is a read-only byte access. You cannot write through it.
  • To modify “a string,” you must convert to a mutable representation, change it, and convert back: b := []byte(s), mutate b, then s = string(b). The conversion copies the bytes (preserving immutability of the original).
  • Strings share storage freely and are safe to use across goroutines precisely because they can’t be mutated.

Note the immutability is about the contents, not the variable: s itself can be reassigned to a new string anytime. The interview answer: string bytes are immutable — reassignable variables, but no in-place content mutation.

2. How do you construct a read-only channel parameter in a function declaration?

Answer: func process(ch <-chan int) — the <-chan direction makes the parameter receive-only (read-only).

Channel directions are part of the type:

  • ch <-chan intreceive-only: the function can read from it but not send.
  • ch chan<- intsend-only: the function can send to it but not receive.
  • ch chan int — bidirectional.

The arrows point in the direction data flows — <-chan means “the channel produces values toward <-” (so you receive); chan<- means values flow into the channel (so you send).

Directional parameters enforce contracts at compile time: a function that only consumes items declares <-chan and physically cannot send, which documents intent and prevents misuse. The interview answer: <-chan int is a receive-only (read-only) channel parameter.

3. What will string([]rune{0x65e5, 0x672c}) produce?

Answer: The UTF-8 string "日本".

Converting a []rune to a string encodes each rune (Unicode code point) into its UTF-8 byte representation and concatenates them.

The runes here:

  • 0x65e5 is the Unicode code point for 日 (the Japanese/Chinese character “sun”).
  • 0x672c is 本 (the character for “book/origin”).

Together they form 日本 (“Japan”). The conversion produces the valid UTF-8 string containing those two characters.

The reverse direction works too — []rune("日本") gives back the code point slice. The interview answer: a UTF-8 string containing 日本.

4. Which type of allocation determines whether a variable stays on the stack or moves to the heap?

Answer: The compiler’s escape analysis.

During compilation, Go’s escape analysis tracks each variable’s lifetime. If a variable’s address escapes the function frame — returned, stored in a global or heap-allocated object, captured by a closure that outlives the call — the variable must be heap-allocated. If it can’t escape, it stays on the stack.

The practical effects:

  • No escape → stack allocation. Cheap (just stack pointer adjustment), zero GC pressure.
  • Escape → heap allocation. Managed by the garbage collector.

The classic example: returning &localVar forces the variable to escape to the heap, because the caller will use it after the function returns. Go sometimes surprises developers with allocations in code that looks stack-safe — go build -gcflags=-m prints the analysis results and shows exactly what escapes.

The interview answer: compiler escape analysis decides stack vs. heap based on whether the variable’s lifetime exceeds its frame.

5. What does reflect.ValueOf(x).Kind() return?

Answer: The underlying primitive kind of the value — reflect.Struct, reflect.Slice, reflect.Int, and so on.

Go’s reflect package describes types on two levels:

  • Type — the full named type. reflect.TypeOf returns the custom type name (type User struct"User").
  • Kind — the underlying primitive kind the type is built on. reflect.ValueOf(u).Kind() for a User struct returns reflect.Struct.

So Kind answers “what fundamental category is this?” — Int, Float64, String, Slice, Map, Struct, Func, Chan, and the rest. Different named types (e.g., type Score int and type Count int) have different types but the same kind (Int).

This distinction is essential for generic-style reflection code that must behave based on the underlying structure. The interview answer: Kind() returns the primitive category (Struct, Slice, Int, …), independent of the custom type name.

6. How does Go handle structure alignment and field order in memory?

Answer: Fields are aligned based on their type sizes, so field ordering affects total struct size via padding bytes.

Go aligns each field in memory to its natural alignment: an int64 on 8-byte boundaries, an int32 on 4, a byte on 1. The compiler inserts padding between fields to satisfy these alignments, and the struct’s total size is a multiple of its largest field’s alignment.

Consider:

struct { a byte; b int64; c byte }   // a at 0, pad 7, b at 8, c at 16, pad 7 → 24 bytes
struct { a byte; c byte; b int64 }    // a at 0, c at 1, pad 6, b at 8 → 16 bytes

Same three fields, different order — 24 vs 16 bytes. Ordering fields from largest to smallest minimizes padding.

Note: Go does not reorder fields for you; memory layout follows source order. This matters for performance-sensitive large structs and for serialization layouts. The interview answer: alignment-based padding means field order affects struct size; largest-first ordering reduces waste.

7. What is the output of the following boolean evaluation?

fmt.Println(true || false && false)

Output: true.

Precedence: && binds tighter than ||. So the expression groups as true || (false && false), not (true || false) && false.

Evaluate the && first: false && false is false. Then true || false is true.

The output is true. The short-circuit also means the right side never even runs its computation here — true || anything is immediately true. The interview point: && before ||, same rule as most languages.

8. What is the purpose of testing.B in Go unit tests?

Answer: It’s the benchmark harness — testing.B controls benchmark loop iterations and measures execution time and memory allocations.

Benchmark functions have the signature func BenchmarkName(b *testing.B). The framework sets b.N — the number of iterations to run — adjusting it until the benchmark runs long enough for reliable timing. Your code runs the operation under test inside a loop over b.N:

func BenchmarkConcat(b *testing.B) {
    for i := 0; i < b.N; i++ {
        // operation being measured
    }
}

testing.B also exposes controls: b.ResetTimer() to exclude setup, b.N for the iteration count, b.ReportAllocs() to report memory allocations, and b.SetBytes for throughput metrics.

Run benchmarks with go test -bench=.. The interview answer: testing.B is the benchmarking type that controls iterations (b.N) and measures performance/allocation metrics.

9. What command measures code line coverage during test runs in Go?

Answer: go test -cover.

The -cover flag makes go test instrument the package, run the tests, and report what percentage of code statements were executed.

Usage: go test -cover prints a coverage percentage per package; go test -coverprofile=coverage.out writes detailed coverage data, which go tool cover -html=coverage.out renders as an annotated HTML report.

The interview answer: go test -cover computes and displays statement coverage for the tested packages.

10. What is the behavior of sync.Once?

Answer: sync.Once guarantees a function passed to Do() runs exactly once, even across all goroutines and repeated calls.

var once sync.Once
once.Do(initSomething)   // runs initSomething
once.Do(initSomething)   // no-op — already done

The guarantees:

  • No matter how many goroutines call Do, the function runs exactly once — concurrent callers block until the first completes, then all see it done.
  • Subsequent Do calls do nothing.

This makes it the standard tool for lazy singleton initialization — a resource (DB connection, config, logger) that should be created at most once, on first use, safely across goroutines. The interview answer: Do(f) runs f exactly once program-wide; every other call is a no-op.

11. What will be the output of fmt.Println(cap(a))?

a := make([]int, 3, 7)

Output: 7.

make([]T, len, cap) takes three arguments: the slice length and its capacity. Here length is 3 and capacity is 7.

  • len(a)3 (visible elements).
  • cap(a)7 (backing array slots available).

The gap between len and cap (4 slots) is headroom: appends up to the capacity won’t reallocate. The interview answer: 7 — the third argument sets the initial capacity.

12. What happens if you re-slice a slice beyond its length but within its capacity (s[1:5] where len(s)=3, cap(s)=8)?

Answer: It succeeds — re-slicing is allowed up to the capacity, extending the slice’s visible length into the capacity window.

The slice bounds rules: the high index can be at most cap(s) (and since Go 1.2, re-slicing may even use the capacity as the limit); the low index at most len(s). Accessing elements beyond len would panic, but re-slicing into spare capacity is legal.

s := make([]int, 3, 8)   // len 3, cap 8
t := s[1:5]              // OK — 5 ≤ cap 8; t has len 4

t now shows elements 1 through 4, reaching into slots that existed in the backing array but weren’t part of s’s length. The new elements are whatever was in the backing array (zeros for freshly made slices). The interview answer: legal — you may re-slice up to the capacity; only exceeding capacity panics.

13. What is the main purpose of building applications with CGO_ENABLED=0?

Answer: It disables C interop, producing statically linked, zero-dependency pure Go binaries — ideal for minimal containers.

Cgo lets Go code call C libraries, which introduces a dependency on the C toolchain and dynamic libraries (libc). With CGO_ENABLED=0:

  • All C bindings are compiled out.
  • The result is a static binary with no external runtime dependencies — it runs in an empty container (scratch/distroless), alpine, or anywhere with a matching architecture.
  • Builds are more reproducible and the binary is self-contained.

Trade-offs: you lose the ability to use cgo-dependent packages (like os/user with cgo, some database drivers, net with cgo DNS) unless they have pure-Go fallbacks. The interview answer: CGO_ENABLED=0 produces static, dependency-free pure Go binaries for lightweight containerized deployments.

14. What is the output of fmt.Println(10 % -3) in Go?

Output: 1.

In Go, the sign of the remainder from % follows the sign of the dividend (the first operand).

10 % -3: the quotient truncates toward zero — 10 / -3 = -3.33... truncated to -3. Then 10 - (-3 * -3) = 10 - 9 = 1. The result is 1, positive because the dividend 10 is positive.

This contrasts with languages where the remainder takes the divisor’s sign. Go’s rule is simple to state: a % b has the sign of a. So -10 % 3 would be -1. The interview answer: 1 — remainder takes the dividend’s sign in Go.

15. How do you convert a string variable s into a byte slice []byte without allocations using standard safe Go?

Answer: You don’t — the standard safe conversion []byte(s) allocates (it copies). There is no safe, allocation-free standard conversion.

The allocation exists to preserve string immutability: if []byte(s) aliased the string’s memory, writing to the byte slice would mutate the string behind its back. So the conversion copies.

b := []byte(s)   // safe, idiomatic — but allocates a copy

The unsafe alternative — unsafe.StringData / unsafe.Slice tricks — avoids the copy but creates an aliased, mutable view of immutable memory: modifying it is undefined behavior and violates Go’s memory safety. So the correct, safe, standard answer is []byte(s), accepting the allocation.

The interview answer: []byte(s). It allocates (a copy) — which is the correct price for safety; allocation-free conversion requires unsafe code and is not recommended.

16. What does the go.mod file specify in a Go project?

Answer: The module’s import path, the Go language version, and the module’s dependency requirements (direct and indirect).

go.mod is the module definition, the modern replacement for GOPATH. A typical file:

module example.com/proj

go 1.22

require (
    github.com/foo/bar v1.2.3
    golang.org/x/exp v0.0.0-...
)

It records:

  • Module path — the root import path for the module’s packages.
  • Go version — the language version the code targets (sets language feature gates).
  • Require block — every third-party dependency and its exact version.

The file is maintained by the Go toolchain (go get, go mod tidy). It works together with go.sum for reproducible builds. The interview answer: go.mod defines the module path, Go version, and dependency requirements.

17. What is the purpose of the go.sum file?

Answer: It stores cryptographic checksums of dependency module versions, ensuring builds are reproducible and tamper-proof.

go.sum contains a hash (SHA-256 based) for every module version the project depends on — both direct and transitive. When go downloads a module, it verifies the content against the recorded hash. Any mismatch — corrupted download, tampered module, a changed version — aborts the build.

This guards against supply-chain tampering: go.sum is committed to the repository, so everyone building the project validates the exact same dependency content. The interview answer: go.sum pins cryptographic hashes of module versions for integrity and reproducibility.

18. What is the output of the following bitwise operation?

x := 5  // 0101
y := 3  // 0011
fmt.Println(x &^ y)

Output: 4.

&^ is Go’s bit clear (AND NOT) operator. x &^ y clears (zeroes) the bits in x wherever the corresponding bit in y is 1.

Binary:

  • x = 0101
  • y = 0011
  • x &^ y: bit 0: 1 cleared by 1 → 0; bit 1: 0 → 0; bit 2: 1, y bit 0 → keep 1; bit 3: 0 → 0. Result 0100 = 4.

Equivalently, x &^ y is x & ^y (x AND NOT y). The interview answer: 4.

19. What happens if you call WaitGroup.Add(-1) when the WaitGroup counter is already 0?

Answer: A runtime panic: panic: negative WaitGroup counter.

The WaitGroup counter must never go below zero. Done() is documented as equivalent to Add(-1), and both must be balanced so the counter stays non-negative. If an Add(-1) (or an unbalanced Done) would drive the counter negative, the runtime panics immediately.

This catches bugs like calling Done() more times than Add() was called — a sign of a misbehaving goroutine. The interview answer: it panics with negative WaitGroup counter.

20. Which standard library package is used to parse command-line flags in Go?

Answer: flag.

The standard library’s flag package parses command-line options:

var name = flag.String("name", "world", "your name")
flag.Parse()
fmt.Println("Hello", *name)

It provides typed flag definitions — flag.String, flag.Int, flag.Bool, etc. — and handles both -name value and --name=value styles, plus auto-generated usage text via flag.Usage. (os.Args gives raw arguments; third-party packages like pflag/cobra offer richer features, but the standard answer is flag.) The interview answer: flag.

My Private Notes

Notes are auto-saved locally to this device.