Пустая структура
В этом посте мы рассмотрим свойства моего любимого типа данных в Go - пустой структуры.
Пустая структура - это тип struct, не имеющий полей. Вот несколько примеров в именованной и анонимной формах
1
2
type Q struct{}
var q struct{}
Итак, если пустая структура не содержит полей, не содержит данных, то зачем она нужна? Что мы можем с ней сделать?
Ширина (width)
Прежде чем перейти к рассмотрению самой пустой структуры, я хотел бы сделать небольшое отступление и поговорить о ширине.
Термин width, как и большинство других терминов, пришел из компилятора gc, хотя его этимология, вероятно, уходит на десятки лет назад.
Ширина описывает количество байт памяти, которое занимает экземпляр типа. Поскольку адресное пространство процесса одномерно, я считаю, что ширина - более подходящий термин, чем размер.
Ширина - это свойство типа. Поскольку каждое значение в программе на Go имеет тип, ширина значения определяется его типом и всегда кратна 8 битам.
Мы можем узнать ширину любого значения, а значит, и ширину его типа с помощью функции unsafe.Sizeof()
.
1
2
3
4
var s string
var c complex128
fmt.Println(unsafe.Sizeof(s)) // выведет 8
fmt.Println(unsafe.Sizeof(c)) // выведет 16
https://go.dev/play/p/4mzdOKW6uQ
Ширина типа массива кратна типу его элемента.
1
2
var a [3]uint32
fmt.Println(unsafe.Sizeof(a)) // prints 12
https://play.golang.org/p/YC97xsGG73
Структуры обеспечивают более гибкий способ определения составных типов, ширина которых равна сумме ширины составляющих их типов плюс подгонка (padding)
1
2
3
4
5
6
7
type S struct {
a uint16
b uint32
}
var s S
fmt.Println(unsafe.Sizeof(s)) // выведет 8, не 6
Russ Cox любезно написал, чтобы объяснить, что ширина не связана с выравниванием. Вы можете прочитать его комментарий.
Пустая структура
Теперь, когда мы изучили ширину, должно быть очевидно, что пустая структура имеет ширину, равную нулю. Она занимает ноль байт памяти.
1
2
var s struct{}
fmt.Println(unsafe.Sizeof(s)) // выведет 0
Поскольку пустая структура занимает ноль байт, из этого следует, что она не нуждается в заполнении. Таким образом, структура, состоящая из пустых структур, также не занимает места.
1
2
3
4
5
6
type S struct {
A struct{}
B struct{}
}
var s S
fmt.Println(unsafe.Sizeof(s)) // выведет 0
https://play.golang.org/p/PyGYFmPmMt
Что можно делать с пустой структурой
В соответствии с принципом ортогональности (orthogonality) Go, пустая структура - это такой же тип структуры, как и любой другой. Все свойства, привычные для обычных структур, одинаково применимы к пустой структуре.
Вы можете объявить массив struct{}
, но они, конечно, не занимают места в памяти.
1
2
var x [1000000000]struct{}
fmt.Println(unsafe.Sizeof(x)) // prints 0
https://play.golang.org/p/0lWjhSQmkc
Слайс struct{}
занимает только места сколько занимает его заголовок(header). Как было показано выше, их массив, содержащий пустые структуры не занимает места.
1
2
var x = make([]struct{}, 1000000000)
fmt.Println(unsafe.Sizeof(x)) // выведет 12
https://play.golang.org/p/vBKP8VQpd8
Все это справедливо и для подслайса (subslice). len
и cap
работают, как и ожидается.
1
2
3
var x = make([]struct{}, 100)
var y = x[:50]
fmt.Println(len(y), cap(y)) // prints 50 100
https://play.golang.org/p/8cO4SbrWVP
Вы можете получить адрес значения struct{}
, оно адресуемо, как и любое другое значение.
1
2
var a struct{}
var b = &a
Интересно, что адрес двух значений struct{}
может быть одинаковым.
1
2
var a, b struct{}
fmt.Println(&a == &b) // true
https://play.golang.org/p/uMjQpOOkX1
Это свойство также можно наблюдать для слайса пустых структур []struct{}
.
1
2
3
4
a := make([]struct{}, 10)
b := make([]struct{}, 20)
fmt.Println(&a == &b) // false, a и b - это разные слайсы
fmt.Println(&a[0] == &b[0]) // true, их внутренние массивы одинаковы
https://play.golang.org/p/oehdExdd96
Почему так? Если задуматься, то пустые структуры не содержат полей, а значит, не могут содержать никаких данных. Если пустые структуры не содержат данных, то невозможно определить, отличаются ли два значения struct{}
. По сути, они являются взаимозаменяемыми.
1
2
3
a := struct{}{} // не zero-value, а реально новый экземпляр struct{}
b := struct{}{}
fmt.Println(a == b) // true
https://go.dev/play/p/K9qjnPiwM8
Примечание: это свойство не является обязательным в спецификации, но в ней отмечается, что две разные переменные нулевого размера могут иметь один и тот же адрес в памяти.
struct{} как получатель методов
Теперь, когда мы продемонстрировали, что пустые структуры ведут себя так же, как и любые другие типы, из этого следует, что мы можем использовать их в качестве получателей методов.
1
2
3
4
5
6
7
8
9
type S struct{}
func (s *S) addr() { fmt.Printf("%p\n", s) }
func main() {
var a, b S
a.addr() // 0x1beeb0
b.addr() // 0x1beeb0
}
https://play.golang.org/p/YSQCczP-Pt
В этом примере показано, что адрес всех значений нулевого размера - 0x1beeb0
. Точный адрес, вероятно, будет отличаться для разных версий Go.
Подытожим
Спасибо, что дочитали до конца. Эта статья, состоящая почти из 800 слов, оказалась длиннее, чем ожидалось, и я планировал написать еще больше.
Хотя эта статья была посвящена language obscura, есть одно важное практическое применение пустых структур, и это конструкция chan struct{}
, используемая для передачи сигналов между подпрограммами go
Данная статья является вольным переводом статьи The empty struct