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 10 - Part 1
GO

Top 10 - Part 1

Practice 10 of the most important and frequently asked Go programming interview questions.

1. What is the default value (zero value) of an uninitialized slice in Go?

Answer: nil.

Every variable in Go starts at its zero value, and each type has a defined one: 0 for numbers, false for booleans, empty string for strings, nil for pointers, maps, functions, interfaces — and slices.

A nil slice is a perfectly valid slice header: its length is 0, its capacity is 0, and it points to no underlying array. In practice, a nil slice behaves like an empty slice for almost everything you do — ranging over it yields nothing, and append on a nil slice works fine (it allocates the backing array on first use).

The distinction worth knowing: nil slice vs. empty slice. var s []int gives you a nil slice. make([]int, 0) (or []int{}) gives you an empty, non-nil slice with a zero-length but potentially allocated backing array. For equality checks in code, len(s) == 0 is the correct test — it’s true for both.

The interview answer: the zero value of a slice is nil, which has length 0 and capacity 0 and points to no underlying array.

2. What happens when you send a value to an unbuffered channel when no goroutine is waiting to receive from it?

Answer: The sending goroutine blocks until another goroutine receives from the channel.

An unbuffered channel is the heart of Go’s “do not communicate by sharing memory; share memory by communicating” philosophy. It has no storage capacity — a send on an unbuffered channel can only complete when a matching receive is happening simultaneously.

So when you do ch <- value and no receiver is ready, the runtime parks the sending goroutine. It stays blocked until another goroutine executes <-ch, at which point the value is handed over directly and both goroutines continue. Nothing is cached, nothing is buffered, nothing is dropped.

This synchronous handoff is the key difference from a buffered channel, which has a queue of fixed capacity: a send fills a slot and returns immediately if there’s room, even with no receiver waiting.

The interview answer: the sender blocks until a receiver is ready. Unbuffered channels are synchronous.

3. What will be the output of the following slice modification?

a := []int{1, 2, 3}
b := a[1:3]
b[0] = 99
fmt.Println(a)

Output: [1 99 3]

The trap is thinking a slice is its own copy of the data. It isn’t.

A slice is a header — three fields: a pointer to an element of an underlying array, a length, and a capacity. Slicing a[1:3] creates a new header (so b has length 2, pointing at a’s element 1), but it points into the same underlying array that a uses.

So b[0] is actually the same memory slot as a[1]. Writing b[0] = 99 changes the shared array element, and a sees it: a becomes [1, 99, 3].

The output is [1 99 3].

This is the fundamental slice gotcha. It’s exactly why the copy idiom exists: if you need an independent array, use copy(b, a[1:3]) with a freshly allocated slice, or append([]int(nil), a[1:3]...). Otherwise, remember that slices are views, and writes through one view show up in all the others.

4. What is the output of reading a closed channel in Go?

Answer: It immediately yields the zero value of the channel’s element type, and the comma-ok check returns false.

Reading from a closed channel never blocks and never panics. The channel has no more values to deliver, so the receive returns the zero value of the element type — 0 for ints, "" for strings, nil for pointers, and so on.

The two-value form is what you should use to detect closure:

val, ok := <-ch

ok is false when the channel is closed and drained; true otherwise. This is the standard way to loop safely:

for val := range ch { ... }   // ranging handles closure automatically

Compare this with the send side: sending to a closed channel panics, but receiving from a closed channel is fine and returns the zero value. The asymmetry is intentional — receivers are the ones that need to keep draining gracefully.

The interview answer: reading a closed channel returns the zero value immediately with ok == false.

5. What will be printed by the following code using defer?

func printVal() {
    x := 10
    defer fmt.Println(x)
    x = 20
}

Output: 10

The critical rule of defer: arguments are evaluated when the defer statement executes, not when the deferred function runs.

The line defer fmt.Println(x) is read while x is still 10. The argument x — the value 10 — is captured at that moment and stored. Later, when the function returns and the deferred call actually runs, it prints the stored 10. The assignment x = 20 after the defer changes the variable, but the deferred call already has its argument.

So the output is 10.

Contrast with a closure:

defer func() { fmt.Println(x) }()

A closure captures the variable, not the value. In that version, the deferred function reads x at return time — it would print 20. The distinction — value captured vs. variable captured — is the whole question.

The interview answer: 10, because deferred arguments are evaluated at defer time.

6. Which of the following statements about map concurrency in Go is true?

Answer: Concurrent reads are safe, but concurrent writes without synchronization cause a fatal runtime crash.

