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 25 - Part 2
GO

Top 25 - Part 2

Practice the remaining 10 questions from the top 25 Go programming interview questions.

1. What happens when a panic occurs and is NOT handled by recover()?

Answer: The panic unwinds the goroutine’s stack, runs all deferred functions, prints the panic message with a stack trace, and terminates the program.

A panic is Go’s mechanism for “something unrecoverably wrong happened at runtime.” The sequence when it’s not recovered:

  1. The current function stops executing and starts unwinding — control returns up the call stack.
  2. Each deferred function along the way still runs (defers execute during unwinding). This is why defer cleanup is reliable even under panics.
  3. If no recover() intercepts it, the runtime prints the panic value and a stack trace, then exits the process with a non-zero status.

Key points: the panic is not converted into an error value returned to main — it kills the process. And the unwinding is per-goroutine: a panic in one goroutine that’s not recovered crashes the whole program (unless that goroutine recovers), because there’s nowhere else to go.

The interview answer: unhandled panics run defers, print the panic and stack trace, and terminate execution.

2. Where can recover() be called to successfully intercept a panic?

Answer: Exclusively inside a deferred function.

recover() only works when called directly from a deferred function. The reason is timing: a panic stops normal execution, so the only code that runs afterward is deferred code. A direct call to recover() during normal execution (not from a defer) always returns nil — there’s no panic in flight at that moment.

The canonical pattern:

func safe() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recovered:", r)
        }
    }()
    panic("boom")   // intercepted by the deferred recover
}

Two constraints make this subtle:

  • recover() must be called within the deferred function (directly, not from a helper it calls).
  • It only catches a panic happening in the same goroutine.

The interview answer: recover() works only inside a deferred function.

3. What is the difference between an unbuffered channel and a buffered channel with capacity 1?

Answer: An unbuffered channel requires a sender and receiver to be simultaneously ready — the send blocks until a receive happens. A buffered channel with capacity 1 lets one send complete without blocking, storing the value in the buffer even with no receiver waiting.

Unbuffered (make(chan T)):

  • Zero storage. A send succeeds only when a matching receive is executing concurrently.
  • Perfect synchronization: the value is handed off directly, and both sides must be present. The sender is guaranteed the receiver has taken the value.

Buffered capacity 1 (make(chan T, 1)):

  • One slot of storage. A send to an empty buffer writes into the slot and returns immediately — no receiver needed. A second send, while the slot is full, blocks until a receive frees it.
  • The buffer simply decouples senders from receivers by that one item’s worth of slack.

The interview answer: unbuffered = synchronous handoff requiring both sides ready; buffered(1) = one item can be sent non-blocking before a receiver arrives.

4. What is the default capacity of a slice created as make([]int, 5)?

Answer: 5.

make([]int, length) — with a single size argument — creates a slice where the capacity defaults to the length. So make([]int, 5) gives a slice with length 5 and capacity 5, all elements zeroed.

If you want a different capacity, pass it explicitly: make([]int, 5, 10) creates a slice with length 5 but capacity 10 — a backing array holding 10 slots, with only the first 5 as the visible length. The extra capacity is headroom so appends don’t allocate immediately.

The interview answer: capacity equals the length (5) when only a length argument is given.

5. Which garbage collection algorithm does the Go runtime use?

Answer: A concurrent tri-color mark-and-sweep collector — non-generational, designed for low pause times.

Go’s GC is built for the runtime’s concurrency: it runs concurrently with the program (application goroutines keep executing while the GC works), keeping pause times very short.

The tri-color scheme is the algorithm’s core: objects are colored white (unmarked), grey (marked but children not yet processed), or black (marked and processed). The collector repeatedly moves grey objects to black while marking their children, and at the end any remaining white objects are unreachable and swept away.

Two deliberate choices stand out:

  • Non-generational — no young/old object split (unlike Java’s generational collectors). This simplifies the runtime at the cost of some throughput.
  • Concurrent — marking and sweeping happen alongside program execution, with only tiny stop-the-world phases, which is why Go can hold pauses to sub-millisecond levels at scale.

