Топ-50 вопросов и задач с Go-собеседования

Собрали 50 самых частых вопросов и задач, которые встречаются на собеседованиях Go-разработчиков. Каждый вопрос — с
кратким ответом и примером кода на Golang.

Используйте как чеклист: пройдитесь по списку и проверьте, что на каждый вопрос можете ответить уверенно. А если где-то
застряли — потренируйтесь решать задачи на Go на Девстанции: десятки задач с автоматической проверкой и AI-ревью кода.


Содержание

  1. Основы языка (1–10)
  2. Слайсы, массивы, map (11–20)
  3. Структуры и интерфейсы (21–28)
  4. Горутины и каналы (29–40)
  5. Алгоритмические задачи (41–50)

Основы языка

1. Какие типы данных есть в Go?

Базовые:

  • int (и варианты int8, int16, int32, int64)
  • uint (и варианты uint8, uint16, uint32, uint64)
  • float32/float64
  • bool
  • string
  • byte (алиас uint8)
  • rune (алиас int32 — для символов Unicode).

Составные: массивы, слайсы, map, структуры, указатели, функции, интерфейсы, каналы.

2. Чем отличается var и короткое объявление :=?

var x int = 10 — работает на уровне пакета и внутри функций.

x := 10 — только внутри функций, при этом тип выводится автоматически.

:= создаёт новую переменную, но минимум одна
переменная слева должна быть новой.

x := 10
x, err := doSomething() // ok: err новая
// x, y := 1, 2 // ошибка: обе переменные уже существуют

3. Что такое нулевое значение (zero value)?

В Go переменные всегда инициализируются: int → 0, bool → false, string → "", указатели, слайсы, map,
интерфейсы, каналы → nil. Это избавляет от неинициализированной памяти, но nil-map и nil-слайс ведут себя
по-разному.

var m map[string]int
m["a"] = 1 // panic: nil map — писать нельзя

var s []int
s = append(s, 1) // ok: append работает с nil-слайсом

4. Как работает defer?

defer откладывает выполнение функции до выхода из текущей функции. Аргументы вычисляются в момент объявления defer,
а вызываются функции в порядке LIFO.

func main() {
  defer fmt.Println("1")
  defer fmt.Println("2")
  fmt.Println("3")
  // Вывод: 3, 2, 1
}

Классика собеседований: defer с замыканием и с вычисленным аргументом.

i := 0
defer fmt.Println(i) // выведет 0 — аргумент уже вычислен
defer func() { fmt.Println(i) }() // выведет 1 — i читается в момент вызова
i = 1

5. Что вернёт len и cap для nil-слайса?

Оба вернут 0 — и это не ошибка. Отличить nil-слайс от пустого можно сравнением с nil.

var s []int
fmt.Println(len(s), cap(s), s == nil) // 0 0 true
s2 := []int{}
fmt.Println(s2 == nil) // false

6. Чем отличается rune от byte?

byte — один байт (uint8), rune — один символ Unicode (int32, до 4 байт в UTF-8). Строка в Go — это
последовательность байт, а не символов.

s := "привет"
fmt.Println(len(s))        // 12 байт (по 2 на кириллицу)
fmt.Println(len([]rune(s))) // 6 символов
for i, r := range s {      // range итерирует по rune
  _ = i
  _ = r
}

7. Как передаются аргументы в функции?

Всегда по значению. Но слайс, map, канал содержат внутри ссылочную структуру (у слайса — указатель на массив), поэтому
их «содержимое» можно менять внутри функции.

func modify(s []int) {
  s[0] = 100 // изменит оригинал
  s = append(s, 1) // может не изменить оригинал: другая структура слайса
}

8. Что такое new и make?

new(T) выделяет память и возвращает указатель на нулевое значение *T. make — только для слайсов, map и каналов,
инициализирует их и возвращает сам тип T.

p := new(int)      // *int, *p == 0
m := make(map[string]int) // готовая к использованию map

9. Как проверить тип переменной?

Через type switch или type assertion.

var i interface{} = "hello"

if s, ok := i.(string); ok {
  fmt.Println(s)
}

switch v := i.(type) {
case string:
  fmt.Println("строка:", v)
case int:
  fmt.Println("число:", v)
}

10. Что такое iota?

Константа-счётчик внутри const-блока. Начинается с 0 и увеличивается на каждой строке.

const (
  _ = iota // пропустить 0
  KB = 1 << (10 * iota) // 1 << 10
  MB                    // 1 << 20
  GB                    // 1 << 30
)

Слайсы, массивы, map

11. Чем отличается массив от слайса?

Массив — фиксированная длина, тип включает размер: [3]int и [5]int — разные типы, передаются по значению (копией).
Слайс — «окно» в массив: указатель, длина и ёмкость. Именно слайсы используются в 99% случаев.

