Библиотека Go для собеса | вопросы с собеседований
6.88K subscribers
225 photos
7 videos
1 file
435 links
Вопросы с собеседований по Go и ответы на них.

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

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

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

Наши каналы: https://t.me/proglibrary/9197
Download Telegram
💬 Что из себя представляет пакет semaphore в Go?

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

📌 Немного практики

Семафоры могут иметь веса, которые задают максимальное количество потоков или горутин, получающих доступ к ресурсу.
Процесс поддерживается с помощью методов Acquire() и Release(), определенных следующим образом:

func (s *Weighted) Acquire(ctx context.Context, n int64) error
func (s *Weighted) Release(n int64)


Второй параметр Acquire() определяет вес семафора.

package main

import (
"context"
"fmt"
"os"
"strconv"
"time"
"golang.org/x/sync/semaphore"
)

var Workers = 4


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

var sem = semaphore.NewWeighted(int64(Workers))


Здесь мы определяем семафор с весом, идентичным максимальному количеству горутин, которые могут выполняться одновременно. Это означает, что получать семафор одновременно могут не более чем Workers горутин.

func worker(n int) int {
square := n * n
time.Sleep(time.Second)
return square
}


Функция worker() выполняется как часть горутины. Однако поскольку мы используем семафор, нет необходимости возвращать результаты в канал.

func main() {
if len(os.Args) != 2 {
fmt.Println("Need #jobs!")
return
}

nJobs, err := strconv.Atoi(os.Args[1])
if err != nil {
fmt.Println(err)
return
}


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

    // где хранить результаты
var results = make([]int, nJobs)
// требуется для Acquire()
ctx := context.TODO()

for i := range results {
err = sem.Acquire(ctx, 1)
if err != nil {
fmt.Println("Cannot acquire semaphore:", err)
break
}


Получаем семафор столько раз, сколько заданий определено nJobs. Если nJobs больше, чем Workers, то вызов Acquire() будет заблокирован и дождется вызовов Release() для разблокировки.

            go func(i int) {
defer sem.Release(1)
temp := worker(i)
results[i] = temp
}(i)
}


Запускаем горутины, которые выполняют эту задачу, и записываем результаты в срез results. Поскольку каждая горутина записывает данные в свой элемент среза, никаких race condition нет.

    err = sem.Acquire(ctx, int64(Workers))
if err != nil {
fmt.Println(err)
}


Получаем все токены таким образом, чтобы вызов sem.Acquire() блокировался до тех пор, пока все рабочие процесссы/горутины не завершат работу. Функционально это похоже на вызов Wait().

    for k, v := range results {
fmt.Println(k, "->", v)
}
}
🤔4🔥31
💬 Как сообщить компилятору Go, что наш тип реализует интерфейс?

В Go, интерфейсы реализуются неявно. Это означает, что нам не нужно явно указывать, что наш тип реализует интерфейс.

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

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

Допустим, у нас есть следующий интерфейс:

type Speaker interface {
Speak() string
}


и тип Person:

type Person struct {
Name string
}

func (p Person) Speak() string {
return "My name is " + p.Name
}


Так как Person определяет метод Speak(), который присутствует в интерфейсе Speaker, Person автоматически реализует интерфейс Speaker. Нет необходимости в дополнительном коде или объявлении для подтверждения этого.
11👍7🌚1
💬 Что такое tight coupling в контексте Go?

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

📌 Это проявляется в нескольких аспектах:

1. Прямая зависимость между структурами: если одна структура в Go содержит или прямо ссылается на другую структуру и прямо использует её методы и свойства, это создаёт tight coupling. Изменения в одной структуре могут потребовать изменений в другой.

2. Использование глобальных переменных: частое использование глобальных переменных, которые доступны во многих частях программы, приводит к tight coupling, т. к. различные части программы становятся зависимыми от глобального состояния.

3. Отсутствие интерфейсов: в Go интерфейсы используются для создания слабой связанности. Если код прямо зависит от конкретных реализаций, а не от абстракций (например, интерфейсов), это увеличивает степень связанности.

4. Тесная интеграция между пакетами: когда один пакет в Go импортирует множество других пакетов и тесно взаимодействует с их компонентами, это создаёт tight coupling. Изменения в одном пакете могут вызвать необходимость изменений во всех зависимых пакетах.
11👍9
💬 Что такое lock-free структуры данных, и есть ли такие в Go?

Lock-free структуры данных — это тип структур данных, разработанных для многопоточных операций без использования традиционных блокировок, таких как мьютексы.

Основная идея заключается в том, чтобы обеспечить безопасность потоков и избежать проблем, связанных с блокировками, включая взаимную блокировку (deadlock) и узкие места производительности (bottlenecks).

