Popular Go Backend Interview Problems and Solutions

I live in Tashkent, and when I was at university I started learning Python in order to write bots. Bots are Uzbek bread, everything is built on them. For example, no one makes applications for ordering food, everything is only in instant messengers.

I learned the language from articles from the Internet – I just took the frame and added further, looked where things fell, constantly solved problems with leetcode. I wrote awful then, but what was, it was. I liked it, but the deeper I went, the more annoying the execution speed, concurrency limitations, and dynamic typing became.

Then I decided to try Go.


Go is simple, cool and in demand

I was attracted by their idea of ​​lightweight competition and liked the fact that you did not need to understand the asynchronous zoo that was in python. At first I wrote something in Go for myself, watched how the language behaves. Then at work, we decided to try one simple project. We got good results – both in terms of development speed and execution speed.

Perhaps Go is not so expressive, perhaps there are controversial solutions there – for example, empty interfaces that were put on all your typing, or a reflex package, which is better not to look into. But in general, ordinary production code in Go is really readable, it is one of the few languages ​​where you can dive into almost any library and understand what is happening there, at least on a mechanical level, without diving into the domain area.

As Rob Pike said, “Simplicity is hard.” Philosophy, everything. Someone scolds golang for being too simple, but I like it.

I quickly mastered the syntax and started writing some simple things in it. At the time, I loved going to social media and seeing what was asked – although I was not yet very familiar with the standard library and the language ecosystem.

One time I got to a signora interview, semi-accidentally. It was the stifling interview I’ve ever seen. There was a feeling that the interviewer was forced to come and listen to me. He immediately started with technical questions, and I realized that he was just asking questions from the article from the Habr – and he followed them in a row, as if 15 minutes before the meeting he had googled “Questions for a Backend Developer Interview”.

Eichars wrote to me a month later and said that they were already preparing an offer. Then they wrote an hour later and said that, unfortunately, they no longer take foreigners.

Thank God I never ran into something like that again. Experience and knowledge were accumulating, there were relatively many vacancies on Go, and adequate ones – for the backend highload system with microservices, and not just for cryptocurrency startups.

I believe that the most adequate ways to understand if a developer is good is to give him tasks and disassemble the code. The job I ended up getting was that way. Here I have collected a few of the most popular problems that often come across at job interviews and wrote how I would begin to solve them.


Popular job interview tasks

The input is two unordered slices of any length. We need to write a function that returns their intersection

This is a standard leetcode problem and is often asked in interviews as a simple warm-up task.

It can be solved by sorting, for a longer time, but without allocating additional memory. Or you can allocate additional memory and solve in linear time.

It is necessary to count the number of occurrences of the elements of the first array (it is better to take the one that is shorter) – we use a dictionary for this. Then go through the second array and subtract those elements from the dictionary that are in it. Along the way, we add to the result those elements whose frequency of occurrence is greater than zero.

Decision

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))
}

Write a random number generator

In principle, an easy task, for basic knowledge of asynchronous communication in Go. For a solution, I would use an unbuffered channel. We will write random numbers there asynchronously and close it when we finish writing.

Plus it can be used in a slightly modified form in the task of merging N channels.

Decision

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)
	}
}

Merge N channels into one

You are given n channels of type chan int. It is necessary to write a function that merges all the data from these channels into one and returns it.

We want the function output to look something like this:

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

       fmt.Println(num)

}

To do this, we will write a function that will asynchronously read from the source channels that will be passed to it as arguments, and write to the resulting channel, which will return from the function.

We create a channel where we will merge all the data. It will be unbuffered because we don’t know how much data will come from the channels.

Next, we read asynchronously from the original channels and close the resulting channel for the merge when all reading is over. To wait for the end of the reading, just wrap this loop through the pipes in a wait group.

Decision

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)
	}()

Make a pipeline of numbers

Two channels are given. The first one is written with numbers. It is necessary that the numbers are read from the first as they arrive, something happens to them (for example, they are squared) and the result is written into the second channel.

Quite a common task, you can read in more detail here https://blog.golang.org/pipelines

The solution is quite straightforward – we launch two goroutines. In one we write to the first channel. In the second, we read from the first channel and write to the second. The main thing is not to forget to close the channels so that nothing gets blocked anywhere.

Decision

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)
	}

Write a WorkerPool with a given function

Quite a common task, plus similar tasks are encountered in practice.

We need to split the processes into several goroutines – and not create a new goroutine every time, but simply reuse the existing ones. To do this, create a channel with jobs and a resulting channel. For each worker, create a goroutine that will wait for a new job, apply the specified function to it, and fire the response into the resulting channel.

Decision

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++ {

Make a custom waitGroup on a semaphore

The semaphore can be easily obtained from the channel. In order not to allocate unnecessary data, we will add empty structures there.

In our case, we want to make a semaphore that will wait for five goroutines to execute. To do this, simply add a buffered channel instead of a regular channel. And inside each goroutine we put a value in it. And at the end we will wait until everything is ok – we will subtract all values ​​from the channel.

Decision

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)

In general, if you liked the tasks and seemed useful, go to the channel Rebrain – free webinars and workshops on Go are often posted there. Then you can discuss who is doing what and how in the community.


Thinking about the future of Go

I think there is no problem right now that Go is being called a newbie language. Conditional python is taught to children at school – this only indicates the availability of the language, which is normal. A programming language is a tool, and if it is out of the box, as is clear to most, what’s wrong with that.

No one scolds the ax for being so clumsy, and indeed he is not a chainsaw.

Go is clearly defined – a simple tool for simple tasks, suitable for either small applications or small microservices that make up a large application.

But now, more and more gigantic systems are written in Go. We created frameworks (the concept of a “framework” itself is contrary to the idea of ​​Go), developed tools to drag it to the frontend, adapt it to the development of huge applications, began to do everything on it that was not nailed down. They began to demand a serious development of the language in the direction of complication – so that it was like in Java and C #. So that we, therefore, also manage to cut grandiose monolithic backends, coat everything with tons of decorators, write universal, super-reusable code and all that.

Have a look at C ++. To cater to the needs of all developers on the plus side, the language has been packed with features so much that it is impossible to seriously sit down and learn C ++. And choosing it as a development tool when there are alternatives is absolutely impossible.

Go has become very popular. My prediction is that in a few years they will stuff it with all the concepts they can, and they will write all possible types of software on it. And then somewhere some guy will take, and again invent a new simple tool for simple tasks.


Article author – @alisher_m, community member Rebrain… Subscribe if you want to share your experience too and ask for advice. There we regularly analyze all these topics and many others that will be useful both in an interview and in work.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *