Пост

Пустая структура

В этом посте мы рассмотрим свойства моего любимого типа данных в 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

Авторский пост защищен лицензией CC BY 4.0 .

Популярные теги