Lock-free структуры данных обычно используют атомарные операции, такие как CAS (compare-and-swap), для обеспечения согласованности данных между потоками. Эти операции позволяют потокам соревноваться за изменение данных, но гарантируют, что только один поток сможет успешно изменить данные в любой момент времени.

В Go, языке с поддержкой конкурентности, есть несколько примеров lock-free или почти lock-free структур данных, особенно в стандартной библиотеке. Например:

1. Каналы: хотя каналы в Go не являются полностью lock-free, они предоставляют высокоуровневый способ обмена данными между горутинами без явного использования блокировок.

2. Атомарные операции: пакет sync/atomic в Go предоставляет примитивы для атомарных операций, которые являются ключевыми компонентами для создания lock-free структур данных.

3. sync.Map: предназначен для использования в кейсах, где ключи в основном не меняются, и он использует оптимизации для уменьшения необходимости блокировок.
👍23
💬 Что из себя представляют метрики рантайма в Go?

В Go, метрики рантайма относятся к данным и статистике, которые отслеживаются и собираются во время выполнения программы.

Они могут быть использованы для мониторинга и оптимизации производительности программы:

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

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

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

4. Блокировки и синхронизация: в Go важно отслеживать использование примитивов синхронизации. Метрики, относящиеся к блокировкам, могут помочь выявить проблемы с мертвыми блокировками или неэффективным использованием ресурсов.

💡Go предоставляет различные встроенные инструменты, включая pprof, для сбора и анализа этих метрик.
👍10
💬 Что такое «семплирующий профайлер»?

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

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

В Go стандартный инструмент для семплирующего профилирования — pprof. Он позволяет собирать различные виды профилей (CPU, память, блокировки и т.д.) и предоставляет удобные средства для их анализа.
🧑‍💻 Статьи для IT: как объяснять и распространять значимые идеи

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

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

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

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

🔸 Создание среза: срез может быть создан с использованием выражения a[low : high], где a — массив или другой срез, low — начальный индекс, а high — конечный индекс (не включительно). Если low равно 0, его можно опустить. Если high равно длине массива, его также можно опустить.

🔸 Границы индексов: значения low и high должны удовлетворять условиям 0 <= low <= high <= cap(a), где cap(a) — это емкость исходного массива или среза. Попытка использовать индексы за пределами этих границ приведёт к панике.

🔸 Пустые срезы: если low и high равны, срез будет пустым, но валидным. Например, a[2:2] создаст пустой срез.

🔸 Выход за границы: если low или high выходят за границы допустимых значений, компилятор выдаст панику. Например, если len(a) равно 5, то a[0:6] вызовет панику, так как 6 превышает допустимую границу.

🔸 Изменение исходного массива: срезы в Go являются ссылками на исходный массив. Это означает, что изменения в срезе отразятся на исходном массиве и на всех других срезах, сделанных из этого массива.

🔸 nil и пустые срезы: срез, который не был инициализирован, имеет значение nil. Он отличается от пустого среза, который был инициализирован, но не содержит элементов. nil срез имеет длину и емкость 0, но пустой срез может иметь ненулевую емкость.

🔸 Увеличение емкости среза: если при добавлении элементов в срез его емкость оказывается недостаточной, Go автоматически создаст новый массив с большей емкостью и скопирует в него элементы из исходного среза.
👍126🔥3
💬 Можно ли в Go закрыть канал со стороны читателя?

Закрытие канала обычно выполняется отправителем, а не получателем. Это связано с тем, что закрытие канала со стороны получателя может привести к панике при попытке отправителя записать в уже закрытый канал.

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

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

func main() {
dataCh := make(chan int)
stopCh := make(chan struct{})

go func() {
for {
select {
case data, ok := <-dataCh:
if !ok {
// Канал закрыт, прекращаем обработку
return
}
// Обработка данных
fmt.Println(data)
case <-stopCh:
// Получен сигнал остановки, закрываем канал dataCh
close(dataCh)
return
}
}
}()

// Отправка данных в канал
dataCh <- 1
dataCh <- 2

// Отправка сигнала остановки
stopCh <- struct{}{}
}


stopCh используется для уведомления горутины о необходимости закрыть канал dataCh. Это безопасный способ обеспечить корректное управление жизненным циклом канала.
👍191
💬 Как устроен сетевой ввод-вывод в Go?

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

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

Go автоматически управляет множеством горутин, что упрощает написание масштабируемого асинхронного сетевого кода по сравнению с традиционными подходами, основанными на потоках.

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

package main

import (
"fmt"
"io"
"net"
"os"
)

