JavaRush /Курсы /Go SELF /new(T) vs make в Go — это разные инструменты

new(T) vs make в Go — это разные инструменты

Go SELF
19 уровень , 2 лекция
Открыта

1. Карта местности: что возвращают new и make

Если вы новичок, то вполне честная реакция на new и make такая: «Почему нельзя было оставить одно слово, а второе удалить с помощью rm -rf?» Понимаю. Но у Go тут вполне взрослая причина: разные типы данных в языке устроены по-разному, и им нужно разное «создание». Одним достаточно получить память под zero value — и они уже готовы к работе. А другим нужно ещё инициализировать внутреннюю структуру, иначе они выглядят как коробка без дна: вроде форма есть, а использовать нельзя.

И вот ключевая идея: new(T) и make(...) решают разные задачи, поэтому они и не заменяют друг друга.

Прежде чем углубляться, полезно иметь «карту местности», чтобы не потеряться. Давайте зафиксируем это как таблицу, потому что таблицы — это списки, которые притворились культурными.

Инструмент Что делает Что возвращает Для каких типов обычно используют
new(T)
Выделяет место под zero value типа T
*T
Для любых T, когда нужен указатель
make(...)
Создаёт готовое к использованию значение со внутренним состоянием
T
Для slice, map, chan

Это различие — основная мысль лекции. Всё остальное ниже — детали, которые сделают эту мысль привычной.

2. new(T): «дай мне указатель на zero value»

Когда вы пишете new(T), Go делает очень простую вещь: выделяет память под значение типа T, записывает туда zero value, и возвращает адрес этого места, то есть *T.

Важно почувствовать вкус формулировки: new возвращает указатель. Не значение, а адрес. Поэтому new(int) — это *int, new(string) — это *string, new(Task) — это *Task и так далее.

Мини‑пример: new(int) и разыменование

package main

import "fmt"

func main() {
	p := new(int)    // p: *int, внутри лежит 0
	fmt.Println(*p)  // 0
	*p = 10
	fmt.Println(*p)  // 10
}

Здесь new(int) даёт нам адрес, по которому лежит 0. Мы меняем *p, и теперь по адресу лежит 10.

new со структурой: new(struct)

Да, структуры (struct) мы будем разбирать подробно отдельно, но сегодня нам нужен самый минимальный вариант: структура — это просто «значение с полями». Без методов, без тегов, без драм.

package main

import "fmt"

type Task struct {
	Title string
	Done  bool
}

func main() {
	t := new(Task)               // t: *Task, поля = zero values
	fmt.Println(t.Title, t.Done) // "" false
}

У string zero value — пустая строка, у boolfalse. Поэтому свежесозданная через new(Task) задача выглядит как «пустая задача и ещё не выполнена». Логично.

4. make: «собери мне рабочий механизм»

В отличие от new, make не про «просто выделить память». make нужен для типов, у которых есть внутреннее состояние, которое должно быть корректно инициализировано.

Классические три типа, для которых существует make:

  • []T (slice),
  • map[K]V,
  • chan T.

Слайсы и карты вы уже активно используете. Каналы мы пока трогать будем очень аккуратно (чисто чтобы понимать, почему они упоминаются рядом с make).

Кстати, полезный факт для мышления: слайс внутри — это не «массив». Это маленькая структура-заголовок, которая хранит ссылку на данные + длину + ёмкость. Именно поэтому слайсы ведут себя «ссылочно» по данным и почему append иногда меняет буфер. Эта модель «указатель + len + cap» — стандартная и очень полезная для понимания поведения слайсов.

make([]T, len, cap): создаём слайс, который можно использовать сразу

Слайс без make тоже бывает (например, var s []int), но тогда он будет nil-слайсом. Он во многих местах удобен и безопасен, но важно понимать разницу: make создаёт слайс уже с заданной длиной и/или ёмкостью.

make со длиной: можно индексировать

package main

import "fmt"

func main() {
	nums := make([]int, 3) // len=3, внутри [0 0 0]
	nums[0] = 7
	fmt.Println(nums)      // [7 0 0]
}

Если вы сделали make([]int, 3), то nums[0], nums[1], nums[2] — легальны.

make с ёмкостью: удобно для append

package main

import "fmt"

func main() {
	nums := make([]int, 0, 5) // len=0, cap=5
	nums = append(nums, 1)
	nums = append(nums, 2)
	fmt.Println(nums)         // [1 2]
}

