· 8 мин 👁 1.7k Начинающий

Язык Go — выделение памяти: new, make и составные литералы

Два примитива выделения памяти в Go — new и make — делают разные вещи и применяются к разным типам. Разбираем разницу, составные литералы и принцип полезного нулевого значения.

newmakeпамятьcomposite literalsслайсыmaps
Содержание

В 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-слайс, второе — готовый к работе слайс