func main() {
// Слушаем на порту 8080
listener, err := net.Listen("tcp", ":8080")
if err != nil {
fmt.Println("Ошибка при создании слушателя:", err)
os.Exit(1)
}
defer listener.Close()
fmt.Println("Сервер запущен и слушает на порту 8080")

for {
// Принимаем входящее подключение
conn, err := listener.Accept()
if err != nil {
fmt.Println("Ошибка при принятии подключения:", err)
continue
}

// Обработка подключения в отдельной горутине
go handleConnection(conn)
}
}

// handleConnection обрабатывает отдельное подключение
func handleConnection(conn net.Conn) {
defer conn.Close()
fmt.Println("Подключился клиент:", conn.RemoteAddr().String())

// Отправляем сообщение клиенту
_, err := io.WriteString(conn, "Привет от сервера!\n")
if err != nil {
fmt.Println("Ошибка при отправке сообщения:", err)
return
}

fmt.Println("Сообщение отправлено клиенту:", conn.RemoteAddr().String())
}
👍141
💬 Что такое table-driven тесты и как их реализовать в Go?

Table-driven тесты в Go — это метод написания тестов, при котором тестовые кейсы организованы в виде таблицы данных.

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

📌 Для реализации table-driven тестов в Go обычно используется следующий шаблон:

1. Определяем структуру, которая описывает тестовый кейс, включая входные данные и ожидаемый результат.
2. Создаем срез этих структур, где каждый элемент представляет отдельный тестовый кейс.
3. Используем цикл for для итерации по срезу тестовых кейсов.
4. Внутри цикла вызываем функцию, которую тестируем, и сравниваем результат с ожидаемым значением.

📌 Пример:

package mypackage

import "testing"

func TestMyFunction(t *testing.T) {
cases := []struct {
name string
input int
want int
}{
{"case1", 1, 2},
{"case2", 2, 4},
// …
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := MyFunction(c.input)
if got != c.want {
t.Errorf("MyFunction(%d) == %d, want %d", c.input, got, c.want)
}
})
}
}


MyFunction
— это функция, которую мы тестируем. Для каждого тестового кейса в срезе cases мы запускаем тест, используя t.Run, что также обеспечивает хорошую организацию вывода тестов и их независимость.
12👍8🥱1
-35% на курс по алгоритмам

🎄 Новый год начинается с подарков, а хороший подарок для себя — новые знания со скидкой 35%!

🌟«Алгоритмы и структуры данных» — 23 390 ₽ (вместо 35 990 ₽)

Полугодовая программа от преподавателей МФТИ и НИУ ВШЭ, которая включает в себя все необходимые знания по алгоритмам для работы.

Самое ценное — это развернутая обратная связь по всем вашим домашним заданиям, а также ссылки на полезные дополнительные материалы.

У вас не будет шансов не усвоить какие-то темы курса👌

🔥 Переходите и активируйте вводные занятия курсаhttps://proglib.io/w/ff97c30a
😁3🤔1
💬 Что такое Minimal Version Selection в контексте Go?

Minimal Version Selection (MVS) в Go — это алгоритм, используемый системой управления зависимостями модулей Go (введённый в Go 1.11 с появлением поддержки модулей).

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

📌 Основные аспекты MVS:

1. Минимальная версия: MVS выбирает минимально возможную версию каждой зависимости, которая удовлетворяет всем требованиям версий, указанным в зависимостях проекта и его модулях. Это означает, что если ваш модуль зависит от модуля A версии 1.2 и модуля B, который в свою очередь зависит от модуля A, но версии 1.1, MVS выберет версию 1.2 модуля A, так как это минимальная версия, удовлетворяющая обоим требованиям.

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

3. Уменьшение риска несовместимости: поскольку MVS выбирает минимальную версию, это помогает избежать непреднамеренного обновления до более новых, потенциально несовместимых версий зависимостей.

4. Файл go.mod: все зависимости и их версии явно указываются в файле go.mod проекта. MVS использует этот файл для определения, какие версии зависимостей использовать.

5. Простота обновления: если нам требуется обновить зависимость до более новой версии, достаточно обновить эту версию в файле go.mod. MVS автоматически учтёт это изменение при следующей сборке.

👉 Подробнее
👍9
💬 Что важно помнить при использовании мапы типа any?

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

📌 Возьмем простой пример:

b := getMessage()
var m map[string]any
err := json.Unmarshal(b, &m)
if err != nil {
return err
}


Добавим следующий JSON:

{
"id": 32,
"name": "foo"
}


Поскольку мы используем общую мапу map[string]any, она автоматически парсит все поля: map[id:32 name:foo]

