Вызов обобщённой функции получает единственный аргумент nil: почему Go не может вывести параметр типа?
nil — это нетипизированное значение без собственного типа. Поэтому из него нельзя вывести, каким должен быть параметр типа: указателем, интерфейсом, каналом, функцией, срезом или другим nil-допустимым типом. Нужно явно указать тип или передать уже типизированное nil-значение.
Обобщения в Go предназначены для повторного использования алгоритмов без отказа от статической проверки типов. Вывод параметров типа уменьшает количество явных записей, но он работает только тогда, когда тип можно однозначно получить из аргументов или доступного контекста.
nil появился в языке как универсальное нулевое значение для нескольких категорий типов, а не как значение одного конкретного типа. Поэтому автоматическое назначение ему произвольного типа нарушило бы однозначность вывода и могло бы менять смысл вызова.
Один и тот же nil допустим, например, для указателя, среза, карты, канала, функции или интерфейса. Если обобщённая функция принимает параметр типа, компилятор не может выбрать один вариант только на основании слова nil.
Попытка положиться на ожидаемый тип результата также делает вызов менее очевидным. Особенно опасен выбор интерфейса: nil-интерфейс и интерфейс, содержащий типизированный nil-указатель, имеют разное поведение при проверках и вызовах методов.
Для вывода параметра типа Go использует типы аргументов. У нетипизированного nil такого типа нет, а ограничение параметра лишь задаёт множество допустимых типов и не выбирает конкретный тип автоматически.
Нужно либо указать аргумент типа явно, либо передать переменную с конкретным типом:
В Keep[*Node](nil) параметр T известен заранее, поэтому nil проверяется как значение типа *Node. В Keep(p) тип выводится из объявления p.
В Keep[any](nil) результатом является значение интерфейсного типа any, находящееся в nil-состоянии. Это отличается от передачи p: после упаковки указателя в интерфейс значение интерфейса содержит динамический тип *Node, даже если сам указатель равен nil.
Практический компромисс прост: явный аргумент типа немного увеличивает запись, но фиксирует намерение и устраняет неоднозначность. Типизированная nil-переменная удобна, когда значение уже участвует в логике программы; передача чистого nil без указания типа подходит только там, где тип можно вывести из другого аргумента.
Допустим, обобщённая функция создаёт значение-заглушку для слоя, работающего с указателями узлов. Вызов с чистым nil не даёт компилятору понять, нужен ли *Node или интерфейс обработчика.
Можно было бы изменить API и принимать any, но это переносит проверку типа на время выполнения и ухудшает статическую безопасность. Можно передавать отдельный аргумент-образец, однако он усложняет интерфейс функции.
Оптимальный вариант — явно написать тип параметра в месте неоднозначного вызова: Keep[*Node](nil). В результате компилятор проверяет допустимость nil на этапе компиляции, а намерение разработчика остаётся однозначным.
1. Выведет ли Go тип из ограничения, если ограничение допускает только указатель на конкретный тип?
Нет, само ограничение обычно не заменяет аргумент типа при передаче нетипизированного nil. Оно проверяет уже выбранный тип, но не превращает nil в типизированное значение. Поэтому надёжный вариант — явно указать тип параметра.
2. Одинаково ли ведут себя результаты с явно заданными типами указателя и интерфейса?
Нет. Результат типа *Node — это nil-указатель. Результат типа any, полученный из чистого nil, — nil-интерфейс без динамического типа. Проверка интерфейса на nil даст разные результаты, а вызов метода через nil-интерфейс завершится паникой.
3. Может ли другой аргумент обобщённой функции устранить неоднозначность nil?
Да, если из другого аргумента параметр типа выводится однозначно или параметры связаны ограничением и типовой сигнатурой. Например, функция, принимающая значение типа T и дополнительный аргумент типа T, может вывести T по второму аргументу, даже если первым передан nil. Если такой связи нет, требуется явный аргумент типа.