make() vs new() в Go: что выбрать в 2025 году? Четкий гайд

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Make или New? Вечный вопрос Go-разработчика

Вы пишете на Go уже неделю или год, но всё равно иногда зависаете на секунду перед выбором: make() или new()? Это нормально. Вопрос "Why would I make() or new()?" на Stack Overflow собрал десятки тысяч просмотров, и он не теряет актуальности даже в 2025 году. Почему? Потому что за кажущейся простотой скрывается фундаментальное различие в философии управления памятью в Go.

Давайте раз и навсегда разберемся, что к чему, чтобы вы больше не гадали. Мы пройдём от базы до реальных кейсов, подсветим ловушки и дадим чек-лист для ежедневной работы. Поехали!

Короткий ответ (для тех, кто спешит)

Если вам нужно выделить память под слайс (slice), мапу (map) или канал (channel) — используйте make(). Во всех остальных случаях — new() или литерал &Type{}. Но дьявол, как всегда, в деталях.

Что такое new() и как он работает

Функция new(T) — это встроенная функция Go, которая выделяет память под тип T, обнуляет её (zero value) и возвращает указатель на эту память: *T.

Проще говоря: new() даёт вам пустой, но валидный объект. Вы получаете указатель, но сам объект пока не готов к полноценной работе, если ему требуется инициализация внутренних структур.

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func main() {
    p := new(Person)
    fmt.Println(p)  // Вывод: &{ 0}
    fmt.Println(p.Name == "") // true
}

Здесь new(Person) выделил память под структуру, обнулил поля (пустая строка и 0) и вернул указатель. Всё честно.

Что такое make() и его суперсила

make() — это тоже встроенная функция, но она работает только с тремя типами: слайсы, мапы и каналы. Почему только они? Потому что эти типы имеют внутреннюю структуру (заголовок), которую нужно инициализировать, а не просто обнулить.

make() не возвращает указатель. Он возвращает инициализированное значение нужного типа (не указатель!).

// Слайс: длина 5, ёмкость 10
s := make([]int, 5, 10)
fmt.Println(len(s), cap(s)) // 5 10

// Мапа: готова к вставке элементов
m := make(map[string]int)
m["key"] = 42
fmt.Println(m) // map[key:42]

// Канал: буферизированный на 3 сообщения
ch := make(chan int, 3)
ch <- 1
fmt.Println(<-ch) // 1

Обратите внимание: если вы попробуете написать m := new(map[string]int), то получите указатель на nil-мапу. Вставка элемента в такую мапу вызовет панику (panic).

Почему new() не работает для слайсов, мап и каналов?

Давайте посмотрим на примере слайса. Слайс в Go — это структура из трёх полей: указатель на массив, длина и ёмкость. new([]int) выделит память под эту структуру и обнулит её. Указатель будет nil, длина и ёмкость — 0. Вы получите &[] — указатель на nil-слайс. Он не сломается при присваивании по индексу? Нет, упадёт.

make([]int, 3) же делает три вещи:

  • Выделяет под капотом реальный массив из 3 элементов (или больше, если указана ёмкость).
  • Инициализирует заголовок слайса (pointer, length, capacity).
  • Возвращает готовый к использованию слайс.

То же самое с мапой: make(map[string]int) создаёт хеш-таблицу и выделяет память под бакеты. new(map[string]int) даёт только nil-мапу.

Когда использовать new() в 2025 году?

Современная практика в Go-сообществе такова: new() используется редко. Чаще всего разработчики предпочитают литералы с взятием адреса: &Type{}. Это более читаемо и позволяет сразу задать поля.

Пример, где new() всё ещё уместен:

// Внутри библиотеки, где нужен указатель на примитив
func NewIntPtr() *int {
    return new(int) // *int со значением 0
}

// Альтернатива (более многословная):
func NewIntPtrV2() *int {
    x := 0
    return &x
}

Ещё один легальный кейс — работа с рефлексией (reflect), когда тип известен только в рантайме:

import "reflect"

t := reflect.TypeOf(42)
ptr := reflect.New(t) // создаёт *int

Но для 95% повседневных задач вам хватит &Struct{}.

Таблица сравнения: make vs new

Для быстрой ориентации держите шпаргалку:

Параметр new(T) make(T, args)
Что возвращает Указатель *T Инициализированное значение T
Для каких типов Любые (struct, int, string и т.д.) Только slice, map, chan
Инициализация Только zero value Выделяет память и настраивает внутреннюю структуру
Можно ли использовать с литералом Обычно заменяют на &Type{} Не заменим (для map и slice есть литералы, но без длины)
Паника при работе Да, если тип требует инициализации (map, slice) Нет, всё готово к работе

Подводные камни и частые ошибки

Ошибка 1: new(map[string]int) и попытка записи

m := new(map[string]int)
(*m)["key"] = 42 // PANIC: assignment to entry in nil map

Блоги

Рекомендаций по книгам