Make e New em Go: Diferenças Práticas na Alocação de Memória
Quando você começa a trabalhar com Go, uma das primeiras confusões que surge é entender quando usar make e quando usar new. À primeira vista, ambos criam espaço na memória, mas servem propósitos completamente diferentes. Neste artigo, vamos desvendar essas diferenças de forma prática, mostrando não apenas o que cada um faz, mas por que você precisa escolher corretamente para evitar bugs silenciosos em produção.
A confusão é natural porque outras linguagens como C e C++ usam uma abordagem unificada para alocação. Go, no entanto, foi projetado com filosofia diferente: new aloca memória e retorna um ponteiro zerado, enquanto make aloca, inicializa e retorna um valor já pronto para uso. Essa distinção existe porque Go trabalha com tipos que precisam de inicialização antes de serem úteis.
Entendendo o new: Alocação Simples
O new é a função mais simples de alocação em Go. Ele recebe um tipo como argumento, aloca memória suficiente para armazenar um valor desse tipo, zera essa memória (preenche com os valores padrão) e retorna um ponteiro para esse espaço alocado.
O aspecto crítico aqui é que new apenas aloca e zera — não inicializa estruturas internas que alguns tipos exigem. Vamos a um exemplo prático:
package main
import "fmt"
func main() {
// new com um inteiro
ptr := new(int)
fmt.Println(*ptr) // Imprime: 0
// new com uma string
strPtr := new(string)
fmt.Println(*strPtr) // Imprime: "" (string vazia)
// new com um slice
slicePtr := new([]int)
fmt.Println(*slicePtr) // Imprime: [] (slice nil)
fmt.Println(len(*slicePtr)) // Imprime: 0
}
Perceba que new retorna sempre um ponteiro. Se você tentar usar diretamente sem desreferenciar, não consegue acessar o valor. Além disso, para tipos como slices, maps e channels, usar new deixa-os em estado nil, o que causa pânicos se você tentar operá-los:
package main
import "fmt"
func main() {
// Tentando usar new com slice
slicePtr := new([]int)
// slicePtr é um ponteiro para um slice nil
// Isto causaria pânico:
// *slicePtr = append(*slicePtr, 1) // panic: assignment to entry in nil map
// Isto funciona, mas é incômodo:
*slicePtr = make([]int, 0)
*slicePtr = append(*slicePtr, 1)
fmt.Println(*slicePtr) // [1]
}
Use new quando você precisa de um ponteiro para um tipo simples (struct, array de tamanho fixo) e quer que seus campos sejam inicializados com valores zero. Não use para slices, maps ou channels.
Make: Alocação Inteligente com Inicialização
O make é mais sofisticado. Funciona apenas com três tipos: slices, maps e channels. Diferentemente de new, make retorna um valor do tipo em si (não um ponteiro) e garante que a estrutura interna esteja completamente inicializada e pronta para uso.
package main
import "fmt"
func main() {
// make com slice
slice := make([]int, 5)
fmt.Println(slice) // [0 0 0 0 0]
fmt.Println(len(slice)) // 5
fmt.Println(cap(slice)) // 5
// make com map
myMap := make(map[string]int)
myMap["age"] = 30
fmt.Println(myMap) // map[age:30]
// make com channel
ch := make(chan int)
// ch agora está pronto para enviar/receber
}
Note as diferenças críticas: make retorna um slice, não um ponteiro para um slice. Você pode usá-lo imediatamente. Para slices, você pode especificar capacidade inicial, o que é crucial para performance em loops:
package main
import (
"fmt"
"time"
)
func main() {
// Sem pre-alocação (ineficiente)
start := time.Now()
slice1 := make([]int, 0)
for i := 0; i < 1000000; i++ {
slice1 = append(slice1, i)
}
fmt.Printf("Sem pre-alocação: %v\n", time.Since(start))
// Com pre-alocação (eficiente)
start = time.Now()
slice2 := make([]int, 0, 1000000)
for i := 0; i < 1000000; i++ {
slice2 = append(slice2, i)
}
fmt.Printf("Com pre-alocação: %v\n", time.Since(start))
}
A pré-alocação com make economiza realocações desnecessárias. Maps também aceitam capacidade inicial:
package main
import "fmt"
func main() {
// Map com capacidade inicial
myMap := make(map[string]int, 100)
// Operações posteriores serão mais eficientes
for i := 1; i <= 100; i++ {
myMap[fmt.Sprintf("key%d", i)] = i
}
fmt.Println(len(myMap)) // 100
}
Aplicações Práticas e Armadilhas Comuns
Agora que você entende a teoria, vamos ver onde essas funções realmente se diferenciam em código do mundo real. A principal armadilha é tentar usar new com tipos que exigem make:
package main
import "fmt"
func processData(data map[string]int) {
// Se alguém passar nil aqui, pânico!
data["count"] = 1 // panic: assignment to entry in nil map
}
func main() {
// ERRADO: isso cria um ponteiro para um map nil
wrongMap := new(map[string]int)
// CORRETO: isso cria um map inicializado
correctMap := make(map[string]int)
correctMap["count"] = 1
processData(correctMap)
}
Outra situação real: ao trabalhar com structs que contêm slices ou maps:
package main
import "fmt"
type Config struct {
Settings map[string]string
Values []int
}
func main() {
// ERRADO: campos map e slice ficarão nil
conf1 := new(Config)
// conf1.Settings["key"] = "value" // pânico
// CORRETO: usando composição com make
conf2 := &Config{
Settings: make(map[string]string),
Values: make([]int, 0),
}
conf2.Settings["key"] = "value"
conf2.Values = append(conf2.Values, 1)
fmt.Println(conf2)
}
Uma boas prática em Go é sempre fornecer funções construtoras (factory functions) para suas structs quando elas contêm tipos que precisam de inicialização:
package main
import "fmt"
type Database struct {
cache map[string]interface{}
logs []string
connected bool
}
// Construtor adequado
func NewDatabase() *Database {
return &Database{
cache: make(map[string]interface{}),
logs: make([]string, 0),
connected: false,
}
}
func main() {
db := NewDatabase()
db.cache["user"] = "João"
db.logs = append(db.logs, "Database inicializado")
fmt.Println(db.cache)
fmt.Println(db.logs)
}
Quadro Comparativo e Guia de Decisão
Para consolidar o aprendizado, aqui está quando usar cada um:
Use new quando:
- Você precisa de um ponteiro para um tipo simples (int, string, struct)
- Os campos dessa struct não contêm slices, maps ou channels não inicializados
- Você quer que tudo seja zerado e não precisa de operações subsequentes
Use make quando:
- Você está trabalhando com slices, maps ou channels
- Precisa especificar tamanho inicial ou capacidade
- Quer um valor pronto para uso imediato
Um exemplo final que demonstra a diferença comportamental:
package main
import "fmt"
func main() {
// new com array de tamanho fixo: funciona bem
arr := new([5]int)
arr[0] = 10 // Funciona porque array de tamanho fixo é como uma struct
fmt.Println(arr) // &[10 0 0 0 0]
// new com slice: problema
slicePtr := new([]int)
fmt.Println(slicePtr) // &[]
fmt.Println(*slicePtr) // []
fmt.Println(len(*slicePtr)) // 0
// Mesmo tentando usar, é problemático
// *slicePtr = append(*slicePtr, 1) // Isto funciona, mas é confuso
// make com slice: natural
slice := make([]int, 5)
slice[0] = 10
fmt.Println(slice) // [10 0 0 0 0]
slice = append(slice, 20)
fmt.Println(slice) // [10 0 0 0 0 20]
}
Conclusão
Aprendemos que new e make servem propósitos distintos e não são intercambiáveis. new aloca memória e retorna um ponteiro zerado — perfeito para tipos simples. make aloca, inicializa e retorna um valor pronto para uso — essencial para slices, maps e channels. A escolha errada causa bugs que podem ser silenciosos em desenvolvimento e explodem em produção. Pratique reconhecendo qual função usar analisando o tipo que você está alocando: se é slice, map ou channel, make é sua resposta; caso contrário, new geralmente é mais apropriado para ponteiros. Por fim, construa hábitos de escrever funções construtoras (factory functions) em suas structs, especialmente quando elas contêm tipos que precisam de inicialização — isso torna seu código mais seguro e expressivo.