The interview answer: concurrent tri-color mark-and-sweep — non-generational, low-latency by design.

6. What is the output of len(“Go-语言”) in Go?

Output: 9.

len() on a string counts bytes, not characters. The string is UTF-8 encoded, and the byte count is the sum of the bytes in each piece:

  • "Go-" — 3 ASCII characters, 1 byte each → 3 bytes.
  • "语" (yǔ, “language”) — 3 bytes in UTF-8.
  • "言" (yán, “speech”) — 3 bytes in UTF-8.

Total: 3 + 3 + 3 = 9.

The trap: the string has 5 visible characters, so an intuition from “length of text” would guess 5. But Go defines string length as raw byte count. This is why len on a string containing non-ASCII text gives a number larger than the character count.

The interview answer: 9 — the byte length of the UTF-8 string.

7. How do you find the character/rune count of a UTF-8 string in Go?

Answer: utf8.RuneCountInString(str).

Since len(str) counts bytes, counting actual characters requires decoding the UTF-8. The unicode/utf8 package provides exactly that: utf8.RuneCountInString(s) counts the Unicode code points (runes) in the string.

utf8.RuneCountInString("Go-语言")   // 5
len("Go-语言")                      // 9

Rune counting is also what happens implicitly when you range over a string — each iteration decodes one rune. The interview answer: utf8.RuneCountInString counts runes; len only counts bytes.

8. What is the output of the following comparison?

var i interface{} = (*int)(nil)
fmt.Println(i == nil)

Output: false.

An interface value is nil only when both halves are nil: its dynamic type and its dynamic value. This is the classic typed-nil trap.

The assignment var i interface{} = (*int)(nil) wraps a nil pointer of type *int into the interface. Now the interface holds:

  • Dynamic type: *int — not nil.
  • Dynamic value: nil (the pointer is nil).

Because the type half is non-nil, the interface as a whole is not nil. So i == nil is false.

This is why “check if err is nil” has a famous gotcha: if a function returns a nil pointer typed as a concrete type inside an interface, the interface is non-nil even though the underlying pointer is nil. The safe habit is to return bare nil for error/interface values rather than typed nil pointers.

The interview answer: false — an interface is nil only if both its type and value are nil.

9. What keyword is used to prevent race conditions when executing code across multiple goroutines?

Answer: sync.Mutex (along with sync.RWMutex) — Go has no synchronized or volatile keywords; mutual exclusion is explicit via the sync package.

A data race is what happens when two goroutines access the same memory without synchronization and at least one writes. The direct fix is a mutex: only one goroutine holds the lock at a time, so the critical section it guards is safe.

var mu sync.Mutex
var count int

mu.Lock()
count++
mu.Unlock()

The family:

  • sync.Mutex — simple mutual exclusion: one holder at a time.
  • sync.RWMutex — allows many concurrent readers, exclusive writer. Better when reads dominate.

Go’s answer to volatile (which exists in some languages as a memory-ordering hint) is different: the sync/atomic package for lock-free atomic operations, and the go test -race detector to catch races during development.

The interview answer: sync.Mutex / sync.RWMutex — explicit locks, with sync/atomic as the lock-free alternative.

10. How does Go handle unused declared local variables inside function bodies?

Answer: It’s a compilation errordeclared and not used.

Go enforces cleanliness at compile time. Declaring a local variable and never using it stops the build with an error. The same rule applies to unused imports — you can’t import "fmt" and never use fmt.

This is stricter than most languages (which warn) and is a deliberate language design decision: unused variables and imports are almost always mistakes or dead code, so Go refuses to compile them, keeping codebases tidy and forcing you to confront the clutter.

The practical workarounds when you genuinely need to ignore a value: use the blank identifier — _ = x, or assign to _:

value, _ := m["key"]   // discard the ok flag

The interview answer: an unused local variable is a compile-time error (declared and not used); assign to _ to discard intentionally.

My Private Notes

Notes are auto-saved locally to this device.