Golang | Вопросы собесов
4.33K subscribers
28 photos
700 links
Download Telegram
🤔 Какой ключевое слово используется для выхода из горутины в Go?
Anonymous Quiz
10%
exit
4%
stop
37%
return
49%
нет специального ключевого слова
📌 Как каналы устроены в Go?

💬 Спрашивают в 92% собеседований

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

🤔 Основы

Могут быть типизированы, что означает, что канал может передавать значения только одного определённого типа. Они могут быть объявлены и инициализированы с помощью ключевого слова chan:
ch := make(chan int) // Канал для передачи значений типа int


🤔 Отправка и получение данных

Для отправки значения в канал используется оператор <-:
ch <- 10 // Отправка значения 10 в канал


Для получения значения из канала тот же оператор используется, но в другом контексте:
value := <-ch // Прочитать значение из канала и присвоить его переменной value


🤔 Блокировки

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

Если горутина пытается отправить данные в канал, она блокируется до тех пор, пока другая горутина не прочитает эти данные.

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

🤔 Буферизация

Могут быть буферизированными, что означает, что они могут хранить ограниченное количество значений без необходимости немедленного получения. Буферизированный канал инициализируется с указанием размера буфера:
ch := make(chan int, 5) // Буферизированный канал с размером буфера 5


В буферизированном канале отправка не блокируется до тех пор, пока буфер не заполнится, и получение не блокируется до тех пор, пока буфер не опустеет.

🤔 Закрытие каналов

Каналы можно закрывать, если больше нет необходимости отправлять через них данные. После закрытия канала нельзя отправлять данные, но можно продолжать получать данные до тех пор, пока канал не опустеет:
close(ch)


Проверка на то, что канал закрыт и данные исчерпаны, возможна в операции чтения:
value, ok := <-ch
if !ok {
// Канал закрыт и все данные получены
}


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

🔥 ТОП ВОПРОСОВ С СОБЕСОВ

🔒 База собесов | 🔒 База тестовых
Please open Telegram to view this post
VIEW IN TELEGRAM
📌 Что такое горутины?

💬 Спрашивают в 83% собеседований

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

Особенности:

1️⃣ Легковесность: Горутины занимают значительно меньше памяти по сравнению с традиционными потоками операционной системы. Один процесс может поддерживать тысячи или даже миллионы горутин благодаря их эффективному управлению памятью и ресурсами.

2️⃣ Масштабируемость: Планировщик в Go автоматически распределяет горутины по доступным процессорным ядрам, оптимизируя использование ресурсов и увеличивая производительность программы.

3️⃣ Простота использования: Синтаксис для создания горутин в Go очень прост. Достаточно использовать ключевое слово go перед вызовом функции:
go myFunction()


Этот вызов создаст новую горутину, которая начнет выполнение функции myFunction.

Допустим, мы хотим одновременно обработать несколько HTTP-запросов. Вместо создания одной горутины на каждый запрос, мы можем написать так:
func handleRequest(request *http.Request) {
// Обработка запроса
}

func main() {
requests := fetchRequests() // Предположим, это функция, которая возвращает список запросов
for _, req := range requests {
go handleRequest(req)
}
}


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

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

🔥 ТОП ВОПРОСОВ С СОБЕСОВ

🔒 База собесов | 🔒 База тестовых
Please open Telegram to view this post
VIEW IN TELEGRAM
📌 Что такое интерфейсы?

💬 Спрашивают в 75% собеседований

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

🤔 Основные характеристики:

1️⃣ Декларативная природа: Интерфейс объявляется как набор методов, но без их реализации. Классический пример — интерфейс Reader из пакета io, который определяет метод Read:
type Reader interface {
Read(p []byte) (n int, err error)
}


Любой тип, который реализует метод Read с такой же сигнатурой, считается реализующим интерфейс Reader.

2️⃣ Неявная реализация: В отличие от многих других языков программирования, не требуется явно указывать, что тип реализует интерфейс. Если методы типа соответствуют интерфейсу, то этот тип считается его реализующим:
type MyReader struct{}

func (mr *MyReader) Read(p []byte) (n int, err error) {
// Реализация
return
}

// MyReader неявно реализует интерфейс Reader