При использовании мапы типа any важно помнить о том, что любое числовое значение, независимо от того, содержит оно десятичное число или нет, преобразуется в тип float64.

Выведем тип m["id"] и убедимся в этом:

fmt.Printf("%T\n", m["id"])

float64


Важно не делать ошибочных предположений и не ожидать, что числовые значения без десятичных знаков будут по умолчанию преобразованы в целые числа.
💯174
💬 Что из себя представляют числовые константы в Go?

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

Они принимают свой тип (например, int, float64) только когда это необходимо, например, при присваивании значения переменной или при использовании в операции, где требуется определённый тип. Это дает гибкость и предотвращает потерю информации из-за ограничений размера типа, особенно при выполнении математических операций с константами.

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

package main

import "fmt"

const (
Big = 1 << 100
Small = Big >> 99
)

func needInt(x int) int { return x*10 + 1 }
func needFloat(x float64) float64 { return x * 0.1 }

func main() {
fmt.Println(needInt(Small))
fmt.Println(needFloat(Small))
fmt.Println(needFloat(Big))
}
👍6
💬 В чем преимущества и недостатки использования ORM по сравнению с использованием встроенных возможностей Go для работы с SQL?

📌 Преимущества ORM:

1. Удобство и скорость разработки: ORM позволяет взаимодействовать с базой данных, используя объектно-ориентированный подход, что часто упрощает и ускоряет процесс разработки.

2. Безопасность: ORM может помочь избежать некоторых распространенных уязвимостей за счет использования встроенных механизмов защиты.

3. Независимость от базы данных: ORM обеспечивает абстракцию, которая позволяет легче переходить между различными СУБД, не изменяя большую часть кода приложения.

4. Упрощение рефакторинга и поддержки: поскольку логика доступа к данным централизована, вносить изменения и поддерживать приложение становится проще.

📌 Недостатки ORM:

1. Производительность: ORM может быть менее эффективным по сравнению с оптимизированными вручную SQL-запросами, особенно в сложных сценариях.

2. Сложность: ORM может добавлять дополнительный уровень сложности, который может быть излишним для простых приложений или простых запросов.

3. Ограничения: некоторые ORM могут ограничивать способность разработчика использовать все функции и возможности конкретной СУБД.

4. Кривая обучения: для эффективного использования ORM требуется время на изучение его особенностей и лучших практик.

📌 Примеры ORM для Go: gorm, Beego ORM, SQLBoiler и другие.
👍7
💬 Почему встраивание в Go не является наследованием, как в классическом ООП?

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

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

В Go, это скорее способ композиции, чем наследования. Встраиваемая структура ничего не знает о том, где она используется, и не может переопределить методы структуры, в которую она встроена.
👍11
🏃 Самоучитель по Go для начинающих. Часть 5. Условные конструкции if-else и switch-case. Цикл for. Вложенные и бесконечные циклы

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

👉 Читать статью
👉 Часть 1
👉 Часть 2
👉 Часть 3
👉 Часть 4
👍6
💬 В каких случаях в Go могут возникнуть deadlocks?

📌 Причины возникновения дедлоков в Go:

1. Горутины, ожидающие друг друга: горутины могут входить в состояние дедлока, если они ожидают ресурсы или сигналы друг от друга, образуя циклическую зависимость.
2. Неправильное использование каналов: попытка чтения из закрытого канала или блокировка на отправке/получении данных из-за отсутствия получателей/отправителей, может привести к дедлоку.
3. Злоупотребление блокировками: использование мьютексов и других примитивов синхронизации без должной осторожности может вызвать дедлоки. Например, попытка захватить мьютекс, который уже захвачен текущей горутиной, приведет к блокировке.
👍169
💬 Можно ли в функциях Go использовать необязательные аргументы?

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

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

Например, можно создать функцию funcStructOpts(opts Opts), которая принимает структуру Opts, и вызвать её как funcStructOpts(Opts{p1: 1, p2: 2, p8: 8, p9: 9, p10: 10}), где Opts — структура с полями p1, p2, p8, p9, p10 и так далее.

2. Функциональные аргументы, предложенные Робом Пайком: этот подход включает определение функции, которая принимает переменное количество функциональных аргументов. Каждый из этих аргументов — это функция, которая модифицирует внутреннее состояние функции.

Например, можно определить функцию funcWithOpts(opts ...Option), и вызвать её как funcWithOpts(WithP1(1), WithP2(2), WithP8(8), WithP9(9), WithP10(10)), где WithP1, WithP2, WithP8, WithP9, WithP10 являются функциями, возвращающими тип Option, который может быть функцией, изменяющей внутреннее состояние вызываемой функции funcWithOpts.
👍108