arr := [3]int{1, 2, 3}
s := []int{1, 2, 3}

12. Что такое ёмкость (capacity) слайса?

Ёмкость — сколько элементов помещит массив под слайсом без перевыделения. При append за пределы ёмкости создаётся
новый массив (обычно в 2 раза больше), и слайс начинает указывать на него.

s := make([]int, 0, 4)
s = append(s, 1, 2, 3, 4, 5) // превысили cap — реаллокация

13. Какие подводные камни у слайсов?

Два слайса могут указывать на один массив. append может как использовать общую память, так и создавать новую — это
источник трудноуловимых багов.

a := []int{1, 2, 3, 4, 5}
b := a[1:3]      // [2 3]
b = append(b, 99) // изменил a[3]! b и a делят массив
fmt.Println(a)   // [1 2 3 99 5]

// Решение — полная копия
c := append([]int(nil), a[1:3]...)

14. Как скопировать слайс?

copy(dst, src) копирует min(len(dst), len(src)) элементов. Для независимой копии:

dst := make([]T, len(src))
copy(dst, src)
// или идиоматично:
dst := append([]T(nil), src...)

15. Как удалить элемент из слайса по индексу?

Классическая задача с собеседования. Три способа — порядок сохраняется или нет:

s := []int{1, 2, 3, 4, 5}
i := 2

// 1. Сохраняя порядок (O(n))
s = append(s[:i], s[i+1:]...) // [1 2 4 5]

// 2. Без сохранения порядка (O(1))
s2 := []int{1, 2, 3, 4, 5}
s2[i] = s2[len(s2)-1]
s2 = s2[:len(s2)-1] // [1 2 5 4]

16. Как работает map внутри?

Map — хэш-таблица. Ключ должен быть сравнимым (==): не подходят слайсы, map, функции. Порядок итерации не гарантирован
и специально рандомизирован рантаймом.

17. Что вернёт обращение к несуществующему ключу map?

Нулевое значение типа. Проверить наличие ключа можно через второе возвращаемое значение.

m := map[string]int{"a": 1}
v, ok := m["b"]
fmt.Println(v, ok) // 0 false

18. Как удалить ключ из map?

Оператором delete — безопасно даже если ключа нет.

delete(m, "a") // нет panic, даже если ключа не было

19. Почему map нельзя конкурентно читать и писать?

Рантайм кидает fatal error: concurrent map read and map write — это не recoverable panic. Нужен sync.Mutex,
sync.RWMutex или sync.Map (для случаев «пишут один раз, читают много»).

var mu sync.RWMutex
mu.Lock()
m["key"] = 1
mu.Unlock()

mu.RLock()
v := m["key"]
mu.RUnlock()

20. Как отсортировать слайс?

Через sort.Slice (не стабильный) или slices.Sort в новых версиях Go (1.21+).

people := []Person{{"Bob", 25}, {"Alice", 30}}
sort.Slice(people, func(i, j int) bool {
  return people[i].Age < people[j].Age
})

Структуры и интерфейсы

21. Чем структура Go отличается от класса?

У структур нет наследования и методов «внутри». Методы определяются отдельно с получателем (receiver). Композиция вместо
наследования — встраивание (embedding).

type Engine struct{ Power int }

type Car struct {
  Engine // встраивание: поля Engine доступны напрямую
  Brand string
}

22. Что такое получатель по значению и по указателю?

Метод со значением-получателем работает с копией, с указателем — с оригиналом. Правило: если нужно менять состояние или
структура большая — используйте указатель и сохраняйте единый стиль.

func (c Counter) Value() int { return c.n }  // копия
func (c *Counter) Inc() { c.n++ }            // меняет оригинал

23. Почему nil-указатель может вызывать методы?

Если метод объявлен с указателем-получателем, но не обращается к полям, вызов на nil работает без panic. Любимый
вопрос на собеседованиях.

type L struct{}

func (l *L) Hello() string { return "hello" }

var l *L = nil
fmt.Println(l.Hello()) // hello — без panic!

24. Как устроены интерфейсы?

Интерфейс — это пара: тип и значение (type, value). Переменная интерфейса равна nil, только когда оба — nil. Поэтому
«типизированный nil» не равен nil.

var e error
var err *MyError = nil
e = err
fmt.Println(e == nil) // false! Внутри тип *MyError

25. Что такое неявная реализация интерфейса?

Тип реализует интерфейс, если у него есть все методы интерфейса — без объявления implements (утиная типизация). Это
проверяется компилятором.

type Speaker interface{ Speak() string }

type Dog struct{}
func (d Dog) Speak() string { return "Гав" }

var s Speaker = Dog{} // Dog реализует Speaker автоматически

26. Чем пустой интерфейс interface{} отличается от any?

