Рассмотрите два способа описать узел односвязного списка. Почему первый вариант отвергается компилятором, а второй допустим?
package main
// type Bad struct {
// Next Bad // invalid recursive type
// }
type Node struct {
Value int
Next *Node
}
func main() {}
Прямое поле типа Bad требует разместить внутри Bad ещё один полный Bad, затем следующий и так далее. Такой тип не имеет конечного размера, поэтому Go запрещает прямую рекурсию типов.
Поле *Node хранит только указатель фиксированного размера. Сам Node поэтому имеет конечный размер, а цепочка узлов создаётся отдельно в памяти.
При описании рекурсивных структур языку нужно отличать рекурсию представления от рекурсии связей. Для списков, деревьев и графов обычно требуется не вкладывать весь дочерний объект внутрь родительского, а хранить ссылку на него.
Go реализует это через типы конечного размера: указатели, срезы, карты и интерфейсы позволяют описывать рекурсивные данные, не делая размер каждого значения бесконечным.
Если бы struct с прямым рекурсивным полем разрешался, компилятор не смог бы определить смещение и размер полей: для вычисления размера Bad потребовался бы размер вложенного Bad без конечной точки.
Неверное понимание этого правила приводит к попыткам описывать дерево или список прямым вложением. Правильная модель должна хранить косвенную связь, например указатель или срез элементов.
В варианте Next Bad поле содержит значение целиком. Размер можно было бы записать как размер Bad, увеличенный на размер ещё одного Bad, но это порождает бесконечную цепочку.
В варианте Next *Node поле содержит адрес. Размер указателя конечен, поэтому размер структуры вычислим: в него входят int и указатель с учётом выравнивания.
При создании head сам объект tail не копируется внутрь head; Next получает адрес уже существующего узла. Поэтому длина списка ограничена доступной памятью, а не размером одного статического значения Node.
Косвенность можно получить не только указателем. Например, поле Children []Node допустимо: срез содержит конечный дескриптор, а элементы размещаются отдельно. Однако []Node и *Node имеют разную семантику владения и доступа: срез обычно представляет набор дочерних значений, указатель — связь с конкретным узлом.
В файловом дереве нужно представить каталог с дочерними каталогами. Вариант с прямым полем Child Directory невозможен из-за бесконечного размера. Вариант с *Directory хорошо подходит для цепочки или единственного потомка, но усложняет управление множеством детей.
Можно использовать Children []Directory: это компактная модель владения, но копирование структуры копирует дескриптор среза, а не все его элементы. Можно использовать Children []*Directory: это удобнее для совместного доступа и стабильных адресов узлов, но требует отдельных выделений памяти и аккуратного управления временем жизни.
Для обычного дерева каталогов выбирают []Directory, если дочерние узлы принадлежат родителю и не должны разделяться. Если узлы имеют несколько связей, идентичность, кэш или меняются независимо, используют указатели. Это сохраняет конечный размер структуры и явно отражает графовую природу данных.
type Node struct { Children []Node }?Да. Срез имеет конечный размер как значение: обычно это указатель на массив, длина и ёмкость. Рекурсивными становятся элементы, находящиеся за пределами самой структуры, поэтому размер Node остаётся вычислимым.
b = a всю рекурсивную структуру, на которую указывают поля *Node?Нет. Копируется сам Node вместе со значением указателя, но не объект, находящийся по этому адресу. После присваивания a.Next и b.Next указывают на один узел, поэтому изменение этого узла через один путь будет видно через другой.
Значение интерфейса имеет конечное представление: оно хранит информацию о динамическом типе и само динамическое значение косвенным способом. Поэтому type Node struct { Next any } допустим. При этом такая рекурсия не гарантирует наличие узла: поле может содержать любое значение, включая nil, и проверять динамический тип нужно отдельно.