SprintCode.pro
Подготовка к алгоритмическим задачам

Собеседование на Go-разработчика: 30 популярных вопросов с ответами
Почему Go востребован на рынке?
Go (Golang) — один из самых быстрорастущих языков программирования:
- 🚀 Используется в Docker, Kubernetes, Terraform, Prometheus
- 💰 Высокие зарплаты для Go-разработчиков
- 🏢 Востребован в Яндекс, Ozon, Avito, VK, Uber, Google
- ⚡ Отличная производительность и простота
- 🔧 Идеален для микросервисов и облачной инфраструктуры
Структура собеседования на Go-разработчика
- Основы Go — синтаксис, типы данных, особенности языка
- Concurrency — горутины, каналы, sync-примитивы
- Интерфейсы и ООП — композиция, полиморфизм
- Управление памятью — указатели, стек/куча, GC
- Практические задачи — алгоритмы, проектирование
- Инфраструктура — тестирование, профилирование, деплой
Часть 1: Основы Go
Вопрос 1: Чем Go отличается от других языков?
Ответ:
// Ключевые особенности Go: // 1. Компилируемый язык со статической типизацией var name string = "Go" // 2. Встроенная поддержка конкурентности go func() { fmt.Println("Горутина") }() // 3. Простой синтаксис без классов, наследования, исключений type User struct { Name string Age int } // 4. Быстрая компиляция // 5. Единственный бинарник без зависимостей // 6. Встроенный форматтер (gofmt) // 7. Garbage Collector с низкой латентностью
Вопрос 2: Что такое zero value в Go?
Ответ:
// Zero value — значение по умолчанию для неинициализированных переменных var i int // 0 var f float64 // 0.0 var b bool // false var s string // "" (пустая строка) var p *int // nil var sl []int // nil var m map[string]int // nil var ch chan int // nil var fn func() // nil // Структуры инициализируются zero values полей type User struct { Name string Age int } var u User // User{Name: "", Age: 0} // Это позволяет избежать null pointer exceptions
Вопрос 3: Разница между make() и new()?
Ответ:
// new(T) — выделяет память, возвращает указатель *T на zero value p := new(int) // *int, указывает на 0 fmt.Println(*p) // 0 user := new(User) // *User fmt.Println(user) // &{ 0} // make(T) — создает и инициализирует slice, map или channel // Возвращает T (не указатель) sl := make([]int, 5) // []int длины 5, capacity 5 sl2 := make([]int, 0, 10) // []int длины 0, capacity 10 m := make(map[string]int) // инициализированная map m["key"] = 1 // OK ch := make(chan int) // небуферизированный канал ch2 := make(chan int, 10) // буферизированный канал // Важно: make нельзя использовать для структур! // s := make(User) // Ошибка компиляции
Пройди собеседование в топ-компанию
Платформа для подготовки
Решай алгоритмические задачи как профи
✓ Популярные алгоритмы✓ Разбор решений✓ AI помощь
Начать сейчас
Вопрос 4: Как работают slices в Go?
Ответ:
// Slice — это "окно" в массив: указатель + длина + capacity // Структура slice под капотом: // type slice struct { // array unsafe.Pointer // указатель на массив // len int // текущая длина // cap int // максимальная емкость // } arr := [5]int{1, 2, 3, 4, 5} sl := arr[1:4] // [2, 3, 4] fmt.Println(len(sl)) // 3 fmt.Println(cap(sl)) // 4 (от индекса 1 до конца массива) // Изменение slice влияет на оригинальный массив! sl[0] = 100 fmt.Println(arr) // [1, 100, 3, 4, 5] // При append сверх capacity создается новый массив sl = append(sl, 6, 7, 8) // Новый массив, arr не изменится // Частая ошибка: передача slice в функцию func modify(s []int) { s[0] = 999 // Изменит оригинал s = append(s, 1) // Не изменит оригинал (если был реаллок) }
Вопрос 5: Что такое defer и как он работает?
Ответ:
// defer откладывает выполнение функции до выхода из текущей функции // Выполняется в порядке LIFO (стек) func example() { defer fmt.Println("1") // Выполнится последним defer fmt.Println("2") // Выполнится вторым defer fmt.Println("3") // Выполнится первым fmt.Println("Основной код") } // Вывод: Основной код, 3, 2, 1 // Аргументы defer вычисляются сразу func deferArgs() { x := 10 defer fmt.Println(x) // Выведет 10, не 20 x = 20 } // Типичные применения defer: func readFile(path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() // Файл закроется при любом выходе // ... работа с файлом return nil } // defer с recover для обработки panic func safeCall() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered:", r) } }() panic("что-то пошло не так") }
Часть 2: Concurrency (Конкурентность)
Вопрос 6: Что такое горутины?
Ответ:
// Горутина — легковесный поток, управляемый Go runtime // Стартует с ~2KB стека (vs ~1MB для OS thread) func main() { // Запуск горутины go sayHello("World") // Анонимная горутина go func(msg string) { fmt.Println(msg) }("Hello") time.Sleep(time.Second) // Плохо! Используйте sync } func sayHello(name string) { fmt.Printf("Hello, %s!\n", name) } // Правильная синхронизация с WaitGroup func main() { var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go func(id int) { defer wg.Done() fmt.Printf("Горутина %d\n", id) }(i) // Важно: передаем i как аргумент! } wg.Wait() // Ждем завершения всех горутин }
Вопрос 7: Как работают каналы (channels)?
Ответ:
// Каналы — типизированные "трубы" для коммуникации между горутинами // Небуферизированный канал (синхронный) ch := make(chan int) go func() { ch <- 42 // Блокируется, пока кто-то не прочитает }() value := <-ch // Блокируется, пока кто-то не запишет fmt.Println(value) // 42 // Буферизированный канал (асинхронный до заполнения буфера) ch := make(chan int, 3) ch <- 1 // Не блокируется ch <- 2 // Не блокируется ch <- 3 // Не блокируется ch <- 4 // Блокируется! Буфер полон // Закрытие канала close(ch) // Чтение из закрытого канала value, ok := <-ch if !ok { fmt.Println("Канал закрыт") } // Range по каналу for value := range ch { fmt.Println(value) // Читает пока канал не закрыт } // Направленные каналы (для безопасности) func producer(ch chan<- int) { // Только запись ch <- 42 } func consumer(ch <-chan int) { // Только чтение fmt.Println(<-ch) }
Вопрос 8: Что такое select?
Ответ:
// select — позволяет ждать несколько операций с каналами func main() { ch1 := make(chan string) ch2 := make(chan string) go func() { time.Sleep(1 * time.Second) ch1 <- "от ch1" }() go func() { time.Sleep(2 * time.Second) ch2 <- "от ch2" }() // Выберется первый готовый канал for i := 0; i < 2; i++ { select { case msg1 := <-ch1: fmt.Println(msg1) case msg2 := <-ch2: fmt.Println(msg2) } } } // select с default (неблокирующий) select { case msg := <-ch: fmt.Println(msg) default: fmt.Println("Канал пуст") } // select с таймаутом select { case msg := <-ch: fmt.Println(msg) case <-time.After(3 * time.Second): fmt.Println("Таймаут!") } // Паттерн: graceful shutdown func worker(done <-chan struct{}) { for { select { case <-done: fmt.Println("Завершаю работу") return default: // Выполняем работу } } }
Вопрос 9: Как избежать race conditions?
Ответ:
// Race condition — неопределенное поведение при конкурентном доступе // ПЛОХО: race condition var counter int func increment() { counter++ // Не атомарно: read -> modify -> write } // Запуск: go run -race main.go (детектор гонок) // Решение 1: sync.Mutex var ( counter int mu sync.Mutex ) func increment() { mu.Lock() defer mu.Unlock() counter++ } // Решение 2: sync.RWMutex (для частого чтения) var ( data map[string]int rw sync.RWMutex ) func read(key string) int { rw.RLock() // Несколько читателей одновременно defer rw.RUnlock() return data[key] } func write(key string, value int) { rw.Lock() // Эксклюзивный доступ defer rw.Unlock() data[key] = value } // Решение 3: sync/atomic var counter int64 func increment() { atomic.AddInt64(&counter, 1) } func get() int64 { return atomic.LoadInt64(&counter) } // Решение 4: Каналы (share memory by communicating) type Command struct { Action string Value int Result chan int } func worker(commands <-chan Command) { counter := 0 for cmd := range commands { switch cmd.Action { case "inc": counter += cmd.Value case "get": cmd.Result <- counter } } }
Вопрос 10: Что такое context и зачем он нужен?
Ответ:
import "context" // Context — для отмены операций, таймаутов и передачи значений // 1. Отмена операции func main() { ctx, cancel := context.WithCancel(context.Background()) go worker(ctx) time.Sleep(2 * time.Second) cancel() // Отменяем все операции с этим контекстом } func worker(ctx context.Context) { for { select { case <-ctx.Done(): fmt.Println("Отменено:", ctx.Err()) return default: fmt.Println("Работаю...") time.Sleep(500 * time.Millisecond) } } } // 2. Таймаут func fetchWithTimeout() error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.example.com", nil) resp, err := http.DefaultClient.Do(req) if err != nil { return err // Включая context.DeadlineExceeded } defer resp.Body.Close() return nil } // 3. Передача значений (осторожно!) type key string func main() { ctx := context.WithValue(context.Background(), key("userID"), 123) processRequest(ctx) } func processRequest(ctx context.Context) { userID := ctx.Value(key("userID")).(int) fmt.Println("User ID:", userID) } // Правила использования context: // - Первый параметр функции // - Не храните в структурах // - Не передавайте nil, используйте context.TODO()
Часть 3: Интерфейсы и типы
Вопрос 11: Как работают интерфейсы в Go?
Ответ:
// Интерфейсы в Go — неявные (implicit) // Тип реализует интерфейс, если имеет все его методы type Writer interface { Write([]byte) (int, error) } type Logger interface { Log(message string) } // Структура автоматически реализует интерфейс type FileLogger struct { file *os.File } func (f *FileLogger) Log(message string) { f.file.WriteString(message + "\n") } func (f *FileLogger) Write(p []byte) (int, error) { return f.file.Write(p) } // FileLogger реализует и Logger, и Writer! // Пустой интерфейс — принимает любой тип func printAny(v interface{}) { // или any в Go 1.18+ fmt.Println(v) } // Type assertion var w Writer = &FileLogger{} // Безопасное приведение типа if fl, ok := w.(*FileLogger); ok { fmt.Println("Это FileLogger") } // Type switch switch v := w.(type) { case *FileLogger: fmt.Println("FileLogger") case *ConsoleLogger: fmt.Println("ConsoleLogger") default: fmt.Printf("Неизвестный тип: %T\n", v) }
Вопрос 12: Чем отличается value receiver от pointer receiver?
Ответ:
type User struct { Name string Age int } // Value receiver — работает с копией func (u User) GetName() string { return u.Name } // Pointer receiver — работает с оригиналом func (u *User) SetName(name string) { u.Name = name } func (u *User) Birthday() { u.Age++ // Изменит оригинал } func main() { user := User{Name: "Alice", Age: 25} user.SetName("Bob") // Go автоматически берет адрес fmt.Println(user.Name) // Bob // Но для интерфейсов важно! var i interface{ SetName(string) } // i = user // Ошибка! User не реализует интерфейс i = &user // OK } // Правила выбора: // Pointer receiver если: // - Метод изменяет получателя // - Структура большая (избегаем копирования) // - Для консистентности (если хотя бы один метод pointer) // Value receiver если: // - Структура маленькая и неизменяемая // - Нужна thread-safety (каждая горутина работает с копией)
Вопрос 13: Что такое embedding в Go?
Ответ:
// Embedding — композиция вместо наследования type Animal struct { Name string } func (a Animal) Speak() { fmt.Println(a.Name, "makes a sound") } // Встраивание (embedding) type Dog struct { Animal // Анонимное поле — встраивание Breed string } func main() { dog := Dog{ Animal: Animal{Name: "Buddy"}, Breed: "Labrador", } // Методы Animal доступны напрямую dog.Speak() // Buddy makes a sound fmt.Println(dog.Name) // Buddy (promoted field) } // Переопределение метода func (d Dog) Speak() { fmt.Println(d.Name, "says woof!") } // Embedding интерфейсов type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } type ReadWriter interface { Reader // Встраивание интерфейса Writer } // Embedding для DI и декораторов type LoggingDB struct { *sql.DB // Все методы sql.DB доступны logger *log.Logger } func (db *LoggingDB) Query(query string, args ...interface{}) (*sql.Rows, error) { db.logger.Printf("Query: %s", query) return db.DB.Query(query, args...) // Вызов оригинального метода }
Часть 4: Управление памятью
Вопрос 14: Как работает Garbage Collector в Go?
Ответ:
// Go использует concurrent, tri-color mark-and-sweep GC // Три цвета: // - Белый: объекты для удаления // - Серый: объекты для проверки // - Черный: достижимые объекты // Фазы GC: // 1. Mark Setup — STW (stop-the-world), включает write barrier // 2. Marking — concurrent, помечает достижимые объекты // 3. Mark Termination — STW, финализация // 4. Sweeping — concurrent, освобождает память // Настройка GC import "runtime/debug" func main() { // GOGC=100 (по умолчанию) — GC запускается при удвоении heap debug.SetGCPercent(50) // Чаще GC, меньше памяти debug.SetGCPercent(200) // Реже GC, больше памяти debug.SetGCPercent(-1) // Отключить авто-GC // Принудительный запуск GC runtime.GC() // Статистика памяти var m runtime.MemStats runtime.ReadMemStats(&m) fmt.Printf("Alloc = %v MB\n", m.Alloc / 1024 / 1024) fmt.Printf("NumGC = %v\n", m.NumGC) } // Советы по оптимизации: // 1. Переиспользуйте объекты (sync.Pool) // 2. Избегайте лишних аллокаций // 3. Используйте массивы вместо slice где возможно // 4. Профилируйте: go tool pprof
Вопрос 15: Stack vs Heap — куда попадают переменные?
Ответ:
// Компилятор Go решает через escape analysis // Stack — быстро, автоматически освобождается func stackAlloc() int { x := 42 // На стеке return x } // Heap — медленнее, нужен GC func heapAlloc() *int { x := 42 return &x // x "убегает" на heap } // Проверка: go build -gcflags="-m" // ./main.go:10:2: moved to heap: x // Что вызывает escape на heap: // 1. Возврат указателя на локальную переменную // 2. Сохранение в глобальную переменную // 3. Отправка указателя в канал // 4. Closure захватывает переменную // 5. Слишком большой объект для стека // 6. Размер неизвестен на этапе компиляции // Пример: slice с неизвестным размером func escapeExample(n int) []int { return make([]int, n) // n неизвестен -> heap } func noEscape() []int { return make([]int, 10) // Известный размер -> может быть stack } // Оптимизация: sync.Pool для переиспользования объектов var bufferPool = sync.Pool{ New: func() interface{} { return make([]byte, 1024) }, } func process() { buf := bufferPool.Get().([]byte) defer bufferPool.Put(buf) // Используем buf... }
Часть 5: Практические задачи
Вопрос 16: Реализуйте worker pool
Ответ:
func workerPool(numWorkers int, jobs <-chan int, results chan<- int) { var wg sync.WaitGroup for i := 0; i < numWorkers; i++ { wg.Add(1) go func(workerID int) { defer wg.Done() for job := range jobs { fmt.Printf("Worker %d processing job %d\n", workerID, job) results <- job * 2 // Пример обработки } }(i) } wg.Wait() close(results) } func main() { jobs := make(chan int, 100) results := make(chan int, 100) // Запускаем pool go workerPool(3, jobs, results) // Отправляем задачи for i := 1; i <= 10; i++ { jobs <- i } close(jobs) // Собираем результаты for result := range results { fmt.Println("Result:", result) } }
Вопрос 17: Реализуйте rate limiter
Ответ:
// Простой rate limiter на основе time.Ticker type RateLimiter struct { ticker *time.Ticker tokens chan struct{} } func NewRateLimiter(rps int) *RateLimiter { rl := &RateLimiter{ ticker: time.NewTicker(time.Second / time.Duration(rps)), tokens: make(chan struct{}, rps), } go func() { for range rl.ticker.C { select { case rl.tokens <- struct{}{}: default: // Буфер полон } } }() return rl } func (rl *RateLimiter) Wait() { <-rl.tokens } func (rl *RateLimiter) Stop() { rl.ticker.Stop() } // Использование func main() { limiter := NewRateLimiter(5) // 5 запросов в секунду defer limiter.Stop() for i := 0; i < 20; i++ { limiter.Wait() fmt.Printf("Request %d at %v\n", i, time.Now()) } } // Альтернатива: golang.org/x/time/rate import "golang.org/x/time/rate" limiter := rate.NewLimiter(rate.Limit(5), 10) // 5 rps, burst 10 if err := limiter.Wait(ctx); err != nil { return err }
Вопрос 18: Реализуйте graceful shutdown
Ответ:
func main() { server := &http.Server{Addr: ":8080"} // Канал для сигналов завершения done := make(chan os.Signal, 1) signal.Notify(done, os.Interrupt, syscall.SIGTERM) // Запускаем сервер в горутине go func() { if err := server.ListenAndServe(); err != http.ErrServerClosed { log.Fatalf("Server error: %v", err) } }() log.Println("Server started on :8080") // Ждем сигнал завершения <-done log.Println("Shutting down...") // Graceful shutdown с таймаутом ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := server.Shutdown(ctx); err != nil { log.Printf("Shutdown error: %v", err) } log.Println("Server stopped") }
Вопрос 19: Напишите функцию для параллельного выполнения с ограничением
Ответ:
// Semaphore pattern — ограничение параллелизма func parallelLimit(tasks []func() error, limit int) []error { var ( wg sync.WaitGroup mu sync.Mutex errors []error sem = make(chan struct{}, limit) ) for _, task := range tasks { wg.Add(1) go func(t func() error) { defer wg.Done() sem <- struct{}{} // Захватываем слот defer func() { <-sem }() // Освобождаем слот if err := t(); err != nil { mu.Lock() errors = append(errors, err) mu.Unlock() } }(task) } wg.Wait() return errors } // Использование с errgroup (рекомендуется) import "golang.org/x/sync/errgroup" func fetchAll(urls []string) error { g, ctx := errgroup.WithContext(context.Background()) g.SetLimit(10) // Максимум 10 параллельных запросов for _, url := range urls { url := url // Важно для closure g.Go(func() error { return fetch(ctx, url) }) } return g.Wait() // Вернет первую ошибку }
Вопрос 20: Реализуйте простой кэш с TTL
Ответ:
type Cache struct { mu sync.RWMutex items map[string]cacheItem } type cacheItem struct { value interface{} expiration int64 } func NewCache() *Cache { c := &Cache{ items: make(map[string]cacheItem), } go c.cleanupLoop() return c } func (c *Cache) Set(key string, value interface{}, ttl time.Duration) { c.mu.Lock() defer c.mu.Unlock() c.items[key] = cacheItem{ value: value, expiration: time.Now().Add(ttl).UnixNano(), } } func (c *Cache) Get(key string) (interface{}, bool) { c.mu.RLock() defer c.mu.RUnlock() item, found := c.items[key] if !found { return nil, false } if time.Now().UnixNano() > item.expiration { return nil, false } return item.value, true } func (c *Cache) cleanupLoop() { ticker := time.NewTicker(time.Minute) for range ticker.C { c.mu.Lock() now := time.Now().UnixNano() for key, item := range c.items { if now > item.expiration { delete(c.items, key) } } c.mu.Unlock() } }
Часть 6: Инфраструктура и тестирование
Вопрос 21: Как писать тесты в Go?
Ответ:
// file: math.go package math func Add(a, b int) int { return a + b } // file: math_test.go package math import "testing" // Базовый тест func TestAdd(t *testing.T) { result := Add(2, 3) if result != 5 { t.Errorf("Add(2, 3) = %d; want 5", result) } } // Table-driven тесты (идиоматичный Go) func TestAddTableDriven(t *testing.T) { tests := []struct { name string a, b int expected int }{ {"positive", 2, 3, 5}, {"negative", -1, -2, -3}, {"zero", 0, 0, 0}, {"mixed", -5, 10, 5}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := Add(tt.a, tt.b) if result != tt.expected { t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected) } }) } } // Бенчмарк func BenchmarkAdd(b *testing.B) { for i := 0; i < b.N; i++ { Add(2, 3) } } // Запуск: // go test -v # Все тесты // go test -run TestAdd # Конкретный тест // go test -bench=. # Бенчмарки // go test -cover # Покрытие // go test -race # Детектор гонок
Вопрос 22: Как профилировать Go-приложения?
Ответ:
import ( "net/http" _ "net/http/pprof" ) func main() { // Включаем pprof endpoint go func() { http.ListenAndServe("localhost:6060", nil) }() // ... основное приложение } // Команды для анализа: // go tool pprof http://localhost:6060/debug/pprof/heap # Память // go tool pprof http://localhost:6060/debug/pprof/profile # CPU (30s) // go tool pprof http://localhost:6060/debug/pprof/goroutine # Горутины // В интерактивном режиме: // (pprof) top # Топ функций // (pprof) list FuncName # Построчный анализ // (pprof) web # Граф в браузере // Трассировка import "runtime/trace" f, _ := os.Create("trace.out") trace.Start(f) defer trace.Stop() // go tool trace trace.out
Часто встречающиеся ошибки
Ошибка 1: Забытая переменная в closure
// ПЛОХО for i := 0; i < 5; i++ { go func() { fmt.Println(i) // Все горутины выведут 5! }() } // ХОРОШО for i := 0; i < 5; i++ { go func(n int) { fmt.Println(n) // Правильно: 0, 1, 2, 3, 4 }(i) }
Ошибка 2: Запись в nil map
// ПЛОХО var m map[string]int m["key"] = 1 // panic: assignment to entry in nil map // ХОРОШО m := make(map[string]int) m["key"] = 1
Ошибка 3: Не закрытые каналы
// ПЛОХО — утечка горутин func process(ch chan int) { for v := range ch { // Никогда не завершится fmt.Println(v) } } // ХОРОШО go func() { // ... отправка данных close(ch) // Не забываем закрыть! }()
Заключение
Подготовка к собеседованию на Go-разработчика требует понимания:
- Особенностей языка (zero values, slices, defer)
- Конкурентности (горутины, каналы, sync-примитивы)
- Интерфейсов и композиции
- Управления памятью и GC
- Паттернов (worker pool, graceful shutdown)
- Инструментов (testing, pprof, race detector)
Go ценит простоту и читаемость кода. На собеседовании важно не только знать ответы, но и писать идиоматичный Go-код.
