Популярные задачи для собеседований бэкенд-разработчиков на Go и их решения

в 16:06, , рубрики: Go, golang, Блог компании Ребреин, задачи, Занимательные задачки, Программирование, собеседование

Я живу в Ташкенте, и когда учился в университете — начал учить Python, чтобы писать ботов. Боты — это узбекский хлеб, у нас на них построено вообще все. Например, никто не делает приложения для заказа еды, все только в мессенджерах. 

Я учил язык по статьям из интернета — просто брал каркас и дальше дописывал, смотрел, где что падает, постоянно решал задачи на leetcode. Писал я тогда ужасно, но что было, то было. Мне нравилось, но чем больше я углублялся, тем сильнее раздражали скорость выполнения, ограничения параллелизма и динамическая типизация.

Тогда я решил попробовать Go.


Go — простой, классный и востребованный 

Меня привлекла их идея легковесной конкурентности и понравилось то, что не нужно разбираться в зоопарке асинхронности, который был в питоне. Сначала я писал на Go что-то для себя, смотрел как себя ведет язык. Потом на работе мы решили попробовать один несложный проект. Получили хорошие результаты — и по скорости разработки, и по скорости выполнения.

Возможно, Go не такой выразительный, возможно, там есть спорные решения — например, пустые интерфейсы, которые клали на всю вашу типизацию, или пакет рефлект, в который лучше не заглядывать. Но в целом, обычный продакшн код на Go и правда читабелен, это один из немногих языков, где ты можешь нырнуть почти в любую библиотеку и понять, что там происходит хотя бы на механическом уровне без погружения в доменную область.

Как говорил Роб Пайк: «Простота — это сложно». Философия, все дела. Кто-то ругает голанг за излишнюю простоту, а мне она нравится.

Я быстро освоил синтаксис и начал писать на нем какие-то простые вещи. В то время мне нравилось ходить на собесы и смотреть, что спрашивают — хотя я еще не особо познакомился со стандартной библиотекой и экосистемой языка.

Один раз я полуслучайно угодил на синьорское собеседование. Это было самое душное из всех интервью, что я видел. Было ощущение, что интервьюера заставили прийти и слушать меня. Он сразу начал с технических вопросов, и я понял, что он просто задает вопросы из статьи с хабра — причем идет по ним подряд, как будто за 15 минут до встречи погуглил «Вопросы для собеседования бэкенд-разработчика».

Эйчары написали мне через месяц и сказали, что уже готовят оффер. Потом написали еще через час и сказали, что, к сожалению, иностранцев они уже не берут.

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

Я считаю, что самые адекватные способы понять, хорош ли разраб — давать ему задачи и разбирать код. На работе, которую я в итоге получил, было именно так. Здесь я собрал несколько самых популярных задач, которые часто попадаются на собесах и написал, как бы я стал их решать.


Популярные задачи на собеседованиях

На вход подаются два неупорядоченных слайса любой длины. Надо написать функцию, которая возвращает их пересечение

Стандартная задача с leetcode и ее довольно часто спрашивают на собеседованиях в качестве простой задачи для разогрева. 

Можно решить сортировкой, за более долгое время, но без выделения дополнительной памяти. А можно выделить дополнительную память и решить за линейное время.

Надо посчитать количество появлений элементов первого массива (лучше брать тот, что покороче) — используем для этого словарь. Потом пройтись по второму массиву и вычитать из словаря те элементы, которые есть в нем. По ходу добавляем в результат те элементы, у которых частота появлений больше нуля.

Решение

package main

import (
	"fmt"
)

// На вход подаются два неупорядоченных массива любой длины.
// Необходимо написать функцию, которая возвращает пересечение массивов
func intersection(a, b []int) []int {
	counter := make(map[int]int)
	var result []int

	for _, elem := range a {
		if _, ok := counter[elem]; !ok {
			counter[elem] = 1
		} else {
			counter[elem] += 1
		}
	}
	for _, elem := range b {
		if count, ok := counter[elem]; ok && count > 0 {
			counter[elem] -= 1	
			result = append(result, elem)
		}
	}
	return result
}