Так вы заранее говорите: «я планирую добавлять элементы, не заставляй рантайм часто расширяться».

make(map[K]V): создаём map, в которую можно писать

У карты (map) есть отдельный важный момент: в nil-map писать нельзя. Читать можно (получите zero value), а вот запись приводит к аварии во время выполнения.

Поэтому если вы хотите создавать карту «с нуля и писать в неё», вам нужен make(map[K]V).

package main

import "fmt"

func main() {
	m := make(map[string]int)
	m["go"] = 25
	fmt.Println(m["go"]) // 25
}

make(chan T): создаём канал

Каналы — это инструмент для общения между горутинами. Мы не будем прямо сейчас строить конкурентность, но нам важно понять механически: канал создаётся через make. Не через new.

package main

import "fmt"

func main() {
	ch := make(chan int)
	fmt.Printf("%T\n", ch) // chan int
}

Мы даже не отправляем и не читаем значения — просто фиксируем: канал — это отдельный тип, и создаётся он make.

5. Почему new и make нельзя поменять местами

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

Почему new([]int) — почти всегда странный выбор

new([]int) возвращает *[]int, то есть указатель на слайс. А внутри этого слайса будет zero value — то есть nil-слайс.

package main

import "fmt"

func main() {
	ps := new([]int)        // ps: *[]int
	fmt.Println(*ps == nil) // true
	fmt.Println(len(*ps))   // 0
}

Вы получаете указатель на nil-слайс. Сам по себе nil-слайс нормальный, но вопрос: зачем вам указатель на него? В большинстве задач — незачем. Слайс и так маленький (заголовок), его удобно передавать и возвращать значением.

Почему new(map[string]int) не заменяет make(map[string]int)

Это одна из самых частых ловушек:

  • new(map[string]int) даёт *map[string]int, внутри — nil-map.
  • nil-map нельзя записывать.
package main

import "fmt"

func main() {
	pm := new(map[string]int) // *map[string]int, внутри nil
	fmt.Println(*pm == nil)   // true

	// (*pm)["a"] = 1          // так делать нельзя: запись в nil-map => panic
	*pm = make(map[string]int) // вот теперь карта настоящая
	(*pm)["a"] = 1
	fmt.Println((*pm)["a"])   // 1
}

Вывод: new(map[...]...) может быть полезен только если вам по смыслу нужен указатель на карту (что редкость). А «чтобы просто создать карту» — нужен make.

6. Указатель или готовая структура данных

Давайте проговорим человеческим языком.

Если вы хотите «создать объект и менять его через указатель» — у вас естественно появляется new(T) или &T{...} (второе мы тоже позже полюбим). Но смысл всегда один: вам нужен адрес, то есть *T.

Если вы хотите создать коллекцию/механизм (slice, map, chan), который внутри должен быть правильно инициализирован — вы используете make.

И это не философия. Это практическое правило, которое делает код предсказуемым.

Нюанс: make возвращает значение, но оно может вести себя «ссылочно»

Этот момент важен, чтобы не запутаться в формулировках. make возвращает значение типа []T, map[K]V или chan T. Не указатель. Но поведение часто похоже на «ссылочное», потому что внутри этих значений есть ссылки на общее состояние.

У слайса это заголовок, который содержит ссылку на массив данных (и поэтому изменения элементов часто видны снаружи). У map и chan внутри тоже есть указатели на структуры рантайма, и поэтому «копия map» всё равно указывает на ту же карту.

Это не противоречит «в Go всё передаётся по значению». Просто значение может быть маленькой «ручкой», которая ведёт к большому общему объекту.

Быстрая интуитивная шпаргалка

Полезно закрепить «интуицию»:

new(T) — это когда вы хотите получить адрес (указатель) на значение T, обычно чтобы:

  • передать и менять одно и то же значение из разных мест,
  • вернуть «большой объект» из функции без копирования его целиком (хотя копирование — отдельная тема),
  • иметь возможность обозначить «нет объекта» через nil (*T может быть nil).

make — это когда вы создаёте контейнер/механизм, который должен быть работоспособным сразу после создания.

7. Учебное приложение TaskBox: new для задач, make для хранилищ

Чтобы не оставлять тему слишком абстрактной, давайте встроим её в маленький кусочек приложения. Пусть у нас будет консольный TaskBox: хранит задачи в памяти, умеет добавить несколько задач и распечатать.

