Irbisa · Шпаргалка

Горутины и каналы

Каналы, select, пулы воркеров, утечки горутин, errgroup

Middle-разработчик · 25 вопросов

  1. 01

    Что такое горутина и чем она отличается от потока ОС?

    Горутина представляет собой функцию, которая выполняется конкурентно под управлением собственного планировщика рантайма Go: она стартует с маленьким стеком, который растёт по необходимости, а рантайм мультиплексирует множество горутин на несколько потоков ОС, поэтому запуск дёшев и обходится без системного вызова.

  2. 02

    Чем конкурентность отличается от параллелизма?

    Конкурентность описывает устройство программы как набора задач, которые продвигаются независимо; параллелизм означает одновременное выполнение нескольких из них на разных ядрах.

  3. 03

    Что происходит с горутинами, которые ещё работают, когда main возвращается, и как заставить main их дождаться?

    Когда main возвращается, процесс завершается, и рантайм обрывает все остальные горутины на полпути, не выполняя их отложенные вызовы; чтобы их дождаться, используйте sync.WaitGroup (или канал).

    func main() {	var wg sync.WaitGroup	for i := range 3 {		wg.Add(1) // before `go`, in the parent		go func() {			defer wg.Done()…
  4. 04

    Как получить результат из горутины, если go f() выбрасывает возвращаемые значения?

    Верните результат через то, чего ждёт вызывающий код: канал, заранее выделенный слайс вместе с sync.WaitGroup или errgroup.Group, когда важны и ошибки.

    func squares(nums []int) []int {	results := make([]int, len(nums))	var wg sync.WaitGroup	for i, n := range nums {		wg.Add(1)		go func() {…
  5. 05

    Чем буферизованный канал отличается от небуферизованного и когда блокируются отправка и приём у каждого из них?

    Небуферизованный канал (make(chan T)) работает как передача из рук в руки: отправка блокируется, пока другая горутина не примет значение, а приём блокируется, пока кто-то не отправит.

    func main() {	unbuf := make(chan int)	go func() { unbuf <- 1 }() // waits until main receives	fmt.Println(<-unbuf)       // 1 	buf := make(chan int, 2)…
  6. 06

    Что происходит при отправке, приёме и закрытии для закрытого канала и что происходит для nil-канала?

    Приём из закрытого канала отдаёт оставшиеся в буфере значения, а потом бесконечно возвращает нулевое значение; отправка и повторный close паникуют.

    func main() {	ch := make(chan int, 2)	ch <- 1	ch <- 2	close(ch)…
  7. 07

    Как работает select: что происходит, когда готовы несколько кейсов, зачем нужен default и как добавить таймаут?

    select ждёт, пока одна из его операций с каналами сможет пройти, и выполняет этот кейс, а если готовы несколько, выбирает случайно; default делает его неблокирующим, а таймаут задаётся ещё одним кейсом case <-time.After(d) или case <-ctx.Done().

    func receive(ctx context.Context, ch <-chan Msg) (Msg, error) {	select {	case m := <-ch:		return m, nil	case <-time.After(time.Second):		return Msg{}, errors.New("timeout")…
  8. 08

    Что напечатает эта программа и почему рантайм сообщает о дедлоке здесь, но молчит в зависшем продакшн-сервисе?

    func main() {	ch := make(chan int, 3)	for i := 1; i <= 3; i++ {		ch <- i	}	for v := range ch {		fmt.Println(v)	}	fmt.Println("done")}

    Она напечатает 1, 2, 3 и упадёт с fatal error: all goroutines are asleep - deadlock!: range ждёт четвёртое значение, потому что ch никто не закрыл, а отправить его некому.

  9. 09

    Кто должен закрывать канал и что дают направленные типы <-chan T и chan<- T?

    Закрывает отправитель (та горутина, которая знает, что значений больше не будет) и никогда не получатель.

    // The stage owns its output: it creates it, sends on it and closes it.func square(in <-chan int) <-chan int {	out := make(chan int)	go func() {		defer close(out) // the only sender closes, once		for v := range in {…
  10. 10

    Эта программа должна напечатать 1000. Что она напечатает на самом деле и как это исправить?

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

    Число меньше 1000, которое меняется от запуска к запуску: count++ состоит из чтения, сложения и записи, поэтому две горутины могут прочитать одно и то же значение, и один инкремент потеряется.

  11. 11

    Найдите баг: fetchAll возвращает пустые строки для страниц, которые загрузились без ошибок.

    func fetchAll(urls []string) []string {	var wg sync.WaitGroup	bodies := make([]string, len(urls))	for i, u := range urls {		go func() {			wg.Add(1)			defer wg.Done()			bodies[i] = fetch(u)		}()	}	wg.Wait()	return bodies}

    wg.Add(1) вызывается внутри новой горутины, поэтому wg.Wait() может увидеть нулевой счётчик ещё до старта горутин и сразу вернуться; fetchAll отдаёт срез, который воркеры ещё заполняют.

  12. 12

    Как канал устроен в рантайме и что происходит с горутиной, которая заблокировалась на отправке?

    Канал представляет собой указатель на рантайм-структуру hchan: блокировка, необязательный кольцевой буфер и две очереди припаркованных горутин, sendq и recvq.

  13. 13

    Когда использовать канал, а когда мьютекс, и почему «делитесь памятью через общение» не всегда правильный ответ?

    Канал нужен, когда владение данными переходит от горутины к горутине или когда надо ждать сразу несколько событий; мьютекс нужен, когда горутины разделяют состояние с коротким критическим участком.

  14. 14

    Что такое утечка горутин, что обычно её вызывает и как её обнаружить?

    Утечкой горутины называют горутину, которая никогда не вернётся, потому что ждёт того, что никогда не случится; она держит живыми свой стек и всё, на что ссылается, поэтому память и число горутин растут вместе с трафиком.

    // wrong: on timeout the handler returns and the worker parks in its send foreverfunc handle(w http.ResponseWriter, r *http.Request) {	out := make(chan Result) // right: make(chan Result, 1)	go func() { out <- compute(r.Context()) }() 	select {…
  15. 15

    Чем различаются time.After, time.NewTimer, time.NewTicker и time.AfterFunc и что поменялось в таймерах в Go 1.23?

    time.After(d) возвращает канал, который сработает один раз; NewTimer делает то же, но его можно остановить и перезапустить; NewTicker срабатывает многократно; AfterFunc вместо этого запускает функцию в отдельной горутине.

    func relay(ctx context.Context, in <-chan Msg) error {	const idle = 5 * time.Minute	t := time.NewTimer(idle) // one timer for the whole loop	defer t.Stop() 	for {…
  16. 16

    Как реализовать пул воркеров на Go, собрать результаты и аккуратно остановить воркеров?

    Запустите N горутин, которые делают range по одному каналу задач и отправляют в один канал результатов; продюсер закрывает канал задач, когда закончил, а отдельная горутина закрывает канал результатов после wg.Wait(), и это завершает range по результатам у вызывающего кода.

    func process(ctx context.Context, jobs []Job, n int) []Result {	in := make(chan Job)	out := make(chan Result) 	var wg sync.WaitGroup	for range n {…
  17. 17

    Как ограничить число одновременно работающих горутин и чем это отличается от пула воркеров?

    Используйте буферизованный канал как счётный семафор (sem := make(chan struct{}, 8): отправка перед работой, приём после неё) или errgroup.Group с SetLimit(8).

    func uploadAll(ctx context.Context, items []Item) error {	sem := make(chan struct{}, 8)	var wg sync.WaitGroup	for _, it := range items {		select {		case sem <- struct{}{}: // acquire before go: the loop waits here…
  18. 18

    Как слить N каналов в один (fan-in) и что будет, если один из входов никогда не закроется?

    Запустите по горутине на каждый вход, которая копирует его значения в общий выход, и ещё одну, которая закрывает выход, когда WaitGroup сообщит, что все копировщики закончили.

    func Merge[T any](ctx context.Context, ins ...<-chan T) <-chan T {	out := make(chan T)	var wg sync.WaitGroup	for _, in := range ins {		wg.Go(func() {			for {…
  19. 19

    Напишите две горутины, которые по очереди печатают числа от 1 до N: одна печатает нечётные, а другая чётные.

    Дайте каждой горутине свой небуферизованный канал как жетон хода: она ждёт свой жетон, печатает число и передаёт жетон другой.

    func player(from, n int, mine <-chan struct{}, next chan<- struct{}) {	for i := from; i <= n; i += 2 {		<-mine // wait for our turn		fmt.Println(i)		if i < n { // nobody is left to take the turn after the last number			next <- struct{}{}…
  20. 20

    Как запустить N задач параллельно, отменить остальные при первой ошибке и дождаться всех?

    Используйте errgroup.WithContext из golang.org/x/sync/errgroup: каждая задача, запущенная через g.Go, работает с общим контекстом, первая ошибка, не равная nil его отменяет, а g.Wait() дожидается всех задач и возвращает эту первую ошибку.

    func fetchAll(ctx context.Context, urls []string) ([]Page, error) {	g, ctx := errgroup.WithContext(ctx)	g.SetLimit(16) // once, before the first Go	pages := make([]Page, len(urls)) 	for i, u := range urls {…
  21. 21

    Как реализовать потокобезопасный rate limiter и когда вместо этого брать golang.org/x/time/rate?

    Используйте token bucket: токены пополняются с фиксированной скоростью до размера всплеска (burst), каждый запрос забирает один, а пустое ведро означает «ждать» или «отказать».

    type Bucket struct {	mu     sync.Mutex	tokens float64	burst  float64	rate   float64 // tokens per second	last   time.Time…
  22. 22

    Как написать потокобезопасный in-memory кэш с TTL, не запуская горутину на каждый ключ?

    Храните map записей со временем истечения под sync.RWMutex, при чтении считайте просроченные записи отсутствующими, а удаляйте их одной горутиной-уборщиком на кэш, которая проходит по map на тикере и останавливается по Close.

    type entry struct {	val     string	expires time.Time} type Cache struct {…
  23. 23

    Как построить пайплайн из стадий на каналах и как стадии завершаются, когда потребитель перестаёт читать раньше времени?

    Каждая стадия принимает входной канал, запускает горутину, которая его читает, и возвращает выходной канал, который закроет по окончании.

    func gen(ctx context.Context, nums ...int) <-chan int {	out := make(chan int)	go func() {		defer close(out)		for _, n := range nums {			select {…
  24. 24

    Что такое backpressure в пайплайне на каналах, что на самом деле даёт буфер и что делать, когда потребитель стабильно медленнее?

    Backpressure (обратное давление) означает, что медленный потребитель притормаживает продюсера: блокирующая отправка в полный канал заставляет продюсера ждать, и работа не копится быстрее, чем обрабатывается.

  25. 25

    Параллельные воркеры ускорили стадию, но перемешали порядок на выходе. Как восстановить порядок входа, не держа весь поток в памяти?

    Зафиксируйте место каждого элемента до того, как работа разойдётся по воркерам: единственный диспетчер даёт каждому элементу свой канал результата на один слот и кладёт эти каналы в FIFO-очередь в порядке входа; потребитель читает очередь и по очереди ждёт каждый результат.

    type job struct {	row Row	res chan Result // cap 1: the worker never waits for a slow consumer} func enrich(rows []Row, n int) <-chan Result {…