Пакетом dot
решается проблема добавления данных во вложенных структурах, ассоциативных массивах, срезах и каналах любой сложности и различных типов данных. Если точный путь к требуемому полю известен, но на этом пути имеется карта, нужно сначала корректно ее инициализировать, проверить наличие такого ключа, а затем вставить значение.
Для простой структуры этот пакет не нужен, а вот для иерархии посложнее проект dot Golang с открытым исходным кодом придется кстати.
Расставьте точки над «i», контролируйте данные
Приведу простой пример проблемы, с которой столкнулся, хотя иерархия структур у меня посложнее и пути к полям в будущем могут поменяться:
type Second struct {
Items []string
}
type First struct {
Store map[string]Second
}
Нельзя написать так:
a := First{}
a.Store["nike"].Items = append(a.Store["nike"].Items, "Air")
Иначе получится ошибка:
cannot assign to struct field a.Store["nike"].Items in map
А так?
a := First{}
a.Store["nike"] = Second{
Items: append(a.Store["nike"].Items, "Air"),
}
Но сначала инициализируем карту…
panic: assignment to entry in nil map
Вот быстрое и короткое решение:
func main() {
a := First{
Store: make(map[string]Second),
}
a.Store["nike"] = Second{
Items: append(a.Store["nike"].Items, "Air"),
}
}
Но корректнее написать его со всеми проверками:
func main() {
a := First{
Store: make(map[string]Second), // Инициализируем карту
}
key := "nike"
value := "Air"
// Проверяем наличие ключа в карте
if second, ok := a.Store[key]; ok {
// Добавляем новое значение в имеющуюся структуру «Second»
second.Items = append(second.Items, value)
a.Store[key] = second
} else {
// Создаем новую структуру «Second» с новым значением и добавляем в карту
a.Store[key] = Second{
Items: []string{value},
}
}
fmt.Println(a.Store["nike"].Items)
}
Для простой структуры это тривиально. Но что, если имеется несколько вложенных структур с данными? Такое часто случается при работе с данными JSON и их преобразовании в структуру.
Применение пакета Dot
С dot
вся работа сводится к такому коду:
func main() {
a := First{}
// Передача структуры в «Dot»
obj, _ := dot.New(&a)
// Указываем путь к полю из структуры и значение
err := obj.Insert("Store.nike.Items.-1", "Air")
}
Описание пути:
Store
— поле из структурыFirst
;nike
— ключ карты;Items
— поле из структурыSecond
;-1
— самое странное, это срез, в самый конец добавляется значение или инициализируется новое.
Функционал
- Вставка через разделенный точками путь: значения вставляются в поля структуры, массивы, срезы, ассоциативные массивы и каналы с помощью разделенного точками пути, например
Field1.Field2.Field3
. - Поддержка вложенных структур, в том числе сложных, глубоко вложенных структур Go.
- Прием любых типов значений: возможность разнообразных манипуляций.
- Поддержка типов коллекций: помимо полей структур, пакет хорош и для вставки в
срезы
,массивы
,ассоциативные массивы
и дажеканалы
. Это по-настоящему универсальный инструмент для манипулирования структурами данных на Go. - Заполнители
ключей карт
для гибкого взаимодействия с картами Go. - Индексация массива и среза: меняем значения в массиве или срезе, указав в пути индекс. Конкретные элементы легко изменяются без перебора всей структуры данных.
- Добавление среза: указав
-1
в пути, легко добавляем в конец среза новое значение. Этим упрощается динамическое изменение данных, повышается универсальность программ Go. - Интеллектуальная замена значений для сохранения данных невредимыми, а кода — чистым независимо от того, работаете вы с базовыми типами или сложными структурами данных.
Начало работы
Сначала загружаем и устанавливаем dot
:
go get github.com/mowshon/dot
Импортируем в файлы Go:
import "github.com/mowshon/dot"
Подробно и с наглядными примерами рассмотрим различные методы и функционал мощного и элегантного dot
, инструмента для точной и удобной работы со сложными структурами данных на Go.
Использование конструктора
Сначала сделаем экземпляр с целевой структурой.
Передача переменной в конструктор
Он создается и возвращается методом конструктора dot.New
с одним аргументом: указателем на переменную. Обычно эта переменная — структура, реже — срез, массив, ассоциативный массив или канал.
Вот базовый пример создания экземпляра dot
:
type MyStruct struct {
Field1 struct {
Field2 string
}
}
func main() {
data := MyStruct{}
obj, err := dot.New(&data)
if err != nil {
// обрабатываем ошибку
}
// Теперь с помощью «obj» можно манипулировать данными...
}
В этом примере сначала определяется структура MyStruct
. Внутри функции main
создаем экземпляр data
этой структуры. Затем в dot.New
передаем указатель на data
, откуда возвращаются экземпляр dot
, то есть obj
, и ошибка err
.
Если при создании экземпляра dot
обнаруживается ошибка, в dot.New
возвращается ненулевое err
. В err
всегда проверяем, что экземпляр dot
создан.
После этого экземпляром dot
, в примере это obj
, с данными data
выполняются различные операции, подробнее о них — ниже.
Создав экземпляр, рассмотрим функциональность dot
: метод Insert
и его универсальность при изменении различных типов данных и структур на Go.
Вставка в поля структуры
Инстанцировав с помощью конструктора объект dot
, приступим к работе со структурами данных. Мощнейший функционал dot
— метод Insert
для вставки значений в поля структуры с помощью разделенного точками пути.
Синтаксис метода Insert
Методу Insert
требуется два аргумента:
- Первый — это строка, путь к полю, куда вставляется значение. Путь строится в виде последовательности разделенных точками имен полей, начиная с верхнего имени поля структуры.
- Второй аргумент — вставляемое значение, любой тип данных Go.
Вот пример вставки значения в поле структуры методом Insert
:
type MyStruct struct {
Field1 struct {
Field2 string
}
}
func main() {
data := MyStruct{}
obj, err := dot.New(&data)
if err != nil {
// обрабатываем ошибку
}
err = obj.Insert("Field1.Field2", "My Value")
if err != nil {
// обрабатываем ошибку
}
fmt.Println(data.Field1.Field2) // Выводится: мое значение My Value
}
В этом примере мы вставляем строку My Value в поле Field2
, вложенное в поле Field1
структуры MyStruct
. Разделенный точками путь Field1.Field2
проходится методом Insert
до нужного места, куда затем вставляется указанное значение.
Если при попытке вставить значение в методе Insert
обнаруживается ошибка, возвращается ненулевое err
. В err
всегда проверяем, что операция успешна.
С dot
поля даже самых глубоко вложенных структур находятся друг от друга лишь на расстоянии разделенного точками пути. Далее расскажем о работе со сложными структурами данных.
Ассоциативные массивы
В dot
поддерживаются не только поля обычных структур, но и сложные типы данных вроде ассоциативных массивов. Рассмотрим, как манипулируют ими в структурах методом Insert
.
Вставка значения в поле ассоциативного массива
Ассоциативный массив — это встроенный тип данных на Go, которым значения ассоциируются с ключами. Методом Insert
значение вставляется в поле ассоциативного массива так же, как в поля структуры, только в пути теперь будет ключ ассоциативного массива.
Вот пример:
type MyStruct struct {
MyMap map[string]int
}
func main() {
data := MyStruct{}
obj, err := dot.New(&data)
if err != nil {
// обрабатываем ошибку
}
err = obj.Insert("MyMap.year", 2023)
if err != nil {
// обрабатываем ошибку
}
fmt.Println(data.MyMap["year"]) // Выводится: 2023
}
Здесь мы вставляем в ассоциативный массив MyMap
число 2023 с ключом "year"
. Ассоциативный массив находится методом Insert
по первой части пути myMap
, затем значение вставляется в указанный в пути ключ ассоциативного массива.
Снова проверяем в err
, возвращаемым методом Insert
, что операция успешна.
Умея так легко работать с ассоциативными массивами, вы скорее освоите управление сложными структурами данных. Далее расскажем о работе dot
со срезами и массивами.
Вставка и замена в срезах
dot
— универсальный инструмент для работы не только с полями структур и ассоциативными массивами, но и динамическими коллекциями вроде срезов. Рассмотрим, как методом Insert
модифицируют срезы в структурах.
Вставка значения в конец среза
Методом Insert
новое значение вставляется в конец среза указанием -1
в пути:
type Data struct {
Title string
}
type MyStruct struct {
Field3 []Data
}
func main() {
data := MyStruct{}
obj, err := dot.New(&data)
if err != nil {
// обрабатываем ошибку
}
err = obj.Insert("Field3.-1", Data{Title: "new Title"})
if err != nil {
// обрабатываем ошибку
}
fmt.Println(data.Field3[0].Title) // Выводится: «new Title»
}
Здесь к концу среза Field3
добавляется Data{Title: "new Title"}
. Этим -1
в пути "Field3.-1"
указывается на добавление к срезу нового значения.
Замена значения по индексу среза
Методом Insert
по конкретному индексу в срезе заменяется значение, сам индекс указывается в пути:
type Data struct {
Title string
}
type MyStruct struct {
Field3 []Data
}
func main() {
data := MyStruct{Field3: []Data{{Title: "old Title"}}}
obj, err := dot.New(&data)
if err != nil {
// обрабатываем ошибку
}
err = obj.Insert("Field3.0.Title", "replace Title")
if err != nil {
// обрабатываем ошибку
}
fmt.Println(data.Field3[0].Title) // Выводится: «replace Title»
}
В этом примере строкой "replace Title"
заменяется строка с указанным индексом в срезе Field3
. Индексом 0
в пути "Field3.0.Title"
определяется положение изменяемого элемента среза.
С dot
срезы в структурах легко модифицируются. Далее расскажем о работе этого пакета с массивами.
Массивы
Массивы на Go — это последовательности фиксированной длины элементов одного типа. Элементы массива изменяются в dot
методом Insert
. Но к массивам фиксированной длины новые элементы не добавляются, поэтому значение -1
в пути не применяется.
Вот пример замены значения в массиве с dot
:
type MyStruct struct {
Field5 [3]int
}
func main() {
data := MyStruct{}
obj, _ := dot.New(&data)
err := obj.Insert("Field5.1", 2023)
if err != nil {
// обрабатываем ошибку
}
fmt.Println(data.Field5[1]) // Выводится: 2023
}
Здесь Field5
— целочисленный массив размером 3. В методе Insert
целое число с индексом 1
заменяется на 2023
. Индексом 1
в пути "Field5.1"
определяется позиция изменяемого элемента массива.
С dot
массивы в структурах легко модифицируются, это дополнительный инструмент для работы со сложными структурами данных на Go.
Вставка в каналы
Каналы — мощный функционал Go для взаимодействия двух горутин и синхронизации их выполнения. С dot
значения в каналы вставляются методом Insert
.
Вставка в канал значения
В структурах методом Insert
значение отправляется в поле канала:
type MyStruct struct {
FieldChannel chan string
}
func main() {
data := MyStruct{}
obj, err := dot.New(&data)
if err != nil {
// обрабатываем ошибку
}
go func() {
err = obj.Insert("FieldChannel", "value for channel")
if err != nil {
// обрабатываем ошибку
}
}()
message := <-data.FieldChannel
fmt.Println(message) // Выводится: значение для канала
}
В этом примере в канал FieldChannel
отправляется "value for channel"
. Необходимые операции для отправки значения в канал выполняются в методе Insert
.
Эта операция эквивалентна коду на Go:
data.FieldChannel <- "value for channel"
Рассмотрев базовое применение метода Insert
в различных контекстах, переходим к расширенному функционалу dot
для работы с ключами карт разных типов с помощью заполнителей.
Работа с различными ключами карт: заполнители
Работать с картами, ключи которых не являются строками, непросто. Но для dot
это не проблема: им определяются заполнители для ключей карт разных типов.
Определение и замена заполнителей
Заполнитель в dot
— это представление ключа карты любого типа. Заполнители здесь определяются методом Replace
:
type Key string
const UniqueID Key = "Some"
type MyStruct struct {
Field4 map[Key]string
}
func main() {
data := MyStruct{}
obj, _ := dot.New(&data)
// Определяем заполнитель
obj.Replace("First", UniqueID)
// Используем заполнитель в пути
err := obj.Insert("Field4.First", "value for map")
if err != nil {
// обрабатываем ошибку
}
fmt.Println(data.Field4[UniqueID]) // Выводится: значение для карты
}
В методе Replace
в этом примере First
определяется как заполнитель для UniqueID
. А после применяется в строке пути, передаваемой в метод Insert
.
Прежде чем вставлять значение, обнаруживаемый в пути заполнитель заменяется методом Insert
на соответствующий ключ карты. Эта операция эквивалентна коду на Go:
data.Field4[UniqueID] = "value for map"
На этом завершаем рассмотрение мощного функционала dot
для работы со сложными структурами данных.
Преимущества, недостатки и варианты применения
Наряду с большой гибкостью и удобством при работе со сложными структурами данных на Go, у пакета dot
имеются потенциальные недостатки.
- Влияние на производительность. Применяемое
dot
отражение при доступе к структурам данных и манипулировании ими медленнее, чем прямой доступ к полям и манипулирование ими. Это потенциальный недостаток не для маленьких, а для высокопроизводительных приложений, где важна каждая миллисекунда. - Обработка ошибок. Если путь не найден или имеется несоответствие типов, пакетом возвращается ошибка. Для отлова ошибок это неплохо, но чревато добавлением сложности коду: эти ошибки нужно корректно обрабатывать.
- Потеря типобезопасности. Одно из преимуществ Go — надежная система типов, в которой многие ошибки отлавливаются во время компиляции. Но с пакетом
dot
пути указываются в виде строк и применяется отражение, поэтому некоторые ошибки отлавливаются только во время выполнения, что чревато увеличением времени отладки.
Несмотря на потенциальные недостатки, пакет dot
невероятно полезен в различных ситуациях.
- Преобразования данных: процесс преобразования сложных структур данных, например вложенных структур, ассоциированных массивов, массивов, с пакетом
dot
значительно упрощается. - Управление конфигурацией: если приложением Go задействуются сложные конфигурационные данные, которые хранятся во вложенных структурах, с
dot
облегчается извлечение и обновление значений конфигурации. - JSON Path-подобные операции: чтобы выполнять их со структурами данных Go, в
dot
имеется аналогичный функционал.
Использование пакета вроде dot
зависит от конкретного рабочего сценария с учетом удобства, сопровождаемости, производительности, скорости разработки.
Результаты тестов
Мы провели сравнительное тестирование пакета dot
и собственного стиля Golang при вставке данных в различные типы данных в 5-уровневой вложенной структуре:
BenchmarkDotInsert-12 320488 3536 ns/op
BenchmarkNativeInsert-12 442920729 2.661 ns/op
Интерпретация
BenchmarkDotInsert-12
— функция, которой тестируется производительность пакетаdot
. В тесте выполнилось320 488
итераций функции за время по умолчанию 1 сек., на каждую операцию потребовалось3536
наносекунд, или3536
микросекунд.BenchmarkNativeInsert-12
— функция, которой тестируется производительность эквивалентных собственных операций Golang. В тесте выполнилось аж442 920 729
итераций за время по умолчанию, на каждую операцию потребовалась всего2,661
наносекунды.
Заключение
По результатам теста очевидно, что собственные операции Golang быстрее операций пакета dot
. Однако в коде важна не только скорость. Собственный стиль Golang может быть быстрее, но детализированнее и сложнее, особенно при работе с глубоко вложенными структурами данных.
С другой стороны, у пакета dot
чище и проще синтаксис: его легче читать и сопровождать. С ним надежно обрабатываются ошибки, а это важное преимуществом во многих сценариях. Следовательно, пакет dot
рекомендуется использовать, когда простота и сопровождаемость кода важнее скорости выполнения.
Читайте также:
- Работа с WebAssembly в Golang
- Реализация односвязного списка в Golang
- Как создавать легкие платформонезависимые приложения на Go — без JS и BS
Читайте нас в Telegram, VK и Дзен
Перевод статьи Vasile B.: Go: Insert the value into the structure with a dot