Ничем — any это алиас interface{}, добавленный в Go 1.18. Употребляйте any в новом коде.

27. Как сравнить две структуры?

Оператором ==, если все поля сравнимы (нет слайсов, map, функций внутри). Иначе — reflect.DeepEqual или сравнение
полей вручную.

type Point struct{ X, Y int }
p1, p2 := Point{1, 2}, Point{1, 2}
fmt.Println(p1 == p2) // true

28. Что такое встраивание интерфейса?

Один интерфейс может включать другие — это стандартный способ компоновки в стандартной библиотеке.

type Reader interface{ Read(p []byte) (int, error) }
type Writer interface{ Write(p []byte) (int, error) }
type ReadWriter interface {
  Reader
  Writer
}

Горутины и каналы

29. Что такое горутина?

Лёгкий поток, управляемый рантаймом Go (а не ОС). Стартует с малого стека (~2 КБ), который растёт по необходимости.
Запускается словом go перед вызовом функции.

go doWork(arg) // функция выполняется параллельно

30. Что будет с горутиной, если main завершился?

Программа завершится, не дожидаясь горутин — незавершённая работа теряется. Поэтому нужен синхронизирующий механизм (
канал, WaitGroup).

var wg sync.WaitGroup
for i := 0; i < 3; i++ {
  wg.Add(1)
  go func(n int) {
    defer wg.Done()
    fmt.Println(n)
  }(i)
}
wg.Wait()

31. Что такое канал?

Типизированный pipe для передачи данных между горутинами. Буферизованный канал не блокирует отправителя, пока буфер не
заполнен.

ch := make(chan int)        // небуферизованный
ch := make(chan int, 10)    // буферизованный, ёмкость 10

32. Что происходит при чтении из nil-канала?

Горутин блокируется навсегда. Аналогично запись в nil-канал. Из закрытого канала чтение возвращает нулевое значение, а
вторая переменная ok — false.

ch := make(chan int, 1)
ch <- 1
close(ch)

v, ok := <-ch // 1, true
v, ok = <-ch  // 0, false — канал закрыт и пуст

33. Зачем нужен select?

select ждёт несколько канальных операций одновременно и выполняет первую готовую. С default — не блокируется.

select {
case msg := <-ch1:
  fmt.Println("ch1:", msg)
case ch2 <- data:
  fmt.Println("отправлено в ch2")
case <-time.After(time.Second):
  fmt.Println("таймаут")
}

34. Что такое deadlock и как его избежать?

Ситуация, когда все горутины заблокированы. Рантайм это детектит и завершает программу. Типичный случай: ch <- v в
буфер 0 без читателя. В select для выхода можно добавить канал-флаг done:

done := make(chan struct{})
go func() {
  for {
	select {
	case v := <-ch:
	  fmt.Println(v)
	case <-done:
	  return
	}
  }
}()
close(done)

35. Как остановить горутину?

Идиоматично — через context. Канал ctx.Done() закрывается при отмене.

func worker(ctx context.Context) {
  for {
	select {
	case <-ctx.Done():
	  return // выход
	case v := <-ch:
	  process(v)
	}
  }
}

ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go worker(ctx)

36. Что такое WaitGroup и когда его использовать?

Счётчик ожидающих горутин: Add(n) — увеличить, Done() — уменьшить, Wait() — ждать нуля. Используется, когда нужно
дождаться N независимых задач.

var wg sync.WaitGroup
wg.Add(2)
go func() { defer wg.Done(); task1() }()
go func() { defer wg.Done(); task2() }()
wg.Wait()

37. Что такое race condition и как его ловить?

Ошибка, когда несколько горутин читают и пишут одну память без синхронизации. Детектор: go test -race или
go run -race. Решение — мьютексы или каналы.

// Гонка: counter++ не атомарен
var counter int
for i := 0; i < 100; i++ {
  go func() { counter++ }()
}

// Решение — sync.Mutex или atomic
var counter int64
for i := 0; i < 100; i++ {
  go func() { atomic.AddInt64(&counter, 1) }()
}

38. Что такое sync.Once?

Гарантирует, что функция выполнится ровно один раз — даже при конкурентных вызовах. Стандартный способ ленивой
инициализации.

var once sync.Once
var instance *DB

func GetDB() *DB {
  once.Do(func() {
	instance = connect()
  })
  return instance
}

39. Какие паттерны работы с каналами знаете?

Паттерн fan-in/fan-out: fan-in — слияние каналов в один, fan-out — распределение задач по воркерам.

// Fan-in: слияние двух каналов
func merge(ch1, ch2 <-chan int) <-chan int {
  out := make(chan int)
  go func() {
	for v := range ch1 { out <- v }
  }()
  go func() {
	for v := range ch2 { out <- v }
  }()
  return out
}