Мы сделаем две вещи:

  • задачи будем создавать через new(Task) (потому что будем хранить указатели на задачи),
  • коллекции будем создавать через make: слайс и карту.

Описываем задачу

package main

type Task struct {
	ID    int
	Title string
	Done  bool
}

Никаких методов, ничего «сложного». Просто поля.

Фабрика задач через new(Task)

Почему фабрика? Потому что так удобнее: вся инициализация в одном месте, а в main не будет каши.

package main

func NewTask(id int, title string) *Task {
	t := new(Task) // *Task с zero values
	t.ID = id
	t.Title = title
	return t
}

Да, можно было бы и иначе, но сегодня нам важно именно new.

Хранилища: make([]*Task, 0, ...) и make(map[int]*Task)

Слайс будет хранить порядок добавления, а карта — быстрый доступ по ID.

package main

func main() {
	tasks := make([]*Task, 0, 8)
	byID := make(map[int]*Task)

	_ = tasks
	_ = byID
}

Обратите внимание: tasks и byID — уже готовы к использованию. В tasks можно append, в byID можно писать byID[id] = ....

Склеиваем всё: добавить пару задач и вывести

Сделаем короткий, но цельный пример.

package main

import "fmt"

type Task struct {
	ID    int
	Title string
	Done  bool
}

func NewTask(id int, title string) *Task {
	t := new(Task)
	t.ID = id
	t.Title = title
	return t
}

func main() {
	tasks := make([]*Task, 0, 8)
	byID := make(map[int]*Task)

	t1 := NewTask(1, "Выучить new vs make")
	t2 := NewTask(2, "Не путать nil-map и пустую map")

	tasks = append(tasks, t1, t2)
	byID[t1.ID] = t1
	byID[t2.ID] = t2

	fmt.Println(tasks[0].Title) // Выучить new vs make
	fmt.Println(byID[2].Title)  // Не путать nil-map и пустую map
}

Здесь важная мысль: NewTask возвращает *Task, и эти указатели мы храним и в слайсе, и в карте. Это значит, что если мы изменим задачу по указателю — изменения будут видны из обеих структур (это нормально, если мы так задумали).

8. Типичные ошибки

Ошибка №1: пытаться заменить make(map[K]V) на new(map[K]V).
new(map[K]V) возвращает *map[K]V, и внутри будет nil-map. Читать из неё можно, но запись m[k] = v приводит к аварии во время выполнения. Если нужна рабочая карта — используйте make(map[K]V), а не new.

Ошибка №2: ожидать, что new([]T) создаст слайс нужной длины.
new([]T) создаёт указатель на zero value слайса, то есть на nil-слайс длины 0. Это не то же самое, что make([]T, n). Если вы планируете индексировать s[i] — вам нужен make с длиной.

Ошибка №3: делать *map или *[]T «на всякий случай».
Новички иногда добавляют указатели везде, где видят возможность. Но map и []T и так обычно достаточно «ссылочны» по поведению, и лишние указатели только усложняют код: добавляются проверки на nil, появляется больше неочевидных связей и становится труднее понять, кто и где меняет данные.

Ошибка №4: думать, что make возвращает указатель.
make возвращает значение типа []T, map[K]V или chan T. Не *[]T и не *map[K]V. Если в коде вы начинаете писать *m после make(map[...]...), значит вы перепутали модель в голове и пора остановиться, выдохнуть и перечитать таблицу из начала лекции.

Ошибка №5: забывать, что у map есть два разных «пустых состояния»: nil и «пустая, но созданная».
var m map[string]int даёт nil-map: читать можно, писать нельзя. m := make(map[string]int) даёт пустую, но рабочую map: и читать, и писать можно. Эти состояния выглядят похоже («там ничего нет»), но ведут себя по-разному — и это одна из самых частых причин неожиданных паник.

1
Задача
Go SELF, 19 уровень, 2 лекция
Недоступна
Память под число
Память под число
1
Задача
Go SELF, 19 уровень, 2 лекция
Недоступна
Коробка чисел
Коробка чисел
1
Задача
Go SELF, 19 уровень, 2 лекция
Недоступна
Словарная подсказка
Словарная подсказка
1
Задача
Go SELF, 19 уровень, 2 лекция
Недоступна
Каталог задач
Каталог задач
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