Библиотека Go для собеса | вопросы с собеседований
6.89K subscribers
222 photos
6 videos
1 file
430 links
Вопросы с собеседований по Go и ответы на них.

По рекламе: @proglib_adv

Учиться у нас: https://proglib.io/w/0b524a15

Для обратной связи: @proglibrary_feeedback_bot

Наши каналы: https://t.me/proglibrary/9197
Download Telegram
💬 На вход подаются два неупорядоченных среза любой длины. Напишите функцию на Go, которая возвращает их пересечение.

Несколько способов, которыми можно решить эту задачу:

1. Использование мапы: наиболее эффективный способ, особенно для больших срезов. Алгоритм:

☑️ Итерируем по первому срезу и добавляем каждый элемент в мапу.
☑️ Итерируем по второму срезу, проверяя наличие элемента в мапе.
☑️ Если элемент найден, добавляем его в результат.

2. Поэлементное сравнение: менее эффективный метод, особенно для больших срезов, поскольку его сложность — O(n*m), где n и m — размеры срезов.

☑️ Двойной цикл для сравнения каждого элемента одного среза с каждым элементом другого среза.
☑️ Если найдено совпадение, добавляем элемент в результат.

3. Сортировка: метод эффективен, если срезы большие и их можно изменять.

☑️ Сначала сортируем оба среза.
☑️ Затем используем два указателя, чтобы итерировать оба среза и находить совпадающие элементы.

📌 Простой пример:

package main

import (
"fmt"
)

func Intersection(slice1, slice2 []int) []int {
// Создание мапы для хранения элементов первого среза
elements := make(map[int]bool)
for _, item := range slice1 {
elements[item] = true
}

// Поиск пересечений
var intersection []int
for _, item := range slice2 {
if _, found := elements[item]; found {
intersection = append(intersection, item)
// Чтобы избежать повторений в результате
delete(elements, item)
}
}

return intersection
}

func main() {
slice1 := []int{1, 3, 5, 7, 9}
slice2 := []int{3, 4, 5, 6, 7}
fmt.Println(Intersection(slice1, slice2)) // [3 5 7]
}
👍231
💬 Каким образом в Go представлена хеш-таблица?

Хеш-таблица в Go представлена ключевым словом map. Мапа использует массив buckets (бакетов) для хранения пар ключ-значение. Ключи хешируются для определения, в какой бакет они должны быть помещены.

При увеличении количества элементов в мапе, количество бакетов может удваиваться, чтобы поддерживать эффективность операций доступа. В случае коллизий используются дополнительные overflow бакеты. Эта структура позволяет оптимизировать производительность при различных операциях с данными.
👍4😁3
💬 Существует ли в Go короткий синтаксис для объявления условного оператора if?

Да, существует. Он позволяет объединить объявление переменной и условие в одном выражении. Этот синтаксис может пригодиться для управления областью видимости переменных и уменьшения объема кода.

📌 Простой пример:

if v, err := someFunction(); err != nil {
// обработка ошибки
} else {
// использование переменной v
}


Функция someFunction() возвращает два значения: результат (v) и ошибку (err). В блоке if сначала выполняется вызов функции, затем проверяется значение err. Если err не равно nil, выполняется блок кода для обработки ошибки. В противном случае, если err равно nil, выполняется блок else, где доступна переменная v. Переменные, объявленные в этой конструкции, ограничены областью видимости блока if и else.
👍8
💬 Даны n каналов типа chan int. Напишите функцию, которая объединит все данные из этих каналов в один и вернет его.

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

📌 Вот как это можно реализовать:

package main

import (
"fmt"
"sync"
)

// mergeChannels объединяет данные из нескольких каналов в один
func mergeChannels(channels ...chan int) chan int {
var wg sync.WaitGroup
merged := make(chan int)

output := func(c chan int) {
for n := range c {
merged <- n
}
wg.Done()
}

wg.Add(len(channels))
for _, c := range channels {
go output(c)
}

// Закрыть merged канал после завершения всех горутин
go func() {
wg.Wait()
close(merged)
}()

return merged
}

func main() {
// Пример использования
c1 := make(chan int)
c2 := make(chan int)
c3 := make(chan int)

// Заполнение каналов данными
go func() { c1 <- 1; close(c1) }()
go func() { c2 <- 2; close(c2) }()
go func() { c3 <- 3; close(c3) }()

// Объединение каналов
for n := range mergeChannels(c1, c2, c3) {
fmt.Println(n)
}
}


