1 Основы Struct
В языке Go struct является составным типом данных, используемым для объединения различных или идентичных типов данных в одну сущность. Structs занимают значительное положение в Go, поскольку они служат фундаментальным аспектом объектно-ориентированного программирования, хотя и с небольшими отличиями от традиционных языков объектно-ориентированного программирования.
Потребность в structs возникает из следующих аспектов:
- Организация переменных с тесными связями для улучшения поддерживаемости кода.
- Предоставление средства имитации "классов", облегчающих инкапсуляцию и агрегационные возможности.
- При взаимодействии с такими структурами данных, как JSON, записями баз данных и т. д., structs предлагают удобный инструмент отображения.
Организация данных с помощью structs позволяет более ясно представлять модели объектов реального мира, такие как пользователи, заказы и т. д.
2 Определение Struct
Синтаксис определения struct выглядит следующим образом:
type ИмяStruct struct {
Поле1 ТипПоля1
Поле2 ТипПоля2
// ... другие переменные-члены
}
- Ключевое слово
type
вводит определение struct. -
ИмяStruct
- это название типа struct, следуя соглашениям Go, обычно начинается с заглавной буквы, чтобы указать на его экспортируемость. - Ключевое слово
struct
обозначает, что это тип struct. - В фигурных скобках
{}
, определяются переменные-члены (поля) struct, каждое с последующим указанием его типа.
Типы переменных-членов struct могут быть любого типа, включая базовые типы (например, int
, string
и т. д.) и сложные типы (например, массивы, срезы, другие structs и т. д.).
Например, определим struct, представляющий человека:
type Человек struct {
Имя string
Возраст int
Почты []string // может включать сложные типы, например, срезы
}
В приведенном выше коде у struct Человек
три переменные-члена: Имя
типа string, Возраст
типа integer и Почты
типа среза строк, указывающего, что у человека может быть несколько адресов электронной почты.
3 Создание и Инициализация Struct
3.1 Создание экземпляра Struct
Существуют два способа создания экземпляра struct: прямое объявление или с использованием ключевого слова new
.
Прямое объявление:
var человек Человек
Приведенный выше код создает экземпляр человек
типа Человек
, где каждая переменная-член struct имеет нулевое значение соответствующего типа.
С использованием ключевого слова new
:
человек := new(Человек)
Создание struct с использованием ключевого слова new
приводит к указателю на struct. Переменная человек
в этот момент имеет тип *Человек
, указывающий на вновь выделенную переменную типа Человек
, где переменные-члены инициализированы нулевыми значениями.
3.2 Инициализация экземпляров Struct
Экземпляры struct могут быть инициализированы сразу при их создании с использованием двух методов: с именами полей или без имен полей.
Инициализация с именами полей:
человек := Человек{
Имя: "Алиса",
Возраст: 30,
Почты: []string{"[email protected]", "[email protected]"},
}
При инициализации формой присвоения полей порядок инициализации не должен совпадать с порядком объявления struct, и любые неинициализированные поля сохранят свои нулевые значения.
Инициализация без имен полей:
человек := Человек{"Боб", 25, []string{"[email protected]"}}
При инициализации без имен полей убедитесь, что начальные значения каждой переменной-члена соответствуют порядку, в котором был определен struct, и ни одно поле не может быть опущено.
Кроме того, struct можно инициализировать с определенными полями, и любые не указанные поля будут принимать нулевые значения:
человек := Человек{Имя: "Чарли"}
В этом примере инициализирован только поле Имя
, в то время как поля Возраст
и Почты
будут принимать соответствующие нулевые значения.
4 Доступ к Переменным-членам Struct
Доступ к переменным-членам struct в Go очень прост и выполняется с помощью оператора точки (.
). Если у вас есть переменная struct, вы можете читать или изменять ее значения таким образом.
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
// Create a variable of type Person
p := Person{"Alice", 30}
// Access struct members
fmt.Println("Name:", p.Name)
fmt.Println("Age:", p.Age)
// Modify member values
p.Name = "Bob"
p.Age = 25
// Access the modified member values again
fmt.Println("\nUpdated Name:", p.Name)
fmt.Println("Updated Age:", p.Age)
}
В этом примере мы сначала определяем структуру Person
с двумя переменными-членами, Name
и Age
. Затем мы создаем экземпляр этой структуры и демонстрируем, как считывать и изменять эти члены.
5. Композиция и встраивание структур
Структуры могут существовать не только независимо, но и могут быть скомпонованы и вложены друг в друга для создания более сложных структур данных.
5.1 Анонимные структуры
Анонимная структура не объявляет новый тип явно, а непосредственно использует определение структуры. Это полезно, когда вам нужно создать структуру один раз и использовать ее просто, избегая создания ненужных типов.
Пример:
package main
import "fmt"
func main() {
// Определение и инициализация анонимной структуры
person := struct {
Name string
Age int
}{
Name: "Eve",
Age: 40,
}
// Доступ к членам анонимной структуры
fmt.Println("Name:", person.Name)
fmt.Println("Age:", person.Age)
}
В этом примере, вместо создания нового типа, мы напрямую определяем структуру и создаем экземпляр этой структуры. В этом примере показано, как инициализировать анонимную структуру и получить доступ к ее членам.
5.2 Встраивание структур
Встраивание структур включает в себя вложение одной структуры в качестве члена другой структуры. Это позволяет нам строить более сложные модели данных.
Пример:
package main
import "fmt"
// Определение структуры Address
type Address struct {
City string
Country string
}
// Встраивание структуры Address в структуру Person
type Person struct {
Name string
Age int
Address Address
}
func main() {
// Инициализация экземпляра Person
p := Person{
Name: "Charlie",
Age: 28,
Address: Address{
City: "New York",
Country: "USA",
},
}
// Доступ к членам встроенной структуры
fmt.Println("Name:", p.Name)
fmt.Println("Age:", p.Age)
// Доступ к членам структуры Address
fmt.Println("City:", p.Address.City)
fmt.Println("Country:", p.Address.Country)
}
В этом примере мы определяем структуру Address
и встраиваем ее в качестве члена в структуру Person
. При создании экземпляра Person
одновременно создается также экземпляр Address
. Мы можем получить доступ к членам встроенной структуры с использованием точечной нотации.
## 6. Методы структур
Возможности объектно-ориентированного программирования (ООП) могут быть реализованы через методы структур.
### 6.1 Основные концепции методов
В языке Go, хотя нет традиционного понятия классов и объектов, аналогичные возможности ООП могут быть достигнуты путем привязки методов к структурам. Метод структуры - это специальный вид функции, связанный с определенным типом структуры (или указателем на структуру), что позволяет этому типу иметь свой набор методов.
```go
// Определение простой структуры
type Rectangle struct {
length, width float64
}
// Определение метода для структуры Rectangle для вычисления площади прямоугольника
func (r Rectangle) Area() float64 {
return r.length * r.width
}
6.2 Получатели значений и указателей
Методы могут быть категоризированы как получатели значений и получатели указателей в зависимости от типа получателя. При использовании получателей значений происходит копирование структуры для вызова метода, в то время как получатели указателей используют указатель на структуру и могут изменять исходную структуру.
// Определение метода с получателем значения
func (r Rectangle) Perimeter() float64 {
return 2 * (r.length + r.width)
}
// Определение метода с получателем указателя, который может изменять структуру
func (r *Rectangle) SetLength(newLength float64) {
r.length = newLength // можно изменить значение исходной структуры
}
В приведенном выше примере Perimeter
- это метод с получателем значения, вызов которого не изменит значение Rectangle
. Однако SetLength
- это метод с получателем указателя, вызов этого метода повлияет на исходный экземпляр Rectangle
.
6.3 Вызов метода
Вы можете вызывать методы структуры с использованием переменной структуры и ее указателя.
func main() {
rect := Rectangle{length: 10, width: 5}
// Вызов метода с получателем значения
fmt.Println("Площадь:", rect.Area())
// Вызов метода с получателем значения
fmt.Println("Периметр:", rect.Perimeter())
// Вызов метода с получателем указателя
rect.SetLength(20)
// Снова вызываем метод с получателем значения, обратите внимание, что длина была изменена
fmt.Println("После изменений, Площадь:", rect.Area())
}
При вызове метода с использованием указателя Go автоматически обрабатывает преобразование между значениями и указателями, независимо от того, был ли ваш метод определен с получателем значения или указателя.
6.4 Выбор типа получателя
При определении методов следует решить, следует ли использовать получатель значения или получатель указателя, исходя из ситуации. Вот некоторые общие рекомендации:
- Если методу необходимо изменить содержимое структуры, используйте получатель указателя.
- Если структура большая, и стоимость копирования высока, используйте получатель указателя.
- Если вы хотите, чтобы метод изменял значение, на которое указывает получатель, используйте получатель указателя.
- Из-за эффективности, даже если вы не изменяете содержимое структуры, разумно использовать получатель указателя для большой структуры.
- Для маленьких структур, или когда требуется только чтение данных без необходимости модификации, получатель значения часто является более простым и эффективным.
Через методы структуры мы можем имитировать некоторые черты объектно-ориентированного программирования в Go, такие как инкапсуляция и методы. Этот подход в Go упрощает концепцию объектов, предоставляя при этом достаточную функциональность для организации и управления связанными функциями.
7 Структуры и сериализация в JSON
В Go часто необходимо сериализовать структуру в формат JSON для сетевой передачи данных или в качестве конфигурационного файла. Точно так же нам нужна возможность десериализовать JSON в экземпляры структуры. Пакет encoding/json
в Go предоставляет эту функциональность.
Вот пример того, как преобразовывать структуру в JSON и обратно:
package main
import (
"encoding/json"
"fmt"
"log"
)
// Определим структуру Person и используем теги json для определения соответствия между полями структуры и именами полей JSON
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Emails []string `json:"emails,omitempty"`
}
func main() {
// Создаем новый экземпляр Person
p := Person{
Name: "John Doe",
Age: 30,
Emails: []string{"[email protected]", "[email protected]"},
}
// Сериализация в JSON
jsonData, err := json.Marshal(p)
if err != nil {
log.Fatalf("Сериализация JSON не удалась: %s", err)
}
fmt.Printf("Формат JSON: %s\n", jsonData)
// Десериализация в структуру
var p2 Person
if err := json.Unmarshal(jsonData, &p2); err != nil {
log.Fatalf("Десериализация JSON не удалась: %s", err)
}
fmt.Printf("Восстановленная структура: %#v\n", p2)
}
В приведенном выше коде мы определили структуру Person
, включая поле типа slice с опцией "omitempty". Эта опция указывает, что если поле пустое или отсутствует, оно не будет включено в JSON.
Мы использовали функцию json.Marshal
для сериализации экземпляра структуры в JSON, и функцию json.Unmarshal
для десериализации JSON данных в экземпляр структуры.
8 Расширенные темы в структурах
8.1 Сравнение структур
В Go можно напрямую сравнивать два экземпляра структур, но это сравнение основано на значениях полей внутри структур. Если все значения полей одинаковы, то два экземпляра структур считаются равными. Следует отметить, что не все типы полей можно сравнивать. Например, структуру, содержащую срезы, нельзя напрямую сравнивать.
Ниже приведен пример сравнения структур:
package main
import "fmt"
type Point struct {
X, Y int
}
func main() {
p1 := Point{1, 2}
p2 := Point{1, 2}
p3 := Point{1, 3}
fmt.Println("p1 == p2:", p1 == p2) // Вывод: p1 == p2: true
fmt.Println("p1 == p3:", p1 == p3) // Вывод: p1 == p3: false
}
В этом примере p1
и p2
считаются равными, потому что все значения их полей одинаковы. А p3
не равно p1
, потому что значение Y
отличается.
8.2 Копирование структур
В Go экземпляры структур могут быть скопированы путем присваивания. Является ли эта копия глубокой или поверхностной, зависит от типов полей внутри структуры.
Если структура содержит только базовые типы (такие как int
, string
и др.), копия будет глубокой. Если структура содержит ссылочные типы (например, срезы, карты и др.), то копия будет поверхностной, и у оригинального экземпляра и вновь скопированного экземпляра будут общие данные ссылочных типов.
Ниже приведен пример копирования структуры:
package main
import "fmt"
type Data struct {
Numbers []int
}
func main() {
// Инициализация экземпляра структуры Data
original := Data{Numbers: []int{1, 2, 3}}
// Копирование структуры
copied := original
// Изменение элементов скопированного среза
copied.Numbers[0] = 100
// Просмотр элементов оригинального и скопированного экземпляров
fmt.Println("Original:", original.Numbers) // Вывод: Original: [100 2 3]
fmt.Println("Copied:", copied.Numbers) // Вывод: Copied: [100 2 3]
}
Как показано в примере, у экземпляров original
и copied
есть общий срез, поэтому изменение данных среза в copied
также повлияет на данные среза в original
.
Для избежания данной проблемы можно достичь истинной глубокой копии, явно скопировав содержимое среза в новый срез:
newNumbers := make([]int, len(original.Numbers))
copy(newNumbers, original.Numbers)
copied := Data{Numbers: newNumbers}
Таким образом, любые изменения в copied
не будут влиять на original
.