Именованные типы и структуры
Скоро сказка сказывается, да не скоро дело делается. Двигаемся вперёд - медленно, но уверенно. Похоже, в этой главке мне нечем вас будет удивить. Как именованные типы, так и структуры весьма и весьма напоминают таковые в С/С++.
Именованные типы
Именованные типы являются по сути псевдонимами тех или иных существующих примитивов, и используются в основном либо для улучшения семантичности кода, либо с целью дальнейшего применения методов. О методах речь пойдёт в соответствующей главе, а сейчас просто объявим именованный тип.
package main
import (
"fmt"
)
// именованный тип для хранения дня недели
type weekday int
// "заполним" неделю при помощи геператора констант
const (
Sunday = iota
Mondey
Wednesday
Tuesday
Thursday
Friday
Saturday
)
// сделаем ассоциативный массив - отображение
var week map[weekday]string
func main() {
// инициализируем отображение
week := map[weekday]string{
Sunday: "Sunday",
Mondey: "Mondey",
Wednesday: "Wednesday",
Tuesday: "Tuesday",
Thursday: "Thursday",
Friday: "Friday",
Saturday: "Saturday",
}
// для счётчика цикла нам понадобится также переменная типа weekday!
var i weekday
for i = 0; i < weekday(len(week)); i++ { // приведение типа!
fmt.Printf("Index: %v, Value: %v\n", i, week[i]) // распечатка отображения
}
}
//Результат:
//Index: 0, Value: Sunday
//Index: 1, Value: Mondey
//Index: 2, Value: Wednesday
//Index: 3, Value: Tuesday
//Index: 4, Value: Thursday
//Index: 5, Value: Friday
//Index: 6, Value: Saturday
Как видите, всё достаточно предсказуемо. Вы вероятно, могли отметить, что использование цикла for по диапазону week выглядело бы синтаксически несколько проще. Это верно. В таком случае мы бы избежали объявления переменной-счётчика итераций и приведения типов. Однако вспомните тот факт, что особенность отображений - недетерминированность порядка следования элементов. Таким образом, использованный нами способ перебора элементов является небольшой хитростью, использованной с целью сортировки элементов в выдаче. Пользуйтесь подобной техникой и вы, при необходимости.
Структуры
Как и в "голом" С, в языке Go нет классов. И, точно так же, структура является единственным составным (агрегированным) типом данных. Синтаксис же таков:
package main
import (
"fmt"
)
type T struct {
x, y int
f float64
s string
b bool
}
func main() {
//переменная t типа T
var t T
fmt.Println(t)
}
//Результат:
//{0 0 0 false}
Элементы структуры называются полями. Очевидно, непроинициализированная структура имеет для каждого своего поля значение по умолчанию, равное значению по умолчанию для соответствующего примитива.
Часто непроинициализированную структуру объявляют таким образом:
package main
import (
"fmt"
)
type T struct {
x, y int
f float64
s string
b bool
}
func main() {
//переменная t типа T
t := T{}
fmt.Println(t)
}
//Результат:
//{0 0 0 false}
Доступ к полям структуры осуществляется через оператор "точка" - .
package main
import (
"fmt"
)
type T struct {
x, y int
f float64
s string
b bool
}
func main() {
var t T
fmt.Println(t)
t.x, t.y = 1, 2
t.f = 0.01
t.s = "qwerty"
t.b = true
fmt.Println(t)
}
//Результат:
//{0 0 0 false}
//{1 2 0.01 qwerty true}
Возможна упрощённая инициализация полей (подобная конструкция именуется структурным литералом):
package main
import (
"fmt"
)
type T struct {
x, y int
f float64
s string
b bool
}
func main() {
t := T{x: 1, y: 2, f: 0.01, s: "qwerty", b: true}
fmt.Println(t)
}
//Результат:
//{1 2 0.01 qwerty true}
Можно ещё более кратко:
package main
import (
"fmt"
)
type T struct {
x, y int
f float64
s string
b bool
}
func main() {
t := T{1, 2, 0.01, "qwerty", true}
fmt.Println(t)
}
//Результат:
//{1 2 0.01 qwerty true}
Именование как самой структуры, так и каждого её поля определяет экспортируемость всей структуры или отдельного поля. Как и везде, работает "правило заглавной буквы". Структура, именованная со строчной буквы экспортироваться не будет. В экспортируемой структуре, поля, не определённые с прописной буквы, также экспортироваться не будут. То есть часть полей может быть как бы public, другая - как бы private, переводя на привычный язык ООП. Структуры прекрасно сериализуются и используются в связке с JSON, весьма часто. Сериализации/десериализации подлежат только экспортируемые поля экспортируемых же структур. Если в результируещем JSON мы хотим получить имена атрибутов с маленькой (строчной) буквы, либо получить вовсе некие иные, не соответствующие наименованию исходных полей структуры, имена - для этой цели предусмотрены так называемые JSON tag descriptors. Приведём пример их применения:
//Экспортируемая структура + JSON tag descriptors
type ResultEntity struct {
Merchant string `json:"merchant"`
PaperState int `json:"paperState"`
Error bool `json:"error"`
ErrorDescription string `json:"errorDescription"`
}
Необходимо отметить, что полями структуры могут быть, помимо примитивов, также массивы, срезы, отображения, а также другие, вложенные структуры:
package main
import (
"fmt"
)
type innerStruct struct {
b bool
}
type T struct {
i int
arr [5]int
s innerStruct
}
func main() {
var t T
fmt.Println(t)
}
//Результат:
//{0 [0 0 0 0 0] {false}}
Важное замечание: структурный тип не может содержать поле собственного типа! Формально это правило звучит так: "агрегатное значение не должно содержать само себя".
Однако, и это необходимо помнить, структура в качестве поля может иметь указатель на структуру своего типа (речь об указателях пойдёт в следующей главе):
//Ошибка компиляции: invalid recursive type T
type T struct {
t T
}
//Допустимо
type T1 struct {
pt *T1
}
Данный приём позволяет нам строить связанные списки и деревья. Эти сущности изучает дисциплина, именуемая структуры данных. Имеется немало алгоритмов, использующих указанные техники. Углубление в эту тему в рамках настоящей главы видится избыточным.
Пустые структуры
Как мы с вами выяснили, структуры могут описывать самые различные сущности, и по сему бывают весьма разнообразны по своему составу. Оказывается, порой используются и так называемые пустые структуры. Под пустой структурой понимается структура такого вида:
type Empty struct{}
то есть структура без полей.
В чём примечательность подобной структуры? Главной особенностью является тот факт, что переменная, содержащая пустую структуру занимает в памяти 0 (нуль) байт. Импортируем встроенный пакет unsafe и проверим данное утверждение:
package main
import (
"fmt"
"unsafe"
)
type Empty struct{}
func main() {
var e Empty
fmt.Printf("Размер %d байт", unsafe.Sizeof(e))
}
//Результат:
//Размер 0 байт
Чем и где это может быть полезно? Да, в общем то, везде, где поможет нам сэкономить память. Обратитесь к своей интуиции! Увы, рассмотреть многие подробности нам пока что не позволяет нехватка знаний. Некоторые полезные приёмы будут рассмотрены позже в главах о методах, интерфейсах и многозадачности. Имея в виду ограниченный объём накопленных нами на данный момент знаний, приведу лишь такой интересный пример: пустые структуры иногда используются в качестве значений в отображениях, в тех случаях, когда программисту могут быть важны ключи а не значения.
package main
import (
"fmt"
)
//Именованный тип - будет хранить неупорядоченный список значений типа int
type intSet map[int]struct{}
func add(m intSet, i int) {
m[i] = struct{}{}
}
func main() {
uniqInts := make(intSet, 0)
add(uniqInts, 1)
add(uniqInts, 2)
add(uniqInts, 1) // повтор
add(uniqInts, 1) // повтор
// выведет только 2 значения
for i, _ := range uniqInts {
fmt.Println(i)
}
}
Как демонстрирует данный пример, мы создали именованный тип, в основе которого лежит отображение (map), ключом которого является значение типа int, а значением - пустая структура. Целью в данном случае является хранение множества значений типа int, реализуя определение множества как совокупности уникальных неупорядоченных значений. Применение структуры в данном случае служит для экономии памяти, ибо значение ключей здесь не представляет интереса. Также подвергните разбору функцию add, которая служит для установки значений. Обратите внимание и на форму объявления переменной uniqInts. Всякие полезные наблюдения помогут вам овладевать языком, закреплять соответствующие навыки, вырабатывать стиль и приёмы.
Анонимные структуры
В языке Go могут существовать специфические конструкции, которые именуются анонимные структуры. В данном случае вместо декларации типа, объявляется переменная структурного типа с которой можно работать, с некоторыми оговорками. Синтаксически это выглядит так:
package main
import (
"fmt"
)
func main() {
var s struct { x, y int}
fmt.Println(s)
s.x, s.y = 1, 2
fmt.Println(s)
fmt.Printf("Тип s: %T", s)
}
//Результат:
//{0 0}
//{1 2}
//Тип s: struct { x int; y int }
Фактически, сигнатурно, s - самая настоящая структура. Однако, s - это переменная, но вовсе не тип. Поэтому инициализация при помощи структурного литерала будет выглядеть так:
s := struct {
x int
y int
}{
x: 1,
y: 2,
}
//или проще:
s := struct {x, y int}{1, 2}
Также, поскольку s не является типом, не сработает привычная конструкция вида:
var v s
var v1 = s{3, 4}
//Ошибка: s is not a type
В этом плане возможно лишь прямое присваивание var v = s (создастся копия по значению).
Анонимной структуре может быть без приведения типа присвоено значение "нормальной" структуры, при их сигнатурном совпадении:
package main
import (
"fmt"
)
type T struct{ x, y int }
func main() {
var t = T{}
var s struct{ x, y int }
s = t
fmt.Println(s)
}
//Результат:
//{0, 0}
В обоих случаях сигнатура структуры - struct { x int; y int }. Внимание! Значение имеет не только тип, но и наименование полей. При несовпадении имён полей даже приведение типов окажется невозможным.
И последнее к вопросу об анонимных структурах. С ними нельзя использовать методы. О методах в контексте анонимных структур мы поговорим в своё время.
Анонимные поля
Говоря о структурах, в языке Go возможен такой фокус, как анонимные поля. И, поскольку имена отсутствуют, будет нетрудным заключение о том, что в такой структуре позволительно лишь одно поле с данными каждого типа (один int, один bool итак далее), ибо именно названия вообще позволяют нам различать однотипные вещи. Выглядеть подобная структура будет как-то так:
package main
import (
"fmt"
)
type T struct {
int
bool
}
func main() {
t := T{0, true}
fmt.Println(t.int, t.bool)
}
Как можно видеть, для доступа к полям вместо привычных имён будут использоваться названия их типов. Объявление "микса" из именованных и неименованных полей допустимо. Неименованные поля всегда закрыты (не экспортируются). Применение методов возможно:
package main
import (
"fmt"
)
type T struct {
int
bool
}
func (t T) f() int {
return t.int * 2
}
func main() {
t := T{2, true}
fmt.Println(t.f())
}
//Результат: 4
Для чего всё это нужно? Резонный вопрос. С одной стороны это в своём роде пустая синтаксическая опция. С другой стороны, это один из способов реализации принципа композиции в Go. Для понимания сказанного, нам придётся немного забежать вперёд - в методы. Пример:
package main
import (
"fmt"
)
type htmlElement struct {
width, height int
}
//Внимание, методы! Nota bene.
func (w *htmlElement) setHeight(height int) { w.height = height }
func (w *htmlElement) setWidth(width int) { w.width = width }
type button struct {
htmlElement
text string
}
type input struct {
htmlElement
type_ string
}
func main() {
b := new (button)
i := new (input)
b.setWidth(3)
b.setHeight(5)
b.text = "submit"
i.setWidth(10)
i.setHeight(20)
i.type_ = "text"
fmt.Println(*b, *i)
}
Здесь тип htmlElement описывает общие для всех элементов HTML формы поля и методы. Путём включения, типы button и input получили как нужные поля, так и методы, с ними работающие. Отдельное имя для поля типа htmlElement в данном случае не принципиально, им можно пренебречь. Данный тип нам понадобился в первую очередь для того, чтобы "протащить" в конструкцию нужные методы.
Волшебный местоблюститель
Сравнение структур
Здесь необходимо выделить несколько основных моментов. Во-первых, структуры с разными сигнатурами - принципиально несравниваемы.
package main
import (
"fmt"
)
type T struct{ a, b int }
type T1 struct{ a1, b1 int }
func main() {
t := T{1, 2}
t1 := T1{1, 2}
fmt.Println(t == t1)
}
//Результат:
//invalid operation: t == t1 (mismatched types T and T1)
Приведение типов T(t1) также невозможно. А вот при совпадении сигнатур возможны варианты. Обратите внимание на применение анонимной структуры - тут даже не потребовалось приведение типов:
package main
import (
"fmt"
)
type T struct{ a, b int }
type T1 struct{ a, b int }
func main() {
t := T{1, 2}
t1 := T1{1, 2}
s := struct{a, b int}{1, 2}
fmt.Println(t == T(t1))
fmt.Println(t == s)
}
//Результат:
//true
//true
Во-вторых, структуры, имеющие в качестве одного или нескольких полей несравниваемые типы (например, срезы) - принципиально несравниваемы.
package main
import (
"fmt"
)
type T struct {
x, y int
b []byte
}
func main() {
t := T{1, 2, []byte{3, 4, 5}}
t1 := T{1, 2, []byte{3, 4, 5}}
fmt.Println(t == t1)
}
//Результат:
//invalid operation: t == t1 (struct containing []byte cannot be compared)
И, наконец, в-третьих, переменные, представляющие собой структуры одного типа и не содержащие несравниваемых полей, а также сигнатурно идентичные им анонимные структуры, всегда сравниваемы.
package main
import (
"fmt"
)
type T struct{ x, y int }
func main() {
t := T{1, 2}
t1 := T{1, 2}
t2 := T{3, 4}
s := struct{ x, y int }{3, 4}
fmt.Println(t == t1)
fmt.Println(t1 == t2)
fmt.Println(t2 == s)
}
//Результат:
//true
//false
//true
Краткое заключение
В рамках данной темы, сообщу также об одной важной особенности: нередко структуры применяются в качестве "обёрток" над переменными или структурами из внешнего подключаемого пакета, для решения некоторых проблем, возникающих при непосредственной работе с таковыми. Подробнее об этом мы будем говорить в разделе методы.
На данном этапе посчитаем эти сведения о структурах и именованных типах достаточными. Прибегнем к разумному редукционизму. Впереди же нас ждёт продолжение этой важной и интересной темы в главах о методах и интерфейсах, указателях и работе с JSON.
Раунд!
Last updated
Was this helpful?