В Go механизм сборки мусора называется Garbage Collector (GC).
Go использует автоматический сборщик мусора с конкурентной, трицветной, инкрементальной стратегией.
выполняется параллельно с работой программы.
объекты помечаются как белые, серые и чёрные.
работает порциями, а не останавливает программу надолго.
мусор, который будет удалён.
те, которые находятся в обработке.
используемые объекты, которые не подлежат удалению.
Чтобы уменьшить нагрузку на GC:
Используйте пул объектов (
sync.Pool
). Минимизируйте аллокации в куче (например, используйте
[]byte
вместо string
, если можно). Ограничивайте долгоживущие объекты, так как они реже сканируются.
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
Алгоритмы балансировки нагрузки:
- Round Robin — по кругу, равномерно.
- Least Connections — запрос уходит на сервер с наименьшим числом активных соединений.
- IP Hash — запросы одного клиента попадают на один сервер.
- Weighted Round Robin — с учётом мощности/приоритета серверов.
- Random — случайное распределение.
- Consistent Hashing — используется в распределённых системах для устойчивого распределения ключей.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
Массив (array) — это структура данных, которая представляет собой фиксированную последовательность элементов одного типа. Все элементы массива размещены в памяти последовательно и имеют одинаковый тип. Используются для хранения коллекций данных, где количество элементов заранее известно и фиксировано.
Размер массива задается при его объявлении и не может изменяться во время выполнения программы.
Все элементы массива имеют один и тот же тип.
Элементы массива хранятся последовательно в памяти, что обеспечивает быстрый доступ к любому элементу по индексу.
Массивы объявляются с указанием типа элементов и фиксированного размера:
var arr [5]int
Могут быть инициализированы при объявлении:
arr := [5]int{1, 2, 3, 4, 5}
Можно также инициализировать массив частично, оставив остальные элементы равными нулям:
arr := [5]int{1, 2}
Осуществляется с использованием индексов, начиная с 0:
fmt.Println(arr[0]) // 1
arr[1] = 10
fmt.Println(arr[1]) // 10
Фиксирована и задается при его объявлении. Ее можно получить с помощью функции
len
:fmt.Println(len(arr)) // 5
При присваивании одного массива другому копируются все элементы:
arr1 := [5]int{1, 2, 3, 4, 5}
arr2 := arr1
arr2[0] = 10
fmt.Println(arr1) // [1 2 3 4 5]
fmt.Println(arr2) // [10 2 3 4 5]
Копируется весь массив:
func modifyArray(a [5]int) {
a[0] = 10
}
arr := [5]int{1, 2, 3, 4, 5}
modifyArray(arr)
fmt.Println(arr) // [1 2 3 4 5]
С помощью оператора
==
, если они имеют одинаковую длину и тип элементов:arr1 := [3]int{1, 2, 3}
arr2 := [3]int{1, 2, 3}
arr3 := [3]int{4, 5, 6}
fmt.Println(arr1 == arr2) // true
fmt.Println(arr1 == arr3) // false
Пример
package main
import (
"fmt"
)
func main() {
// Объявление и инициализация массива
arr := [5]int{1, 2, 3, 4, 5}
// Доступ к элементам
fmt.Println("First element:", arr[0]) // First element: 1
// Изменение элементов
arr[1] = 10
fmt.Println("Modified array:", arr) // Modified array: [1 10 3 4 5]
// Длина массива
fmt.Println("Length of array:", len(arr)) // Length of array: 5
// Копирование массива
arr2 := arr
arr2[0] = 20
fmt.Println("Original array:", arr) // Original array: [1 10 3 4 5]
fmt.Println("Copied array:", arr2) // Copied array: [20 10 3 4 5]
// Передача массива в функцию
modifyArray(arr)
fmt.Println("Array after modifyArray call:", arr) // Array after modifyArray call: [1 10 3 4 5]
}
func modifyArray(a [5]int) {
a[0] = 10
}
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Несмотря на свои многие преимущества, такие как простота, скорость и эффективность в работе с многопоточностью, имеет ряд недостатков, которые могут повлиять на выбор языка для конкретных проектов или задач.
До введения дженериков, одним из основных недостатков языка было отсутствие поддержки дженериков, что приводило к тому, что разработчики должны были использовать интерфейсы и тип
interface{}
для создания функций и структур, работающих с различными типами данных. Это могло привести к ухудшению производительности и увеличению сложности кода.Управление памятью осуществляется сборщиком мусора, который, хоть и очень эффективен, может быть непредсказуемым в плане времени выполнения. Это может быть критично для приложений с жёсткими требованиями к времени отклика, таких как высокопроизводительные игры или реалтаймовые системы.
Поскольку типы интерфейсов проверяются во время выполнения, а не во время компиляции, может возникнуть проблема с отладкой ошибок, связанных с типами данных. Кроме того, использование интерфейсов может привести к дополнительным накладным расходам на производительность.
Неизменяемы, что может привести к неэффективному использованию памяти и времени процессора при частой модификации строк, например, в циклах или больших объёмах текстовых данных.
До введения модулей в Go 1.11 управление зависимостями было сложным и могло привести к конфликтам и трудностям в сопровождении кода. Хотя модули значительно улучшили ситуацию, система версионирования и управления зависимостями в Go все еще может быть не такой гибкой, как в некоторых других языках.
Такие ошибки, как работа с нулевыми указателями, остаются довольно распространенными в Go, поскольку язык обладает автоматическим разыменованием указателей, что может привести к панике в рантайме.
Стремится быть простым и эффективным языком, что иногда ведет к отсутствию поддержки некоторых более сложных или специализированных функций, доступных в других языках, таких как метапрограммирование, генерики (до Go 1.18) или тонкая настройка управления памятью.
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
💊6🤔2
defer реализует концепт отложенного выполнения — аналог "гарантированного освобождения ресурса" или финализации.
Это полезно для:
- Закрытия файлов или соединений.
- Освобождения ресурсов.
- Логирования.
- Обработки ошибок в recover.
Фактически defer помогает имитировать поведение finally из других языков.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
Таймауты (
timeout
) в HTTP-запросах предотвращают зависание приложения при медленных или недоступных серверах. Они позволяют ограничить время ожидания ответа, чтобы избежать бесконечного ожидания и высвободить ресурсы.В Golang таймауты можно устанавливать на разных уровнях:
Timeout для всего запроса (включает подключение, отправку и получение данных)
client := &http.Client{
Timeout: 5 * time.Second, // Запрос не может длиться дольше 5 секунд
}
Таймаут на установку соединения
transport := &http.Transport{
DialContext: (&net.Dialer{
Timeout: 2 * time.Second, // 2 секунды на подключение
}).DialContext,
}
client := &http.Client{Transport: transport}
Таймаут на чтение и запись
transport := &http.Transport{
ResponseHeaderTimeout: 3 * time.Second, // 3 секунды на заголовки
}
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
Это интерфейс, который не содержит методов. Поскольку интерфейсы в Go определяют поведение, которое тип должен реализовать, пустой интерфейс, не имеющий методов, автоматически реализуется всеми типами. Это делает пустой интерфейс универсальным контейнером для значений любого типа.
Пустой интерфейс может содержать значение любого типа, потому что все типы в Go автоматически реализуют пустой интерфейс.
Пустой интерфейс широко используется для создания обобщенных (generic) структур данных, функций и методов, которые могут работать с данными любых типов.
Пустой интерфейс может использоваться для хранения значений различных типов в одной переменной.
package main
import "fmt"
func main() {
var i interface{}
i = 42
fmt.Println(i) // Output: 42
i = "hello"
fmt.Println(i) // Output: hello
i = true
fmt.Println(i) // Output: true
}
Пустой интерфейс позволяет создавать функции, которые могут принимать параметры любого типа.
package main
import "fmt"
func printValue(v interface{}) {
fmt.Println(v)
}
func main() {
printValue(42)
printValue("hello")
printValue(true)
}
Пустой интерфейс используется для создания структур данных, которые могут хранить значения различных типов.
package main
import "fmt"
func main() {
var values []interface{}
values = append(values, 42, "hello", true)
for _, v := range values {
fmt.Println(v)
}
}
Пустой интерфейс используется для обработки данных различных типов, например, при парсинге JSON.
package main
import (
"encoding/json"
"fmt"
)
func main() {
jsonData := `{"name": "Alice", "age": 30}`
var result map[string]interface{}
json.Unmarshal([]byte(jsonData), &result)
fmt.Println(result)
}
При работе с пустым интерфейсом часто возникает необходимость проверить тип хранимого значения и привести его к конкретному типу. Это можно сделать с помощью утверждения типа (
type assertion
) или конструкции switch
.Утверждение типа позволяет проверить и преобразовать значение пустого интерфейса к конкретному типу.
package main
import "fmt"
func main() {
var i interface{} = "hello"
s, ok := i.(string)
if ok {
fmt.Println("String:", s) // Output: String: hello
} else {
fmt.Println("Not a string")
}
n, ok := i.(int)
if ok {
fmt.Println("Integer:", n)
} else {
fmt.Println("Not an integer")
}
}
Конструкция
switch
позволяет обрабатывать значения различных типов, хранящиеся в пустом интерфейсе.package main
import "fmt"
func printType(i interface{}) {
switch v := i.(type) {
case string:
fmt.Println("String:", v)
case int:
fmt.Println("Integer:", v)
case bool:
fmt.Println("Boolean:", v)
default:
fmt.Printf("Unknown type: %T\n", v)
}
}
func main() {
printType("hello") // Output: String: hello
printType(42) // Output: Integer: 42
printType(true) // Output: Boolean: true
printType(3.14) // Output: Unknown type: float64
}
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
Builder — это порождающий паттерн, позволяющий создавать сложные объекты пошагово, контролируя процесс построения.
Он особенно полезен, когда объект:
- имеет много опциональных параметров,
- требует разной конфигурации при создании,
- должен быть иммутабельным после построения.
Пример: создание HTTP-запроса или SQL-запроса с цепочкой .setX().setY().build().
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Ключи в map (карте) должны обладать определенными свойствами, чтобы быть допустимыми и корректно работать с хеш-таблицей. Важно понимать, что ключи должны поддерживать операцию сравнения и быть хешируемыми.
Ключи должны поддерживать операцию сравнения с использованием оператора
==
. Это необходимо для проверки равенства ключей при выполнении операций вставки, поиска и удаления в карте.Ключи должны быть хешируемыми. Это значит, что для ключей должна существовать функция, которая преобразует их в хеш-код. Хеш-код используется для определения позиции ключа в хеш-таблице.
Булевый тип (bool)
m := map[bool]string{
true: "Yes",
false: "No",
}
Целочисленные типы (int, int8, int16, int32, int64, а также их беззнаковые эквиваленты uint, uint8, uint16, uint32, uint64)
m := map[int]string{
1: "One",
2: "Two",
}
Числа с плавающей точкой (float32, float64)
m := map[float64]string{
1.1: "One point one",
2.2: "Two point two",
}
Строки (string)
m := map[string]int{
"Alice": 25,
"Bob": 30,
}
Составные типы (структуры, массивы), при условии, что все их поля также поддерживают сравнение с помощью оператора ==
type Key struct {
FirstName string
LastName string
}
m := map[Key]int{
{"Alice", "Smith"}: 1,
{"Bob", "Johnson"}: 2,
}
slice
)Срезы не поддерживают операцию сравнения с использованием оператора
==
(только сравнение с nil
).map
)Карты не поддерживают операцию сравнения.
func
)Функции не поддерживают операцию сравнения.
package main
import "fmt"
type Person struct {
FirstName string
LastName string
}
func main() {
// Создаем карту с ключами типа Person
m := make(map[Person]int)
// Вставляем значения в карту
m[Person{"Alice", "Smith"}] = 25
m[Person{"Bob", "Johnson"}] = 30
// Выводим значения из карты
fmt.Println(m[Person{"Alice", "Smith"}]) // Выводит: 25
fmt.Println(m[Person{"Bob", "Johnson"}]) // Выводит: 30
}
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
Закрытие закрытого канала вызывает panic.
Рекомендуется не закрывать канал дважды и проверять, закрыт ли он (recover или sync.Once для защиты от дублирующего закрытия).
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
Этот подход обеспечивает ряд преимуществ, которые способствуют улучшению качества кода, его поддерживаемости и соответствию требованиям.
Написание тестов перед кодом помогает разработчикам четко определить требования к функции или модулю. Тесты выступают в роли спецификации, описывающей, как должен работать код. Это помогает избежать недоразумений и гарантирует, что все требования учтены с самого начала.
TDD заставляет разработчиков сосредоточиться на минимальной функциональности, необходимой для прохождения тестов. Это предотвращает избыточное проектирование и разработку ненужных функций. Каждый тест фокусируется на одном аспекте функциональности, что помогает создавать чистый и целенаправленный код.
Написание тестов перед кодом стимулирует создание модульного и хорошо структурированного кода. Поскольку тесты проверяют небольшие и изолированные части кода, это побуждает разработчиков разделять большие задачи на более мелкие и управляемые модули, что улучшает дизайн и архитектуру системы.
Тесты, написанные перед кодом, обеспечивают немедленную обратную связь. Если код не проходит тест, это означает, что он не соответствует требованиям. Это позволяет разработчикам быстро обнаруживать и исправлять ошибки, повышая уверенность в корректности кода.
Тесты служат живой документацией кода. Они показывают, как должен работать код и какие случаи использования поддерживаются. Это облегчает понимание кода другим разработчикам и упрощает его поддержку в будущем.
Постоянное написание тестов и их прохождение помогает поддерживать высокое качество кода. Тесты выявляют дефекты на ранних стадиях разработки и обеспечивают, что новые изменения не нарушают существующую функциональность.
package main
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Expected 5, but got %d", result)
}
}
На этом этапе тест не пройдет, так как функция
Add
еще не реализована.package main
func Add(a, b int) int {
return a + b
}
Теперь тест пройдет, так как функция
Add
реализована корректно.При необходимости выполняется рефакторинг кода для улучшения его структуры, при этом тесты продолжают проходить, что гарантирует сохранение функциональности.
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
Стандартный map небезопасен для многопоточного доступа. Используйте синхронизацию или специальные структуры данных.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5💊1
Контексты (context.Context) представляют собой интерфейс, который используется для передачи мета-данных, управления сроками действия и отменой операций в иерархии вызовов функций. Основная цель контекста — обеспечение способа для остановки выполнения программы (например, запросов или подпроцессов) по требованию. Это особенно полезно в сетевых приложениях, где вам может потребоваться прервать выполнение операции, которая больше не требуется или занимает слишком много времени.
Контекст предназначен для безопасной передачи по горутинам, что означает, что он может быть передан между различными частями вашей программы, выполняющимися в разных горутинах.
Контекст может быть отменен, что автоматически сигнализирует всем получателям этого контекста о необходимости остановить свою работу. Это обеспечивается через канал, который закрывается, когда контекст отменяется.
Контекст может иметь установленный таймаут или дедлайн, после достижения которого он автоматически отменяется.
Контексты могут нести значения — пары ключ-значение, которые можно устанавливать и получать. Эти значения обычно используются для передачи данных, специфичных для запроса, таких как идентификаторы сессий или токены авторизации.
🟠context.Background()
Возвращает пустой контекст, который никогда не отменяется. Обычно используется в основной функции и при инициализации.
context.TODO()
Используется для указания, что должен быть предоставлен подходящий контекст. Обычно применяется в разработке и при рефакторинге.
context.WithCancel(parent Context) (ctx Context, cancel CancelFunc)
Создает новый контекст с возможностью отмены.
context.WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
Создает контекст, который автоматически отменяется в указанный
deadline
.context.WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
Аналогичен
WithDeadline
, но устанавливает время жизни контекста на основе заданного таймаута.func operation1(ctx context.Context) {
select {
case <-time.After(5 * time.Second):
fmt.Println("operation1 completed")
case <-ctx.Done():
fmt.Println("operation1 cancelled")
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
go operation1(ctx)
// Дожидаемся завершения или отмены операции
<-ctx.Done()
if err := ctx.Err(); err != nil {
fmt.Println("main:", err)
}
}
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
len
и capacity
в слайсе Go? 2. capacity: общая ёмкость слайса, включая длину и свободное пространство, доступное для добавления новых элементов
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
Это язык программирования, разработанный Google для создания масштабируемых и эффективных систем. Основные цели языка включают простоту, высокую производительность и надежность.
Go — компилируемый язык, обеспечивающий высокую производительность.
Язык избегает избыточности, улучшая читаемость кода.
Автоматическое управление памятью.
Встроенные средства для работы с параллельными вычислениями, такие как горутины и каналы.
Широкий набор инструментов и библиотек.
Простого веб-сервера:
package main
import (
"fmt"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}
func main() {
http.HandleFunc("/", helloHandler)
fmt.Println("Starting server on :8080")
http.ListenAndServe(":8080", nil)
}
Легковесные потоки для параллельных вычислений.
go func() {
fmt.Println("Hello from a goroutine!")
}()
Средства обмена данными между горутинами.
ch := make(chan int)
go func() {
ch <- 42
}()
value := <-ch
fmt.Println(value)
Абстракция поведения без привязки к реализации.
type Stringer interface {
String() string
}
Организация кода для управления и повторного использования.
package mypackage
import "fmt"
func MyFunction() {
fmt.Println("Hello from mypackage")
}
Go используется для серверного ПО, облачных сервисов, микросервисов, инструментов командной строки и систем с высокой производительностью. Известные проекты на Go включают Kubernetes, Docker, Prometheus, а также Google, Dropbox и Netflix.
Быстро компилируемый язык.
Минимум синтаксического шума.
Простые механизмы параллельных вычислений.
Широкий набор встроенных инструментов.
Ранее отсутствовали, добавлены в Go 1.18.
Используется управление ошибками через возвращаемые значения.
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2💊2🤔1
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
В Go, проверка типа интерфейса может быть выполнена несколькими способами: с помощью утверждения типа (type assertion) и с помощью конструкции
switch
для выбора типа. Утверждение типа позволяет проверить, является ли значение определенного интерфейса конкретным типом. Если да, то оно преобразует интерфейс в этот тип.
value, ok := interfaceValue.(ConcreteType)
value
Значение типа
ConcreteType
, если утверждение типа успешно.ok
Булевое значение, указывающее, удалось ли преобразование.
package main
import "fmt"
func main() {
var i interface{} = "hello"
// Утверждение типа
s, ok := i.(string)
if ok {
fmt.Println("String:", s)
} else {
fmt.Println("Not a string")
}
// Утверждение типа, которое вызовет панику, если тип не соответствует
// Uncomment the line below to see the panic
// s := i.(string)
// fmt.Println(s)
}
Конструкция
switch
позволяет проверить значение интерфейса на соответствие нескольким возможным типам.switch v := interfaceValue.(type) {
case ConcreteType1:
// v имеет тип ConcreteType1
case ConcreteType2:
// v имеет тип ConcreteType2
default:
// v имеет другой тип
}
package main
import "fmt"
func printType(i interface{}) {
switch v := i.(type) {
case string:
fmt.Println("String:", v)
case int:
fmt.Println("Integer:", v)
case bool:
fmt.Println("Boolean:", v)
default:
fmt.Printf("Unknown type: %T\n", v)
}
}
func main() {
printType("hello")
printType(42)
printType(true)
printType(3.14)
}
Пример использования для проверки и работы с интерфейсами
package main
import (
"fmt"
)
type Person struct {
Name string
Age int
}
// Реализация интерфейса fmt.Stringer для типа Person
func (p Person) String() string {
return fmt.Sprintf("%s (%d years old)", p.Name, p.Age)
}
func printValue(i interface{}) {
if str, ok := i.(fmt.Stringer); ok {
fmt.Println("Stringer:", str.String())
} else {
fmt.Println("Not a Stringer")
}
}
func main() {
p := Person{Name: "Alice", Age: 30}
printValue(p) // Проверка типа fmt.Stringer
printValue("Hello, world!") // Строка не реализует fmt.Stringer
}
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Forwarded from easyoffer
🎉 easyoffer 2.0 — релиз уже в этом месяце!
Вас ждут новые фичи, о которых мы ранее даже не упоминали. Они сделают путь к офферам ещё быстрее и эффективнее. Расскажу о них чуть позже 👀
В честь запуска мы готовим ограниченную акцию:
Первые 500 покупателей получат:
🚀 PRO тариф на 1 год с 50% скидкой
Что нужно сделать:
🔔 Подпишитесь на этот Telegram-канал, чтобы первыми узнать о старте релиза. Сообщение появится в нем раньше, чем где-либо еще — вы успеете попасть в число первых 500 и получить максимальную выгоду. 🎁 А еще только для подписчиков канала ценный бонус в подарок к PRO тарифу.
📅 Официальный запуск — уже совсем скоро.
Следите за новостями и не пропустите старт!
Вас ждут новые фичи, о которых мы ранее даже не упоминали. Они сделают путь к офферам ещё быстрее и эффективнее. Расскажу о них чуть позже 👀
В честь запуска мы готовим ограниченную акцию:
Первые 500 покупателей получат:
🚀 PRO тариф на 1 год с 50% скидкой
Что нужно сделать:
🔔 Подпишитесь на этот Telegram-канал, чтобы первыми узнать о старте релиза. Сообщение появится в нем раньше, чем где-либо еще — вы успеете попасть в число первых 500 и получить максимальную выгоду. 🎁 А еще только для подписчиков канала ценный бонус в подарок к PRO тарифу.
📅 Официальный запуск — уже совсем скоро.
Следите за новостями и не пропустите старт!
- При нехватке места создаётся новый массив большей ёмкости.
- Обычно размер удваивается, пока не достигает определённого порога, после чего рост может становиться более линейным или адаптивным (зависит от языка/реализации).
Это даёт амортизированную сложность O(1) на добавление, несмотря на редкие дорогостоящие перераспределения.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1