Как пройти собеседование по Go: топ вопросов с примерами кода
Собеседование на позицию Go-разработчика — это не только проверка синтаксиса, но и глубокое понимание модели конкурентности, работы с памятью и идиоматического стиля языка. В этой статье мы разобрали топ-10 вопросов, которые чаще всего задают на собеседованиях по Golang, с подробными ответами и примерами кода. Материал подойдет как для Junior, так и для Middle-уровня.
1. Горутины vs потоки ОС: в чем разница?
Это базовый вопрос, который проверяет понимание модели выполнения Go. Ответ должен быть четким:
- Размер стека: Горутина стартует с ~2 КБ стека, который динамически растет. Поток ОС имеет фиксированный стек (1-8 МБ).
- Переключение контекста: Горутины переключаются в пространстве пользователя (Go scheduler). Потоки — через системный вызов (дороже).
- Стоимость создания: Горутина стоит ~4 КБ памяти, поток — ~1 МБ. Можно запустить тысячи горутин.
// Пример: 10000 горутин
func main() {
for i := 0; i < 10000; i++ {
go func(n int) {
fmt.Println("Горутина", n)
}(i)
}
time.Sleep(time.Second)
}
2. Каналы: буферизированные и небуферизированные
Вопрос на понимание синхронизации. Объясните разницу на примере:
- Небуферизированный канал: Отправка блокируется до получения. Гарантирует синхронность.
- Буферизированный канал: Отправка не блокируется, пока буфер не заполнен. Позволяет асинхронную передачу.
// Небуферизированный канал
ch := make(chan int)
go func() { ch <- 42 }()
val := <-ch // Блокируется, пока не придет значение
// Буферизированный канал
bufCh := make(chan int, 2)
bufCh <- 1 // Не блокируется
bufCh <- 2 // Не блокируется
// bufCh <- 3 // Блокируется - буфер полон
3. Select и обработка множества каналов
Оператор select позволяет ожидать несколько операций с каналами. Часто спрашивают про default и таймауты:
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() { time.Sleep(2 * time.Second); ch1 <- "from ch1" }()
go func() { time.Sleep(1 * time.Second); ch2 <- "from ch2" }()
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
case <-time.After(3 * time.Second):
fmt.Println("Таймаут")
}
}
4. Интерфейсы: пустой интерфейс и type assertion
Проверка понимания interface{} (any) и безопасного приведения типов:
func describe(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Целое: %d\n", v)
case string:
fmt.Printf("Строка: %s\n", v)
default:
fmt.Printf("Неизвестный тип: %T\n", v)
}
}
func main() {
describe(42)
describe("hello")
describe(3.14)
}
5. Управление памятью: стек и куча, escape analysis
Вопрос на понимание того, как Go решает, где размещать переменные. Ключевой момент — escape analysis:
- Если компилятор видит, что переменная может быть использована после возврата из функции, она «убегает» в кучу.
- Использование указателей, интерфейсов и замыканий часто вызывает аллокацию в куче.
// Переменная x "убегает" в кучу
func createPointer() *int {
x := 42
return &x // x не может быть на стеке
}
6. Defer: порядок выполнения и особенности
Defer выполняется по принципу LIFO (Last In, First Out). Аргументы вычисляются в момент объявления, а не выполнения:
func main() {
x := 10
defer fmt.Println("defer 1:", x) // выведет 10
x = 20
defer fmt.Println("defer 2:", x) // выведет 20
// вывод: "defer 2: 20", "defer 1: 10"
}
7. Паника и recover: как обрабатывать ошибки
Паника — это аварийная остановка. recover позволяет перехватить панику, но работает только внутри defer:
func safeDivision(a, b int) (result int) {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
result = 0
}
}()
if b == 0 {
panic("деление на ноль")
}
return a / b
}
func main() {
fmt.Println(safeDivision(10, 0)) // 0
}
8. Структуры и методы: value vs pointer receiver
Важный вопрос для понимания мутабельности и производительности:
- Value receiver — метод работает с копией. Безопасно для горутин, но не изменяет оригинал.
- Pointer receiver — метод может изменять структуру. Используется для больших структур (экономия памяти).
type Counter struct {
Value int
}
// Value receiver
func (c Counter) Increment() {
c.Value++
}
// Pointer receiver
func (c *Counter) IncrementPtr() {
c.Value++
}
func main() {
c := Counter{Value: 0}
c.Increment() // не изменит
c.IncrementPtr() // изменит
fmt.Println(c.Value) // 1
}
9. sync.WaitGroup и синхронизация горутин
Базовый паттерн ожидания завершения группы горутин:
func main() {
var wg sync.WaitGroup
for i := 0; i <