Собрали 50 самых частых вопросов и задач, которые встречаются на собеседованиях Go-разработчиков. Каждый вопрос — с
кратким ответом и примером кода на Golang.
Используйте как чеклист: пройдитесь по списку и проверьте, что на каждый вопрос можете ответить уверенно. А если где-то
застряли — потренируйтесь решать задачи на Go на Девстанции: десятки задач с автоматической проверкой и AI-ревью кода.
Базовые:
int (и варианты int8, int16, int32, int64)uint (и варианты uint8, uint16, uint32, uint64)float32/float64boolstringbyte (алиас uint8)rune (алиас int32 — для символов Unicode).Составные: массивы, слайсы, map, структуры, указатели, функции, интерфейсы, каналы.
var и короткое объявление :=?var x int = 10 — работает на уровне пакета и внутри функций.
x := 10 — только внутри функций, при этом тип выводится автоматически.
:= создаёт новую переменную, но минимум одна
переменная слева должна быть новой.
x := 10
x, err := doSomething() // ok: err новая
// x, y := 1, 2 // ошибка: обе переменные уже существуютВ 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-слайсом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 = 1len и 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) // falserune от 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
}Всегда по значению. Но слайс, map, канал содержат внутри ссылочную структуру (у слайса — указатель на массив), поэтому
их «содержимое» можно менять внутри функции.
func modify(s []int) {
s[0] = 100 // изменит оригинал
s = append(s, 1) // может не изменить оригинал: другая структура слайса
}new и make?new(T) выделяет память и возвращает указатель на нулевое значение *T. make — только для слайсов, map и каналов,
инициализирует их и возвращает сам тип T.
p := new(int) // *int, *p == 0
m := make(map[string]int) // готовая к использованию mapЧерез 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)
}Константа-счётчик внутри const-блока. Начинается с 0 и увеличивается на каждой строке.
const (
_ = iota // пропустить 0
KB = 1 << (10 * iota) // 1 << 10
MB // 1 << 20
GB // 1 << 30
)Массив — фиксированная длина, тип включает размер: [3]int и [5]int — разные типы, передаются по значению (копией).
Слайс — «окно» в массив: указатель, длина и ёмкость. Именно слайсы используются в 99% случаев.
arr := [3]int{1, 2, 3}
s := []int{1, 2, 3}Ёмкость — сколько элементов помещит массив под слайсом без перевыделения. При append за пределы ёмкости создаётся
новый массив (обычно в 2 раза больше), и слайс начинает указывать на него.
s := make([]int, 0, 4)
s = append(s, 1, 2, 3, 4, 5) // превысили cap — реаллокацияДва слайса могут указывать на один массив. 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]...)copy(dst, src) копирует min(len(dst), len(src)) элементов. Для независимой копии:
dst := make([]T, len(src))
copy(dst, src)
// или идиоматично:
dst := append([]T(nil), src...)Классическая задача с собеседования. Три способа — порядок сохраняется или нет:
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]Map — хэш-таблица. Ключ должен быть сравнимым (==): не подходят слайсы, map, функции. Порядок итерации не гарантирован
и специально рандомизирован рантаймом.
Нулевое значение типа. Проверить наличие ключа можно через второе возвращаемое значение.
m := map[string]int{"a": 1}
v, ok := m["b"]
fmt.Println(v, ok) // 0 falseОператором delete — безопасно даже если ключа нет.
delete(m, "a") // нет panic, даже если ключа не былоРантайм кидает 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()Через 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
})У структур нет наследования и методов «внутри». Методы определяются отдельно с получателем (receiver). Композиция вместо
наследования — встраивание (embedding).
type Engine struct{ Power int }
type Car struct {
Engine // встраивание: поля Engine доступны напрямую
Brand string
}Метод со значением-получателем работает с копией, с указателем — с оригиналом. Правило: если нужно менять состояние или
структура большая — используйте указатель и сохраняйте единый стиль.
func (c Counter) Value() int { return c.n } // копия
func (c *Counter) Inc() { c.n++ } // меняет оригиналnil-указатель может вызывать методы?Если метод объявлен с указателем-получателем, но не обращается к полям, вызов на nil работает без panic. Любимый
вопрос на собеседованиях.
type L struct{}
func (l *L) Hello() string { return "hello" }
var l *L = nil
fmt.Println(l.Hello()) // hello — без panic!Интерфейс — это пара: тип и значение (type, value). Переменная интерфейса равна nil, только когда оба — nil. Поэтому
«типизированный nil» не равен nil.
var e error
var err *MyError = nil
e = err
fmt.Println(e == nil) // false! Внутри тип *MyErrorТип реализует интерфейс, если у него есть все методы интерфейса — без объявления implements (утиная типизация). Это
проверяется компилятором.
type Speaker interface{ Speak() string }
type Dog struct{}
func (d Dog) Speak() string { return "Гав" }
var s Speaker = Dog{} // Dog реализует Speaker автоматическиinterface{} отличается от any?Ничем — any это алиас interface{}, добавленный в Go 1.18. Употребляйте any в новом коде.
Оператором ==, если все поля сравнимы (нет слайсов, map, функций внутри). Иначе — reflect.DeepEqual или сравнение
полей вручную.
type Point struct{ X, Y int }
p1, p2 := Point{1, 2}, Point{1, 2}
fmt.Println(p1 == p2) // trueОдин интерфейс может включать другие — это стандартный способ компоновки в стандартной библиотеке.
type Reader interface{ Read(p []byte) (int, error) }
type Writer interface{ Write(p []byte) (int, error) }
type ReadWriter interface {
Reader
Writer
}Лёгкий поток, управляемый рантаймом Go (а не ОС). Стартует с малого стека (~2 КБ), который растёт по необходимости.
Запускается словом go перед вызовом функции.
go doWork(arg) // функция выполняется параллельноПрограмма завершится, не дожидаясь горутин — незавершённая работа теряется. Поэтому нужен синхронизирующий механизм (
канал, 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()Типизированный pipe для передачи данных между горутинами. Буферизованный канал не блокирует отправителя, пока буфер не
заполнен.
ch := make(chan int) // небуферизованный
ch := make(chan int, 10) // буферизованный, ёмкость 10Горутин блокируется навсегда. Аналогично запись в nil-канал. Из закрытого канала чтение возвращает нулевое значение, а
вторая переменная ok — false.
ch := make(chan int, 1)
ch <- 1
close(ch)
v, ok := <-ch // 1, true
v, ok = <-ch // 0, false — канал закрыт и пустselect ждёт несколько канальных операций одновременно и выполняет первую готовую. С default — не блокируется.
select {
case msg := <-ch1:
fmt.Println("ch1:", msg)
case ch2 <- data:
fmt.Println("отправлено в ch2")
case <-time.After(time.Second):
fmt.Println("таймаут")
}Ситуация, когда все горутины заблокированы. Рантайм это детектит и завершает программу. Типичный случай: ch <- v в
буфер 0 без читателя. В select для выхода можно добавить канал-флаг done:
done := make(chan struct{})
go func() {
for {
select {
case v := <-ch:
fmt.Println(v)
case <-done:
return
}
}
}()
close(done)Идиоматично — через 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)Счётчик ожидающих горутин: 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()Ошибка, когда несколько горутин читают и пишут одну память без синхронизации. Детектор: 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) }()
}Гарантирует, что функция выполнится ровно один раз — даже при конкурентных вызовах. Стандартный способ ленивой
инициализации.
var once sync.Once
var instance *DB
func GetDB() *DB {
once.Do(func() {
instance = connect()
})
return instance
}Паттерн 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
}Ограничивает параллелизм фиксированным числом воркеров, обрабатывающих задачи из канала.
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)
}Классика для разминки: числа от 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
}Учитывая, что строка — байты, для 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)
}Два указателя с начала и конца, игнорируем регистр.
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
}Массив содержит 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
}Найти в массиве пару с заданной суммой за 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
}Стандартная задача на map.
func wordCount(text string) map[string]int {
counts := make(map[string]int)
for _, w := range strings.Fields(text) {
counts[w]++
}
return counts
}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
}Задача, объединяющая горутины и каналы: обойти список 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
}Буферизованный канал как семафор: не более 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()
}Идиоматичный пример паттерна «конвейер» (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-ревью
кода и следите за прогрессом в профиле.