💬Что из себя представляет тип данных map в Go?
В Go map используется как тип dictionary в большинстве других ЯП. Он сопоставляет ключи со значениями, создавая пары ключ-значение, представляющие собой полезный способ хранения данных в Go.
📌Как создать map?
🔸С помощью ключевого слова map с последующим указанием типа данных ключа в квадратных скобках [ ] и типа данных значения. Пары ключ-значение заключаются в фигурные скобки { }:
🔸Функция make представляет альтернативный вариант создания map. Она создает пустую хеш-таблицу:
📌Что будет, если попытаться получить значение по несуществующему ключу из map?
Мы получим нулевое значение для типа значений map. Например, если это
📌Как проверить, существует ли ключ в map?
При получении значения из map можно использовать второй возвращаемый аргумент, который будет булевым значением, указывающим, существует ли ключ:
📌Является ли map потокобезопасным типом данных?
Нет, map не является потокобезопасным, и для доступа к нему из нескольких горутин одновременно может потребоваться синхронизация, например, с помощью sync.Mutex.
В Go map используется как тип dictionary в большинстве других ЯП. Он сопоставляет ключи со значениями, создавая пары ключ-значение, представляющие собой полезный способ хранения данных в Go.
📌Как создать map?
🔸С помощью ключевого слова map с последующим указанием типа данных ключа в квадратных скобках [ ] и типа данных значения. Пары ключ-значение заключаются в фигурные скобки { }:
map[key]value{}
🔸Функция make представляет альтернативный вариант создания map. Она создает пустую хеш-таблицу:
m := make(map[string]int)
📌Что будет, если попытаться получить значение по несуществующему ключу из map?
Мы получим нулевое значение для типа значений map. Например, если это
map[string]int
, то значение будет 0. Если это map[string]*SomeStruct
, значение будет nil.📌Как проверить, существует ли ключ в map?
При получении значения из map можно использовать второй возвращаемый аргумент, который будет булевым значением, указывающим, существует ли ключ:
value, exists := m["key"]
📌Является ли map потокобезопасным типом данных?
Нет, map не является потокобезопасным, и для доступа к нему из нескольких горутин одновременно может потребоваться синхронизация, например, с помощью sync.Mutex.
🔥6👍3
💬Как работает управление памятью в Go?
📌Go использует сборщик мусора для автоматического управления памятью. Разработчику не нужно явно выделять и освобождать память, как в языках типа C или C++. Однако нужно быть внимательным при работе с большими структурами данных, чтобы избежать утечек памяти.
📌Некоторые ключевые аспекты управления памятью в Go:
🔸Go применяет алгоритм сборки мусора с маркировкой и освобождением. Сборщик мусора отмечает активные объекты, после чего освобождает память от неактивных.
🔸В Go можно работать с указателями, но нет прямого управления выделением и освобождением памяти через них. Память выделяется при создании объектов и автоматически освобождается сборщиком мусора.
🔸Хотя Go управляет памятью автоматически, неправильное использование, например, из-за циклических ссылок, может вызвать утечки памяти. Поэтому важно контролировать использование ресурсов.
🔸Срезы в Go — это динамические массивы, обеспечивающие автоматическое управление памятью при изменении их размера.
🔸Go разделяет память на стек и кучу. Стек — для локальных переменных и контекста функций; каждый поток имеет свой стек. Куча — для долгоживущих объектов и данных, которые могут быть доступны из разных частей программы. Управление памятью в куче осуществляется сборщиком мусора.
🔸Escape analysis в Go определяет, следует ли объекту быть на стеке или в куче, опираясь на его использование в программе. Этот анализ помогает оптимизировать управление памятью, делая его более эффективным.
📌Go использует сборщик мусора для автоматического управления памятью. Разработчику не нужно явно выделять и освобождать память, как в языках типа C или C++. Однако нужно быть внимательным при работе с большими структурами данных, чтобы избежать утечек памяти.
📌Некоторые ключевые аспекты управления памятью в Go:
🔸Go применяет алгоритм сборки мусора с маркировкой и освобождением. Сборщик мусора отмечает активные объекты, после чего освобождает память от неактивных.
🔸В Go можно работать с указателями, но нет прямого управления выделением и освобождением памяти через них. Память выделяется при создании объектов и автоматически освобождается сборщиком мусора.
🔸Хотя Go управляет памятью автоматически, неправильное использование, например, из-за циклических ссылок, может вызвать утечки памяти. Поэтому важно контролировать использование ресурсов.
🔸Срезы в Go — это динамические массивы, обеспечивающие автоматическое управление памятью при изменении их размера.
🔸Go разделяет память на стек и кучу. Стек — для локальных переменных и контекста функций; каждый поток имеет свой стек. Куча — для долгоживущих объектов и данных, которые могут быть доступны из разных частей программы. Управление памятью в куче осуществляется сборщиком мусора.
🔸Escape analysis в Go определяет, следует ли объекту быть на стеке или в куче, опираясь на его использование в программе. Этот анализ помогает оптимизировать управление памятью, делая его более эффективным.
👍15🔥1
💬Что такое интерфейсы в Go и как они используется?
Интерфейсы в Go предоставляют способ указания поведения объекта.
1⃣ Определение интерфейса
Интерфейс в Go представляет собой набор методов, для которых не указаны конкретные реализации:
2⃣ Реализация интерфейса
Если определенный тип предоставляет методы, соответствующие всем методам интерфейса, считается, что этот тип реализует данный интерфейс. В Go не требуется явно указывать, что тип реализует интерфейс — это определяется неявно.
3⃣ Пустой интерфейс
Интерфейс без методов называется пустым и записывается как interface{}. Любой тип удовлетворяет пустому интерфейсу, что делает его полезным для создания универсальных функций и структур.
4⃣ Встраивание интерфейсов
Можно комбинировать несколько интерфейсов, встраивая один интерфейс в другой:
5⃣ Интерфейсы и методы со значениями и указателями
Методы, определенные с получателем-указателем, могут быть частью интерфейса только если используется указатель на тип. Это важно учитывать при проектировании интерфейсов.
6⃣ Использование интерфейсов
Интерфейсы позволяют задавать требования к поведению типов, обеспечивая полиморфное поведение. Таким образом, функции могут принимать параметры интерфейсного типа, что дает большую гибкость при работе с различными типами.
7⃣ Type assertion и type switch
При работе с интерфейсами иногда требуется приведение типов или определение конкретного типа значения интерфейса. Для этих задач используются операции type assertion и type switch.
8⃣ Значение интерфейса в Go состоит из двух компонентов: указателя на конкретное значение и указателя на таблицу методов этого типа.
9⃣
Простой пример интерфейса на Go представлен ниже👇
Допустим, мы хотим определить интерфейс для геометрических фигур, которые могут вычислять свою площадь.
• У нас есть интерфейс Shape с методом Area.
• Circle и Square — две структуры, которые реализуют этот интерфейс.
• В функции main мы создаем экземпляры Circle и Square, добавляем их в срез shapes типа Shape, а затем итерируемся по этому срезу, выводя площадь каждой фигуры.
Этот пример показывает силу интерфейсов в Go: они предоставляют общий способ работы с разными типами, имеющими общий функционал.
Интерфейсы в Go предоставляют способ указания поведения объекта.
1⃣ Определение интерфейса
Интерфейс в Go представляет собой набор методов, для которых не указаны конкретные реализации:
type Writer interface {
Write([]byte) (int, error)
}
2⃣ Реализация интерфейса
Если определенный тип предоставляет методы, соответствующие всем методам интерфейса, считается, что этот тип реализует данный интерфейс. В Go не требуется явно указывать, что тип реализует интерфейс — это определяется неявно.
3⃣ Пустой интерфейс
Интерфейс без методов называется пустым и записывается как interface{}. Любой тип удовлетворяет пустому интерфейсу, что делает его полезным для создания универсальных функций и структур.
4⃣ Встраивание интерфейсов
Можно комбинировать несколько интерфейсов, встраивая один интерфейс в другой:
type ReadWriter interface {
Reader
Writer
}
5⃣ Интерфейсы и методы со значениями и указателями
Методы, определенные с получателем-указателем, могут быть частью интерфейса только если используется указатель на тип. Это важно учитывать при проектировании интерфейсов.
6⃣ Использование интерфейсов
Интерфейсы позволяют задавать требования к поведению типов, обеспечивая полиморфное поведение. Таким образом, функции могут принимать параметры интерфейсного типа, что дает большую гибкость при работе с различными типами.
7⃣ Type assertion и type switch
При работе с интерфейсами иногда требуется приведение типов или определение конкретного типа значения интерфейса. Для этих задач используются операции type assertion и type switch.
8⃣ Значение интерфейса в Go состоит из двух компонентов: указателя на конкретное значение и указателя на таблицу методов этого типа.
9⃣
nil
может быть допустимым значением интерфейса. Если интерфейс содержит nil
и на нем вызывается метод, это вызовет ошибку времени выполнения.Простой пример интерфейса на Go представлен ниже👇
Допустим, мы хотим определить интерфейс для геометрических фигур, которые могут вычислять свою площадь.
• У нас есть интерфейс Shape с методом Area.
• Circle и Square — две структуры, которые реализуют этот интерфейс.
• В функции main мы создаем экземпляры Circle и Square, добавляем их в срез shapes типа Shape, а затем итерируемся по этому срезу, выводя площадь каждой фигуры.
Этот пример показывает силу интерфейсов в Go: они предоставляют общий способ работы с разными типами, имеющими общий функционал.
❤8👍3
💬Что такое zero value в Go?
◾В Go каждый тип имеет zero value — это значение по умолчанию для типа, которое он получает при инициализации, если явно не задано другое значение.
◾Для чисел это 0, для строк — пустая строка "", для булевых значений – false, для указателей, функций и интерфейсов — nil.
◾В Go каждый тип имеет zero value — это значение по умолчанию для типа, которое он получает при инициализации, если явно не задано другое значение.
◾Для чисел это 0, для строк — пустая строка "", для булевых значений – false, для указателей, функций и интерфейсов — nil.
❤8
💬Что делает ключевое слово defer в Go?
📌defer в Go — ключевое слово, которое используется для отложенного выполнения функции или метода до тех пор, пока текущая функция не завершится. Когда встречается defer, Go добавляет вызов функции или метода в стек отложенных вызовов, а затем продолжает выполнение текущей функции.
🔸Закрытие ресурсов. Один из самых распространенных примеров использования defer — убедиться, что ресурсы, такие как файлы, сетевые подключения или соединения с базой данных, будут закрыты после их использования.
🔸Множественные отложенные вызовы: мы можем использовать несколько операторов defer в одной функции. Они будут выполнены в порядке LIFO.
🔸Передача аргументов: аргументы функции, вызываемой с помощью defer, вычисляются в момент вызова defer, а не в момент выполнения отложенной функции.
🔸Использование с паникой: defer часто используется совместно с recover(), чтобы обрабатывать или логировать панику, которая может произойти в функции.
🔸Зависимость от контекста: отложенные функции имеют доступ к локальным переменным и могут изменять их значения, что делает defer мощным инструментом для выполнения последних действий с переменными перед выходом из функции.
🔸Затраты производительности: хотя ключевое слово defer удобно и безопасно, использование его внутри интенсивных по производительности циклов может вызвать незначительные, но всё же заметные накладные расходы.
📌defer в Go — ключевое слово, которое используется для отложенного выполнения функции или метода до тех пор, пока текущая функция не завершится. Когда встречается defer, Go добавляет вызов функции или метода в стек отложенных вызовов, а затем продолжает выполнение текущей функции.
🔸Закрытие ресурсов. Один из самых распространенных примеров использования defer — убедиться, что ресурсы, такие как файлы, сетевые подключения или соединения с базой данных, будут закрыты после их использования.
file, err := os.Open("file.txt")
if err != nil {
//обработка ошибки
}
defer file.Close()
🔸Множественные отложенные вызовы: мы можем использовать несколько операторов defer в одной функции. Они будут выполнены в порядке LIFO.
func example() {
defer fmt.Println("1")
defer fmt.Println("2")
fmt.Println("Function body")
}
🔸Передача аргументов: аргументы функции, вызываемой с помощью defer, вычисляются в момент вызова defer, а не в момент выполнения отложенной функции.
func example(a int) {
defer fmt.Println(a)
a *= 2
return
}
example(5) //5
🔸Использование с паникой: defer часто используется совместно с recover(), чтобы обрабатывать или логировать панику, которая может произойти в функции.
func mightPanic() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()
//код, который может вызвать панику
}
🔸Зависимость от контекста: отложенные функции имеют доступ к локальным переменным и могут изменять их значения, что делает defer мощным инструментом для выполнения последних действий с переменными перед выходом из функции.
🔸Затраты производительности: хотя ключевое слово defer удобно и безопасно, использование его внутри интенсивных по производительности циклов может вызвать незначительные, но всё же заметные накладные расходы.
👍6❤2
💬Массивы и срезы в Go: продолжение
❗️Важно иметь представление о том, как работают массивы и срезы в Go, так как это позволяет эффективно управлять памятью и предотвращать ошибки.
🔸Внутренняя структура и работа
• Массивы в Go имеют фиксированный размер. Элементы массива располагаются последовательно в памяти. Размер массива является его частью, так что массивы
• Срезы — более гибкая структура данных, которая представляют последовательность элементов одного типа переменной длины. Срез состоит из трех компонентов: указателя на начало среза в массиве, длины среза и capacity, которая показывает, сколько элементов может содержать срез от начальной позиции до конца массива.
🔸Операции над массивами и их сложность
• Вставка или удаление элемента в массиве Go требует создания нового массива, так как размер массива неизменен. Однако для срезов можно использовать встроенные функции append и copy для добавления и удаления элементов.
🔸Срезы
• Когда создается срез, он указывает на определенный раздел исходного массива. Модификация элементов среза приведет к модификации соответствующих элементов исходного массива и наоборот.
• Изменение размера среза с помощью append может привести к созданию нового массива.
🔸Проблемы и оптимизации
• Фрагментация памяти: так как срезы могут указывать на разные участки массива, неиспользуемые части массива могут оставаться в памяти до тех пор, пока срез не будет удален сборщиком мусора.
🔸Многомерные массивы
• В Go можно создать многомерные массивы, например,
🔸Особенности в Go
• Важно помнить, что когда срез передается в функцию, он передается по ссылке, а не копируется. Таким образом, изменения, внесенные в срез внутри функции, отразятся на исходном срезе. Для массивов этого не происходит, так как они передаются по значению.
• Функция
❗️Важно иметь представление о том, как работают массивы и срезы в Go, так как это позволяет эффективно управлять памятью и предотвращать ошибки.
🔸Внутренняя структура и работа
• Массивы в Go имеют фиксированный размер. Элементы массива располагаются последовательно в памяти. Размер массива является его частью, так что массивы
[3]int
и [4]int
— это разные типы.• Срезы — более гибкая структура данных, которая представляют последовательность элементов одного типа переменной длины. Срез состоит из трех компонентов: указателя на начало среза в массиве, длины среза и capacity, которая показывает, сколько элементов может содержать срез от начальной позиции до конца массива.
🔸Операции над массивами и их сложность
• Вставка или удаление элемента в массиве Go требует создания нового массива, так как размер массива неизменен. Однако для срезов можно использовать встроенные функции append и copy для добавления и удаления элементов.
🔸Срезы
• Когда создается срез, он указывает на определенный раздел исходного массива. Модификация элементов среза приведет к модификации соответствующих элементов исходного массива и наоборот.
• Изменение размера среза с помощью append может привести к созданию нового массива.
🔸Проблемы и оптимизации
• Фрагментация памяти: так как срезы могут указывать на разные участки массива, неиспользуемые части массива могут оставаться в памяти до тех пор, пока срез не будет удален сборщиком мусора.
🔸Многомерные массивы
• В Go можно создать многомерные массивы, например,
var matrix [3][3]int
. Точно так же можно работать с многомерными срезами.🔸Особенности в Go
• Важно помнить, что когда срез передается в функцию, он передается по ссылке, а не копируется. Таким образом, изменения, внесенные в срез внутри функции, отразятся на исходном срезе. Для массивов этого не происходит, так как они передаются по значению.
• Функция
make
часто используется для создания срезов с заданной начальной длиной и capacity.Telegram
Библиотека Go для собеса | вопросы с собеседований
💬 В чем отличие между массивами и срезами в Go?
📌Массив в Go представляет собой структуру данных, состоящую из упорядоченной последовательности элементов, емкость которой определяется в момент создания. После определения размера массива его нельзя изменить.…
📌Массив в Go представляет собой структуру данных, состоящую из упорядоченной последовательности элементов, емкость которой определяется в момент создания. После определения размера массива его нельзя изменить.…
❤3👍1
💬Чем отличается make от new в Go?
Хотя Go предоставляет сборщик мусора, который автоматически очищает неиспользуемую память, нам все равно требуется выделять память для новых переменных. Go предоставляет две встроенные функции для этой цели: new и make.
🔸Функция new(T) выделяет память для новой переменной типа T, инициализирует её нулевым значением этого типа и возвращает адрес этой переменной (т. е. указатель на T). Например:
• В этом примере p будет указателем на новый int, который был инициализирован значением 0.
🔸Функция make используется для инициализации срезов, типа map и каналов, которые являются встроенными типами данных в Go, требующими дополнительной инициализации перед использованием.
• Срезы: когда мы создаем срез с помощью make, мы задаем начальную емкость и размер. Это позволяет Go предварительно выделить необходимую память.
• map: используя make, мы можем инициализировать тип map перед добавлением элементов.
• Каналы: make также используется для создания каналов.
📌Основное различие:
• new возвращает указатель на тип, инициализированный нулевым значением этого типа.
• make возвращает инициализированный (не нулевой) экземпляр встроенного типа (срез, map или канал).
• В большинстве случаев, когда разработчикам требуется выделить память для новой переменной, они используют литералы типа или make.
• Функция new используется реже, и чаще всего она применяется в более специфичных случаях.
Хотя Go предоставляет сборщик мусора, который автоматически очищает неиспользуемую память, нам все равно требуется выделять память для новых переменных. Go предоставляет две встроенные функции для этой цели: new и make.
🔸Функция new(T) выделяет память для новой переменной типа T, инициализирует её нулевым значением этого типа и возвращает адрес этой переменной (т. е. указатель на T). Например:
p := new(int)
// p — это указатель на int, инициализированный нулем• В этом примере p будет указателем на новый int, который был инициализирован значением 0.
🔸Функция make используется для инициализации срезов, типа map и каналов, которые являются встроенными типами данных в Go, требующими дополнительной инициализации перед использованием.
• Срезы: когда мы создаем срез с помощью make, мы задаем начальную емкость и размер. Это позволяет Go предварительно выделить необходимую память.
s := make([]int, 5, 10)
• map: используя make, мы можем инициализировать тип map перед добавлением элементов.
m := make(map[string]int)
• Каналы: make также используется для создания каналов.
ch := make(chan int)
📌Основное различие:
• new возвращает указатель на тип, инициализированный нулевым значением этого типа.
• make возвращает инициализированный (не нулевой) экземпляр встроенного типа (срез, map или канал).
• В большинстве случаев, когда разработчикам требуется выделить память для новой переменной, они используют литералы типа или make.
• Функция new используется реже, и чаще всего она применяется в более специфичных случаях.
👍15
💬Что такое embedding в Go?
📌В Go нет наследования в традиционном смысле ООП, но есть механизм, который позволяет добиться похожего эффекта — это embedding. Embedding позволяет одному типу включать в себя поля и методы другого типа без явного наследования.
◾Простота: embedding очень прост в использовании — просто определите один тип внутри другого.
◾Композиция вместо наследования: вместо того чтобы наследовать методы и поля, Go предпочитает композицию, где один тип может включать в себя другой, дополняя его функциональностью.
◾Поведение и интерфейсы: если встроенный тип реализует определенный интерфейс, то и тип, в который он встроен, автоматически реализует этот интерфейс.
📌Рассмотрим на примере:
📌Пример со встроенными методами:
👉 Теперь
📌Важно:
🔸Имена полей и конфликты: если встроенный и внешний типы имеют поля или методы с одинаковыми именами, приоритет будет у внешнего типа.
🔸Неявное поведение: одним из возможных «подводных камней» является то, что методы встроенного типа становятся частью внешнего типа, что может быть не всегда очевидным при чтении кода.
🔸Интерфейсы и встраивание: в Go можно также встраивать интерфейсы, что позволяет создавать сложные интерфейсы на основе уже существующих.
📌В Go нет наследования в традиционном смысле ООП, но есть механизм, который позволяет добиться похожего эффекта — это embedding. Embedding позволяет одному типу включать в себя поля и методы другого типа без явного наследования.
◾Простота: embedding очень прост в использовании — просто определите один тип внутри другого.
◾Композиция вместо наследования: вместо того чтобы наследовать методы и поля, Go предпочитает композицию, где один тип может включать в себя другой, дополняя его функциональностью.
◾Поведение и интерфейсы: если встроенный тип реализует определенный интерфейс, то и тип, в который он встроен, автоматически реализует этот интерфейс.
📌Рассмотрим на примере:
type Engine struct {
Power int
Type string
}
type Car struct {
Engine
Brand string
Model string
}
func main() {
c := Car{
Engine: Engine{Power: 150, Type: "Petrol"},
Brand: "Ford",
Model: "Fiesta",
}
fmt.Println(c.Power)
}
📌Пример со встроенными методами:
type Writer interface {
Write([]byte) (int, error)
}
type Logger struct {
Writer
}
👉 Теперь
Logger
автоматически реализует интерфейс Writer
, но только в том случае, если его встроенное поле Writer
также реализует методы этого интерфейса. 📌Важно:
🔸Имена полей и конфликты: если встроенный и внешний типы имеют поля или методы с одинаковыми именами, приоритет будет у внешнего типа.
🔸Неявное поведение: одним из возможных «подводных камней» является то, что методы встроенного типа становятся частью внешнего типа, что может быть не всегда очевидным при чтении кода.
🔸Интерфейсы и встраивание: в Go можно также встраивать интерфейсы, что позволяет создавать сложные интерфейсы на основе уже существующих.
👍8❤1
💬Какие изменения в Go 1.21 коснулись срезов?
🔥 В Go 1.21 в стандартную библиотеку добавили пакет slices, который значительно облегчил работу со срезами. Вот только некоторые новые функции:
🔸Поиск элемента в срезе:
🔸Сортировка (для любых ordered типов):
🔸Поиск максимума:
🔸Удаление элемента:
🔸Сравнение двух срезов:
🔸Compact — заменяет последовательные серии одинаковых элементов одной копией — как команда uniq в *nix.
🔸Equal — проверяет, равны ли два среза: одинаковая длина и все элементы равны.
🔸Clip — удаляет неиспользуемую емкость из среза, возвращая
🔸Clone — возвращает копию среза.
🔸BinarySearch — ищет элемент в отсортированном срезе и возвращает позицию, где элемент найден, или позицию, где он появится в порядке сортировки.
Подробнее:
🔗 Release notes & pkg.go.dev & Ardan Labs
🔸Поиск элемента в срезе:
slices.Contains(s, v)
🔸Сортировка (для любых ordered типов):
slices.Sort(s)
🔸Поиск максимума:
maxVal := slices.Max(s)
🔸Удаление элемента:
letters := []string{"a", "b", "c", "d", "e"}
letters = slices.Delete(letters, 1, 4)
🔸Сравнение двух срезов:
areEqual := slices.Compare(a, b)
🔸Compact — заменяет последовательные серии одинаковых элементов одной копией — как команда uniq в *nix.
seq := []int{0, 1, 1, 2, 3, 5, 8}
seq = slices.Compact(seq)
🔸Equal — проверяет, равны ли два среза: одинаковая длина и все элементы равны.
numbers := []int{0, 42, 8}
fmt.Println(slices.Equal(numbers, []int{0, 42, 8}))
// true🔸Clip — удаляет неиспользуемую емкость из среза, возвращая
s[:len(s):len(s)]
.list = slices.Clip(list)
🔸Clone — возвращает копию среза.
🔸BinarySearch — ищет элемент в отсортированном срезе и возвращает позицию, где элемент найден, или позицию, где он появится в порядке сортировки.
names := []string{"Alice", "Bob", "Vera"}
n, found := slices.BinarySearch(names, "Vera")
Подробнее:
🔗 Release notes & pkg.go.dev & Ardan Labs
Please open Telegram to view this post
VIEW IN TELEGRAM
tip.golang.org
Go 1.21 Release Notes - The Go Programming Language
👍32
💬Какие основные отличия горутины от thread (потока) в Go
1⃣Уровень абстракции:
• Горутины — это абстракции уровня языка, предоставляемые Go. Они позволяют выполнять функции или методы конкурентно.
• Потоки — это более традиционные сущности операционной системы для параллельного выполнения задач.
2⃣Размер стека:
• Горутины начинаются с очень маленького стека, который может динамически расти и сокращаться в зависимости от потребности (обычно начинается с 2KB).
• Потоки, в зависимости от ОС, обычно имеют стек фиксированного размера, который может быть значительно больше (обычно от 1MB и выше).
3⃣Создание и переключение:
• Горутины легко создать (просто используя ключевое слово go перед вызовом функции), и они дешевы в плане создания и переключения контекста.
• Потоки дороже по стоимости создания и контекстного переключения, так как это требует прямого взаимодействия с операционной системой.
4⃣Планировщик:
• Горутины управляются планировщиком Go, который работает в пользовательском пространстве (user space) и распределяет горутины по доступным ОС потокам (обычно один поток на ядро CPU).
• Потоки управляются планировщиком ОС.
5⃣Изоляция:
• Ошибка в одной горутине (например, паника) может повлиять на все другие горутины в той же программе.
👀 Выше представлен пример создания горутины и потока в Go. Мы создаем горутину с помощью ключевого слова. Затем, меняя GOMAXPROCS, мы фактически заставляем Go использовать дополнительный поток ОС, что делает выполнение кода более похожим на многопоточное.
1⃣Уровень абстракции:
• Горутины — это абстракции уровня языка, предоставляемые Go. Они позволяют выполнять функции или методы конкурентно.
• Потоки — это более традиционные сущности операционной системы для параллельного выполнения задач.
2⃣Размер стека:
• Горутины начинаются с очень маленького стека, который может динамически расти и сокращаться в зависимости от потребности (обычно начинается с 2KB).
• Потоки, в зависимости от ОС, обычно имеют стек фиксированного размера, который может быть значительно больше (обычно от 1MB и выше).
3⃣Создание и переключение:
• Горутины легко создать (просто используя ключевое слово go перед вызовом функции), и они дешевы в плане создания и переключения контекста.
• Потоки дороже по стоимости создания и контекстного переключения, так как это требует прямого взаимодействия с операционной системой.
4⃣Планировщик:
• Горутины управляются планировщиком Go, который работает в пользовательском пространстве (user space) и распределяет горутины по доступным ОС потокам (обычно один поток на ядро CPU).
• Потоки управляются планировщиком ОС.
5⃣Изоляция:
• Ошибка в одной горутине (например, паника) может повлиять на все другие горутины в той же программе.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤14👍6🔥1
💬Что из себя представляет паттерн «Одиночка» (Singleton) и как его реализовать на Go
Одиночка — это порождающий паттерн, который гарантирует существование только одного объекта определённого класса/типа и предоставляет к нему глобальную точку доступа.
В первом примере для гарантии создания одного экземпляра структуры используются мьютексы (sync.Mutex). Мьютекс блокирует выполнение кода так, чтобы только одна горутина могла его выполнять в данный момент времени.
📌Важно:
• В начале нужна nil-проверка, с ее помощью мы убеждаемся, что первый экземпляр — пустой. Благодаря этому мы можем избежать ресурсоемких операций блокировки при каждом вызове
• Структура
• После блокировки используется еще одна
📌Существуют и другие методы создания экземпляра одиночки в Go.
1⃣Функция init: Go предоставляет функцию init, которая вызывается автоматически при инициализации пакета. Мы можем использовать эту функцию для создания экземпляра одиночки.
2⃣sync.Once: Этот метод гарантирует, что функция будет выполнена только один раз, даже если она будет вызвана из нескольких горутин.
Одиночка — это порождающий паттерн, который гарантирует существование только одного объекта определённого класса/типа и предоставляет к нему глобальную точку доступа.
В первом примере для гарантии создания одного экземпляра структуры используются мьютексы (sync.Mutex). Мьютекс блокирует выполнение кода так, чтобы только одна горутина могла его выполнять в данный момент времени.
📌Важно:
• В начале нужна nil-проверка, с ее помощью мы убеждаемся, что первый экземпляр — пустой. Благодаря этому мы можем избежать ресурсоемких операций блокировки при каждом вызове
obtainUnique
. Если эта проверка не пройдена, тогда поле uniqueItem
уже заполнено.• Структура
uniqueItem
создается внутри блокировки.• После блокировки используется еще одна
nil
-проверка. В случаях, когда первую проверку проходит более одного потока, вторая обеспечивает создание экземпляра одиночки единым потоком. В противном случае, все потоки создавали бы свои экземпляры структуры одиночки.📌Существуют и другие методы создания экземпляра одиночки в Go.
1⃣Функция init: Go предоставляет функцию init, которая вызывается автоматически при инициализации пакета. Мы можем использовать эту функцию для создания экземпляра одиночки.
2⃣sync.Once: Этот метод гарантирует, что функция будет выполнена только один раз, даже если она будет вызвана из нескольких горутин.
👍12🔥5
💬Что такое контекст (context) в Go и для чего он применяется?
Context в Go — это специальный пакет, предназначенный для передачи параметров между API и управления жизненным циклом горутин.
Основное его назначение — передача метаданных, установка временных рамок выполнения и отслеживание отмены долгосрочных операций.
📌Основные моменты:
🔹Context введен в Go 1.7 и с тех пор является предпочтительным механизмом для управления временем выполнения и отменами.
🔹Context Interface: интерфейс
🔹Основные методы: WithCancel, WithDeadline, WithTimeout и WithValue.
• WithCancel — возвращает копию переданного контекста и cancelFunc. Вызов cancelFunc отменяет этот контекст.
• WithDeadline & WithTimeout — позволяют задать временные рамки контексту.
• WithValue — позволяет передать произвольные пары ключ/значение в context.
🔹Отмена родительского context автоматически отменяет все дочерние.
🔹Context используется для уведомления о том, что пора завершать работу, — это особенно удобно через канал
🔹Возвращаемая функция
🔹Не храните в context чувствительные данные: контекст может быть выведен и сохранен в логах, что может раскрыть чувствительные данные.
🔹При отмене context можно узнать причину через
📌Пример:
📌Важно:
•
• Когда context отменяется (через cancel, timeout или deadline),
• Context следует передавать первым аргументом в функцию:
Context в Go — это специальный пакет, предназначенный для передачи параметров между API и управления жизненным циклом горутин.
Основное его назначение — передача метаданных, установка временных рамок выполнения и отслеживание отмены долгосрочных операций.
📌Основные моменты:
🔹Context введен в Go 1.7 и с тех пор является предпочтительным механизмом для управления временем выполнения и отменами.
🔹Context Interface: интерфейс
context.Context
является основным типом, который вы передаете между функциями.🔹Основные методы: WithCancel, WithDeadline, WithTimeout и WithValue.
• WithCancel — возвращает копию переданного контекста и cancelFunc. Вызов cancelFunc отменяет этот контекст.
• WithDeadline & WithTimeout — позволяют задать временные рамки контексту.
• WithValue — позволяет передать произвольные пары ключ/значение в context.
🔹Отмена родительского context автоматически отменяет все дочерние.
🔹Context используется для уведомления о том, что пора завершать работу, — это особенно удобно через канал
ctx.Done()
.🔹Возвращаемая функция
cancel
позволяет рано завершить context.🔹Не храните в context чувствительные данные: контекст может быть выведен и сохранен в логах, что может раскрыть чувствительные данные.
🔹При отмене context можно узнать причину через
ctx.Err()
, где возможные значения — context.Canceled
или context.DeadlineExceeded
.📌Пример:
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // освобождаем ресурсы
go someOperation(ctx)
if ctx.Err() == context.Canceled {
fmt.Println("Operation was canceled")
}
📌Важно:
•
context.Background()
и context.TODO()
— одно и то же. Разница лишь в том, что context.TODO()
выставляется в местах, где пока нет понимания, что необходимо использовать context.Background()
и возможно его надо заменить на дочерний context.• Когда context отменяется (через cancel, timeout или deadline),
ctx.Done()
возвращает закрытый канал. Это удобный механизм для оповещения горутин о том, что пора завершать работу.• Context следует передавать первым аргументом в функцию:
func DoSomething(ctx context.Context, arg Arg) error
👍16🔥5
💬Что из себя представляет паттерн «Функциональные опции» в Go и как его реализовать?
▪️Паттерн «Функциональные опции» в Go предлагает гибкий способ настройки структур без раскрытия внутренних полей.
▪️Он особенно полезен, если вы хотите предоставить дополнительную конфигурацию, или в будущем могут появиться дополнительные параметры конфигурации. Он также отлично подходит для библиотек.
📌Базовый пример без функциональных опций:
▪️Со временем наши требования меняются, и нам может понадобиться поддерживать больше вариантов конфигурации. Вместо изменения сигнатуры функции
👉 Сначала мы определяем функциональную опцию:
👉 и функцию, удовлетворяющую типу:
👉 изменяем нашу функцию NewServer:
👉 и теперь мы можем делать вот так:
📌Этот паттерн позволяет нам гибко настраивать параметры, сохраняя при этом читабельность и не раскрывая внутренние поля.
▪️Паттерн «Функциональные опции» в Go предлагает гибкий способ настройки структур без раскрытия внутренних полей.
▪️Он особенно полезен, если вы хотите предоставить дополнительную конфигурацию, или в будущем могут появиться дополнительные параметры конфигурации. Он также отлично подходит для библиотек.
📌Базовый пример без функциональных опций:
type Server struct {
host string
port int
protocol string
}
func NewServer(host string, port int) *Server {
return &Server{
host: host,
port: port,
protocol: "http",
}
}
▪️Со временем наши требования меняются, и нам может понадобиться поддерживать больше вариантов конфигурации. Вместо изменения сигнатуры функции
NewServer
, что может быть проблематично и несовместимо с предыдущими версиями, мы можем использовать функциональные опции.👉 Сначала мы определяем функциональную опцию:
type ServerOption func(*Server)
👉 и функцию, удовлетворяющую типу:
func WithPort (port int) ServerOption {
return func(s *Server) {
s.port = port
}
👉 изменяем нашу функцию NewServer:
func NewServer(host string, opts ...ServerOption) *Server {
server : = &Server{
host: host,
port: 443,
protocol: "https"
}
for _, opt : = range opts {
opt (server)
}
return server
}
👉 и теперь мы можем делать вот так:
server1 := NewServer("localhost")
server2 := NewServer("localhost", WithPort(8080))
📌Этот паттерн позволяет нам гибко настраивать параметры, сохраняя при этом читабельность и не раскрывая внутренние поля.
👍37
💬Для чего предназначен и как работает планировщик Go (Go Scheduler) на базовом уровне?
📌Планировщик Go отвечает за управление горутинами и переключение контекста между ними, обеспечивая эффективное использование ресурсов процессора.
🔸M:N планирование: Go использует модель M:N для планирования, где M горутин могут быть запланированы на N потоках ОС. Это позволяет Go создавать тысячи горутин, минимизируя накладные расходы, связанные с созданием и управлением реальными потоками ОС.
🔸G, M и P:
• G (goroutine) — горутина
• M (machine thread) — поток ОС, на котором выполняются горутины
• P (processor) — контекст выполнения, который содержит очередь горутин и другую необходимую информацию. Число P обычно равно числу ядер процессора
🔸Работа планировщика: когда горутина начинает выполнение, она захватывает P и привязывается к M. Если горутина блокируется (например, ожидает ввода/вывода), то P может быть отсоединен и присоединен к другой горутине, которая готова к выполнению.
🔸Рост и уменьшение: планировщик динамически увеличивает или уменьшает количество потоков ОС (M), в зависимости от нагрузки и блокировок.
🔸Взаимодействие с планировщиком: горутины взаимодействуют с планировщиком Go. Они могут быть прерваны, чтобы освободить поток ОС для других горутин. Этот механизм прерывания позволяет гарантировать, что одна горутина не занимает поток ОС слишком долго, предоставляя возможность другим горутинам получить время на выполнение.
📌Планировщик Go отвечает за управление горутинами и переключение контекста между ними, обеспечивая эффективное использование ресурсов процессора.
🔸M:N планирование: Go использует модель M:N для планирования, где M горутин могут быть запланированы на N потоках ОС. Это позволяет Go создавать тысячи горутин, минимизируя накладные расходы, связанные с созданием и управлением реальными потоками ОС.
🔸G, M и P:
• G (goroutine) — горутина
• M (machine thread) — поток ОС, на котором выполняются горутины
• P (processor) — контекст выполнения, который содержит очередь горутин и другую необходимую информацию. Число P обычно равно числу ядер процессора
🔸Работа планировщика: когда горутина начинает выполнение, она захватывает P и привязывается к M. Если горутина блокируется (например, ожидает ввода/вывода), то P может быть отсоединен и присоединен к другой горутине, которая готова к выполнению.
🔸Рост и уменьшение: планировщик динамически увеличивает или уменьшает количество потоков ОС (M), в зависимости от нагрузки и блокировок.
🔸Взаимодействие с планировщиком: горутины взаимодействуют с планировщиком Go. Они могут быть прерваны, чтобы освободить поток ОС для других горутин. Этот механизм прерывания позволяет гарантировать, что одна горутина не занимает поток ОС слишком долго, предоставляя возможность другим горутинам получить время на выполнение.
👍19
💬Что из себя представляют дженерики в Go и для чего они нужны?
▪️Поддержка дженериков была добавлена в Go 1.18. Это своего рода механизм, который предоставляет инструмент для создания функций и структур данных, способных взаимодействовать с множеством типов, не требуя их явного указания при объявлении.
▪️Преимуществом использования дженериков является то, что они позволяют уменьшить дублирование кода, предоставляя универсальные решения для общих задач, и улучшают производительность, избегая необходимости в преобразованиях типов.
▪️До введения дженериков разработчики использовали интерфейсы и преобразование типов для достижения подобной гибкости, что могло приводить к потере производительности и безопасности типов.
▪️Дженерики позволяют писать более чистый, эффективный и безопасный код, минимизируя повторение кода и предоставляя строгую проверку типов. Они делают Go более универсальным и конкурентоспособным по сравнению с другими языками программирования, в которых дженерики давно были стандартом.
📌А теперь немного кода:
👉 Без дженериков
• Раньше, если вы хотели создать функцию для поиска минимума в срезе, вам пришлось бы создавать отдельные функции для каждого типа или использовать пустой интерфейс
👉 С дженериками
• С помощью дженериков, мы можем создать одну функцию, которая работает с различными типами:
• Здесь
🔸Обобщенные структуры данных
• Допустим, вы хотите создать структуру данных типа очередь (Queue).
👉 Без дженериков
• Использование
👉 С дженериками
• Здесь T может быть любым типом, и нет необходимости в преобразовании типов.
🤔В общем и целом, большинство разработчиков приветствует введение дженериков в Go, но при этом подчеркивают важность их правильного и обдуманного применения.
▪️Поддержка дженериков была добавлена в Go 1.18. Это своего рода механизм, который предоставляет инструмент для создания функций и структур данных, способных взаимодействовать с множеством типов, не требуя их явного указания при объявлении.
▪️Преимуществом использования дженериков является то, что они позволяют уменьшить дублирование кода, предоставляя универсальные решения для общих задач, и улучшают производительность, избегая необходимости в преобразованиях типов.
▪️До введения дженериков разработчики использовали интерфейсы и преобразование типов для достижения подобной гибкости, что могло приводить к потере производительности и безопасности типов.
▪️Дженерики позволяют писать более чистый, эффективный и безопасный код, минимизируя повторение кода и предоставляя строгую проверку типов. Они делают Go более универсальным и конкурентоспособным по сравнению с другими языками программирования, в которых дженерики давно были стандартом.
📌А теперь немного кода:
👉 Без дженериков
• Раньше, если вы хотели создать функцию для поиска минимума в срезе, вам пришлось бы создавать отдельные функции для каждого типа или использовать пустой интерфейс
interface{}
.func MinInts(arr []int) int { /*...*/ }
func MinFloats(arr []float64) float64 { /*...*/ }
👉 С дженериками
• С помощью дженериков, мы можем создать одну функцию, которая работает с различными типами:
func Min[T comparable](arr []T) T { /*...*/ }
• Здесь
T
является параметром типа, и comparable означает, что функция работает с любым типом, который можно сравнить.🔸Обобщенные структуры данных
• Допустим, вы хотите создать структуру данных типа очередь (Queue).
👉 Без дженериков
type Queue struct {
data []interface{}
}
• Использование
interface{}
снижает производительность из-за преобразования типов.👉 С дженериками
type Queue[T any] struct {
data []T
}
• Здесь T может быть любым типом, и нет необходимости в преобразовании типов.
🤔В общем и целом, большинство разработчиков приветствует введение дженериков в Go, но при этом подчеркивают важность их правильного и обдуманного применения.
👍13🔥1
💬Что такое и для чего предназначен Escape analysis в Go?
▫️Escape analysis — это техника компилятора Go, которая позволяет определить, где лучше всего размещать переменные: на стеке или в куче. Этот анализ важен для оптимизации производительности и эффективного использования памяти.
▫️В Go нет ни одного ключевого слова или функции, которую вы могли бы использовать, чтобы указать компилятору какое решение ему принять. Только то, как вы пишете свой код, условно позволяет повлиять на это решение.
📌Как это работает?
▫️Компилятор анализирует область видимости и жизненный цикл переменных в программе. Если переменная не выходит за пределы своей области видимости, то она может быть безопасно размещена на стеке. Это быстрее и эффективнее с точки зрения памяти, так как стек очищается автоматически при выходе из функции.
▫️Если же она выходит — например, возвращается из функции или сохраняется в глобальной переменной — то она будет размещена в куче, и за её освобождение будет отвечать сборщик мусора.
🚀Теперь немного кода
• Переменная остается в рамках функции:
• Переменная не ограничена локальной областью видимости и может быть доступна извне:
• Переменная сохраняется в глобальной переменной:
Эти примеры упрощены, но они иллюстрируют базовую концепцию. Escape analysis помогает языку Go работать эффективно, оптимизируя использование памяти и улучшая производительность.
▫️Escape analysis — это техника компилятора Go, которая позволяет определить, где лучше всего размещать переменные: на стеке или в куче. Этот анализ важен для оптимизации производительности и эффективного использования памяти.
▫️В Go нет ни одного ключевого слова или функции, которую вы могли бы использовать, чтобы указать компилятору какое решение ему принять. Только то, как вы пишете свой код, условно позволяет повлиять на это решение.
📌Как это работает?
▫️Компилятор анализирует область видимости и жизненный цикл переменных в программе. Если переменная не выходит за пределы своей области видимости, то она может быть безопасно размещена на стеке. Это быстрее и эффективнее с точки зрения памяти, так как стек очищается автоматически при выходе из функции.
▫️Если же она выходит — например, возвращается из функции или сохраняется в глобальной переменной — то она будет размещена в куче, и за её освобождение будет отвечать сборщик мусора.
🚀Теперь немного кода
• Переменная остается в рамках функции:
func sum(a, b int) int {
result := a + b
return result
}
• Переменная не ограничена локальной областью видимости и может быть доступна извне:
func newInt() *int {
result := 42
return &result
}
• Переменная сохраняется в глобальной переменной:
var globalVar *int
func setGlobalVar() {
x := 100
globalVar = &x
}
Эти примеры упрощены, но они иллюстрируют базовую концепцию. Escape analysis помогает языку Go работать эффективно, оптимизируя использование памяти и улучшая производительность.
👍22
💬Что из себя представляет паттерн «Прототип» в Go и как его реализовать?
🔻Паттерн Прототип позволяет создавать новые объекты, путем копирования (клонирования) созданного ранее объекта-оригинала-продукта (прототипа).
🔻Паттерн описывает процесс создания объектов-клонов на основе имеющегося объекта-прототипа, другими словами, он позволяет описать способ организации процесса клонирования.
📌А теперь практика (код прикреплен к посту)
• Рассмотрим паттерн Прототип на примере иерархии файловой системы. В такой системе есть директории, которые могут содержать в себе как файлы, так и другие директории, создавая многоуровневую структуру.
• Каждый файл или директория может быть представлен интерфейсом Element. Этот интерфейс имеет метод clone.
Файл и директория (назовем их Document и Directory) реализуют методы display и clone, соответствующие интерфейсу Element. При клонировании мы добавляем суффикс “
• В примере выше мы видим, как изначальная иерархия файловой системы успешно клонировалась, причем каждый элемент в клонированной версии имеет суффикс "
Результат выполнения:
🔻Паттерн Прототип позволяет создавать новые объекты, путем копирования (клонирования) созданного ранее объекта-оригинала-продукта (прототипа).
🔻Паттерн описывает процесс создания объектов-клонов на основе имеющегося объекта-прототипа, другими словами, он позволяет описать способ организации процесса клонирования.
📌А теперь практика (код прикреплен к посту)
• Рассмотрим паттерн Прототип на примере иерархии файловой системы. В такой системе есть директории, которые могут содержать в себе как файлы, так и другие директории, создавая многоуровневую структуру.
• Каждый файл или директория может быть представлен интерфейсом Element. Этот интерфейс имеет метод clone.
Файл и директория (назовем их Document и Directory) реализуют методы display и clone, соответствующие интерфейсу Element. При клонировании мы добавляем суффикс “
_copy
” к имени элемента.• В примере выше мы видим, как изначальная иерархия файловой системы успешно клонировалась, причем каждый элемент в клонированной версии имеет суффикс "
_copy
".Результат выполнения:
Displaying structure for
Dir2
Dir2
Dir1
Doc1
Doc2
Doc3
Displaying structure for copied Directory
Dir2_copy
Dir1_copy
Doc1_copy
Doc2_copy
Doc3_copy
👍6
💬Для чего в Go используется ключевое слово fallthrough
🔹В Go ключевое слово fallthrough используется внутри оператора switch. Оно позволяет продолжить выполнение кода в следующем блоке case, даже если его условие не выполняется.
🔹Это поведение аналогично тому, как switch работает в некоторых других языках, таких как C или C++, но в Go вы должны явно использовать fallthrough.
👀 Пример
• В этом примере, когда x равно 3, программа выведет:
• Хотя x не равно 4, из-за использования fallthrough в case 3, выполнение продолжается с case 4.
🔹В Go ключевое слово fallthrough используется внутри оператора switch. Оно позволяет продолжить выполнение кода в следующем блоке case, даже если его условие не выполняется.
🔹Это поведение аналогично тому, как switch работает в некоторых других языках, таких как C или C++, но в Go вы должны явно использовать fallthrough.
package main
import "fmt"
func main() {
x := 3
switch x {
case 3:
fmt.Println("x is 3")
fallthrough
case 4:
fmt.Println("x is 4")
default:
fmt.Println("default case")
}
}
• В этом примере, когда x равно 3, программа выведет:
x is 3
x is 4
• Хотя x не равно 4, из-за использования fallthrough в case 3, выполнение продолжается с case 4.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8