Перейти к содержанию

Задачи, которые нужно знать на 100%

Это подборка задач на компилятор и на конкурентность, которые встречаются на почти каждом собесе на Go. Источник — реальные собесы из telegram-чата «Разбор резюме» и личный архив.

Ответ к каждой задаче — внутри <details>. Сначала подумай, потом разверни.


Часть 1. Слайсы и массивы

C-001. append в общий backing array

package main

import "fmt"

func main() {
    a := []int{1, 2, 3, 4, 5}
    b := a[:3]
    b = append(b, 99)
    fmt.Println(a)
    fmt.Println(b)
}
Ответ

[1 2 3 99 5]
[1 2 3 99]
b — слайс длины 3 с cap=5. append на 4-й элемент укладывается в capacity, не выделяется новый массив, и пишет в общий backing → a[3] тоже становится 99.

C-002. append y и z от одного исходного

x := []int{0, 1, 2}
y := append(x, 3)
z := append(x, 4)
fmt.Println(y)
fmt.Println(z)
Ответ

[0 1 2 4]
[0 1 2 4]
x имеет cap=3 (точно по len). Первый append выделяет новый массив (обычно cap2 = 6), копирует туда [0 1 2 3]. Второй append от того же x снова видит cap=3, снова выделяет новый массив, копирует [0 1 2 4]. Поскольку y «успел» получить отдельный backing — третий элемент в нём уже не будет затёрт. Гочча*: если бы первый append остался в исходном массиве (если бы там был запас), то второй append его перезаписал бы.

C-003. Мутация через функцию

func mod1(s []int) { s[0] = 1 }
func mod2(s []int) { s = append(s, 99); s[0] = 7 }

a := []int{0, 0, 0}
fmt.Println(a)
mod1(a); fmt.Println(a)
mod2(a); fmt.Println(a)
Ответ

[0 0 0]
[1 0 0]
[1 0 0]
mod1 пишет в общий backing → видим. mod2 делает append, который превышает cap=3 → создаётся новый массив, изменения s[0]=7 не отражаются в исходном.

C-004. Захват переменной цикла горутиной (до Go 1.22)

for i := 0; i < 3; i++ {
    go func() {
        fmt.Print(i, " ")
    }()
}
time.Sleep(time.Second)
Ответ

До Go 1.22: «3 3 3» (или 3 разных значения, но скорее всего 3 3 3, т.к. цикл успевает закончить раньше горутин). С Go 1.22: «0 1 2» (порядок не гарантирован), потому что переменная цикла теперь свежая на каждой итерации.

C-005. Take address of map element

m := map[string]int{"one": 1}
p := &m["one"]
fmt.Println(p)
Ответ

Compile error: cannot take the address of m["one"]. Map в Go может быть переаллоцирован при росте, поэтому адреса элементов нестабильны и не дают.


Часть 2. defer

C-006. Defer вычисляет аргументы сразу

i := 0
defer fmt.Println("deferred:", i)
i++
fmt.Println("now:", i)
Ответ

now: 1
deferred: 0
Аргументы defer вычисляются в момент defer, а не выполнения.

C-007. Defer в цикле — LIFO

for i := 1; i <= 5; i++ {
    defer fmt.Print(i, " ")
}
fmt.Println("end")
Ответ

end
5 4 3 2 1
defer пушит в стек, при выходе снимается сверху.

C-008. Defer метод vs замыкание

type S struct{ v int }
func (s S) P() { fmt.Println(s.v) }

s := S{v: 1}
defer s.P()
defer func() { s.P() }()
s.v = 2
Ответ

2
1
defer s.P() копирует receiver сразу (s.v=1 на момент defer). defer func() { s.P() }() — замыкание, читает s в момент выполнения (s.v=2). Стек: сначала вторая (LIFO).


Часть 3. nil и интерфейсы

C-009. Typed nil interface

type MyError struct{ msg string }
func (e *MyError) Error() string { return e.msg }

func mayFail() error {
    var p *MyError = nil
    return p
}

err := mayFail()
fmt.Println(err == nil)
Ответ

false. Interface хранит (type, value). После возврата *MyError(nil) type == *MyError (≠ nil), value == nil. err == nil истинно только когда оба компонента nil.

C-010. var s *string; i = s; i == nil

var s *string
var i interface{} = s
fmt.Println(s == nil, i == nil)
Ответ

true false. Тот же случай: interface хранит type *string, поэтому != nil.

C-011. Запись в nil map — panic

var m map[string]int
m["a"] = 1
Ответ

runtime panic: assignment to entry in nil map. Чтение из nil map вернёт zero value (без panic), запись — panic.