func main() {

	a := []int{23, 3, 1, 2}
	b := []int{6, 2, 4, 23}
	// [2, 23]
	fmt.Printf("%vn", intersection(a, b))
	a = []int{1, 1, 1}
	b = []int{1, 1, 1, 1}
	// [1, 1, 1]
	fmt.Printf("%vn", intersection(a, b))
}

Написать генератор случайных чисел

В принципе, легкая задача, на базовые знания по асинхронному взаимодействию в Go. Для решения я бы использовал небуфферезированный канал. Будем асинхронно писать туда случайные числа и закроем его, когда закончим писать.

Плюс ее можно использовать в немного измененном виде в задаче на слияние N каналов.

Решение

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func randNumsGenerator(n int) <-chan int {
	r := rand.New(rand.NewSource(time.Now().UnixNano()))

	out := make(chan int)
	go func() {
		for i := 0; i < n; i++ {
			out <- r.Intn(n)
		}
		close(out)
	}()
	return out
}

func main() {
	for num := range randNumsGenerator(10) {
		fmt.Println(num)
	}
}

Слить N каналов в один

Даны n каналов типа chan int. Надо написать функцию, которая смерджит все данные из этих каналов в один и вернет его.

Мы хотим, чтобы результат работы функции выглядел примерно так:

for num := range joinChannels(a, b, c) {

       fmt.Println(num)

}

Для этого напишем функцию, которая будет асинхронно читать из исходных каналов, которые ей передадут в качестве аргументов, и писать в результирующий канал, который вернется из функции. 

Создаем канал, куда будем сливать все данные. Он будет небуферезированный, потому что мы не знаем, сколько данных придет из каналов.

Дальше асинхронно прочитаем из исходных каналов и закроем результирующий канал для мерджа, когда все чтение закончится. Чтобы дождаться конца чтения, просто обернем этот цикл по каналам в wait group. 

Решение

package main

import (
	"fmt"
	"sync"
)

func joinChannels(chs ...<-chan int) <-chan int {
	mergedCh := make(chan int)

	go func() {
		wg := &sync.WaitGroup{}

		wg.Add(len(chs))

		for _, ch := range chs {
			go func(ch <-chan int, wg *sync.WaitGroup) {
				defer wg.Done()
				for id := range ch {
					mergedCh <- id
				}
			}(ch, wg)
		}

		wg.Wait()
		close(mergedCh)
	}()

	return mergedCh
}

func main() {

	a := make(chan int)
	b := make(chan int)
	c := make(chan int)

	go func() {
		for _, num := range []int{1, 2, 3} {
			a <- num
		}
		close(a)
	}()

	go func() {
		for _, num := range []int{20, 10, 30} {
			b <- num
		}
		close(b)
	}()

	go func() {
		for _, num := range []int{300, 200, 100} {
			c <- num
		}
		close(c)
	}()

	for num := range joinChannels(a, b, c) {
		fmt.Println(num)
	}

}

Сделать конвейер чисел

Даны два канала. В первый пишутся числа. Нужно, чтобы числа читались из первого по мере поступления, что-то с ними происходило (допустим, возводились в квадрат) и результат записывался во второй канал. 

Довольно частая задача, более подробно можно почитать тут https://blog.golang.org/pipelines.

Решается довольно прямолинейно — запускаем две горутины. В одной пишем в первый канал. Во второй читаем из первого канала и пишем во второй. Главное — не забыть закрыть каналы, чтобы ничего нигде не заблокировалось.

Решение

package main

import (
	"fmt"
)

func main() {
	naturals := make(chan int)
	squares := make(chan int)

	go func() {
		for x := 0; x <= 10; x++ {
			naturals <- x
		}
		close(naturals)
	}()

	go func() {
		for x := range naturals {
			squares <- x * x
		}
		close(squares)
	}()

	for x := range squares {
		fmt.Println(x)
	}
}

