Маленькая книга о Go – Глава 2: Структуры
Go не является объектно-ориентированным языком (ОО-языком), таким как C++, Java, Ruby или C#. В нем нет объектов, нет наследования и многих других понятий, свойственных ОО-языкам, полиморфизма или перегрузки.
В Go есть структуры, которые могут быть связаны с методами. В Go также есть простая, но эффективная форма композиции. В целом, это приводит к более простому коду, но бывают случаи когда вам будет не хватать некоторых возможностей ООП (стоит отметить, что композиция вместо наследования старый лозунг и Go первый язык, который я использовал, занимающий твердую позицию по этому вопросу).
Хотя Go и не использует ООП, вы заметите много похожего между определением структуры и использованием классов. В качестве простого примера возьмем структуру Saiyan
:
type Saiyan struct {
Name string
Power int
}
Мы скоро увидим, как добавить метод к этой структуре, как вы добавляли бы методы к классам. Но перед тем, как это сделать, мы вернемся назад к объявлениям.
Объявления и инициализация
Когда мы впервые рассматривали переменные и их объявление, мы видели только встроенные типы данных, такие как целые числа и строки. Сейчас, когда мы говорим о структурах, мы должны дополнить этот разговор, включив в него указатели.
Самый простой способ создать значение нашей структуры выглядит так:
goku := Saiyan{
Name: "Goku",
Power: 9000,
}
Примечание: Запятая в конце каждой строки внутри структуры является обязательной, включая последнюю строку. Без нее, компилятор выдаст ошибку. Вы оцените это соглашение по требованию замыкающей запятой если вы раньше использовали какой-то язык или формат, в котором этого требования не было.
Не обязательно задавать все значения или даже какое-то одно. Обе записи являются корректными:
goku := Saiyan{}
// или
goku := Saiyan{Name: "Goku"}
goku.Power = 9000
Точно также, как и с переменными, необъявленные поля содержат нулевые значения.
Кроме того, можно опустить имена полей и указать сразу значения в порядке их следования при объявлении структуры (хотя для верности, лучше использовать этот способ только для структур с несколькими полями):
goku := Saiyan{"Goku", 9000}
Все из приведенных выше примеров объявляют переменную goku
и присваивают ей значение.
Иногда нам не нужна переменная, которая напрямую связана со своим значением, а нужна переменная, которая хранит указатель на это значение. Указатель это адрес в памяти. Это то место, где можно найти фактическое значение. Это степень косвенности. Грубо говоря, между значением и указателем та же разница, что и между домом и его адресом.
Почему стоит использовать указатель вместо фактического значения? Это объясняется тем, что Go передает аргументы в функции как копии. Зная этот факт, что выведет следующий код?
func main() {
goku := Saiyan{"Goku", 9000}
Super(goku)
fmt.Println(goku.Power)
}
func Super(s Saiyan) {
s.Power += 10000
}
Ответ 9000, а не 19000. Почему? Потому, что Super
изменяет копию оригинального значения goku
и таким образом изменения, сделанные в Super
не отражаются на переданном значении. Для того, чтобы код работал как ожидается, нужно передать указатель на значение:
func main() {
goku := &Saiyan{"Goku", 9000}
Super(goku)
fmt.Println(goku.Power)
}
func Super(s *Saiyan) {
s.Power += 10000
}
Мы сделали два изменения. Первое в том, что мы использовали оператор &
для получения адреса. Затем мы изменили входной параметр в функции Super
. Ожидалось значения типа Saiyan
но теперь стал тип *Saiyan
, где *X
означает указатель на значение типа X. Очевидно, что существует связь между типами Saiyan
и *Saiyan
, но это все равно два разных типа.
Отметим, что мы все еще передаем копию значения переменной goku
в Super
, но теперь значением goku
является адрес. И это копия того же адреса, что хранится в оригинальной переменной . Думайте об этом как о копии пути в ресторан. То, что у вас есть – это копия, но она ведет к тому же ресторану, что и оригинал.
Мы можем проверить, что это копия, изменив указатель (хотя это вероятно не то, что вы хотели бы сделать):
func main() {
goku := &Saiyan{"Goku", 9000}
Super(goku)
fmt.Println(goku.Power)
}
func Super(s *Saiyan) {
s = &Saiyan{"Gohan", 1000}
}
Код выше снова выведет 9000. Так ведут себя многие языки, включая Ruby, Python, Java и C#. Go и в некоторой степени C#, просто делают этот факт очевидным.
Очевидно, что копирование указателя будет стоить дешевле с точки зрения ресурсов, чем копирование сложной структуры. В 64 битной системе указатель занимает 64 бита памяти. Если мы имеем структуру со множеством полей, то создание копии будет дорогой операцией. Смысл указателей в том, что они дают общий доступ к значениям. Хотим ли мы чтобы Super
изменил копию goku
или вместо этого изменил общее значение goku
?
Все это не означает, что вам всегда нужно использовать указатели. В конце этой главы, после того, как мы увидим немного больше операций со структурами, мы пересмотрим вопрос указатели-против-значения.
Функции в структурах
Мы можем ассоциировать метод со структурой:
type Saiyan struct {
Name string
Power int
}
func (s *Saiyan) Super() {
s.Power += 10000
}
В коде выше мы говорим, что тип *Saiyan
это получатель метода Super
. Мы можем вызвать Super
так:
goku := &Saiyan{"Goku", 9001}
goku.Super()
fmt.Println(goku.Power) // will print 19001
Конструкторы
Структуры не имеют конструкторов. Вместо этого, вы создаёте функцию, которая возвращает экземпляр нужного типа (как фабрика):
func NewSaiyan(name string, power int) *Saiyan {
return &Saiyan{
Name: name,
Power: power,
}
}
Этот шаблон направляет многих разработчиков на неверный путь. С одной стороны – это лишь небольшое изменение синтаксиса, с другой – это позволяет чувствовать себя немного менее разобщенным.
Наша фабрика не должна возвращать указатель; это абсолютно справедливо:
func NewSaiyan(name string, power int) Saiyan {
return Saiyan{
Name: name,
Power: power,
}
}
New
Несмотря на отсутсвие конструкторов, в Go есть встроенная функция new
, которая используется для выделения памяти, требуемой каким-то типом данных. Результат от new(X)
будет такой же, как и от &X{}
:
goku := new(Saiyan)
// тоже самое
goku := &Saiyan{}
Какой метод использовать, решать вам, но многие люди предпочитают второй, так как он позволяет сразу инициализировать поля, что более удобно для чтения:
goku := new(Saiyan)
goku.name = "goku"
goku.power = 9001
// против
goku := &Saiyan {
name: "goku",
power: 9000,
}
Какой бы способ вы не выбрали, если вы будете использовать шаблон с фабрикой выше, вы сможете отградить остальной код от деталей инициализации.
Поля структур
В примерах, которые мы видели ранее, структура Saiyan
имела два поля Name
и Power
типа string
и int
соответственно. Поля могут быть любого типа, включая другие структуры и типы, которые мы еще не рассматривали, такие как: массивы, карты, интерфейсы и функции.
Например, мы могли бы расширить определение Saiyan
:
type Saiyan struct {
Name string
Power int
Father *Saiyan
}
и инициализировали бы так:
gohan := &Saiyan{
Name: "Gohan",
Power: 1000,
Father: &Saiyan {
Name: "Goku",
Power: 9001,
Father: nil,
},
}
Композиция
Go поддерживает композицию, которая является включением одной структуры в другую. В некоторых языках это называется трейт (trait) или примесь (mixin). Языки, которые не имеют явной поддержки механизма композиции всегда могут пойти долгим путем. В Java:
public class Person {
private String name;
public String getName() {
return this.name;
}
}
public class Saiyan {
// говорим, что Saiyan включает Person
private Person person;
// мы переадресуем вызов классу Person
public String getName() {
return this.person.getName();
}
...
}
Это довольно утомительно. Каждый метод класса Person
нужно продублировать в классе Saiyan
. Go избегает этого занудства:
type Person struct {
Name string
}
func (p *Person) Introduce() {
fmt.Printf("Hi, I'm %s\n", p.Name)
}
type Saiyan struct {
*Person
Power int
}
// и для использования этого:
goku := &Saiyan{
Person: &Person{"Goku"},
Power: 9001,
}
goku.Introduce()
Структура Saiyan
имеет поле типа *Person
. Так как мы не дали явного имени полю, мы получаем косвенный доступ к полям и методам составного типа. Однако, компилятор Go дал имя этому полю, что прекрасно видно:
goku := &Saiyan{
Person: &Person{"Goku"},
}
fmt.Println(goku.Name)
fmt.Println(goku.Person.Name)
Оба метода выведут “Goku”.
Композиция лучше наслеования? Многие люди считают, что это более надежный способ делиться кодом. При использовании наследования, ваш класс тесно связан с суперклассом и в конечном итоге вы сфокусированы на иерархии, а не на поведении.
Перегрузка
Перегрузка не является специфичной операцией для структур, она стоит адресации. Проще говоря, Go не поддерживает перегрузку. По этой причине вы увидите (и напишете) множество функций вроде Load
, LoadById
, LoadByName
и так далее.
Тем не менее, поскольку неявная композиция на самом деле это трюк компилятора, мы можем “переписать” функции композитного типа. Например, наша структура Saiyan
может иметь собственную функцию Introduce
:
func (s *Saiyan) Introduce() {
fmt.Printf("Hi, I'm %s. Ya!\n", s.Name)
}
Композитная функция всегда доступна через s.Person.Introduce()
.
Указатели против значений
Когда вы пишете код на Go, вполне естественно задать себе вопрос: должен ли я использовать значение или указатель на это значение? Есть две хорошие новости. Первая – ответ не зависит от следующих элементов, перечисленных ниже:
- Присваивание локальной переменной
- Поле в структуре
- Возвращение значения из функции
- Параметры функции
- Получатель метода
Вторая – если вы не уверены, используйте указатель.
Как мы уже видели, передача значения является хорошим способом сделать данные неизменяемыми (изменения, совершенные в функции не влияют на исходное значение). Иногда это тот результат которого вы хотите, но чаще это не так.
Даже если вы не собираетесь изменять данные, учитывайте стоимость создания копий больших структур. И наоборот, возможно, у вас есть маленькие структуры:
type Point struct {
X int
Y int
}
В таких случаях стоимость копирования структуры будет смещена в пользу прямого доступа к X
и Y
непосредственно без какой-либо косвенности.
Опять же, это всё тонкие случаи. Если вы не производите итерацию по тысячам или десяткам тысяч таких указателей, вы, возможно, не заметите разницу.
Перед тем как продолжить
С практической точки зрения, эта глава является введением в структуры, о том, как сделать экземпляр структуры получающий функцию и добавляет указатели в вашим знаниям о системе типов Go. В следующих главах мы будем опираться на то, что уже знаем о структурах и о том как они работают.
Далее: Глава 3 – Карты, массивы и срезы