Разбор нетипичной задачи с собеседования на должность middle Go разработчика

Предыстория #

Смотрел запись собеседования на должность middle Go разработчика в российский бигтех на youtube.com и увидел нетипичную задачу. Решил расписать один из вариантов её решения.

Для понимания того, что будет происходить далее, вы должны быть знакомы с основами языка программирования Go.
Самый простой способ пощупать Go – это пройти интерактивный Go tour от создателей языка.

Условие задачи #

Дан код:

package main

import (
    "time"
    "fmt"
)

func printNumber(num int) {
	fmt.Printf("Process %d\n", num)
	time.Sleep(time.Second)
}

func main() {
	// ...
}

Необходимо:

  1. Последовательно напечатать числа от 1 до 10, используя функцию printNumber. Код функции printNumber изменять нельзя.
  2. Распараллелить печать чисел от 1 до 10. Порядок не важен. Печатать при помощи функции printNumber. Код функции printNumber изменять нельзя.
  3. Напечатать числа от 1 до 10 таким образом, чтобы за раз выводилось по 5 чисел. Порядок не важен. Печатать при помощи функции printNumber. Код функции printNumber изменять нельзя.

Решение задачи 1 #

Проходимся в цикле от 1 до 10 и выводим числа, ничего сверхъестественного :)

package main

import (
	"fmt"
	"time"
)

func printNumber(num int) {
	fmt.Printf("Process %d\n", num)
	time.Sleep(time.Second)
}

func main() {
	for i := 1; i <= 10; i++ {
		printNumber(i)
	}
}

Решение задачи 2 #

Идея решения: выполнить printNumber для каждого числа в отдельной горутине. В основной горутине дождаться выполнения запущенных горутин.
Ожидать можно по разному (используя счетчики, подсчет значений из канала и прочие извращения), но самым простым вариантом будет использование структруы sync.Mutex.

package main

import (
	"fmt"
	"sync"
	"time"
)

func printNumber(num int) {
	fmt.Printf("Process %d\n", num)
	time.Sleep(time.Second)
}

func main() {
	nums := make([]int, 0, 10)
	for i := 1; i <= 10; i++ {
		nums = append(nums, i)
	}
	printNumsParallel(nums)
}

func printNumsParallel(numbers []int) {
	var wg sync.WaitGroup
	wg.Add(len(numbers)) // 1
	for _, num := range numbers {
		// запуск горутин для печати
		go func() {
			defer wg.Done()  // 2
			printNumber(num) // 3
		}()
	}
	wg.Wait() // 4
}
  1. Увеличиваем счетчик группы ожидания ДО запуска горутины.
  2. Через отложенный вызов вызываем уменьшение счетчик группы ожидания. Такой вызов отработает даже в том случае, если следующий за ним код в анонимной функции вызовет панику.
  3. Печатаем num. Начиная с Go 1.22 можно не передавать num в параметрах функции, т.к. на каждой итерации цикла переменная num будет создаваться заново, поэтому у каждой горутины num будет своя и уникальная (подробнее). Если вас попросят решить эту задачу в Go до 1.22, то num нужно будет явно передавать через параметры функции при запуске функции в отдельной горутине. Иначе поведение будет непрогнозируемое, но почти наверное вместо чисел от 1 до 10 вы получите что-то другое, т.к. порядок запуска горутин не определен.
  4. Ожидаем выполнение всех запущенных горутин.

Решение задачи 3 #

При решении текущей задачи хотелось бы каким-то образом “одновременно” запускать выполнение 5 печатающих задач. Для этих целей подходит паттерн конкурентного программирования worker pool.

Идея:

  1. Записать числа для печати (от 1 до 10) в буфферизированный канал с размером буффера, равным количеству выводимых чисел (5). После записи всех числе закрыть канал, чтобы читающие горутины могли остановить свою работу.
  2. Запустить горутины для печати чисел, которые будут вычитывать числа из канала. Количество горутин должно быть равно размеру буффера канала, это позволит создать эффект одновременной печати, который требуется в условиях задачи.
  3. Подождать пока все печатающие числа горутины отработают.
package main

import (
	"fmt"
	"sync"
	"time"
)

func printNumber(num int) {
	fmt.Printf("Process %d\n", num)
	time.Sleep(time.Second)
}

func main() {
	numbers := make([]int, 0, 10)
	for i := 1; i <= 10; i++ {
		numbers = append(numbers, i)
	}
	printNumbersOfKAtTime(numbers, 5)
}

func printNumbersOfKAtTime(numbers []int, k int) {
	var wg sync.WaitGroup
	wg.Add(k)
	// создание буфферизированного канала для передачи
	in := make(chan int, k)
	for i := 0; i < k; i++ {
		// запуск печатающих горутин
		go func() {
			defer wg.Done()
			// каждая горутина вычитывает из канала до тез пор, пока он не будет закрыт
			for num := range in {
				printNumber(num)
			}
		}()
	}
	// запуск горутины для записи числе в канал, из которого будут читать печатающие горутины.
	// Её можно не отслеживать в счетчике wg,
	// т.к. читающие горутины не завершат свою работу до тех пор, пока не отработает эта.
	go func() {
		for _, num := range numbers {
			in <- num
		}
		// закрываем канал после записи
		close(in)
	}()
	// ожидаем выполнение всех печатающих горутин
	wg.Wait()
}

Ссылки на дополнительные материалы #