Часть 4. Методы и receiver

C-012. Value vs pointer receiver

type Counter struct{ n int }
func (c Counter) IncVal()  { c.n++ }
func (c *Counter) IncPtr() { c.n++ }

c := Counter{}
c.IncVal(); c.IncVal()
c.IncPtr()
fmt.Println(c.n)
Ответ

1. IncVal работает с копией, изменения теряются. Только IncPtr видит исходный c.


Часть 5. Каналы и горутины

C-013. Send и receive в буферизованном канале

ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch)
for v := range ch {
    fmt.Print(v, " ")
}
Ответ

1 2. Буфер 2 принимает оба send без блокировки. close + range отдаёт всё, что в буфере, и завершается.

C-014. nil канал в select

var ch chan int
select {
case v := <-ch: fmt.Println("got", v)
default: fmt.Println("default")
}
Ответ

default. nil-канал в select никогда не срабатывает.

C-015. Send в закрытый канал

ch := make(chan int, 1)
close(ch)
ch <- 1
Ответ

runtime panic: send on closed channel.

C-016. Receive из закрытого канала

ch := make(chan int, 1)
ch <- 5
close(ch)
v, ok := <-ch
fmt.Println(v, ok)
v, ok = <-ch
fmt.Println(v, ok)
Ответ

5 true
0 false
После исчерпания буфера ok=false, v=zero value.

C-017. Deadlock на unbuffered канале

ch := make(chan int)
ch <- 1
fmt.Println(<-ch)
Ответ

fatal error: all goroutines are asleep - deadlock! Unbuffered канал требует одновременного receiver, его нет.

C-018. select с двумя готовыми кейсами

ch1 := make(chan int, 1); ch1 <- 1
ch2 := make(chan int, 1); ch2 <- 2
select {
case v := <-ch1: fmt.Println("ch1:", v)
case v := <-ch2: fmt.Println("ch2:", v)
}
Ответ

Один из двух — выбирается случайно. Это не баг, это документированное поведение Go runtime.


Часть 6. Конкурентность

M-001. Гонка на counter

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

Запусти go run -race. Что увидишь и как исправить?

Ответ

WARNING: DATA RACE. counter++ — это не атомарная операция (load, add, store). Исправление:

var counter int64
atomic.AddInt64(&counter, 1)
Или sync.Mutex вокруг counter++.

M-002. Worker pool — закрытие канала

jobs := make(chan int, 100)
var wg sync.WaitGroup
for w := 0; w < 3; w++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        for j := range jobs {
            _ = j * 2
        }
    }()
}
for i := 0; i < 10; i++ { jobs <- i }
// что не так?
wg.Wait()
Ответ

wg.Wait() повиснет навсегда. Воркеры в for j := range jobs ждут closed-сигнал. Нужно close(jobs) после последнего send.

M-003. Параллельный fetch с лимитом 10

Что добавить, чтобы одновременно работало не больше 10 горутин?

sem := make(chan struct{}, 10)
for _, url := range urls {
    sem <- struct{}{}     // занять слот, блокируется если > 10 в полёте
    go func(u string) {
        defer func() { <-sem }()
        fetch(u)
    }(url)
}

M-004. Утечка горутин из-за context'а

func work(ctx context.Context) {
    ch := make(chan int)
    go func() {
        time.Sleep(10 * time.Second)
        ch <- 42
    }()
    select {
    case v := <-ch:
        fmt.Println(v)
    case <-ctx.Done():
        fmt.Println("cancelled")
    }
}

В чём утечка?

Ответ

Если ctx отменяется раньше — основная функция выходит, но горутина внутри продолжает спать 10 секунд и пытается отправить в ch, который никто не читает → горутина висит вечно. Исправление: канал с буфером 1, либо select внутри горутины с case <-ctx.Done(): return.


Часть 7. Прочее (часто на собесах)

C-019. type assertion и ok-idiom

var i interface{} = "hello"
n, ok := i.(int)
fmt.Println(n, ok)
Ответ

0 false. Без ok — был бы panic.

C-020. comparing structs

type Point struct{ X, Y int }
p1 := Point{1, 2}; p2 := Point{1, 2}
fmt.Println(p1 == p2)
Ответ

true. Структуры сравнимы поэлементно, если все поля comparable. Сравнение структуры со слайсом — compile error.


Совет от ментора

Не зубри ответы. Запусти каждую задачу руками в go run. Когда увидишь что-то странное — открой профайлер, обсуди с ментором, прочитай конспект по теме. Цель — понять, а не запомнить.