Функция mergeChannels принимает переменное количество каналов chan int и возвращает один канал, в который будут направлены все данные из входных каналов.

Для каждого входного канала создается горутина, которая читает данные из этого канала и отправляет их в общий канал merged. Использование sync.WaitGroup позволяет дождаться завершения всех горутин, после чего общий канал закрывается.
👍214
💬 Чем пустой интерфейс отличается от nil интерфейса в Go?

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

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

2. Nil интерфейс в Go — это интерфейс, у которого не установлены ни тип, ни значение. Такой интерфейс не ссылается ни на какой объект или значение.

Важно отметить, что если интерфейс хранит указатель, который является nil, сам интерфейс при этом не будет nil. Это значит, что интерфейс с nil значением отличается от полностью nil интерфейса.
👏14👍8
💬Как можно гарантировать, что тип удовлетворяет интерфейсу?

Мы можем «попросить» компилятор проверить, реализует ли тип T интерфейс I, попытавшись выполнить присваивание с использованием нулевого значения для T или указателя на T, в зависимости от ситуации:

type T struct{}
var _ I = T{} // Проверяем, реализует ли T интерфейс I
var _ I = (*T)(nil) // Проверяем, реализует ли *T интерфейс I


Если T (или *T соответственно) не реализует I, ошибка будет обнаружена во время компиляции. Если мы хотим, чтобы реализация интерфейса была более очевидной, мы можем включить в определение интерфейса специальный метод с уникальным названием.

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

type Fooer interface {
Foo()
ImplementsFooer()
}


Тип должен тогда реализовать метод ImplementsFooer, чтобы быть Fooer, ясно документируя этот факт и объявляя его в выводе go doc.

type Bar struct{}
func (b Bar) ImplementsFooer() {}
func (b Bar) Foo() {}


Большинство кода не использует такие ограничения, поскольку они ограничивают полезность идеи интерфейса. Однако иногда они необходимы для разрешения неоднозначностей между похожими интерфейсами.
🔥12💯1
💬 Будет ли программа на Go работать быстрее при увеличении количества CPU?

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

Иногда добавление большего количества CPU может замедлить программу. На практике программы, которые тратят больше времени на синхронизацию или коммуникацию, чем на полезные вычисления, могут испытывать снижение производительности при использовании нескольких потоков операционной системы. Это связано с тем, что передача данных между потоками влечет за собой смену контекстов, что обходится «дорого», и эти затраты могут увеличиваться с ростом числа CPU. Например, пример с prime sieve из спецификации Go не имеет значительного параллелизма, хотя в нем запускается множество горутин; увеличение числа потоков (CPU) скорее замедлит его, чем ускорит.
👍14
💬 Что происходит с замыканиями, выполняемыми как горутины?

При использовании замыканий с конкурентностью может возникнуть путаница. Рассмотрим пример:

func main() {
done := make(chan bool)

values := []string{"a", "b", "c"}
for _, v := range values {
go func() {
fmt.Println(v)
done <- true
}()
}

// ждем завершения всех горутин перед выходом
for _ = range values {
<-done
}
}


Можно ошибочно ожидать увидеть следующий вывод: a, b, c. Вместо этого получаем c, c, c. Это происходит потому, что каждая итерация цикла использует один и тот же экземпляр переменной v, так что каждое замыкание делится этой единственной переменной.

Когда замыкание выполняется, оно выводит значение v в момент выполнения fmt.Println, но v могла измениться с момента запуска горутины. Чтобы помочь обнаружить эту и другие проблемы до их возникновения, необходимо использовать go vet.

Чтобы привязать текущее значение v к каждому замыканию при его запуске, необходимо изменить внутренний цикл так, чтобы на каждой итерации создавалась новая переменная. Один из способов — передать переменную в качестве аргумента замыканию:

for _, v := range values {
go func(u string) {
fmt.Println(u)
done <- true
}(v)
}


В этом примере значение v передается в качестве аргумента анонимной функции. Это значение затем доступно внутри функции как переменная u.

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

for _, v := range values {
v := v // создаем новую 'v'.
go func() {
fmt.Println(v)
done <- true
}()
}
🔥12👍9
💬 Почему значение ошибки типа nil не равно nil?

Под капотом интерфейсы реализованы как два элемента: тип T и значение V. V — это конкретное значение, такое как int, структура или указатель. Например, если мы сохраняем значение int 3 в интерфейсе, полученное значение интерфейса схематически будет (T=int, V=3).

