Go: точечная вставка значения в структуру

Пакетом 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 рекомендуется использовать, когда простота и сопровождаемость кода важнее скорости выполнения.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Vasile B.: Go: Insert the value into the structure with a dot

Предыдущая статьяПринципы SOLID в React: так ли все с ними гладко?
Следующая статьяПолучение одного события разными группами получателей в Kafka с Spring Boot