3️⃣ Использование интерфейсов для абстракции: Интерфейсы можно использовать для создания функций, которые принимают параметры интерфейсного типа, позволяя передавать в них любой объект, который реализует данный интерфейс:
func process(r Reader) {
// функция работает с любым объектом, который удовлетворяет интерфейсу Reader
}


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

Допустим, у нас есть интерфейс Shape с методом Area, который должен возвращать площадь фигуры. Мы можем реализовать этот интерфейс в различных структурах:
type Shape interface {
Area() float64
}

type Circle struct {
Radius float64
}

func (c *Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}

type Square struct {
Side float64
}

func (s *Square) Area() float64 {
return s.Side * s.Side
}

func printArea(shape Shape) {
fmt.Println(shape.Area())
}


Теперь функция printArea может принимать любой объект, который реализует интерфейс Shape.

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

🔥 ТОП ВОПРОСОВ С СОБЕСОВ

🔒 База собесов | 🔒 База тестовых
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔 Как создать новую горутину в Go?
Anonymous Quiz
5%
go new(func())
2%
new(func())
90%
go func()
3%
func() go
📌 В чем разница слайсов и массивов?

💬 Спрашивают в 67% собеседований

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

🤔 Массивы

Это структуры фиксированного размера, который определяется при их объявлении и не может быть изменен. Они предоставляют простой способ хранения фиксированного количества элементов одного типа. Вот как можно объявить массив в Go:
var a [5]int


В этом примере a — это массив из пяти целых чисел. Размер массива является частью его типа, поэтому массивы с разным размером представляют разные типы данных.

🤔 Слайсы

Это более динамичные структуры данных по сравнению с массивами. Они предоставляют гибкий способ работы с последовательностями элементов того же типа. Слайсы не хранят собственные данные. Они являются просто "окном" в базовый массив. Слайсы имеют три компонента: указатель на элемент массива, длину (количество элементов в слайсе) и вместимость (максимальное количество элементов, которое слайс может содержать до следующего расширения). Вот пример создания слайса:
s := []int{1, 2, 3}


Здесь s — это слайс, который ссылается на массив, содержащий три элемента.

Основные различия

1️⃣ Размер:

Массивы имеют фиксированный размер, который указывается при их объявлении и не может быть изменен.

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

2️⃣Типы данных:

Размер массива является частью его типа. Это означает, что массивы разного размера представляют разные типы данных.

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

3️⃣ Производительность:

Операции с массивами могут быть немного быстрее, так как они работают напрямую с памятью.

Слайсы немного медленнее из-за дополнительной индирекции, но предлагают значительно большую гибкость.

4️⃣ Использование:

Массивы идеально подходят, когда вам известно точное количество элементов, которое не изменится.

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

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

🔥 ТОП ВОПРОСОВ С СОБЕСОВ

🔒 База собесов | 🔒 База тестовых
Please open Telegram to view this post
VIEW IN TELEGRAM
📌 В чем разница между буферизированными и небуферизированными каналами?

💬 Спрашивают в 25% собеседований

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

🤔 Небуферизированные каналы

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

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

Получение данных из него блокирует получателя до тех пор, пока другая горутина не отправит данные в канал.
ch := make(chan int) // Создание небуферизированного канала
go func() {
val := <-ch // Блокируется, ожидая данные
fmt.Println("Received:", val)
}()
ch <- 3 // Блокируется, пока данные не будут получены


🤔 Буферизированные каналы

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

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

Получение из буферизированного канала блокируется, только если канал пуст. Если в канале есть данные, получение происходит без блокировки.
ch := make(chan int, 2) // Создание буферизированного канала с емкостью 2
ch <- 1 // Отправка данных без блокировки
ch <- 2 // Отправка данных без блокировки
go func() {
val := <-ch // Получение данных без блокировки
fmt.Println("Received:", val)
}()


🤔 Основные различия:

1️⃣ Синхронизация: Небуферизированные каналы обеспечивают точную синхронизацию между отправителем и получателем, так как каждая операция передачи требует готовности обеих сторон. Буферизированные каналы снижают необходимость немедленной готовности получателя за счет введения буфера.

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

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

🔥 ТОП ВОПРОСОВ С СОБЕСОВ

🔒 База собесов | 🔒 База тестовых
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔 Какой тип используется для представления массивов в Go?
Anonymous Quiz
31%
slice
60%
array
6%
map
3%
struct
📌 Что будет, если попытатся писать в закрытом канале?