Значение V также известно как динамическое значение интерфейса, поскольку одна и та же переменная интерфейса может хранить разные значения V (и соответствующие типы T) во время выполнения программы.

Значение интерфейса является nil только если и V, и T не установлены. В частности, nil интерфейс всегда будет содержать nil тип. Если мы сохраняем nil указатель типа *int внутри значения интерфейса, внутренний тип будет *int независимо от значения указателя: (T=*int, V=nil). Таким образом, значение интерфейса будет не nil, даже когда значение указателя V внутри является nil.

Эта ситуация может быть запутанной и возникает, когда nil значение сохраняется внутри значения интерфейса, например, в возвращаемом значении ошибки:

func returnsError() error {
var p *MyError = nil
if bad() {
p = ErrBad
}
return p // Всегда вернет не nil ошибку.
}


Если всё идет хорошо, функция возвращает nil p, так что возвращаемое значение — это значение интерфейса ошибки, содержащее (T=*MyError, V=nil). Это означает, что если вызывающий сравнивает возвращенную ошибку с nil, он всегда будет видеть, как будто произошла ошибка, даже если ничего плохого не случилось. Чтобы вернуть правильную nil ошибку вызывающему, функция должна явно вернуть nil:

func returnsError() error {
if bad() {
return ErrBad
}
return nil
}


Хорошей практикой для функций, возвращающих ошибки, является использование типа error в их сигнатуре (как сделано выше), а не конкретного типа, например *MyError, чтобы помочь гарантировать, что ошибка создается правильно.
👍20
💬 Почему Go-процесс может использовать много виртуальной памяти?

Аллокатор памяти в Go резервирует большую область виртуальной памяти как арену для выделения. Эта виртуальная память локальна для конкретного процесса Go; резервирование не лишает другие процессы памяти.

Чтобы узнать количество фактически выделенной памяти процессу Go, можно использовать команду top в Unix (столбцы RES (Linux) или RSIZE (macOS)).
1
⚡️Самые полезные каналы по Go в одной папке

В ней:
интересные задачи
основной канал
книги по Go
лучшие вакансии из сферы
и наш чат, в котором можно общаться и задавать вопросы

Добавляйте 👉 тык сюда
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥42👍2
💬 Можно ли преобразовать []T в []interface{}?

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

В примере срез типа int преобразуется в срез типа interface{}:

t := []int{1, 2, 3, 4}
s := make([]interface{}, len(t))
for i, v := range t {
s[i] = v
}
👍10
🧑‍💻 Статьи для IT: как объяснять и распространять значимые идеи

Напоминаем, что у нас есть бесплатный курс для всех, кто хочет научиться интересно писать — о программировании и в целом.

Что: семь модулей, посвященных написанию, редактированию, иллюстрированию и распространению публикаций.

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

👉Материалы регулярно дополняются, обновляются и корректируются. А еще мы отвечаем на все учебные вопросы в комментариях курса.
1
💬 Как в Go осуществляется доступ к полям структуры?

В Go доступ к полям структуры осуществляется с использованием символа '.'. Каждое поле структуры имеет своё имя, и к этим полям можно обращаться, используя имя экземпляра структуры, за которым следует точка и имя поля.

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

📌 Простой пример:

package main

import "fmt"

// Определяем структуру Person
type Person struct {
Name string // Экспортируемое поле
age int // Неэкспортируемое поле
}

func main() {
// Создаем экземпляр структуры Person
p := Person{Name: "Alice", age: 30}

// Доступ к экспортируемому полю Name
fmt.Println("Name:", p.Name)

// Доступ к неэкспортируемому полю age (возможен, т. к. мы находимся в том же пакете)
fmt.Println("Age:", p.age)
}


Доступ к полям структуры осуществляется через экземпляр p структуры Person с использованием точки (p.Name и p.age).
🥱22👍82
💬 Для чего используется дочерний контекст в Go, и как он связан с родительским контекстом? Приведите пример сценария, где это может быть полезно.

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

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

Дочерний контекст создается из существующего родительского контекста с помощью таких функций, как context.WithCancel, context.WithDeadline, context.WithTimeout и context.WithValue.

📌 Связь с родительским контекстом: если родительский контекст отменяется, все связанные с ним дочерние контексты также отменяются. Это позволяет централизованно управлять группой задач и их временем жизни.

parentCtx, cancel := context.WithCancel(context.Background())
defer cancel()

childCtx, childCancel := context.WithTimeout(parentCtx, 10*time.Second)
defer childCancel()

// Здесь childCtx будет отменен либо по истечению 10 секунд, либо когда будет отменен parentCtx


