Пакетом 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





