Aprendendo Go: New vs Make

Aprendendo Go: New vs Make

Conheça a diferença entre os tipos de alocação de memória em Go

A linguagem Go, é muito eficiente e simples, porém existem alguns conceitos que são chave para que mesmo com toda simplicidade se mantenha muito eficiente, hoje vamos falar sobre alocação de memória em Go.

Esse artigo foi baseado no documento, gratuito e disponível em Effective Go

Go possui, nativamente, dois diferente tipos de alocação primitiva e são as funções new e make. As quais tem propostas diferentes e se aplicam a diferentes tipos, isso pode se tornar confuso porém uma vez entendidas as regras, se torna algo simples.

New

New é uma função nativa que aloca memória, entretanto, apesar do nome representar algo diferente em outras linguagens, aqui, o New não inicializa um tipo e um tamanho em um espaço de memória, ele apenas escreve um endereço que contenha um valor zerado. Logo, quando fazemos v := new(T) o método aloca um espaço de memória do tipo *T com o valor zero e v retorna o endereço desse ponteiro.

Uma vez que o valor no endereço retornado pelo new está zerado, isso se torna útil para modelar e inicializar suas estruturas de dados, onde, cada tipo da sua estrutura é preenchido pelo conceito do valor zerado e não se faz necessário uma inicialização mais complexa. Por exemplo, na documentação de bytes.Buffer é reportado The zero value for Buffer is an empty buffer ready to use. Ou seja, o valor de zero para um tipo Buffer inicializado é um buffer vazio pronto para ser usado. De maneira parecida, sync.Mutex não tem um construtor exposto ou um método inicializador, ao invés disso, o valor zero para sync.Mutex define um estado unlocked para a Mutex em questão.

O inicializar um tipo zerado é útil para quando o contexto funciona de maneira mutável. Considere o tipo abaixo

type SyncedBuffer struct {
    lock    sync.Mutex
    buffer  bytes.Buffer
}

Os valores do tipo SyncedBuffer estão prontos para serem usados imediatamente com a alocação ou apenas declaração. Abaixo, tanto p quanto v funcionarão corretamente sem qualquer tipo de declaração adicional.

p := new(SyncedBuffer)  // return *SyncedBuffer
var v SyncedBuffer      // return SyncedBuffer

Make

Make(T, args) é uma função nativa que tem uma proposta diferente de New(T). Se concentrando apenas em criar slices, maps e channels. Estes tem por retorno um valor inicializado, todavia, o valor não é zerado e seu tipo se concentra em T e não *T.

A razão pela qual existe essa diferença está na representação que esses três tipos representam, por debaixo dos panos, referências a estruturas de dados precisam ser necessáriamente inicializadas antes de serem utilizadas. Um slice, por exemplo, é um three-item descriptor uma representação de três partes que contém um ponteiro para o dado que está dentro do próprio array, o comprimento da parte recortada e a capacidade da parte recortada, até que todos esses três parametros sejam estabelecidos slice tem valor nil.

Para slices, maps e channels, make inicializa a estrutura de dados e prepara os valores para serem utilizados, por exemplo

make([]int, 10, 100)

O código acima aloca um array com 100 inteiros e cria um slice com tamanho 10 e capacidade de 100, apontando para os primeiros 10 elementos do array. Quando você faz um slice, a capacidade pode ser omitida, entretanto, new([]int) retorna um ponteiro para um novo endereço, com um slice zerado, à isso chamamos a pointer to a nil slice value ou um ponteiro para um slice de valor anulado.

These examples illustrate the difference between new and make.

var p *[]int = new([]int) // *p == nil, ponteiro para slice de valor anulado
var v  []int = make([]int, 100) // Slice v se torna um novo array de 100 inteiros

// Complexo demais
var p *[]int = new([]int)
*p = make([]int, 100, 100)

// Recomendado
v := make([]int, 100)

Lembre-se de que o make se aplica apenas as estruturas citadas e não retorna um ponteiro. Para obter o ponteiro alocado você precisa utiliza new ou pegar o endereço da variável que recebe o valor da função.