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

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

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

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

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

Метод типа (методы на типах) — это функция, которая привязана к определенному типу данных. Методы типов на самом деле являются функциями, однако определяются и используются немного по-другому.

Функционал методов типа добавляет в Go некоторые возможности ООП. Кроме того, методы типа нужны для работы интерфейсов.

📌 Создание методов типа

Предположим, что мы хотим выполнить вычисления с помощью матриц 2×2. Самым естественным способом реализации является определение нового типа данных и методов типа для сложения, вычитания и умножения матриц 2×2 с использованием этого нового типа данных.

Имея тип данных ar2x2, мы можем создать для него метод типа functionName следующим образом:

func (a ar2x2) FunctionName(parameters) <return values> {
...
}


(a ar2x2) — то, что делает функцию functionName() методом типа, поскольку связывает ее с типом данных ar2x2. Никакой другой тип данных не сможет использовать эту функцию. Однако мы можем без проблем реализовать functionName() для других типов данных или в виде обычной функции. Если у нас есть переменная ar2x2 с именем varAr, мы можем вызвать functionName() как varAr.functionName(...), что выглядит как выбор поля структуры.

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

func FunctionName(a ar2x2, parameters...) <return values> {
...
}
🔥6
💬 Можно ли давать имена возвращаемым значениям функции Go?

В отличие от C, Go позволяет называть возвращаемые значения Go-функции. Кроме того, когда такая функция имеет оператор return без каких-либо аргументов, она автоматически возвращает текущее значение каждого именованного возвращаемого значения в том порядке, в котором они были объявлены в сигнатуре.

Следующая функция включена в namedReturn.go:

func minMax(x, y int) (min, max int) {
if x > y {
min = y
max = x
return min, max


Здесь оператор return возвращает значения, сохраненные в переменных min и max. Обе переменные определены в сигнатуре, а не в теле функции.

    }

min = x
max = y
return
}


А здесь он эквивалентен return min, max. В его основе — сигнатура функции и использование именованных возвращаемых значений. При выполнении namedReturn.go мы получаем такой вывод:

$ go run namedReturn.go 1 -2
-2 1
-2 1
👍10🥱5🌚1
💬 Что из себя представляет буферизованный и небуферизованный файловый ввод-вывод?

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

🔸 Небуферизованный файловый ввод-вывод: буфер для временного хранения данных не используется перед их фактическим чтением или записью‚ что может повлиять на производительность.

📌 Когда какой использовать?

При работе с критически важными данными небуферизованный файловый ввод-вывод, как правило, является лучшим выбором, поскольку буферизованное чтение может привести к использованию устаревших данных, а небуферизованная запись — к потере данных в случае сбоя. Однако в большинстве случаев однозначного ответа на этот вопрос нет.
👍6
🧠Чему вы бы хотели научиться?

Расскажите нам о ваших пожеланиях: какие навыки вы хотели бы прокачать в ближайшее время или какую профессию хотели бы приобрести?

За прохождение опроса вы получите промокод на скидку 15% на все наши курсы до конца 2024 года.

👉Опрос по ссылке👈
💬 Опишите популярный кейс использования /dev/random в Go и представьте простой пример.

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

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

package main
import (
"encoding/binary"
"fmt"
"os"
)

func main() {
f, err := os.Open("/dev/random")
defer f.Close()
if err != nil {
fmt.Println(err)
return }
var seed int64
binary.Read(f, binary.LittleEndian, &seed)
fmt.Println("Seed:", seed)
}


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

💡Реальный пример порядка — это то, как в разных языках по-разному читается текст.

💡В представлении с обратным порядком байты считываются слева направо, в то время как прямой порядок подразумевает считывание справа налево. Для значения 0x01234567, хранение которого требует четырех байтов, представление с обратным порядком равно 01|23|45|67‚ тогда как прямой порядок выглядит как 67|45|23|01.
1👍1
💬 Каково основное назначение и чем полезен пакет runtime/coverage, представленный в Go 1.20?

