💬Представьте, что вам необходимо внедрить информацию о версии и другие метаданные в ваше Go-приложение во время сборки, не изменяя исходный код. Как это реализовать?
📌Мы можем использовать флаг
📌Простые юзкейсы:
◆ Установка значения переменной: мы можем установить значение переменной во время компиляции. Например,
◆ Уменьшение размера бинарного файла: использование
👉 Подробнее
📌Мы можем использовать флаг
-ldflags
, который позволяет управлять поведением компоновщика при сборке Go-программ. Он позволяет определять опции сборки на этапе компиляции. 📌Простые юзкейсы:
◆ Установка значения переменной: мы можем установить значение переменной во время компиляции. Например,
go build -ldflags "-X main.version=1.0.0"
устанавливает переменную version в пакете main в значение 1.0.0.◆ Уменьшение размера бинарного файла: использование
go build -ldflags "-w -s"
позволяет уменьшить размер исполняемого файла, отключая отладочную информацию и символы таблицы.👉 Подробнее
Digitalocean
Использование ldflags для установки информации о версиях в приложениях Go | DigitalOcean
При развертывании приложений в производственной среде сборка двоичных файлов с информацией о версии и другими метаданными помогает улучшить процессы монитори…
👍10
💬В чем разница между nil и пустым срезом в Go?
◾️Чтобы избежать распространенных ошибок, важно понимать разницу между nil и пустым срезом. Оба представляют собой срезы нулевой длины и нулевой емкости, но только nil срез не требует выделения памяти.
◾️Nil срез равен nil, в то время как пустой срез имеет нулевую длину. Nil срез является пустым, но пустой срез не обязательно является nil.
📌Мы можем инициализировать срез в зависимости от контекста, используя:
☑️
☑️
☑️
◾️
◾️Чтобы избежать распространенных ошибок, важно понимать разницу между nil и пустым срезом. Оба представляют собой срезы нулевой длины и нулевой емкости, но только nil срез не требует выделения памяти.
◾️Nil срез равен nil, в то время как пустой срез имеет нулевую длину. Nil срез является пустым, но пустой срез не обязательно является nil.
📌Мы можем инициализировать срез в зависимости от контекста, используя:
☑️
var s []string
, если мы не уверены в окончательной длине и срез может быть пустым☑️
[]string(nil)
как синтаксический сахар для создания nil и пустого среза☑️
make([]string, length)
, если будущая длина известна◾️
[]string{}
следует избегать, если мы инициализируем срез без элементов.👍5
💬 Если ключ или значение типа map имеют размер более 128 байт, каким образом Go их будет хранить?
📌Если ключ или значение мапы превышает 128 байт, Go не сохранит его непосредственно в бакете мапы. Вместо этого Go сохраняет указатель на ключ или значение.
📌Хоть все происходит под капотом, это может значительно повлиять на производительность и управление памятью.
👉 Читайте подробнее об утечках памяти при работе с мапами
📌Если ключ или значение мапы превышает 128 байт, Go не сохранит его непосредственно в бакете мапы. Вместо этого Go сохраняет указатель на ключ или значение.
📌Хоть все происходит под капотом, это может значительно повлиять на производительность и управление памятью.
👉 Читайте подробнее об утечках памяти при работе с мапами
100go.co
Maps and memory leaks (#28) - 100 Go Mistakes and How to Avoid Them
None
👍9🔥1
💬Как использовать операторы == и != для эффективного сравнения значений в Go?
📌Мы можем использовать эти операторы с операндами, которые сравнимы:
• Логические: равны ли два логических значения.
• Числовые (int, float, complex): равны ли два числовых значения.
• Строки: равны ли две строки.
• Каналы: созданы ли два канала одним вызовом make или оба равны nil.
• Интерфейсы: имеют ли два интерфейса идентичные динамические типы и равные динамические значения или оба равны nil.
• Указатели: указывают ли два указателя на одно и то же значение в памяти или оба равны nil.
• Структуры и массивы: состоят ли они из аналогичных типов.
📌Также мы можем использовать операторы <=,
📌Например, в Go мы можем использовать
📌Важно помнить, что в стандартной библиотеке есть некоторые существующие методы сравнения, такие как
📌Мы можем использовать эти операторы с операндами, которые сравнимы:
• Логические: равны ли два логических значения.
• Числовые (int, float, complex): равны ли два числовых значения.
• Строки: равны ли две строки.
• Каналы: созданы ли два канала одним вызовом make или оба равны nil.
• Интерфейсы: имеют ли два интерфейса идентичные динамические типы и равные динамические значения или оба равны nil.
• Указатели: указывают ли два указателя на одно и то же значение в памяти или оба равны nil.
• Структуры и массивы: состоят ли они из аналогичных типов.
📌Также мы можем использовать операторы <=,
>=
, <
и >
с числовыми типами для сравнения значений и со строками для сравнения их лексического порядка. Если операнды несравнимы, мы должны использовать другие варианты, такие как рефлексия. 📌Например, в Go мы можем использовать
reflect.DeepEqual
. Эта функция сообщает, равны ли два элемента, рекурсивно обходя два значения. Элементы, которые она принимает, это базовые типы, массивы, структуры, срезы, мапы, указатели, интерфейсы и функции. Однако основной недостаток — это производительность.📌Важно помнить, что в стандартной библиотеке есть некоторые существующие методы сравнения, такие как
bytes.Compare
, slices.Compare
и другие.❤5👍2
💬Как эффективно инициализировать тип map в Go?
🔸Мапа представляет собой неупорядоченную коллекцию пар ключ-значение, в которой все ключи различны. Под капотом мапа основана на структуре данных хеш-таблицы, которая в свою очередь представляет собой массив бакетов, где каждый бакет — это указатель на массив пар ключ-значение.
🔸Если мы заранее знаем количество элементов, которые будет содержать мапа, эффективнее будет создать ее, указав начальный размер. Это позволяет избежать потенциального расширения мапы, что довольно сложно с точки зрения вычислений, поскольку требует перераспределения достаточного пространства памяти и перебалансировки всех элементов.
🔸Мапа представляет собой неупорядоченную коллекцию пар ключ-значение, в которой все ключи различны. Под капотом мапа основана на структуре данных хеш-таблицы, которая в свою очередь представляет собой массив бакетов, где каждый бакет — это указатель на массив пар ключ-значение.
🔸Если мы заранее знаем количество элементов, которые будет содержать мапа, эффективнее будет создать ее, указав начальный размер. Это позволяет избежать потенциального расширения мапы, что довольно сложно с точки зрения вычислений, поскольку требует перераспределения достаточного пространства памяти и перебалансировки всех элементов.
❤7👍3🥱1
💬Какие подводные камни необходимо учитывать при работе с числами с плавающей точкой в Go?
📌В Go существует два типа чисел с плавающей точкой (если не учитывать комплексные числа):
📌Чтобы избежать неприятных сюрпризов, нам нужно помнить, что арифметика с плавающей точкой является приближением к реальной арифметике.
📌Для примера посмотрим на умножение:
◾️Мы могли бы ожидать, что этот код выведет результат умножения 1.
◾️Поскольку типы fl
* При сравнении двух чисел с плавающей точкой проверяйте, что их разница находится в приемлемом диапазоне.
* При выполнении сложений или вычитаний группируйте операции с похожим порядком величины для большей точности.
* Чтобы повысить точность, если последовательность операций требует сложения, вычитания, умножения или деления, сначала выполняйте операции умножения и деления.
📌В Go существует два типа чисел с плавающей точкой (если не учитывать комплексные числа):
float32
и float64
. Концепция числа с плавающей точкой была изобретена для решения основной проблемы целых чисел: их неспособности представлять дробные значения. 📌Чтобы избежать неприятных сюрпризов, нам нужно помнить, что арифметика с плавающей точкой является приближением к реальной арифметике.
📌Для примера посмотрим на умножение:
var n float32 = 1.0001
fmt.Println(n * n)
◾️Мы могли бы ожидать, что этот код выведет результат умножения 1.
0001 * 1.0001 = 1.00020001.
Однако, если запустить его на большинстве процессоров x86, он выведет 1.0002.
◾️Поскольку типы fl
oat32 и
float64 в
Go являются приближениями, нам нужно помнить несколько правил:* При сравнении двух чисел с плавающей точкой проверяйте, что их разница находится в приемлемом диапазоне.
* При выполнении сложений или вычитаний группируйте операции с похожим порядком величины для большей точности.
* Чтобы повысить точность, если последовательность операций требует сложения, вычитания, умножения или деления, сначала выполняйте операции умножения и деления.
👍9
💬Что важно учитывать при работе с циклом range в Go?
📌
📌 Цикл
• Например:
• Массив a
📌
Value
в цикле range
является копией. Следовательно, чтобы изменить структуру, необходимо обращаться к ней через индекс или использовать классический цикл for (если только элемент или поле, которое мы хотим модифицировать, не является указателем).📌 Цикл
range
оценивает предоставленное выражение только один раз, до начала цикла, путем создания копии (независимо от типа). Важно помнить об этом поведении, чтобы избежать распространенных ошибок, которые, например, могут привести к доступу к неправильному элементу.• Например:
a := [3]int{0, 1, 2}
for i, v := range a {
a[2] = 10
if i == 2 {
fmt.Println(v)
}
}
• Массив a
и
нициализируется значениями [0, 1, 2]
, при этом изменение a[2]
н
а 10 н
е влияет на итерацию, так как массив был оценен до начала цикла. Поэтому, когда индекс i р
авен 2, переменная v (
которая является копией элемента массива на момент начала цикла) все еще содержит исходное значение 2, а не обновленное значение 10.👍14
💬Как реализовать тайм-ауты для каналов в Go?
🔸Применение
🔸В некоторых языках для реализации тайм-аута может потребоваться реализовать управление потоками, но
🔸Здесь нет оператора de
🔸Применение
select
для мультиплексирования каналов открывает широкие возможности и помогает сделать сложные или утомительные задачи тривиально простыми. 🔸В некоторых языках для реализации тайм-аута может потребоваться реализовать управление потоками, но
select
с вызовом функции time.After
, возвращающей канал, через который будет отправлено сообщение после истечения указанного времени, делает эту задачу очень простой:
var ch chan int
select {
case m := <-ch:
fmt.Println(m)
case <-time.After(10 * time.Second):
fmt.Println("Timed out")
}
🔸Здесь нет оператора de
fault,
поэтому select
з
аблокируется до выполнения одного из условий. Если канал ch н
е станет доступным для чтения до того, как в канал, возвращаемый функцией time.After,
будет записано сообщение, то сработает второй оператор case и инструкция select
з
авершится по тайм-ауту.🔥13❤2
🧑💻 Статьи для IT: как объяснять и распространять значимые идеи
Напоминаем, что у нас есть бесплатный курс для всех, кто хочет научиться интересно писать — о программировании и в целом.
Что: семь модулей, посвященных написанию, редактированию, иллюстрированию и распространению публикаций.
Для кого: для авторов, копирайтеров и просто программистов, которые хотят научиться интересно рассказывать о своих проектах.
👉Материалы регулярно дополняются, обновляются и корректируются. А еще мы отвечаем на все учебные вопросы в комментариях курса.
Напоминаем, что у нас есть бесплатный курс для всех, кто хочет научиться интересно писать — о программировании и в целом.
Что: семь модулей, посвященных написанию, редактированию, иллюстрированию и распространению публикаций.
Для кого: для авторов, копирайтеров и просто программистов, которые хотят научиться интересно рассказывать о своих проектах.
👉Материалы регулярно дополняются, обновляются и корректируются. А еще мы отвечаем на все учебные вопросы в комментариях курса.
💬 В чем преимущества и недостатки импорта через точку в Go?
В Go, импорт пакета с использованием точки (dot import) является специальной формой импорта, позволяющей обращаться к экспортируемым идентификаторам пакета непосредственно, без указания имени пакета.
📌 Обычный импорт:
Здесь
📌 Импорт через точку:
Здесь
📌 Преимущества и недостатки:
1. Удобство: импорт через точку может сделать код более лаконичным, особенно если из пакета часто используются различные функции или типы.
2. Читаемость и конфликты имен: этот подход может ухудшить читаемость, так как становится неясно, из какого пакета происходит тот или иной идентификатор. Также повышается риск конфликта имен, если два импортированных таким образом пакета содержат идентификаторы с одинаковыми именами.
3. Применение: чаще всего этот подход используется в тестах или в кейсах, где минимизация количества кода является приоритетом, и риск конфликта имен низок.
💡Такой подход загрязняет пространство имен текущего пакета. Каждая функция или тип, которые мы импортируем через точку, удаляет возможность записи локальной функции или типа с тем же именем.
В Go, импорт пакета с использованием точки (dot import) является специальной формой импорта, позволяющей обращаться к экспортируемым идентификаторам пакета непосредственно, без указания имени пакета.
📌 Обычный импорт:
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
Здесь
Println
вызывается с использованием имени пакета fmt
.📌 Импорт через точку:
import . "fmt"
func main() {
Println("Hello, World!") // Используется без указания пакета
}
Здесь
Println
вызывается напрямую, без упоминания fmt
.📌 Преимущества и недостатки:
1. Удобство: импорт через точку может сделать код более лаконичным, особенно если из пакета часто используются различные функции или типы.
2. Читаемость и конфликты имен: этот подход может ухудшить читаемость, так как становится неясно, из какого пакета происходит тот или иной идентификатор. Также повышается риск конфликта имен, если два импортированных таким образом пакета содержат идентификаторы с одинаковыми именами.
3. Применение: чаще всего этот подход используется в тестах или в кейсах, где минимизация количества кода является приоритетом, и риск конфликта имен низок.
💡Такой подход загрязняет пространство имен текущего пакета. Каждая функция или тип, которые мы импортируем через точку, удаляет возможность записи локальной функции или типа с тем же именем.
👍8
💬 Что из себя представляет тип any в Go?
В Go,
☑️
☑️ С введением дженериков в Go 1.18,
☑️ Поскольку
☑️ Используя
📌 Функция, принимающая любой тип:
📌 Хранение различных типов в срезе:
В Go,
any
— это псевдоним для интерфейса interface{}
, который по сути может представлять любой тип данных. Это удобно, когда мы не знаем заранее, какой тип данных будет использоваться. ☑️
any
может хранить значение любого типа, от примитивов до сложных кастомных структур. Это делает его идеальным для случаев, когда тип данных заранее неизвестен.☑️ С введением дженериков в Go 1.18,
any
стал широко использоваться для создания обобщенных функций и типов. Он позволяет определять параметры и структуры, которые могут работать с любым типом данных.☑️ Поскольку
any
может представлять любой тип, он полезен в ситуациях, где нужна гибкость типов данных, например, при работе с JSON или при динамическом приведении типов.☑️ Используя
any
в сочетании с рефлексией, можно создавать функции и структуры, способные адаптироваться к разным типам данных во время выполнения программы.📌 Функция, принимающая любой тип:
func PrintValue(v any) {
fmt.Println(v)
}
func main() {
PrintValue(5) // 5
PrintValue("hello") // hello
PrintValue(3.14) // 3.14
}
📌 Хранение различных типов в срезе:
func main() {
values := []any{5, "hello", 3.14}
for _, v := range values {
fmt.Println(v)
}
}
👍11❤2
💬 Что такое pprof и как его использовать в Go?
📌 pprof — это инструмент для визуализации и анализа профилей производительности, встроенный в экосистему Go. Он помогает в обнаружении узких мест в коде и понимании того, как программа использует ресурсы, такие как CPU и память.
📌 Использование
1. Импорт пакета
2. Запуск HTTP сервера:
Мы можем запустить сервер на определенном порту, используя:
Это позволит получить доступ к профилям через
3. Сбор данных профиля: мы можем собрать различные типы профилей, такие как профиль CPU, памяти, блокировок и других. Например, для CPU-профиля:
Этот код запускает сбор данных о производительности CPU.
4. Анализ профиля: после сбора данных профиля мы можем использовать инструмент командной строки
В интерактивном режиме
5. Визуализация:
📌 pprof — это инструмент для визуализации и анализа профилей производительности, встроенный в экосистему Go. Он помогает в обнаружении узких мест в коде и понимании того, как программа использует ресурсы, такие как CPU и память.
📌 Использование
pprof
на практике: 1. Импорт пакета
pprof
: испортируем пакет import _ "net/http/pprof"
, что позволит автоматически добавить обработчики профайлера к HTTP-серверу. 2. Запуск HTTP сервера:
pprof
использует HTTP-сервер для сбора и предоставления данных профиля. Мы можем запустить сервер на определенном порту, используя:
go
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
Это позволит получить доступ к профилям через
https://localhost:6060/debug/pprof/
. 3. Сбор данных профиля: мы можем собрать различные типы профилей, такие как профиль CPU, памяти, блокировок и других. Например, для CPU-профиля:
go
f, err := os.Create("cpu.prof")
if err != nil {
log.Fatal(err)
}
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
Этот код запускает сбор данных о производительности CPU.
4. Анализ профиля: после сбора данных профиля мы можем использовать инструмент командной строки
go tool pprof
для анализа собранных данных. Например: shell
go tool pprof cpu.prof
В интерактивном режиме
pprof
мы можем визуализировать данные, искать узкие места, и просматривать статистику вызовов. 5. Визуализация:
pprof
также поддерживает визуализацию данных профиля в виде графиков. Мы можем использовать команды внутри pprof
, такие как web
или svg
, для создания графического представления профиля.👍12❤1
💬 Какие пакеты Go используются для работы с DNS и разрешением доменных имен?
📌 Для работы с DNS и выполнения разрешения доменных имен в Go можно использовать библиотеку net. Например:
🔸net.LookupHost: преобразовывает имя домена в список соответствующих IP-адресов.
🔸net.LookupMX: получает mx-запись для указанного домена, что полезно при разработке почтовых систем.
📌 Для работы с DNS и выполнения разрешения доменных имен в Go можно использовать библиотеку net. Например:
🔸net.LookupHost: преобразовывает имя домена в список соответствующих IP-адресов.
func main() {
ips, err := net.LookupHost("example.com")
if err != nil {
fmt.Println("Error:", err)
return
}
for _, ip := range ips {
fmt.Println(ip)
}
}
🔸net.LookupMX: получает mx-запись для указанного домена, что полезно при разработке почтовых систем.
func main() {
mxRecords, err := net.LookupMX("example.com")
if err != nil {
fmt.Println("Error:", err)
return
}
for _, mx := range mxRecords {
fmt.Println(mx.Host, mx.Pref)
}
}
👍14👏2
💬 В каких случаях следует использовать интерфейсы в Go?
🤔 Это довольно обширная и сложная тема, но если отвечать кратко, то интерфейсы в Go следует создавать в нескольких сценариях:
1️⃣ Общее поведение — использование интерфейсов, когда несколько типов реализуют общее поведение. Тогда можно заключить это поведение внутрь какого-то интерфейса.
В стандартной библиотеке много таких примеров. Например, сортировка какой-либо коллекции может быть разложена на три действия:
✹ Получение данных о количестве элементов в коллекции.
✹ Сообщение о том, должен ли один элемент быть размещен перед другим.
✹ Перестановка двух элементов.
2️⃣ Снижение связанности (decoupling) — отделение кода от его реализации. Если мы полагаемся на абстракцию вместо конкретной реализации, сама реализация может быть заменена на другую без необходимости менять код. Это и есть принцип подстановки Лисков. Одно из преимуществ снижения связанности может относиться к юнит-тестам.
3️⃣ Ограничение поведения. Представим, что мы реализуем конфигурационный пакет для работы с динамической конфигурацией. Мы создаем специальный контейнер для конфигураций
✹ Теперь предположим, что мы получили
✹ Cоздав абстракцию, которая ограничивает поведение только получением значения конфигурации:
Тогда в коде можно указать только
✹ В примере геттер конфигурации внедряется в фабричный метод
🤔 Это довольно обширная и сложная тема, но если отвечать кратко, то интерфейсы в Go следует создавать в нескольких сценариях:
1️⃣ Общее поведение — использование интерфейсов, когда несколько типов реализуют общее поведение. Тогда можно заключить это поведение внутрь какого-то интерфейса.
В стандартной библиотеке много таких примеров. Например, сортировка какой-либо коллекции может быть разложена на три действия:
✹ Получение данных о количестве элементов в коллекции.
✹ Сообщение о том, должен ли один элемент быть размещен перед другим.
✹ Перестановка двух элементов.
2️⃣ Снижение связанности (decoupling) — отделение кода от его реализации. Если мы полагаемся на абстракцию вместо конкретной реализации, сама реализация может быть заменена на другую без необходимости менять код. Это и есть принцип подстановки Лисков. Одно из преимуществ снижения связанности может относиться к юнит-тестам.
3️⃣ Ограничение поведения. Представим, что мы реализуем конфигурационный пакет для работы с динамической конфигурацией. Мы создаем специальный контейнер для конфигураций
int
с помощью структуры IntConfig
, в которой определены два метода: Get
и Set
. Вот как будет выглядеть такой код:type IntConfig struct {
// ...
}
func (c *IntConfig) Get() int {
// Получить конфигурацию
}
func (c *IntConfig) Set(value int) {
// Обновить конфигурацию
}
✹ Теперь предположим, что мы получили
IntConfig
, который содержит в себе определенную конфигурацию, например какое-то пороговое значение. Но в нашем коде нас интересует только получение значения этой конфигурации, и мы хотим предотвратить его обновление. Как мы можем обеспечить, чтобы семантически эта конфигурация была доступна только для чтения, если мы не хотим изменять пакет конфигурации? ✹ Cоздав абстракцию, которая ограничивает поведение только получением значения конфигурации:
type intConfigGetter interface {
Get() int
}
Тогда в коде можно указать только
intConfigGetter
вместо конкретной реализации:type Foo struct {
threshold intConfigGetter
}
func NewFoo(threshold intConfigGetter) Foo {
return Foo{threshold: threshold}
}
func (f Foo) Bar() {
threshold := f.threshold.Get()
// ...
}
✹ В примере геттер конфигурации внедряется в фабричный метод
NewFoo
. Он не влияет на потребителя этой функции, поскольку он по-прежнему может передавать структуру IntConfig
по мере реализации intConfigGetter
. Затем в методе Bar
можно только прочитать конфигурацию, но не изменить ее. Поэтому мы также можем использовать интерфейсы, чтобы ограничить тип определенным поведением, например, если нужно соблюсти семантику.👍9
💬 Как обнаружить целочисленное переполнение при инкрементировании в Go?
📌 Чтобы обнаружить целочисленное переполнение при выполнении операции инкрементального увеличения значения переменной типа, основанного на определенном размере (
🔸 Например, в случае с
🔸 Эта функция проверяет, достигла ли переменная значения
🔸 А что насчет типов
До версии Go 1.17 приходилось создавать эти константы вручную. Теперь же
Логика та же самая для
📌 Чтобы обнаружить целочисленное переполнение при выполнении операции инкрементального увеличения значения переменной типа, основанного на определенном размере (
int8
, int16
, int32
, int64
, uint8
, uint16
, uint32
или uint64
), можно сравнивать это значение с математическими константами. 🔸 Например, в случае с
int32
:func Inc32(counter int32) int32 {
if counter == math.MaxInt32 {
panic("int32 overflow")
}
return counter + 1
}
🔸 Эта функция проверяет, достигла ли переменная значения
math.MaxInt32
. Если да, то ее увеличение приведет к переполнению.🔸 А что насчет типов
int
и uint
? До версии Go 1.17 приходилось создавать эти константы вручную. Теперь же
math.MaxInt
, math.MinInt
и math.MaxUint
стали частью пакета math
. Если нужно проверить на переполнение переменную типа int
, можно сделать это с помощью math.MaxInt
:func IncInt(counter int) int {
if counter == math.MaxInt {
panic("int overflow")
}
return counter + 1
}
Логика та же самая для
uint
. Можно использовать math.MaxUint
:func IncUint(counter uint) uint {
if counter == math.MaxUint {
panic("uint overflow")
}
return counter + 1
}
👍11
💬 Какие могут быть побочные эффекты от именованных параметров результата функции?
◆ Именованные параметры результата могут оказаться полезны в некоторых ситуациях, но поскольку их инициализация происходит с присваиванием им нулевого значения, то их применение иногда может привести к малозаметным багам.
🤔 Что не так с этим кодом?
◆ На первый взгляд ошибка может быть неочевидной. Ошибка, возвращаемая в области видимости
◆ Код скомпилируется, потому что
◆ Один из возможных выходов — сделать переменную err равной
◆ Мы продолжаем возвращать
◆ Важно помнить, что каждый такой параметр инициализируется своим нулевым значением.
◆ Другой вариант — использовать пустой оператор return:
◆ Но при этом нарушается правило о том, что не нужно смешивать в одном фрагменте кода пустые операторы
💡 Применение именованных параметров результата не всегда равно требованию применять пустые операторы
◆ Именованные параметры результата могут оказаться полезны в некоторых ситуациях, но поскольку их инициализация происходит с присваиванием им нулевого значения, то их применение иногда может привести к малозаметным багам.
🤔 Что не так с этим кодом?
func (l loc) getCoordinates(ctx context.Context, address string) (
lat, lng float32, err error) {
isValid := l.validateAddress(address)
if !isValid {
return 0, 0, errors.New("invalid address")}
if ctx.Err() != nil {
return 0, 0, err
}
}
◆ На первый взгляд ошибка может быть неочевидной. Ошибка, возвращаемая в области видимости
if ctx.Err() != nil
, — это err
. Но мы не присвоили переменной err
никакого значения. Ей по-прежнему присвоено nil
. Следовательно, этот код всегда будет возвращать ошибку nil
.◆ Код скомпилируется, потому что
err
была инициализирована нулевым значением благодаря именованным параметрам результата. Без присвоения имени мы получили бы ошибку компиляции:Unresolved reference 'err'
◆ Один из возможных выходов — сделать переменную err равной
ctx.Err()
:if err := ctx.Err(); err != nil {
return 0, 0, err
}
◆ Мы продолжаем возвращать
err
, но сначала присваиваем ей результат ctx.Err()
. Обратите внимание, что err
в этом примере затеняет переменную результата.◆ Важно помнить, что каждый такой параметр инициализируется своим нулевым значением.
◆ Другой вариант — использовать пустой оператор return:
if err = ctx.Err(); err != nil {
return
}
◆ Но при этом нарушается правило о том, что не нужно смешивать в одном фрагменте кода пустые операторы
return
с такими же операторами, но с аргументами. 💡 Применение именованных параметров результата не всегда равно требованию применять пустые операторы
return
. Иногда можно просто использовать именованные параметры результата, чтобы сделать сигнатуру функции более чистой.👍7
💬 Какие подводные камни существуют при создании копии срезов в Go?
📌 Встроенная функция
🔸В следующем примере мы создаем один срез, копируем его элементы в другой и получаем
🔸Чтобы эффективно использовать функцию
🔸В предыдущем примере
🔸Если мы хотим выполнить полное копирование, второй срез должен иметь длину больше или равную длине исходного. Здесь мы устанавливаем длину, отталкиваясь от параметров исходного среза:
🔸Поскольку
💡Другая распространенная ошибка — инвертировать порядок аргументов при вызове функции
🔹Использование встроенной функции
🔹Мы добавляем элементы из исходного среза в другой, нулевой. Следовательно, этот код создает копию среза длиной
📌 Встроенная функция
copy
позволяет копировать элементы из исходного среза в другой. Рассмотрим распространенную ошибку, которая приводит к копированию неправильного количества элементов.🔸В следующем примере мы создаем один срез, копируем его элементы в другой и получаем
[]
вместо [0 1 2]
:src := []int{0, 1, 2}
var dst []int
copy(dst, src)
fmt.Println(dst)
🔸Чтобы эффективно использовать функцию
copy
, важно понимать, что число элементов, скопированных в другой срез, определяется минимумом между: длиной исходного среза и длиной второго среза.🔸В предыдущем примере
src
— это срез длиной 3
, а dst
— срез с нулевой длиной, поскольку он инициализируется со своим нулевым значением. Поэтому функция copy
копирует количество элементов, равное минимуму в наборе 3
и 0
: здесь этот минимум будет равен 0
. Поэтому полученный срез будет пустым. 🔸Если мы хотим выполнить полное копирование, второй срез должен иметь длину больше или равную длине исходного. Здесь мы устанавливаем длину, отталкиваясь от параметров исходного среза:
src := []int{0, 1, 2}
dst := make([]int, len(src))
copy(dst, src)
fmt.Println(dst)
🔸Поскольку
dst
теперь срез, инициализированный с длиной, равной 3
, то копируются три элемента. На этот раз результатом будет [012]
.💡Другая распространенная ошибка — инвертировать порядок аргументов при вызове функции
copy
. Помните, что срез, в который происходит копирование, — первый аргумент, а срез-источник — второй.🔹Использование встроенной функции
copy
— не единственный способ копирования элементов среза. Есть другие альтернативы:src := []int{0, 1, 2}
dst := append([]int(nil), src...)
🔹Мы добавляем элементы из исходного среза в другой, нулевой. Следовательно, этот код создает копию среза длиной
3
и емкостью 3
. Однако использование функции copy
более идиоматично и, следовательно, легче для понимания, даже несмотря на то, что требует больше кода.👍11❤1
💬 Какие подводные камни могут возникнуть при работе с циклом range в Go?
📌 Синтаксис цикла
◆ Рассмотрим пример, где к срезу добавляется элемент, по которому мы выполняем итерацию.
🤔 Завершится ли этот цикл?
◆ Чтобы понять суть, следует помнить, что при использовании цикла
◆ В этом контексте слово «вычисляется» означает, что предоставленное выражение копируется во временную переменную, а затем цикл
◆ Каждый шаг приводит к добавлению нового элемента. Но за три шага мы прошлись по всем его элементам. Длина временного среза, используемого в
◆ Такое поведение отличается от классического цикла
◆ В этом примере цикл никогда не закончится. Значение выражения
💡 Чтобы правильно использовать циклы в Go, важно помнить об этой разнице. При использовании
📌 Синтаксис цикла
range
требует наличия выражения. Например, в цикле for i, v := range exp
, exp
— это выражение. Это может быть строка, массив, указатель на массив, срез, map или канал. ◆ Рассмотрим пример, где к срезу добавляется элемент, по которому мы выполняем итерацию.
🤔 Завершится ли этот цикл?
s := []int{0, 1, 2}
for range s {
s = append(s, 10)
}
◆ Чтобы понять суть, следует помнить, что при использовании цикла
range
указываемое выражение вычисляется только один раз — перед началом цикла. ◆ В этом контексте слово «вычисляется» означает, что предоставленное выражение копируется во временную переменную, а затем цикл
range
выполняет итерации. В этом примере при вычислении выражения s
результатом будет копия среза. Цикл range
использует эту временную переменную. Исходный срез s
также обновляется во время каждой итерации. ◆ Каждый шаг приводит к добавлению нового элемента. Но за три шага мы прошлись по всем его элементам. Длина временного среза, используемого в
range
, остается равна 3
, поэтому цикл завершается после трех итераций.◆ Такое поведение отличается от классического цикла
for
:s := []int{0, 1, 2}
for i := 0; i < len(s); i++ {
s = append(s, 10)
}
◆ В этом примере цикл никогда не закончится. Значение выражения
len(s)
вычисляется во время каждой итерации, и раз мы продолжаем добавлять элементы, то никогда не достигнем состояния завершения цикла. 💡 Чтобы правильно использовать циклы в Go, важно помнить об этой разнице. При использовании
range
помните, что вышеописанное поведение (выражение вычисляется только один раз) также применимо ко всем типам данных.🔥26👍5❤4
💬 Что такое deadline в контексте Go context?
🔸
•
•
🔸Семантика
🔸Рассмотрим пример, который каждые 4 секунды получает от радара данные о позиции самолета. Получив позицию, мы хотим поделиться ею с другими приложениями, для которых интерес представляет только последняя позиция.
🔸В нашем распоряжении есть интерфейс
🔸Этот метод принимает контекст и позицию. Предполагается, что конкретная реализация вызывает функцию для публикации сообщения брокеру (например, использование Sarama для публикации сообщения Kafka).
🔸Эта функция контекстно зависимая, это означает, что она может отменить запрос после отмены контекста. Предполагая, что мы не получаем существующий контекст, что нужно предоставить методу
🔸Создаваемый контекст должен сообщать о ней через 4 секунды, а если мы не смогли опубликовать позицию, то следует остановить вызов
🔸Код создает контекст с помощью функции
🔸В чем смысл вызова функции
🔸
deadline
указывает на момент времени, определяемый одним из следующих способов:•
time.Duration
с настоящего момента (например, через 250 мс); •
time.Time
(например, 2023-02-07 00:00:00 UTC).🔸Семантика
deadline
означает, что текущая деятельность должна быть остановлена, если этот крайний срок наступил. «Деятельность» — это, например, запрос типа ввод/вывод или горутина в состоянии ожидания получения сообщения из канала.🔸Рассмотрим пример, который каждые 4 секунды получает от радара данные о позиции самолета. Получив позицию, мы хотим поделиться ею с другими приложениями, для которых интерес представляет только последняя позиция.
🔸В нашем распоряжении есть интерфейс
publisher
, содержащий в себе одинединственный метод:type publisher interface {
Publish(ctx context.Context, position flight.Position) error
}
🔸Этот метод принимает контекст и позицию. Предполагается, что конкретная реализация вызывает функцию для публикации сообщения брокеру (например, использование Sarama для публикации сообщения Kafka).
🔸Эта функция контекстно зависимая, это означает, что она может отменить запрос после отмены контекста. Предполагая, что мы не получаем существующий контекст, что нужно предоставить методу
Publish
в качестве аргумента контекста? 🔸Создаваемый контекст должен сообщать о ней через 4 секунды, а если мы не смогли опубликовать позицию, то следует остановить вызов
Publish
:type publishHandler struct {
pub publisher
}
func (h publishHandler) publishPosition(position flight.Position) error {
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()
return h.pub.Publish(ctx, position)
}
🔸Код создает контекст с помощью функции
context.WithTimeout
, которая принимает тайм-аут и контекст. Поскольку publishPosition
не получает существующий контекст, мы создаем его из пустого контекста с помощью context.Background
. Между тем context.WithTimeout
возвращает две переменные: созданный контекст и функцию отмены func()
, которая отменит контекст после вызова. Передача созданного контекста в метод Publish
должна произойти не позднее чем через 4 секунды.🔸В чем смысл вызова функции
cancel
как функции defer
? WithTimeout
создает горутину, которая будет храниться в памяти в течение 4 секунд или до тех пор, пока не будет вызвана cancel
. Следовательно, вызов cancel
в качестве функции defer
означает, что при выходе из родительской функции контекст будет отменен, а созданная горутина остановлена. Это мера предосторожности, чтобы при возвращении мы не оставили в памяти сохраненные объекты.👍12
💬 В Go существует несколько способов возврата структур или их частей. Назовите основные.
1. Возврат копии структуры:
Здесь функция
2. Возврат указателя на структуру:
В этом случае функция
3. Изменение структуры, переданной по указателю:
Функция
4. Возврат части структуры:
Здесь функция
5. Возврат через интерфейс:
Предположим, у нас есть интерфейс
Функция
6. Использование срезов и мап структур:
7. Возврат структуры через канал:
Здесь функция отправляет структуру в канал, что позволяет использовать её в конкурентных операциях или между горутинами.
💡Эти подходы могут комбинироваться и адаптироваться под различные сценарии использования, учитывая требования к производительности и управлению памятью.
👉 Вдохновлено вопросом на StackOverflow
1. Возврат копии структуры:
type MyStruct struct { Value int }
func returnCopy() MyStruct {
return MyStruct{Value: 1}
}
Здесь функция
returnCopy
возвращает копию структуры MyStruct
. Изменения возвращаемой копии не затронут оригинал.2. Возврат указателя на структуру:
func returnPointer() *MyStruct {
return &MyStruct{Value: 2}
}
В этом случае функция
returnPointer
возвращает указатель на структуру MyStruct
. Это позволяет избежать копирования и работать непосредственно с объектом.3. Изменение структуры, переданной по указателю:
func modifyStruct(s *MyStruct) {
s.Value = 3
}
Функция
modifyStruct
ожидает указатель на структуру и изменяет её напрямую. Это позволяет функции влиять на исходный объект.4. Возврат части структуры:
func returnValue(s MyStruct) int {
return s.Value
}
Здесь функция
returnValue
возвращает только значение поля Value
из структуры.5. Возврат через интерфейс:
Предположим, у нас есть интерфейс
MyInterface
, и структура MyStruct
его реализует.type MyInterface interface { DoSomething() }
type MyStruct struct { /* ... */ }
func (m MyStruct) DoSomething() { /* ... */ }
func returnInterface() MyInterface {
return MyStruct{}
}
Функция
returnInterface
возвращает экземпляр MyStruct
, но тип возврата — интерфейс MyInterface
.6. Использование срезов и мап структур:
func returnSlice() []MyStruct {
return []MyStruct{{Value: 4}, {Value: 5}}
}
func returnMap() map[string]MyStruct {
return map[string]MyStruct{"first": {Value: 6}, "second": {Value: 7}}
}
7. Возврат структуры через канал:
func returnThroughChannel(ch chan MyStruct) {
ch <- MyStruct{Value: 8}
}
Здесь функция отправляет структуру в канал, что позволяет использовать её в конкурентных операциях или между горутинами.
💡Эти подходы могут комбинироваться и адаптироваться под различные сценарии использования, учитывая требования к производительности и управлению памятью.
👉 Вдохновлено вопросом на StackOverflow
Stack Overflow
Pointers vs. values in parameters and return values
In Go there are various ways to return a struct value or slice thereof. For individual ones I've seen:
type MyStruct struct {
Val int
}
func myfunc() MyStruct {
return MyStruct{Val: 1}
}
...
type MyStruct struct {
Val int
}
func myfunc() MyStruct {
return MyStruct{Val: 1}
}
...
👍12