Go Admin

Dominando Make e New em Go: Diferenças Práticas na Alocação de Memória em Projetos Reais Já leu

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 e quando usar . À 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: aloca memória e retorna um ponteiro zerado, enquanto 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 : Alocação Simples O é a função mais simples de alocação em Go. Ele recebe um tipo como argumento, aloca memória suficiente para armazenar

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.

Referências


Artigos relacionados