Написать WorkerPool с заданной функцией

Довольно распространенная задача, плюс подобные задачи встречаются на практике. 

Нам нужно разбить процессы на несколько горутин — при этом не создавать новую горутину каждый раз, а просто переиспользовать уже имеющиеся. Для этого создадим канал с джобами и результирующий канал. Для каждого воркера создадим горутину, который будет ждать новую джобу, применять к ней заданную функцию и пулять ответ в результирующий канал.

Решение

package main

import (
	"fmt"
)

func worker(id int, f func(int) int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        results <- f(j)
    }
}

func main() {

    const numJobs = 5
    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)

    multiplier := func(x int) int {
	return x * 10
    }

    for w := 1; w <= 3; w++ {
        go worker(w,  multiplier, jobs, results)
    }

    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)

    for i := 1; i <= numJobs; i++ {
        fmt.Println(<-results)
    }
}

Сделать кастомную waitGroup на семафоре

Семафор можно легко получить из канала. Чтоб не аллоцировать лишние данные, будем складывать туда пустые структуры.

В нашем случае мы хотим сделать семафор, который будет ждать выполнения пяти горутин. Для этого просто добавим вместо обычного канала буфферизированный. И внутри каждой горутины положим в него значение. А в конце будем дожидаться, что все ок — мы вычитаем все значения из канала. 

Решение

package main

import (
	"fmt"
)

type sema chan struct{}

func New(n int) sema {
	return make(sema, n)
}

func (s sema) Inc(k int) {
	for i := 0; i < k; i++ {
		s <- struct{}{}
	}
}

func (s sema) Dec(k int) {
	for i := 0; i < k; i++ {
		<-s
	}
}

func main() {
	numbers := []int{1, 2, 3, 4, 5}
	n := len(numbers)

	sem := New(n)

	for _, num := range numbers {
		go func(n int) {
			fmt.Println(n)
			sem.Inc(1)
		}(num)
	}

	sem.Dec(n)

}

В общем, если понравились задачи и показались полезными — заходите в канал Rebrain — там часто выкладывают бесплатные вебинары и практикумы по Go. Потом можно обсуждать кто, что и как делает в сообществе.


Мысли о будущем Go

Я думаю, сейчас нет никаких проблем, что Go обзывают языком для новичков. Условный питон преподают детям в школе — это только говорит о доступности языка, что нормально. Язык программирования — это инструмент и если он из коробки, как есть понятен большинству, что в этом плохого. 

Никто не ругает топор, за то что он такой топорный, да и вообще он не бензопила. 

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

Но сейчас на Go все чаще пишут гигантские системы. Наделали фреймворков (само по себе понятие «фреймворк» — противоречит идее Go), разработали инструменты, чтобы затащить его на фронтенды, адаптировать к разработке огромных приложений, стали делать на нем все, что не приколочено. Начали требовать серьезного развития языка в сторону усложнения — чтобы было как в Java и C#. Чтобы у нас, значит, тоже получалось пилить грандиозные монолитные бэкенды, обмазывать всё тоннами декораторов, писать универсальный, сверхпереиспользуемый код и все такое.

Посмотрите на С++. Чтобы угодить потребностям всех разработчиков на плюсах, язык набили фичами настолько, что всерьез вот сесть и выучить С++ невозможно. А уж выбрать его в качестве инструмента для разработки, когда есть альтернативы — абсолютно исключено.

Go стал очень популярным. Мой прогноз — через несколько лет они напичкают его всеми концепциями, которыми только смогут, и будут писать на нем все возможные виды софта. И тогда где-то какой-то парень возьмет, и снова изобретет новый простой инструмент для простых задач.


Автор статьи — @alisher_m, участник сообщества Rebrain. Подписывайтесь, если хотите тоже поделиться опытом и поспрашивать советов. Там регулярно разбираем все эти темы и многие другие, которые пригодятся и на собеседовании, и в работе.

Автор: editor_rebrain

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js