Новый пакет предоставляет API для записи данных профиля покрытия во время выполнения для долгосрочных и/или серверных программ, которые не завершаются стандартным образом через os.Exit().

Это позволяет собирать данные о покрытии кода тестами в реальном времени, что особенно полезно для мониторинга и улучшения качества кода в продакшн окружениях, где традиционные методы сбора данных о покрытии могут быть неэффективны или неприменимы.
🔥4
🏃 Самоучитель по Go для начинающих. Часть 4. Переменные. Типы данных и их преобразования. Основные операторы.

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

👉 Читать статью
👉 Часть 1
👉 Часть 2
👉 Часть 3
💬 Опишите основные возможности и пример использования пакета io/fs, добавленного в Go 1.16.

io/fs предлагает интерфейс файловой системы FS, доступный только для чтения. embed.FS реализует интерфейс fs.FS, что дает возможность пользоваться некоторыми функциональными возможностями пакета io/fs.

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

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

func list(f embed.FS) error {
return fs.WalkDir(f, ".", walkFunction)
}

func walkFunction(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
fmt.Printf("Path=%q, isDir=%v\n", path, d.IsDir())
return nil
}

func extract(f embed.FS, filepath string) ([]byte, error) {
s, err := fs.ReadFile(f, filepath)
if err != nil {
return nil, err
}
return s, nil
}


Функция ReadFile() используется для извлечения файла, который идентифицируется по пути к файлу, из файловой системы embed.FS в виде байтового среза, возвращаемого из функции extract().

func walkSearch(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.Name() == searchString {


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

        fileInfo, err := fs.Stat(f, path)
if err != nil {
return err
}
fmt.Println("Found", path, "with size", fileInfo.Size())
return nil
}


Прежде чем выводить совпадение, мы вызываем fs.Stat() с целью получить о нем более подробную информацию.

    return nil 
}
👍8
💬 Что такое атомарная операция и для чего предназначен пакет atomic?

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

Стандартная библиотека Go содержит пакет atomic, который в некоторых простых случаях может помочь избежать использования мьютекса. С помощью него мы получаем доступ к атомарным счетчикам из нескольких горутин, не имея проблем с синхронизацией и не беспокоясь о race condition.

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

package main
import (
"fmt"
"sync"
"sync/atomic"
)

type atomCounter struct {
val int64 }


Это структура для хранения требуемой атомарной переменной int64.

func (c *atomCounter) Value() int64 {
return atomic.LoadInt64(&c.val)
}


Это вспомогательная функция, которая возвращает текущее значение атомарной переменной int64, используя atomic.LoadInt64().

func main() {
X := 100
Y := 4
var waitGroup sync.WaitGroup
counter := atomCounter{}
for i := 0; i < X; i++ {


Мы создаем множество горутин, которые изменяют общую переменную. Благодаря использованию пакета atomic для работы с общей переменной мы получаем простой способ избежать race condition при изменении ее значения.

        waitGroup.Add(1)
go func(no int) {
defer waitGroup.Done()
for i := 0; i < Y; i++ {
atomic.AddInt64(&counter.val, 1)
}


Функция atomic.AddInt64() безопасно изменяет значение поля val структуры counter.

            }(i) 
}

waitGroup.Wait()
fmt.Println(counter.Value())
}
👍143🔥1
💬 Что такое Duck Typing и как эта концепция применяется в Go?

Duck Typing — это концепция в программировании, которая гласит, что важно не то, какой тип у объекта, а то, какие операции с ним можно выполнять. Её суть заключается в фразе: «Если это выглядит как утка, плавает как утка и крякает как утка, то это, вероятно, утка».

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

В Go Duck Typing реализуется через интерфейсы. Интерфейс в Go определяет набор методов, но не указывает, какие структуры или типы должны эти методы реализовывать. Любой тип, который реализует все методы интерфейса, автоматически считается реализующим этот интерфейс. Это означает, что в Go нет необходимости явно указывать, что конкретный тип реализует определенный интерфейс.
👍154🥱2
💬 Что из себя представляет пакет 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