💬 Спрашивают в 25% собеседований

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

Вот пример кода, который вызывает панику при попытке отправки в закрытый канал:
package main

import "fmt"

func main() {
ch := make(chan int)
close(ch) // закрытие канала
ch <- 1 // попытка записи в закрытый канал вызовет панику
}


Запуск этого кода приведет к следующему выводу:
panic: send on closed channel


🤔 Обработка такой ситуации

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

1️⃣ Четкая ответственность: Одна горутина или чётко определенный набор горутин должен быть ответственен за закрытие канала. Это помогает избежать ситуаций, когда один поток данных пытается записать в канал после его закрытия другим потоком.

2️⃣ Использование синхронизации: С помощью механизмов синхронизации, таких как мьютексы или условные переменные (sync.Cond), можно координировать доступ к каналу, чтобы обеспечить его безопасное закрытие.

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

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

🔥 ТОП ВОПРОСОВ С СОБЕСОВ

🔒 База собесов | 🔒 База тестовых
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔 Какое ключевое слово используется для объявления интерфейса в Go?
Anonymous Quiz
20%
type
1%
func
76%
interface
4%
struct
📌 Как работает Map?

💬 Спрашивают в 58% собеседований

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

🤔 Создание и инициализация

Map можно создать с помощью встроенной функции make или через литерал map. Вот примеры обоих методов:
// Создание map с помощью функции make
m := make(map[string]int)

// Создание map с помощью литерала
n := map[string]int{"foo": 1, "bar": 2}


В этих примерах m и n являются map, где ключи — это строки, а значения — целые числа.

Добавление или изменение элемента в map происходит путем присваивания значения ключу:
m["baz"] = 3


В этом примере ключу "baz" присваивается значение 3. Если ключ уже существует, его значение будет перезаписано.

Для получения значения по ключу используется следующий синтаксис:
value := m["baz"]


Если ключ существует, value будет содержать соответствующее значение. Если ключа нет в map, value получит нулевое значение для типа данных значения (например, 0 для int, "" для string и так далее).

🤔 Проверка существования ключа

Чтобы проверить, существует ли ключ в map и избежать нулевых значений, можно использовать второе возвращаемое значение при доступе к элементу:
value, ok := m["baz"]
if ok {
fmt.Println("Value:", value)
} else {
fmt.Println("Key not found")
}


Для удаления элемента из map используется встроенная функция delete:
delete(m, "baz")


Это удаляет элемент с ключом "baz" из map m.

🤔 Как map реализован

Внутренне, map реализован как хеш-таблица. Хеш-таблицы обеспечивают очень быстрый доступ к данным по ключу за среднее время O(1), что делает их идеальными для использования в ситуациях, где требуется частое извлечение или изменение данных по ключу.

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

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

🔥 ТОП ВОПРОСОВ С СОБЕСОВ

🔒 База собесов | 🔒 База тестовых
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔 Какой тип используется для представления динамических массивов в Go?
Anonymous Quiz
7%
array
90%
slice
2%
map
1%
struct
Please open Telegram to view this post
VIEW IN TELEGRAM
📌 Как устроен Map в Go?

💬 Спрашивают в 42% собеседований

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

🤔 Внутренняя структура

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

1️⃣ Хеш-функция: Ключ, который вы используете в map, преобразуется с помощью хеш-функции, которая определяет, в каком "bucket" (или "корзине") будет храниться значение. Хеш-функция в Go спроектирована так, чтобы минимизировать коллизии (где разные ключи имеют один и тот же хеш).

2️⃣ Buckets (Корзины): Хеш-таблица разделена на несколько корзин. Каждый бакет может содержать несколько пар ключ-значение, которые имеют один и тот же или близкий хеш. Это помогает организовать данные таким образом, чтобы операции с map были максимально эффективными.

3️⃣ Обработка коллизий: Коллизии в хеш-таблице (когда два ключа дают одинаковый хеш) решаются с помощью метода цепочек, где каждый элемент в бакете содержит указатель на следующий элемент с тем же хешем. Это позволяет нескольким элементам быть связанными в одном бакете.

