Функции make и new
В этой теме мы разберем:
-
что такое функции make и new;
-
чем различаются эти функции;
-
какие рекомендации нужно учитывать при работе с этими функциями.
Как отметил Роб Пайк на Gophercon в 2014 году (рекомендуем посмотреть выступление по ссылке), в Go есть много способов инициализации переменных. Среди них — возможность получить адрес литерала структуры, что приводит к нескольким способам сделать одно и то же.
Справедливо, что многие указывают на эту избыточность в языке, и это иногда приводит к поиску других несоответствий, в первую очередь — к избыточности между make и new.
На первый взгляд кажется, что make и new делают очень похожие вещи, так в чем же причина наличия обеих функций?
Функция make
В Go нет определяемых пользователем универсальных типов, но есть несколько встроенных типов, которые могут работать как универсальные списки, карты, словари и очереди; срезы, карты и каналы.
Поскольку make предназначен для создания этих трех встроенных универсальных типов, он должен быть реализован в среде выполнения (runtime), так как нет возможности однозначно выразить сигнатуру функции make в Go.
Несмотря на то, что функцией make создаются общие значения среза, карты и канала, они по-прежнему являются обычными значениями; функции make не возвращают значения указателя, кроме случаев, если возвращаемый тип сам не является указателем внутри, например hashmap. В случае hashmap, то что он является указателем скрыто от программиста, функция make вернет как тип без указателя, хотя внутри реализации hashmap будет ссылка на структуру. То есть при объявлении hashmap через var, мы просто объявляем ссылочный тип, по умолчанию все ссылочные типы имеют значения nil. Всегда используйте make, для инициализации map.
Если бы new был бы удален в пользу make, как бы ты построил указатель на инициализированное значение?
Переменные x1 и x2 имеют тот же тип *int, x2 указывает на инициализированную память и может быть безопасно разыменована, переменная x1 не будет инициализирована, значением переменной x1 будет nil.
Функция new
Хотя new используется редко, его поведение хорошо определено.
new(T) всегда возвращает *T указатель на инициализированный T, где T произвольный тип. Поскольку в Go нет конструкторов, значение будет инициализировано T нулевым значением или значением по умолчанию.
Использование new для создания указателя на срез, карту или нулевое значение канала работает сегодня и согласуется с поведением new.
Различие make и new
Из-за путаницы, которую могут вызвать функции make и new, они последовательны; make только создает срезы, карты и каналы, new возвращает только указатели на инициализированную память.
Да, new можно было бы расширить для работы в качестве функции make со срезами, картами и каналами, но это бы создало определенные проблемы.
New имело бы особое поведение, если бы переданный тип new был слайсом, картой или каналом. Это правило должен был бы помнить каждый программист на Go.
Для срезов и каналов new должен был бы стать вариативным, принимая возможную длину, размер буфера или емкость, как требуется. Опять же, нужно помнить больше особых случаев, тогда как в текущем случае new принимается только один аргумент, тип.
new всегда возвращает *T для T, переданного ему. Пример:
Невозможно было бы разрешить ситуацию *new([]byte, length).
Рекомендации
Функции Make и new обладают своими зонами ответственности.
Если ты переходишь с другого языка, особенно с того, который использует конструкторы, может показаться, что new — это все, что тебе нужно, но Go — это не тот тип языков, здесь нет конструкторов из-под коробки.
Используй new по минимуму, почти всегда есть более простые и чистые способы написать программу без него. Например, явно указывать ссылки на типы, с явной (explicit) инициализацией. Пример:
Самое время протестировать код!
Использование функции new, так же, как и использование именованных возвращаемых аргументов в функциях, является сигналом того, что код пытается сделать что-то умное, и программисту нужно уделить больше внимания для понимания кода. Возможно, код действительно умный, но, скорее всего, его можно упростить (kiss — keep it simple and stupid), чтобы сделать его более понятным и идиоматичным.
При создании второго объекта для разных операций используй длину первого объекта, как показано на примере.
Самое время протестировать код!