Модуль: Алгоритмы · Уровень: Middle+/Senior
TL;DR#
- На Go-интервью ценят идиоматичность:
slices/maps/sort,map[T]struct{}как set, multiple return +comma-ok, ранние возвраты. - Главные ловушки: range копирует значение, shared backing array при slicing, байты vs руны в строках, nil map panic, захват переменной цикла в горутине/замыкании.
- Знать stdlib:
sort,slices(1.21+),maps,strings,strconv,container/heap. Не изобретать велосипед. - Senior отличают: чистый код, обработка edge cases, понимание аллокаций/escape, написанные тесты (table-driven), проговаривание сложности.
Теория#
Идиоматичные приёмы для алгоритмов#
// comma-ok для map и type assertion:
if v, ok := m[key]; ok { _ = v }
// multiple return — норма, не "костыль":
func minmax(a []int) (mn, mx int) { /* ... */ return }
// set через map[T]struct{} (0 байт на значение):
seen := map[int]struct{}{}
seen[x] = struct{}{}
_, ok := seen[x]
// swap без temp:
a[i], a[j] = a[j], a[i]
// ранний возврат вместо вложенности:
if len(a) == 0 { return nil }Слайсы: то, что спросят#
s := make([]int, 0, n) // pre-alloc cap — меньше реаллокаций
s = append(s, vals...) // распаковка слайса
b := slices.Clone(s) // независимая копия (Go 1.21+)
s = slices.Delete(s, i, i+1) // удаление с сохранением порядка
idx := slices.Index(s, x) // поиск, -1 если нет
ok := slices.Contains(s, x)
slices.Reverse(s)
mx := slices.Max(s)
// 2D-слайс — правильная инициализация (каждый ряд отдельно!):
grid := make([][]int, rows)
for i := range grid {
grid[i] = make([]int, cols)
}Map: то, что спросят#
m := make(map[string]int, hint) // hint = ожидаемый размер
m[k]++ // работает даже если k нет (zero value 0)
delete(m, k)
v := m[absent] // zero value, без panic (для ЧТЕНИЯ)
// детерминированный обход (map рандомизирован!):
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
slices.Sort(keys)
for _, k := range keys { _ = m[k] }
// maps пакет (1.21+):
import "maps"
clone := maps.Clone(m)
for k := range maps.Keys(m) { _ = k } // iterator (1.23+)Строки и руны (UTF-8!)#
Строка в Go — неизменяемая последовательность байтов (UTF-8). Индексация s[i] даёт байт, range даёт руны.
s := "héllo"
len(s) // байты (может быть > числа символов!)
s[0] // byte
for i, r := range s { // i — байтовый индекс, r — rune
_ = i; _ = r
}
runes := []rune(s) // конвертация в руны: теперь len = число символов
n := utf8.RuneCountInString(s) // число рун без аллокации
// Разворот строки с учётом Unicode:
func reverseStr(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
// Эффективная конкатенация (вместо += в цикле — O(n²)!):
var b strings.Builder
b.Grow(estimatedSize) // pre-alloc
for _, p := range parts {
b.WriteString(p)
}
result := b.String()Пакет sort и slices (сортировка)#
// Go 1.21+ — предпочтительно:
slices.Sort(nums) // Ordered
slices.SortFunc(xs, func(a, b T) int { // -1 / 0 / +1
return cmp.Compare(a.Key, b.Key)
})
slices.SortStableFunc(xs, cmpFn) // стабильная
// До 1.21 / для совместимости:
sort.Ints(nums)
sort.Strings(strs)
sort.Slice(xs, func(i, j int) bool { return xs[i].Key < xs[j].Key }) // less
sort.SliceStable(xs, less)
sort.Sort(myInterface) // полный sort.Interface
// Сортировка по нескольким полям:
slices.SortFunc(people, func(a, b Person) int {
if c := cmp.Compare(a.Age, b.Age); c != 0 {
return c
}
return cmp.Compare(a.Name, b.Name) // tie-breaker
})sort.Slice использует pdqsort (introsort), O(n log n), нестабильна. Компаратор less(i,j) возвращает bool, а SortFunc — int (это путают).
Числа и конвертации#
import "strconv"
n, err := strconv.Atoi("42")
s := strconv.Itoa(42)
f, _ := strconv.ParseFloat("3.14", 64)
import "math"
math.MaxInt, math.MinInt // границы (Go 1.17+)
math.Abs(x) // только float64
// целочисленный max/min (Go 1.21+ встроенные!):
m := max(a, b)
n := min(x, y, z)Подводные камни / gotchas#
rangeкопирует элемент.for _, v := range s { v.X = 1 }меняет копию, не оригинал. Чтобы мутировать —s[i].X = 1по индексу.- Захват переменной цикла. До Go 1.22
for _, v := range s { go func(){ use(v) }() }захватывал одну общуюv. В 1.22+ переменная цикла новая на каждой итерации — но на старых версиях / в собеседовании проговорите явно. - Shared backing array.
b := a[1:3]; b = append(b, x)может затеретьa.slices.Cloneдля изоляции. - nil map panic.
var m map[K]V; m[k]=v→ panic. Чтение из nil map безопасно. Всегдаmake. - Байты vs руны.
len("привет")= 12, не 6.s[i]— байт, не символ. Для строковых алгоритмов часто нужен[]rune. appendк чужому слайсу как параметру — может либо повлиять на вызывающего (если был cap), либо нет (если реаллокация). Не полагайтесь — возвращайте результат append.- Сравнение через
==— структуры из comparable полей сравнимы; слайсы/map/функции — нет (только с nil). Для слайсов —slices.Equal. - Integer overflow молчаливый —
intпереполняется без паники. В задачах с произведениями/суммами больших чисел держите в уме. deferв цикле копит до выхода из функции — в горячем цикле это утечка/тормоза.- Деление на ноль для int — panic; для float — Inf/NaN.
Вопросы / задачи на собеседовании#
В: В чём разница s[i] и range по строке?
О: s[i] возвращает байт (byte/uint8); строка — это UTF-8 байты. range декодирует UTF-8 и выдаёт руны (rune/int32) вместе с байтовым индексом начала символа. Для алгоритмов с многобайтовыми символами работайте через []rune.
В: Почему for _, v := range items { go process(v) } могло работать неправильно?
О: До Go 1.22 переменная цикла v переиспользовалась — все горутины видели одно (последнее) значение. Фикс: передать параметром go func(v T){...}(v) или v := v. В Go 1.22+ это исправлено на уровне языка, но знание истории — признак опыта.
В: Как сделать множество (set) в Go?
О: map[T]struct{} — struct{} занимает 0 байт. seen[x]=struct{}{}, проверка _, ok := seen[x]. map[T]bool проще читается, но тратит память и допускает двусмысленность false vs отсутствие.
В: Разница sort.Slice и slices.SortFunc?
О: sort.Slice(less func(i,j) bool) — индексы и предикат «меньше», рефлексия внутри, есть с Go 1.8. slices.SortFunc(cmp func(a,b) int) — дженерик, компаратор возвращает -1/0/1, типобезопасен и быстрее, с Go 1.21. Обе O(n log n), нестабильны (есть *Stable варианты).
В: Как избежать O(n²) при сборке большой строки?
О: strings.Builder с Grow для пред-аллокации и WriteString. Конкатенация += в цикле каждый раз создаёт новую строку (строки иммутабельны) → O(n²). Для []byte — bytes.Buffer или append к слайсу.
В: Что вернёт чтение из nil map? А запись?
О: Чтение — zero value соответствующего типа, без паники (удобно для счётчиков и т.п.). Запись в nil map — panic. Поэтому map всегда инициализируют через make перед записью.
В: Как написать table-driven тест для алгоритма?
О: Слайс структур {name, input, want}, цикл с t.Run(tc.name, ...), сравнение reflect.DeepEqual или slices.Equal. Покрыть edge cases: пустой вход, один элемент, дубликаты, граничные значения.
func TestTwoSum(t *testing.T) {
tests := []struct {
name string
in []int
target int
wantI, wantJ int
}{
{"basic", []int{2, 7, 11}, 9, 0, 1},
{"empty", nil, 5, -1, -1},
{"no pair", []int{1, 2}, 100, -1, -1},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
i, j := twoSum(tc.in, tc.target)
if i != tc.wantI || j != tc.wantJ {
t.Errorf("got (%d,%d), want (%d,%d)", i, j, tc.wantI, tc.wantJ)
}
})
}
}В: Как вернуть детерминированный обход map?
О: Map итерируется в случайном порядке намеренно. Собрать ключи в слайс, отсортировать (slices.Sort), обойти слайс. Полагаться на порядок итерации map нельзя никогда.
На что копают на senior+#
- Идиоматичность, а не «Go как Java/Python»: использование stdlib (
slices,maps,sort,strings.Builder), comma-ok, multiple return, ранние возвраты. - Понимание value vs reference семантики: что копируется (range, передача структур, массивы), что разделяется (слайсы, map).
- Аллокации и производительность: pre-sizing, escape analysis, избегание O(n²) на строках, осознанные heap/stack решения; умение подкрепить бенчмарком.
- Edge cases и устойчивость: nil, пустой вход, переполнение, Unicode — обрабатываются до того, как спросят.
- Тестируемость: table-driven тесты, чистые функции, продумывание граничных случаев как часть решения.
- Чистый код под давлением: понятные имена, маленькие функции, отсутствие дублирования даже в live-coding; проговаривание решения вслух и сложности.
- Знание тонкостей версий (loop var в 1.22, generics-утилиты в 1.21) — показывает, что человек следит за языком.