4️⃣ Рост и перехеширование: По мере того как элементы добавляются в map, количество корзин может увеличиваться для поддержания производительности операций. Когда фактор загрузки (отношение количества элементов к количеству корзин) достигает определенного порога, происходит процесс, называемый перехешированием, в котором элементы распределяются заново среди нового, большего количества корзин.

Поскольку map является встроенным типом, его использование не требует специальных библиотек:
m := make(map[string]int) // Создание map
m["apple"] = 5 // Добавление элемента
m["banana"] = 10 // Добавление другого элемента

value, exists := m["apple"] // Проверка существования ключа и получение значения
if exists {
fmt.Println("Value:", value)
}

delete(m, "apple") // Удаление элемента


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

🔥 ТОП ВОПРОСОВ С СОБЕСОВ

🔒 База собесов | 🔒 База тестовых
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔 Как объявить новую переменную в Go?
Anonymous Quiz
2%
let
95%
var
1%
dim
2%
new
📌 Как строки работают в Golang внутри?

💬 Спрашивают в 42% собеседований

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

1️⃣ Неизменяемость

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

2️⃣ Структура строки

Внутренне строка представлена структурой, которая содержит два поля:

Указатель на массив байтов: Это указатель на первый элемент массива байт, который фактически хранит символы строки в кодировке UTF-8.

Длина: Количество байт в строке, а не количество рун или символов. Это важное различие, поскольку в UTF-8 один символ может занимать от 1 до 4 байт.

3️⃣ UTF-8 как стандартная кодировка

Go использует UTF-8 как стандартную кодировку для строк. Это позволяет эффективно работать с международным текстом, поддерживая широкий спектр символов без сложностей, связанных с другими кодировками. Однако это также означает, что операции, такие как получение длины строки в рунах (символах) или доступ к отдельному символу, могут потребовать дополнительных вычислений для обработки многобайтовых символов.

4️⃣ Срезы строк

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

5️⃣ Производительность и память

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

Вот простой пример демонстрирующий работу со строками:
s := "Hello, world"      // Создание строки
t := s[7:] // Срез строки, создает новую строку "world"

fmt.Println(s) // Выводит: Hello, world
fmt.Println(t) // Выводит: world


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

🔥 ТОП ВОПРОСОВ С СОБЕСОВ

🔒 База собесов | 🔒 База тестовых
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔 Какой пакет используется для форматированного ввода-вывода в Go?
Anonymous Quiz
2%
input
19%
io
73%
fmt
6%
scan
Зачем аppend возвращает слайс ?
Спросят с вероятностью 8%

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

Почему он возвращает слайс

1️⃣Изменение емкости слайса:
Когда вы добавляете элементы в слайс с помощью него, Go может решить, что текущей емкости недостаточно для размещения новых элементов.
В таком случае выделяется новый массив с большей емкостью, копирует существующие элементы в новый массив и добавляет новые элементы.

2️⃣Изменение базового массива:
Если емкость слайса увеличивается, то базовый массив, на который указывает слайс, изменяется.
Возвращая новый слайс, функция append гарантирует, что вы работаете с актуальной версией слайса, которая указывает на правильный базовый массив.

Демонстрация поведения append
package main

import "fmt"

func main() {
slice := make([]int, 2, 2) // Длина 2, емкость 2
slice[0] = 1
slice[1] = 2

fmt.Println("Before append:", slice) // [1 2]

// Добавляем элемент
slice = append(slice, 3)

fmt.Println("After append:", slice) // [1 2 3]

// Показать емкость после добавления
fmt.Println("Capacity after append:", cap(slice)) // Capacity может быть больше 3, в зависимости от стратегии роста

// Показать новый базовый массив
fmt.Printf("Slice data pointer: %p\n", &slice[0])
}


Здесь:
Изначально слайс slice имеет длину 2 и емкость 2.
После вызова append емкость слайса увеличивается, и слайс указывает на новый базовый массив.

Возврат нового слайса

Поскольку базовый массив может измениться, важно присвоить результат функции append обратно слайсу. Если этого не сделать, можно продолжить использовать старый слайс, который указывает на уже неактуальный массив данных.
// Правильное использование append
slice = append(slice, 4)


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

👉 Можно посмотреть Примеры как отвечают люди на этот вопрос, или перейти К списку 349 вопроса на Golang разработчика. Ставь 👍 если нравится контент

🔐 База собесов | 🔐 База тестовых