Язык Go — выделение памяти: new, make и составные литералы
Два примитива выделения памяти в Go — new и make — делают разные вещи и применяются к разным типам. Разбираем разницу, составные литералы и принцип полезного нулевого значения.
Содержание
В Go два встроенных примитива выделения памяти: new и make. Они делают разные вещи и применяются к разным типам. Путаница между ними — одна из первых ловушек для тех, кто приходит из других языков.
new: выделить и обнулить
new(T) выделяет память для значения типа T, заполняет её нулями и возвращает указатель на неё — то есть значение типа *T. Никакой инициализации не происходит, только обнуление.
p := new(SyncedBuffer) // p имеет тип *SyncedBuffer, память обнулена
Это полезно, потому что в Go принято проектировать типы так, чтобы их нулевое значение было сразу готово к использованию.
Принцип полезного нулевого значения
bytes.Buffer — нулевое значение это пустой буфер, готовый к работе. sync.Mutex — нулевое значение это разблокированный мьютекс, никакого Init() не нужно. Этот принцип работает транзитивно: если тип состоит из полей, нулевые значения которых уже рабочие, то и сам тип рабочий сразу после создания.
type SyncedBuffer struct {
lock sync.Mutex // нулевое значение — разблокированный мьютекс
buffer bytes.Buffer // нулевое значение — пустой буфер
}
p := new(SyncedBuffer) // тип *SyncedBuffer, готов к работе без дополнительной настройки
var v SyncedBuffer // тип SyncedBuffer, тоже готов к работе сразу
Оба варианта — p и v — работают сразу, без дополнительных вызовов конструктора.
Конструкторы и составные литералы
Иногда нулевого значения недостаточно и нужна явная инициализация. Классический вариант:
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := new(File) // выделяем память, все поля обнулены
f.fd = fd // заполняем поля вручную
f.name = name
f.dirinfo = nil // явно nil, хотя new и так обнулил
f.nepipe = 0 // явный 0, хотя new и так обнулил
return f
}
Много лишнего кода. То же самое через составной литерал — выражение, которое создаёт новый экземпляр типа и сразу инициализирует его поля:
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := File{fd, name, nil, 0} // поля по порядку, все должны быть указаны
return &f // возвращаем адрес локальной переменной — в Go это нормально
}
В отличие от C, возвращать адрес локальной переменной в Go абсолютно безопасно. Память, связанная с переменной, живёт столько, сколько на неё есть ссылки.
Взятие адреса составного литерала само по себе выделяет новый экземпляр — поэтому два последних шага можно объединить:
return &File{fd, name, nil, 0} // создаёт File и сразу возвращает указатель на него
Поля можно указывать в любом порядке, используя синтаксис поле: значение. Пропущенные поля получают нулевые значения:
// dirinfo и nepipe получат нулевые значения автоматически
return &File{fd: fd, name: name}
Если составной литерал не содержит полей вообще, результат эквивалентен new:
new(File) // выделяет *File с нулевыми полями
&File{} // то же самое
Составные литералы работают не только для структур, но и для массивов, слайсов и мап. Метки — это индексы или ключи:
// массив фиксированного размера, размер выводится из содержимого (...)
a := [...]string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
// слайс
s := []string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
// map
m := map[int]string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
make: создать и инициализировать
make(T, args) работает принципиально иначе, чем new. Он применяется только к трём типам: слайсам, мапам и каналам. И возвращает не указатель, а само инициализированное значение типа T.
Причина в том, что эти три типа внутри представляют собой ссылки на структуры данных, которые должны быть инициализированы перед использованием. Слайс, например, — это дескриптор из трёх частей: указатель на массив данных, длина и ёмкость. Пока они не инициализированы, слайс равен nil и непригоден к работе. make как раз выполняет эту инициализацию.
// выделяет массив из 100 int. создаёт дескриптор слайса:
// длина 10, ёмкость 100, указатель на первые 10 элементов массива
make([]int, 10, 100)
Разница между new и make на слайсах:
// выделяет структуру слайса; *p == nil; почти никогда не нужно
var p *[]int = new([]int)
// слайс v ссылается на новый массив из 100 int, готов к работе
var v []int = make([]int, 100)
Излишне усложнённый вариант (для понимания механики):
var p *[]int = new([]int) // p — указатель на nil-слайс
*p = make([]int, 100, 100) // теперь инициализируем слайс через разыменование
Идиоматичный вариант:
v := make([]int, 100) // просто и понятно
Главное отличие new от make
new — обнуляет память, возвращает указатель.
make — инициализирует внутреннюю структуру, возвращает значение (не указатель).
make работает только со слайсами, мапами и каналами.
Если нужен указатель на слайс, map или канал — используйте new или явно берите адрес переменной:
// получить указатель на слайс (редкая задача)
s := make([]int, 100)
p := &s // p имеет тип *[]int
Итоги
new(T)обнуляет память и возвращает*T— указатель на нулевое значение типа- Проектируйте типы так, чтобы их нулевое значение было рабочим — это идиома Go
- Составные литералы заменяют конструкторы с ручным заполнением полей, работают для структур, массивов, слайсов и мап
- Возвращать адрес локальной переменной в Go безопасно
makeинициализирует слайсы, мапы и каналы и возвращает готовое значение, а не указательnew([]int)иmake([]int, n)— это разные вещи: первое возвращает указатель на nil-слайс, второе — готовый к работе слайс