Advanced Go Interview: From Goroutines to Generics
\n\nAn interview for a mid-level or senior Go developer position is not just a syntax check. Employers expect a deep understanding of the concurrency model, runtime behavior, memory management, and typing specifics. In this article, we will cover the top advanced Golang questions that will help you confidently pass a technical interview. We won't dwell on basics like "what is a slice" — we'll jump straight to complex topics with code examples.
\n\n1. Goroutines, Channels, and Synchronization Patterns
\n\nQuestions about goroutines and channels are classics in any interview. However, at an advanced level, you will be asked not only "what is a goroutine" but also about the intricacies of the scheduler (GMP model) and the correct use of synchronization primitives.
\n\nQuestion: How does the goroutine scheduler work in Go? What are G, M, P?
\nThe Go scheduler uses the GMP model: G (goroutine) — a lightweight thread, M (machine) — an OS thread, P (processor) — an execution context. P is bound to a logical processor and manages the goroutine queue. When a goroutine blocks (e.g., on a system call), P can switch M to another goroutine, enabling efficient multitasking without creating thousands of OS threads.
Question: What is the difference between a buffered and an unbuffered channel? Provide an example of a deadlock.
\n\n// Unbuffered channel — blocks the sender until received\nch := make(chan int)\ngo func() {\n ch <- 42 // blocks until main reads\n}()\nfmt.Println(<-ch) // 42\n\n// Buffered channel — does not block until the buffer is full\nchBuf := make(chan int, 2)\nchBuf <- 1\nchBuf <- 2 // ok, buffer not full\n// chBuf <- 3 // deadlock if no reader\n\n// Typical deadlock: reading from an empty unbuffered channel\nchDead := make(chan int)\n// <-chDead // fatal error: all goroutines are asleep — deadlock!\n\n\nQuestion: How to safely close a channel? What is "range over channel"?
\nOnly the sender should close a channel. The receiver can use the second parameter (v, ok := <-ch) to check. The iteration for v := range ch will terminate when the channel is closed. Attempting to write to a closed channel causes a panic.
ch := make(chan int)\ngo func() {\n for i := 0; i < 3; i++ {\n ch <- i\n }\n close(ch) // must close\n}()\nfor v := range ch {\n fmt.Println(v) // 0, 1, 2\n}\n\n\n2. Interfaces, Empty Interface, and Type Assertions
\n\nInterfaces in Go are contracts. In an interview, it is important to understand how they are structured internally (the iface and eface structures) and how static typing differs from dynamic typing.
Question: What is an empty interface (interface{}) and when should it be used?
An empty interface has no methods, so any type implements it. It is used for working with data of an unknown type (e.g., json.Unmarshal). However, overusing the empty interface violates Go's strict typing and can lead to runtime errors.
Question: How does type assertion work? What is the difference from a type switch?
\n\nvar val interface{} = "hello"\n\n// type assertion\nstr, ok := val.(string)\nif ok {\n fmt.Println("This is a string:", str)\n}\n\n// type switch\nswitch v := val.(type) {\ncase string:\n fmt.Println("String:", v)\ncase int:\n fmt.Println("Number:", v)\ndefault:\n fmt.Println("Unknown type")\n}\n\n\nQuestion: How are interfaces stored in memory?
\nEach interface is two pointers: one to the type (itable), and one to the value (data). For an empty interface (interface{}), it is