Как называются ресиверы в Go и когда использовать ресивер по значению или по указателю
Что проверяют
Знаете ли вы термин и понимаете ли последствия выбора: наборы методов, реализацию интерфейсов, копирование. Частый уточняющий вопрос: почему значение T не проходит как интерфейс, а &T проходит.
Как отвечать
Ресивер (receiver, получатель) это параметр перед именем метода: func (c Counter) Get() с ресивером по значению, func (c *Counter) Inc() с ресивером по указателю. Метод с ресивером по значению работает с копией, изменения до вызывающего не доходят.
Правила выбора, принятые в сообществе Go:
- указатель: метод меняет состояние; структура содержит
sync.Mutexили другое поле, которое нельзя копировать; структура большая; - значение: маленькие неизменяемые типы вроде
time.Time, базовые типы, а также map, func и chan, которые и так ссылаются на общие данные; - согласованность: если хотя бы одному методу типа нужен указатель, делайте указатели у всех, смешивать не рекомендуется.
Дальше наборы методов. У типа T в набор входят только методы с ресивером по значению, у *T все. Если Inc объявлен на *Counter, интерфейс с Inc реализует только *Counter, и var i Incrementer = Counter{} не скомпилируется.
Вызов c.Inc() на переменной компилятор сам превращает в (&c).Inc(), но только для адресуемых значений. Элемент map или результат функции не адресуем, и такой вызов не соберётся.
Где спотыкаются
Выбирают указатель «ради скорости» для любой структуры. Указатель может увести объект в кучу, и маленькая структура по значению часто обходится дешевле. Решает профилировщик, а не магический порог в байтах.
Копируют мьютекс, объявив методы на значении. Каждый вызов блокирует свою копию, защиты нет. go vet находит такое проверкой copylocks.
Уверены, что вызов метода на nil-указателе всегда паника. Метод с ресивером-указателем на nil вызывается нормально, падает только разыменование внутри, и иногда так делают специально.