Задачи, которые нужно знать на 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)
}
Ответ
b — слайс длины 3 с cap=5. append на 4-й элемент укладывается в
capacity, не выделяется новый массив, и пишет в общий backing →
a[3] тоже становится 99.
C-002. append y и z от одного исходного¶
Ответ
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)
Ответ
mod1 пишет в общий backing → видим. mod2 делает append, который
превышает cap=3 → создаётся новый массив, изменения s[0]=7 не
отражаются в исходном.
C-004. Захват переменной цикла горутиной (до Go 1.22)¶
Ответ
До Go 1.22: «3 3 3» (или 3 разных значения, но скорее всего 3 3 3, т.к. цикл успевает закончить раньше горутин). С Go 1.22: «0 1 2» (порядок не гарантирован), потому что переменная цикла теперь свежая на каждой итерации.
C-005. Take address of map element¶
Ответ
Compile error: cannot take the address of m["one"]. Map в Go может
быть переаллоцирован при росте, поэтому адреса элементов нестабильны
и не дают.
Часть 2. defer¶
C-006. Defer вычисляет аргументы сразу¶
C-007. Defer в цикле — LIFO¶
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
Ответ
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¶
Ответ
true false. Тот же случай: interface хранит type *string, поэтому
!= nil.
C-011. Запись в nil map — panic¶
Ответ
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 в буферизованном канале¶
Ответ
1 2. Буфер 2 принимает оба send без блокировки. close + range отдаёт
всё, что в буфере, и завершается.
C-014. nil канал в select¶
Ответ
default. nil-канал в select никогда не срабатывает.
C-015. Send в закрытый канал¶
Ответ
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)
C-017. Deadlock на unbuffered канале¶
Ответ
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). Исправление:
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¶
Ответ
0 false. Без ok — был бы panic.
C-020. comparing structs¶
Ответ
true. Структуры сравнимы поэлементно, если все поля comparable.
Сравнение структуры со слайсом — compile error.
Совет от ментора¶
Не зубри ответы. Запусти каждую задачу руками в go run. Когда увидишь
что-то странное — открой профайлер, обсуди с ментором, прочитай конспект
по теме. Цель — понять, а не запомнить.