📌 Простой пример: в веб-сервере, обрабатывающем HTTP-запросы, для каждого запроса создается дочерний контекст от основного контекста сервера. Если сервер должен быть остановлен, отмена основного контекста приведет к отмене всех обрабатываемых запросов, что позволяет корректно и быстро завершить работу сервера.
13👍7🥱2
💬 Для чего предназначен буфер в каналах Go?

Буфер в каналах используется для управления потоком данных между горутинами и предоставляет несколько ключевых преимуществ:

✔️ Асинхронная передача данных: буферизированный канал позволяет отправителю передавать данные без немедленного ожидания получателя. Это означает, что горутина-отправитель может продолжать свою работу после помещения данных в буфер канала, не блокируясь до тех пор, пока буфер не заполнится полностью.

✔️ Уменьшение блокировок: в небуферизированных каналах отправитель и получатель должны быть готовы к обмену данными одновременно, что может привести к блокировкам. Буферизированные каналы снижают вероятность таких блокировок, поскольку они позволяют временно хранить данные до их обработки.

✔️ Контроль потока: буферизированные каналы могут использоваться для контроля потока данных в приложении. Размер буфера определяет, сколько данных может быть отправлено без блокировки, что позволяет более тонко настраивать производительность и ресурсоемкость приложения.

✔️ Упрощение некоторых паттернов конкурентности: в некоторых случаях использование буферизированных каналов может упростить реализацию определенных паттернов конкурентности, таких как worker pools или регулирование нагрузки между горутинами.
👍7
🏃 Самоучитель по Go для начинающих. Часть 6. Функции и аргументы. Области видимости. Рекурсия. Defer

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

👉 Читать статью
👉 Часть 1
👉 Часть 2
👉 Часть 3
👉 Часть 4
👉 Часть 5
👍4
💬 Что из себя представляет тип Stringer в Go?

Stringer — это интерфейс, определённый в стандартной библиотеке fmt. Он содержит один метод String() string.

Когда тип реализует этот интерфейс, он может контролировать своё строковое представление. Это особенно полезно, когда необходимо вывести структурированные данные или для логирования.

Простой пример:

type Person struct {
Name string
Age int
}

func (p Person) String() string {
return fmt.Sprintf("%s is %d years old", p.Name, p.Age)
}


Тип Person реализует Stringer, так что каждый раз, когда экземпляр Person выводится с использованием функций из пакета fmt, будет использовано кастомное строковое представление, определённое в методе String().
👍9🥱5
💬 Какие существуют распространенные паттерны конкурентности в Go?

Паттерны конкурентности в Go обычно строятся вокруг горутин и каналов.

Вот несколько основных паттернов конкурентности, которые широко используются в Go:

🔸 Worker pools: подразумевает создание нескольких горутин (воркеров) для выполнения задач из очереди.

🔸 Fan-in (собирает данные из множества источников) и Fan-out (распределяет задачи между несколькими обработчиками).

🔸 Pipeline: организация горутин в серию обработчиков, где каждая горутина выполняет определенную подзадачу. Каждая стадия пайплайна читает из одного канала и пишет в другой, формируя цепочку обработки данных.

🔸 Publish/Subscribe: создание механизма, в котором одни горутины (издатели) публикуют сообщения в канал, а другие горутины (подписчики) читают эти сообщения.

🔸 Context passing: использование пакета context для управления жизненным циклом и отмены горутин. Это особенно полезно в сетевых приложениях и при выполнении запросов к базам данных.

🔸 Errgroup: использование пакета errgroup для параллельного выполнения задач с возможностью обработки ошибок и отмены всех задач при возникновении первой ошибки.

🔸 Select statement: использование оператора select для ожидания нескольких операций с каналами. select позволяет горутине ожидать несколько коммуникационных операций, блокируясь до готовности одной из них.

👉 Подробнее: доклад Go Concurrency Patterns Роба Пайка (слайды) и Advanced Go Concurrency Patterns (слайды) Sameer Ajmani
👍181
💬 Поддерживает ли Go операции инкремента и декремента?

В Go поддерживаются операции инкремента (++) и декремента (--), но есть некоторые особенности:

🔸 В Go операции инкремента и декремента могут использоваться только как самостоятельные операторы. Это означает, что они не могут быть частью более сложных выражений. Например, нельзя использовать: a = b++ или c = ++d.

🔸 Постфиксная форма: в Go поддерживается только постфиксная форма этих операторов (то есть i++ и i--).
👍10🥱4🤔1