Avatar ·

Зачем в Go нужны функции make() и new()? В чём разница?

Всем привет! Я начал изучать Go недавно и никак не могу понять, зачем нужны две отдельные функции для выделения памяти: make() и new(). Вроде обе создают объекты, но примеры из документации меня только запутали.

Вот что я пытаюсь сделать. Я хочу создать срез (slice) и сразу добавить в него элементы:

package main

import "fmt"

func main() {
    // Пытаюсь создать срез через new
    slice1 := new([]int)
    fmt.Println(slice1)  // Выводит: &[]
    // Но как добавить элемент? slice1 = append(slice1, 1) не работает
    // Ошибка: first argument to append must be slice; have *[]int

    // Пытаюсь создать срез через make
    slice2 := make([]int, 5)
    fmt.Println(slice2) // Выводит: [0 0 0 0 0]
    // Работает, но почему длина сразу 5? Можно ли без длины?

    // А с мапой та же история
    var m1 map[string]int
    // m1["key"] = 1 // panic: assignment to entry in nil map

    m2 := make(map[string]int)
    m2["key"] = 1 // Всё ок
}

Почему new() возвращает указатель, а make() — нет? Зачем вообще нужен new(), если make() сразу делает рабочие срезы и мапы? И почему для обычных структур, например type Person struct { Name string }, используют &Person{}, а не make()?

Объясните, пожалуйста, простыми словами для новичка.

<<>>

Главное различие: make() для ссылочных типов, new() — для всего остального

В Go два механизма выделения памяти для разных целей. Запомните простое правило:

  • make() — только для срезов (slice), мап (map) и каналов (channel). Возвращает значение (не указатель). Инициализирует внутреннюю структуру.
  • new() — для любых типов, включая структуры и базовые типы. Возвращает указатель на нулевое значение.

Почему make() не возвращает указатель?

Срезы, мапы и каналы в Go — это ссылочные типы. Они уже содержат указатель на внутренние данные. make() выделяет память для этих внутренних структур и возвращает готовый к использованию объект. Если бы make() вернул указатель, получился бы «указатель на указатель» — это избыточно.

Пример с срезом:

// new() — бесполезно для срезов
s1 := new([]int)
*s1 = append(*s1, 1, 2, 3) // Можно, но неудобно
fmt.Println(*s1) // [1 2 3]

// make() — правильно
s2 := make([]int, 0, 3) // длина 0, ёмкость 3
s2 = append(s2, 1, 2, 3)
fmt.Println(s2) // [1 2 3]

Обратите внимание: make() требует указать длину (для среза) или ёмкость (опционально). Если не знаете размер, используйте make([]int, 0) — это создаст пустой срез, готовый к append.

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

new() полезен для структур и базовых типов, когда вам нужен указатель:

type Person struct {
    Name string
    Age  int
}

// Способ 1: new()
p1 := new(Person)
p1.Name = "Alice"
p1.Age = 30
fmt.Println(p1) // &{Alice 30}

// Способ 2: &Person{} — более идиоматично
p2 := &Person{Name: "Bob", Age: 25}
fmt.Println(p2) // &{Bob 25}

// Для базовых типов
num := new(int)
*num = 42
fmt.Println(*num) // 42

Разницы между new(T) и &T{} нет — оба возвращают *T. Но &T{} позволяет сразу задать поля, поэтому его используют чаще.

Почему var m map[string]int не работает?

Объявление var m map[string]int создаёт nil-мапу. В неё нельзя писать — будет паника. make() инициализирует внутреннюю хеш-таблицу:

// Неправильно
var m1 map[string]int
// m1["key"] = 1 // panic

// Правильно
m2 := make(map[string]int)
m2["key"] = 1 // OK

// Также можно создать пустую мапу литералом
m3 := map[string]int{}
m3["key"] = 1 // OK

Резюме

ФункцияДля каких типовВозвращаетИнициализация
make()slice, map, channelЗначение (не указатель)Выделяет внутренние структуры
new()Любые типыУказатель (*T)Обнуляет память

Практический совет: Используйте make() для срезов, мап и каналов. Для структур — &Struct{}. new() оставьте для редких случаев, когда нужно получить указатель на базовый тип или когда вы пишете обобщённый код с дженериками.

<<>>

Дополнение: new() может быть полезен в дженериках и рефлексии

Хотя в повседневном коде new() используется редко, есть сценарии, где без него не обойтись:

  1. Дженерики (Go 1.18+): Если вы пишете обобщённую функцию, которая должна создать экземпляр типа T, new() — единственный способ:
func Zero[T any]() *T {
    return new(T) // Работает для любого T
}

func main() {
    p := Zero[Person]()
    fmt.Println(p) // &{ 0}
}
  1. Рефлексия (reflect): При работе с типами через reflect.New() вы получаете указатель, аналогичный new().
  2. Производительность: В некоторых бенчмарках new() может быть чуть быстрее &T{} для простых структур, но разница обычно незначительна.

Также стоит помнить: make() не работает с типами, определёнными пользователем. Если у вас type MySlice []int, создавать его нужно через make(MySlice, 0), а не через new(MySlice).

Итог: Начинающим можно вообще забыть про new() и использовать make() для трёх типов, а для всего остального — &Type{}. Но знать о new() полезно для глубокого понимания языка.