Goroutines в Go: как работают и что спрашивают на интервью
Глубокий разбор горутин в Go: планировщик M:N, стек, каналы, sync-примитивы, паттерны конкурентности, утечки горутин и context — с примерами кода и реальными вопросами с интервью.
- 1. Конкурентность vs параллелизм
- 2. Планировщик Go: модель GMP
- 3. Горутина vs OS-поток: детали реализации
- 4. Базовое использование горутин
- 5. Каналы: механизм общения горутин
- 6. Синхронизация: sync-примитивы
- 7. Паттерны конкурентности
- Done-канал: контролируемая отмена
- Pipeline: цепочка обработки
- Fan-out / Fan-in: распределение и сбор
- Worker pool: ограничение параллелизма
- 8. context.Context: отмена и таймауты
- 9. Утечки горутин
- 10. Data race и race detector
- 11. Что спрашивают на интервью
- Итоги
Горутины — это главная суперсила Go. Именно они позволяют запускать сотни тысяч конкурентных задач на обычном сервере, не платя при этом цену OS-потоков. За простым синтаксисом go f() скрывается сложная машина: планировщик с work-stealing, динамический стек, кооперативное и преемптивное переключение. Понять это — значит не просто использовать горутины, а правильно их проектировать.
Если ты готовишься к Go-собеседованию в целом — загляни в 50 вопросов для junior и middle Go-разработчика: там разбираются интерфейсы, ошибки, каналы и многое другое. В этой статье мы уходим глубже — разбираем конкурентность как отдельную тему, потому что на середнячковых и сеньорских интервью именно она проверяется особенно тщательно.
Читатель познакомится с моделью GMP, поймёт чем горутина отличается от потока на уровне реализации, научится правильно применять каналы, sync-примитивы и context, распознавать утечки горутин и data race. И, конечно, получит развёрнутые ответы на вопросы, которые реально задают на интервью.
Go для собеседований: 50 реальных вопросов
Горутины, каналы, ошибки, интерфейсы — с разборами и примерами кода.
1. Конкурентность vs параллелизм
Роб Пайк сформулировал разницу точно: «конкурентность — о структуре программы, параллелизм — об исполнении». Конкурентная программа составлена из независимых частей, которые могут выполняться в перекрывающиеся промежутки времени. Параллельная программа физически исполняет несколько вычислений одновременно на разных CPU.
Представь кухню ресторана. Один повар может быть конкурентным: поставил воду кипятиться, пока ждёт — нарезает овощи, потом возвращается к плите. Два повара на двух плитах — это параллелизм. Go позволяет и то, и другое: писать конкурентную структуру программы через горутины, а параллелизм получать автоматически через GOMAXPROCS.
Важный вывод: конкурентность не требует нескольких ядер. Программа с тысячей горутин прекрасно работает на одном ядре — планировщик будет переключать их по очереди. Параллелизм — это уже про физическое одновременное исполнение на нескольких ядрах. Go даёт оба инструмента, и понимание разницы помогает принимать правильные архитектурные решения.
2. Планировщик Go: модель GMP
Планировщик Go реализует схему M:N — N горутин мультиплексируются на M OS-потоков. В основе лежит модель из трёх сущностей: G, M и P.
G (Goroutine) — горутина. Структура в памяти рантайма, содержащая стек, регистры, статус и указатель на текущую функцию. Создаётся дёшево, может их быть миллионы.
M (Machine) — OS-поток. Именно M выполняет код. M нужен P, чтобы исполнять горутины. Количество M не ограничено фиксированно, но рантайм создаёт новые потоки осторожно — обычно их около GOMAXPROCS.
P (Processor) — логический процессор, контекст выполнения. P держит локальную очередь горутин (до 256 штук) и предоставляет M ресурсы для исполнения. Количество P задаётся GOMAXPROCS и по умолчанию равно числу логических CPU.
import (
"fmt"
"runtime"
)
func main() {
// По умолчанию = runtime.NumCPU()
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))
fmt.Println("Goroutines:", runtime.NumGoroutine())
// Явное ограничение (полезно для тестов и бенчмарков)
runtime.GOMAXPROCS(1)
}
Глобальная очередь и work-stealing. Помимо локальных очередей P, есть глобальная очередь горутин. Когда локальная очередь P пуста, он сначала смотрит в глобальную, а если и там пусто — крадёт половину горутин из очереди другого P. Это work-stealing: балансировка нагрузки без централизованного координатора.
Преемптивное планирование с Go 1.14. До версии 1.14 Go использовал только кооперативное планирование — горутина уступала процессор добровольно при вызовах функций, операциях с каналами или I/O. Это создавало проблему: горутина в плотном CPU-цикле могла заблокировать поток надолго. С Go 1.14 появился sysmon — служебный поток, который периодически посылает сигнал SIGURG работающим горутинам, принудительно прерывая их.
3. Горутина vs OS-поток: детали реализации
Разница между горутиной и OS-потоком — не просто «один легче другого». Это принципиально разные абстракции.
Стек. OS-поток получает фиксированный стек при создании — обычно 1–8 МБ. Горутина начинается с 2–8 КБ и растёт динамически по мере необходимости, вплоть до 1 ГБ (настраивается через runtime/debug.SetMaxStack). До Go 1.3 использовались segmented stacks (цепочка сегментов), потом перешли на contiguous stacks — при переполнении рантайм аллоцирует новый непрерывный стек большего размера и копирует туда содержимое старого.
Стоимость создания. Горутина создаётся примерно за 2–4 мкс и занимает ~4 КБ. OS-поток создаётся за ~1 мс и занимает ~1–8 МБ стека. Разница в 100–500 раз по времени и в 250–2000 раз по памяти. Именно поэтому в Go легко запустить миллион горутин там, где несколько тысяч потоков уже убьют систему.
Переключение контекста. Переключение между OS-потоками — это системный вызов, требующий сохранения и восстановления всех регистров процессора, TLB-промахи, обновление таблицы страниц. Переключение между горутинами происходит внутри рантайма Go, без syscall ОС, с сохранением только небольшого набора регистров Go. Это на порядок быстрее.
Масштаб. На практике Go-приложения без проблем держат 100 000 горутин. Веб-сервер с горутиной на каждый запрос при нагрузке 10 000 RPS будет иметь десятки тысяч активных горутин одновременно — это штатный режим работы.
4. Базовое использование горутин
Запустить горутину проще некуда:
go sayHello("world") // горутина из именованной функции
go func() { // анонимная горутина
fmt.Println("anonymous")
}()
Классическая ловушка: замыкание в цикле. Это один из самых частых источников гонок данных у начинающих:
// Неправильно: все горутины захватывают одну переменную i
for i := 0; i < 5; i++ {
go func() {
fmt.Println(i) // i — это переменная цикла, не значение
}()
}
// Выведет что-то вроде: 5 5 5 5 5
// Правильно: передаём значение как параметр
for i := 0; i < 5; i++ {
go func(n int) {
fmt.Println(n)
}(i)
}
В Go 1.22 поведение переменной цикла изменилось — теперь каждая итерация создаёт новую переменную, и старый баг ушёл. Но знать оба варианта важно, потому что большинство продакшн-кодов написано на более ранних версиях.
sync.WaitGroup: ждём завершения горутин. Без синхронизации main завершится, не дожидаясь горутин:
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1) // Add вызываем ДО запуска горутины
go func(n int) {
defer wg.Done()
fmt.Printf("worker %d done\n", n)
}(i)
}
wg.Wait() // блокируемся до Done() * 5
Распространённая ошибка — вызов wg.Add(1) внутри горутины. Если wg.Wait() выполнится раньше, чем горутина успеет сделать Add, программа завершится преждевременно. Правило: Add всегда вызывается в том же потоке управления, что и Wait.
5. Каналы: механизм общения горутин
Каналы — основной инструмент координации горутин в Go. Философия языка: «не общайтесь через разделяемую память — разделяйте память через общение».
Небуферизированный канал синхронный: отправитель блокируется до тех пор, пока получатель не готов принять. Это гарантирует, что передача данных произошла.
ch := make(chan int) // небуферизированный
go func() {
ch <- 42 // блокируется, пока main не прочитает
}()
val := <-ch // блокируется, пока горутина не отправит
fmt.Println(val)
Буферизированный канал действует как очередь фиксированного размера: отправитель блокируется только когда буфер полон, получатель — когда буфер пуст.
ch := make(chan int, 3) // буфер на 3 элемента
ch <- 1 // не блокирует, буфер не полон
ch <- 2
ch <- 3
// ch <- 4 // заблокировало бы отправителя
fmt.Println(<-ch) // 1
Направленность каналов — важный инструмент выразительности и безопасности. В сигнатурах функций принято указывать направление:
func producer(out chan<- int) { // только запись
for i := 0; i < 5; i++ {
out <- i
}
close(out)
}
func consumer(in <-chan int) { // только чтение
for v := range in {
fmt.Println(v)
}
}
for range по каналу читает значения до тех пор, пока канал не закрыт и не пуст. Это самый удобный способ потребления потока данных.
select позволяет ждать на нескольких каналах одновременно:
select {
case msg := <-ch1:
fmt.Println("ch1:", msg)
case msg := <-ch2:
fmt.Println("ch2:", msg)
case <-time.After(1 * time.Second):
fmt.Println("timeout")
default:
// выполняется немедленно, если ни один канал не готов
fmt.Println("nothing ready")
}
Если несколько каналов готовы одновременно, select выбирает один из них псевдослучайно. Это важный факт для интервью.
Закрытие канала. Закрытый канал сигнализирует получателю, что данных больше не будет. Запись в закрытый канал вызывает panic. Чтение из закрытого канала немедленно возвращает нулевое значение:
ch := make(chan int, 1)
ch <- 42
close(ch)
v, ok := <-ch
fmt.Println(v, ok) // 42 true (данные из буфера)
v, ok = <-ch
fmt.Println(v, ok) // 0 false (канал закрыт и пуст)
Правило: закрывает канал только отправитель, и только один раз. Никогда не закрывай канал на стороне получателя.
Deadlock. Deadlock возникает когда все горутины заблокированы и нет возможности продолжить выполнение. Go-рантайм обнаруживает это и вызывает панику с сообщением all goroutines are asleep - deadlock!. Классический пример — попытка отправить в небуферизированный канал без получателя:
ch := make(chan int)
ch <- 42 // deadlock: некому читать
6. Синхронизация: sync-примитивы
Каналы хороши для передачи данных и сигналов, но для защиты разделяемого состояния sync-примитивы часто удобнее и производительнее.
sync.Mutex защищает критическую секцию от одновременного доступа:
type SafeCounter struct {
mu sync.Mutex
value int
}
func (c *SafeCounter) Inc() {
c.mu.Lock()
defer c.mu.Unlock() // defer гарантирует разблокировку даже при панике
c.value++
}
func (c *SafeCounter) Get() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
sync.RWMutex оптимален для read-heavy нагрузок: множество читателей могут работать одновременно, но запись — эксклюзивна:
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) (string, bool) {
c.mu.RLock() // несколько читателей одновременно
defer c.mu.RUnlock()
v, ok := c.data[key]
return v, ok
}
func (c *Cache) Set(key, value string) {
c.mu.Lock() // эксклюзивная запись
defer c.mu.Unlock()
c.data[key] = value
}
sync.WaitGroup — см. раздел 4. Ключевое правило: не копируй WaitGroup после первого использования.
sync.Once гарантирует, что функция выполнится ровно один раз, независимо от числа вызывающих горутин — классический lazy singleton:
var (
instance *Database
once sync.Once
)
func GetDB() *Database {
once.Do(func() {
instance = &Database{} // выполнится ровно один раз
instance.Connect()
})
return instance
}
sync.Map — конкурентно-безопасная map без ручных блокировок. Оптимальна когда записи редки, а чтения часты, или когда разные горутины работают с непересекающимися ключами:
var m sync.Map
m.Store("key", "value")
if v, ok := m.Load("key"); ok {
fmt.Println(v)
}
m.Range(func(k, v interface{}) bool {
fmt.Println(k, v)
return true // false остановит обход
})
Когда канал, когда mutex? Канал — когда нужно передать ownership данных или отправить сигнал между горутинами. Mutex — когда несколько горутин должны обращаться к одному разделяемому состоянию. Простой критерий: если горутины общаются («пришли новые данные, обрабатывай»), используй канал. Если несколько горутин просто читают/пишут одну структуру — используй mutex.
7. Паттерны конкурентности
Go-экосистема выработала несколько устойчивых паттернов. Знание их — это то, что отличает человека, который «писал горутины», от того, кто умеет проектировать конкурентные системы.
Done-канал: контролируемая отмена
Самый простой способ остановить горутину — сигнальный канал. Закрытие канала (close) будет немедленно разблокировать все горутины, которые его слушают:
func worker(done <-chan struct{}) {
for {
select {
case <-done:
fmt.Println("worker stopped")
return
default:
// выполняем работу
time.Sleep(100 * time.Millisecond)
}
}
}
func main() {
done := make(chan struct{})
go worker(done)
time.Sleep(500 * time.Millisecond)
close(done) // останавливаем все воркеры разом
}
Pipeline: цепочка обработки
Паттерн pipeline строит цепочку горутин, где каждая читает из входного канала, обрабатывает данные и пишет в выходной:
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func main() {
nums := generate(2, 3, 4, 5)
squares := square(nums)
for v := range squares {
fmt.Println(v) // 4 9 16 25
}
}
Fan-out / Fan-in: распределение и сбор
Fan-out: один источник → несколько воркеров. Fan-in: несколько источников → один потребитель:
func fanOut(in <-chan int, workers int) []<-chan int {
channels := make([]<-chan int, workers)
for i := 0; i < workers; i++ {
channels[i] = square(in) // несколько воркеров на одном входном канале
}
return channels
}
func fanIn(channels ...<-chan int) <-chan int {
var wg sync.WaitGroup
merged := make(chan int)
output := func(c <-chan int) {
defer wg.Done()
for v := range c {
merged <- v
}
}
wg.Add(len(channels))
for _, c := range channels {
go output(c)
}
go func() {
wg.Wait()
close(merged)
}()
return merged
}
Worker pool: ограничение параллелизма
Worker pool ограничивает количество одновременно работающих горутин через буферизированный канал задач:
func workerPool(jobs <-chan int, results chan<- int, workers int) {
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
results <- job * job // тяжёлая работа
}
}()
}
go func() {
wg.Wait()
close(results)
}()
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
workerPool(jobs, results, 5) // 5 воркеров
for i := 1; i <= 20; i++ {
jobs <- i
}
close(jobs)
for r := range results {
fmt.Println(r)
}
}
Go для собеседований: 50 реальных вопросов
Горутины, каналы, ошибки, интерфейсы — с разборами и примерами кода.
8. context.Context: отмена и таймауты
context.Context — стандартный способ распространять отмену, дедлайны и метаданные запроса через цепочку горутин. Это не просто удобство — это идиома, которую используют все стандартные библиотеки Go.
context.WithCancel даёт ручной контроль над отменой:
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // важно: всегда вызывать cancel, чтобы освободить ресурсы
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("goroutine stopped:", ctx.Err())
return
default:
doWork()
}
}
}(ctx)
time.Sleep(time.Second)
cancel() // останавливаем горутину
context.WithTimeout и context.WithDeadline добавляют автоматическую отмену по времени:
// Отмена через 2 секунды
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
// Отмена в конкретный момент времени
deadline := time.Now().Add(5 * time.Second)
ctx, cancel = context.WithDeadline(context.Background(), deadline)
defer cancel()
Реальный пример: HTTP-обработчик с отменяемой горутиной. Когда клиент закрывает соединение, контекст запроса отменяется — и все горутины, работающие в его рамках, могут это обнаружить:
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() // контекст запроса отменяется при закрытии соединения
resultCh := make(chan string, 1)
go func() {
result, err := fetchFromDB(ctx) // передаём ctx в запрос к БД
if err != nil {
return // ctx.Done() уже сработал, не пишем в resultCh
}
resultCh <- result
}()
select {
case result := <-resultCh:
fmt.Fprintln(w, result)
case <-ctx.Done():
http.Error(w, "request cancelled", http.StatusRequestTimeout)
}
}
Правило про хранение Context. Никогда не храни context.Context в структуре — передавай явно первым параметром функции. Это сделано намеренно: контекст привязан к запросу/операции, а не к объекту. Хранение в структуре маскирует время жизни контекста и усложняет отладку.
9. Утечки горутин
Утечка горутины — горутина запустилась и никогда не завершится: она заблокирована на приёме из канала, в который никто не пишет, или на отправке в канал, из которого никто не читает. Горутина занимает память и удерживает ссылки на объекты — GC не может их собрать.
Классический пример утечки:
func leaky(ch <-chan int) {
// Если никто не отправит в ch — горутина висит вечно
val := <-ch
fmt.Println(val)
}
func main() {
ch := make(chan int)
go leaky(ch)
// ch никогда не получает данных
// горутина leaky утекла
}
Исправленная версия с context:
func safe(ctx context.Context, ch <-chan int) {
select {
case val := <-ch:
fmt.Println(val)
case <-ctx.Done():
fmt.Println("context cancelled, stopping")
return
}
}
Диагностика. Самый быстрый способ — runtime.NumGoroutine() в начале и конце теста. Если число растёт — есть утечка:
func TestNoLeak(t *testing.T) {
before := runtime.NumGoroutine()
// запускаем тестируемый код
doWork()
time.Sleep(100 * time.Millisecond) // ждём завершения горутин
after := runtime.NumGoroutine()
if after > before {
t.Errorf("goroutine leak: before=%d after=%d", before, after)
}
}
Для детальной диагностики — pprof. Добавь в HTTP-сервер:
import _ "net/http/pprof"
// Затем: curl http://localhost:6060/debug/pprof/goroutine?debug=1
Для автоматической проверки в тестах используй go.uber.org/goleak:
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m) // провалит тест при любой утечке горутин
}
10. Data race и race detector
Data race возникает, когда две или более горутин одновременно обращаются к одной переменной, и хотя бы одно из обращений — запись. Результат неопределён: можно получить некорректные данные, краш, или всё может работать «нормально» до того дня, когда нагрузка вырастет.
// Data race: counter читается и пишется без синхронизации
var counter int
func increment() {
counter++ // не атомарная операция: read → increment → write
}
func main() {
for i := 0; i < 1000; i++ {
go increment()
}
time.Sleep(time.Second)
fmt.Println(counter) // результат непредсказуем
}
Race detector встроен в Go-тулчейн. Включается флагом -race:
go test -race ./...
go run -race main.go
go build -race -o app .
При обнаружении гонки программа выведет полный стек трейс — какие горутины обращались к переменной и откуда. В продакшне -race не включают (накладные расходы ~2–20x по памяти и CPU), но в тестах — обязательно.
sync/atomic позволяет выполнять атомарные операции без мьютекса. Атомарные операции неделимы — гонки невозможны:
import "sync/atomic"
var counter int64
func increment() {
atomic.AddInt64(&counter, 1) // атомарно: нет data race
}
func get() int64 {
return atomic.LoadInt64(&counter) // атомарное чтение
}
Atomic достаточен для простых счётчиков и флагов. Когда нужно атомарно обновить несколько полей структуры или выполнить несколько зависимых операций — используй mutex.
11. Что спрашивают на интервью
Этот раздел — концентрат реальных вопросов, которые задают на Go-собеседованиях от middle до senior.
Объясни модель GMP.
G — горутина (виртуальный поток), M — OS-поток (Machine), P — логический процессор (Processor). P хранит локальную очередь горутин и предоставляет контекст выполнения. M для работы должен захватить P. Количество P задаётся GOMAXPROCS. Если у P заканчиваются горутины, он крадёт из очереди другого P (work-stealing). Когда горутина блокируется на syscall, M отпускает P, P подхватывает другой M — это обеспечивает непрерывную работу CPU.
Сколько горутин можно создать?
Теоретически — ограничено памятью. На практике Go легко держит сотни тысяч горутин. Первоначальный стек ~4 КБ, при 100 000 горутин — около 400 МБ только на стеки. Плюс накладные расходы планировщика. Разумный ориентир: если нужны миллионы горутин — стоит задуматься о worker pool.
Что такое deadlock? Как возникает?
Deadlock — все горутины заблокированы и ни одна не может продолжить работу. Типичные причины: горутина ждёт приёма из канала, в который никто не отправляет; горутина ждёт отправки в канал, из которого никто не читает; циклическое ожидание мьютексов. Рантайм Go обнаруживает полный deadlock и завершает программу паникой: all goroutines are asleep - deadlock!.
Разница между буферизированным и небуферизированным каналом?
Небуферизированный канал (make(chan T)) синхронен: отправитель блокируется до тех пор, пока получатель не готов принять. Буферизированный канал (make(chan T, N)) асинхронен: отправитель блокируется только когда буфер заполнен, получатель — только когда буфер пуст. Небуферизированный канал гарантирует рандеву — отправитель знает, что получатель взял данные.
Как правильно остановить горутину?
Три идиоматичных способа. Первый — done-канал: передать done <-chan struct{}, горутина проверяет его в select. Второй — context.Context: передать ctx, горутина проверяет ctx.Done(). Третий — явный сигнал через канал. Прямого способа убить горутину снаружи нет — это намеренное решение языка для безопасности.
Что будет при записи в закрытый канал?
Запись в закрытый канал вызывает panic: send on closed channel. Чтение из закрытого канала немедленно возвращает нулевое значение и false вторым аргументом. Чтобы проверить, закрыт ли канал: v, ok := <-ch — если ok == false, канал закрыт. Закрывает канал только отправитель, один раз. Получатель никогда не закрывает канал.
Когда использовать sync.Mutex, а когда канал?
Канал — для коммуникации: передачи данных и ownership между горутинами, для сигналов и событий. Mutex — для защиты разделяемого состояния, к которому несколько горутин обращаются одновременно. Простое правило: если хочешь «сообщить о событии» — канал; если хочешь «защитить доступ» — mutex. В read-heavy сценариях sync.RWMutex эффективнее обычного mutex.
Что такое data race? Как обнаружить?
Data race — одновременный доступ нескольких горутин к одной переменной, где хотя бы одно обращение — запись, без синхронизации. Результат непредсказуем. Обнаружить: go test -race ./... или go run -race main.go. Race detector использует LLVM ThreadSanitizer и при обнаружении гонки выводит полный стектрейс. В продакшне не включают из-за накладных расходов.
Как работает select с несколькими готовыми каналами?
Если несколько каналов в select готовы одновременно, Go выбирает один из них псевдослучайно. Это намеренно — предотвращает голодание (starvation), при котором один канал всегда обрабатывается раньше других.
Что такое утечка горутин?
Горутина, которая запустилась и никогда не завершится — заблокирована на канале или синхронизации без возможности выхода. Утечка горутин постепенно исчерпывает память. Диагностика: runtime.NumGoroutine() в динамике, pprof /debug/pprof/goroutine, goleak в тестах.
Итоги
Горутины — это не просто «лёгкие потоки». Это целая система: планировщик GMP с work-stealing, динамический стек, преемптивное переключение, каналы как механизм координации, богатый набор sync-примитивов. Понимание этого стека позволяет писать не просто рабочий, а корректный и предсказуемый конкурентный код.
Ключевые принципы, которые стоит помнить: всегда давай горутинам способ завершиться, передавай context.Context для управления временем жизни, не копируй sync-примитивы, ищи гонки через -race, следи за утечками в тестах через goleak.
Если хочешь углубиться в другие аспекты Go-собеседований — читай 50 вопросов для junior и middle Go-разработчика: там разбираются интерфейсы, обработка ошибок, работа с памятью и другие темы, которые часто встречаются рядом с вопросами о конкурентности.
Тренируй Go с AI-интервьюером
Задавай вопросы, получай обратную связь в реальном времени — по горутинам, каналам, паттернам конкурентности.
Go-вопросы в Telegram
Ежедневные разборы горутин, каналов и паттернов.
Автор
Lexicon Team
Читайте также
backend
Go: 50 вопросов на собеседовании junior–middle
Разбор 50 реальных вопросов по Go для junior и middle: горутины, каналы, обработка ошибок, интерфейсы, управление памятью и структура проекта. Примеры кода и объяснения.
backend
Go memory model простыми словами: happens-before, гонки и sync
Разбираем модель памяти Go: happens-before, data races, гарантии каналов, Mutex, Once и атомики — с примерами кода и реальными вопросами с интервью.
backend
Context в Go: отмена, дедлайны и best practices
Полный разбор context.Context в Go: WithCancel, WithTimeout, WithDeadline, WithValue, дерево контекстов, propagation отмены, best practices и антипаттерны — с примерами и вопросами с интервью.