40. Worker Pool — зачем и как?

Ограничивает параллелизм фиксированным числом воркеров, обрабатывающих задачи из канала.

func workerPool(jobs <-chan Job, results chan<- Result, workers int) {
  var wg sync.WaitGroup
  for i := 0; i < workers; i++ {
	wg.Add(1)
	go func() {
	  defer wg.Done()
	  for j := range jobs {
	    results <- process(j)
	  }
	}()
  }
  wg.Wait()
  close(results)
}

Алгоритмические задачи

41. FizzBuzz

Классика для разминки: числа от 1 до n, кратные 3 → «Fizz», кратные 5 → «Buzz», оба → «FizzBuzz».

func fizzBuzz(n int) []string {
  result := make([]string, 0, n)
  for i := 1; i <= n; i++ {
	switch {
	case i%15 == 0:
	  result = append(result, "FizzBuzz")
	case i%3 == 0:
	  result = append(result, "Fizz")
	case i%5 == 0:
	  result = append(result, "Buzz")
	default:
	  result = append(result, strconv.Itoa(i))
	}
  }
  return result
}

42. Разворот строки

Учитывая, что строка — байты, для Unicode лучше работать со слайсом рун.

func reverseString(s string) string {
  runes := []rune(s)
  for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
	runes[i], runes[j] = runes[j], runes[i]
  }
  return string(runes)
}

43. Проверка палиндрома

Два указателя с начала и конца, игнорируем регистр.

func isPalindrome(s string) bool {
  runes := []rune(strings.ToLower(s))
  for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
	if runes[i] != runes[j] {
	  return false
	}
  }
  return true
}

44. Поиск пропущенного числа

Массив содержит n-1 чисел из диапазона 0..n. Найти пропущенное через сумму.

func missingNumber(nums []int) int {
  n := len(nums)
  sum := n * (n + 1) / 2
  for _, v := range nums {
    sum -= v
  }
  return sum
}

45. Пары чисел, дающие сумму

Найти в массиве пару с заданной суммой за O(n) через map.

func twoSum(nums []int, target int) (int, int, bool) {
  seen := make(map[int]int, len(nums))
  for i, v := range nums {
	if j, ok := seen[target-v]; ok {
	  return j, i, true
	}
	seen[v] = i
  }
  return 0, 0, false
}

46. Подсчёт частоты слов

Стандартная задача на map.

func wordCount(text string) map[string]int {
  counts := make(map[string]int)
  for _, w := range strings.Fields(text) {
	counts[w]++
  }
  return counts
}

47. Слияние отсортированных слайсов

func mergeSorted(a, b []int) []int {
  result := make([]int, 0, len(a)+len(b))
  i, j := 0, 0
  for i < len(a) && j < len(b) {
	if a[i] <= b[j] {
	  result = append(result, a[i])
	  i++
	} else {
	  result = append(result, b[j])
	  j++
	}
  }
  result = append(result, a[i:]...)
  result = append(result, b[j:]...)
  return result
}

48. Параллельная обработка URL

Задача, объединяющая горутины и каналы: обойти список URL параллельно и собрать результаты.

func fetchAll(urls []string) []string {
  results := make([]string, len(urls))
  var wg sync.WaitGroup
  for i, url := range urls {
	wg.Add(1)
	go func(i int, url string) {
	  defer wg.Done()
	  results[i] = fetch(url)
	}(i, url)
  }
  wg.Wait()
  return results
}

49. Ограничение параллелизма через семафор

Буферизованный канал как семафор: не более N задач одновременно.

func withLimit(tasks []Task, limit int) {
  sem := make(chan struct{}, limit)
  var wg sync.WaitGroup
  for _, t := range tasks {
	wg.Add(1)
	sem <- struct{}{} // занимает слот
	go func(t Task) {
	  defer wg.Done()
	  defer func() { <-sem }() // освобождает слот
	  process(t)
	}(t)
  }
  wg.Wait()
}

50. Генерация чисел Фибоначчи через каналы

Идиоматичный пример паттерна «конвейер» (pipeline).

func fib(n int) <-chan int {
  ch := make(chan int)
  go func() {
	defer close(ch)
	a, b := 0, 1
	for i := 0; i < n; i++ {
	  ch <- a
	  a, b = b, a+b
	}
  }()
  return ch
}

// Использование
for v := range fib(10) {
  fmt.Println(v)
}

Что дальше

Прошлись по всем 50 и нигде не споткнулись — вы готовы к собеседованию. Где-то задумались — это сигнал: лучше один раз
решить задачу руками, чем десять раз прочитать ответ.

Тренируйте алгоритмические задачи на Go на Девстанции: решайте задачи с автоматической проверкой, получайте AI-ревью
кода и следите за прогрессом в профиле.