Go maps are not safe for concurrent modification. The runtime deliberately detects concurrent map access and responds harshly: it crashes the program with a fatal error like fatal error: concurrent map writes. This is not a recoverable panic — recover won’t help; the process dies.

The precise situation:

  • Concurrent reads — safe as long as nothing is writing.
  • A read racing with a write, or two writes racing — fatal error.

Because the crash is fatal, the rules are simple to state: don’t write to a map from more than one goroutine, and don’t read while another goroutine writes, without synchronization.

The standard solutions: a sync.Mutex or sync.RWMutex guarding the map, or the purpose-built sync.Map for specific high-contention patterns (append-only, write-once-read-many). In modern Go, maps hold pointers internally, and the race is against the internal resizing/layout — which is exactly why the runtime refuses to let it happen silently.

The interview answer: reads are safe concurrently, but any concurrent write (or read-during-write) is a fatal, non-recoverable runtime error.

7. How are interfaces satisfied in Go?

Answer: Implicitly — a type satisfies an interface automatically if it implements all the methods the interface declares. There is no implements keyword.

Go’s interfaces are structural. You don’t declare “this type implements that interface.” Instead, the compiler checks: does the type have a method set that covers everything the interface requires? If yes, the type is assignable to the interface — at compile time for statically-known values, or at runtime for interface values.

type Writer interface { Write([]byte) (int, error) }
type File struct{}
func (File) Write(p []byte) (int, error) { return len(p), nil }

var w Writer = File{}   // File satisfies Writer automatically

This is often called duck typing — “if it walks like a duck…” — but with static checking. The compiler verifies the method set rather than trusting runtime reflection.

Two consequences follow. First, a type can satisfy many unrelated interfaces, and interfaces can be composed (io.Reader + io.Writerio.ReadWriter). Second, an empty interface (interface{} / any) is satisfied by every type, because it requires no methods.

The interview answer: Go interfaces are satisfied implicitly via structural typing — implement the methods, and the type fits the interface.

8. What is the result of using append() when a slice’s capacity is exceeded?

Answer: Go allocates a new, larger underlying array, copies the existing elements over, and returns a new slice header pointing at the new storage.

append works on the slice’s capacity, not just its length. When there’s room — len < cap — the element is written into the existing backing array and len increments. Fast, no allocation.

When the slice is full (len == cap), append must grow. It allocates a brand-new backing array — the new capacity is typically about double the old one for small slices, tapering toward a smaller growth factor (often 1.25×) for very large ones — copies all existing elements across, and writes the new element. The returned slice header points to the new array.

The critical caveat that follows: because the new slice may point to a different array, you must always capture the return values = append(s, x). Using append(s, x) without reassignment can silently write to the new array while the old variable still references the old one.

The interview answer: append grows the slice by allocating a larger backing array, copying elements over, and returning the updated slice header.

9. What is the time complexity of looking up a key in a Go map?

Answer: O(1) average, O(n) worst-case.

A Go map is a hash table built from buckets. To look up a key, Go computes the key’s hash, uses part of the hash to select a bucket, then compares the key against the entries in that bucket (with a small per-bucket lookup structure for speed). When keys hash and distribute well, this is constant time — O(1) on average.

The O(n) worst case arises from hash collisions: if many keys land in the same bucket, lookups degrade to scanning within that bucket, which can grow to O(n) in pathological cases (e.g., adversarial keys that all hash identically, or a badly-behaved hash function). In practice, Go’s randomized per-map hash seed and bucket overflow handling keep this from being a real problem.

The interview answer: O(1) average for map lookups, degrading to O(n) worst-case under severe collisions — standard hash-table behavior.

10. What will happen if you send a value to a closed channel in Go?

Answer: It triggers an immediate runtime panic: panic: send on closed channel.

Closing a channel is a declaration: “no more values will be sent.” The runtime enforces that contract. If a goroutine tries to send after the channel is closed, the runtime panics — the panic propagates, and unless recovered, the program crashes.

This is the asymmetric counterpart of receiving (which is safe on a closed channel and returns the zero value). Sending is never safe on a closed channel, and there’s no way to “reopen” a channel.

The discipline that follows: only the sender should close a channel (it’s a send-side statement), a channel should be closed only once, and receivers should coordinate with ok checks or range rather than relying on sends stopping. When you see send on closed channel, it’s almost always a race between a sender finishing and another goroutine closing the channel too early.

The interview answer: a send on a closed channel panics immediately with panic: send on closed channel.

My Private Notes

Notes are auto-saved locally to this device.