Volver al blog

var vs make en Go: cuándo usar cada uno (con ejemplos)

var te da el valor cero y make inicializa slices, maps y channels. Cuándo usar cada uno en Go y por qué escribir en un map nil provoca un panic mientras que hacer append a un slice nil no.

var vs make en Go: cuándo usar cada uno (con ejemplos)

Go tiene dos formas de crear una variable, var y make, y elegir la equivocada provoca muchos errores de principiante. El clásico es un map que provoca un panic en su primera escritura. Muy cerca le sigue un channel que se bloquea para siempre. Este artículo explica cuándo usar cada uno y por qué los slices, los maps y los channels son los casos especiales. Todos los ejemplos se pueden ejecutar.

Índice

¿Cuál es la diferencia entre var y make en Go?

var declara una variable y le asigna el valor cero de su tipo. Funciona con cualquier tipo del lenguaje. make solo acepta un slice, un map o un channel, los tres tipos integrados que necesitan preparación en tiempo de ejecución, y devuelve un valor que puede contener datos desde el primer momento. El valor cero de esos tres tipos es nil, así que var se salta la preparación que hace make.

En concreto, make reserva la tabla hash que hay detrás de un map, prepara las colas de envío y recepción de un channel y le da a un slice un array subyacente. Si declaras cualquiera de los tres con var, obtienes nil en su lugar.

El mismo map, declarado de las dos formas:

example.gogo
package main

import "fmt"

func main() {
	var counts map[string]int          // nil map, no hash table yet
	made := make(map[string]int)       // initialized, ready to write

	fmt.Println(counts == nil)         // true
	fmt.Println(made == nil)           // false

	made["/health"] = 1                // fine, made is initialized
	fmt.Println(made["/health"])       // 1

	// counts["/health"] = 1           // panic: assignment to entry in nil map
}

Mucha gente espera que reservar memoria devuelva un puntero. make no lo hace. Reserva la memoria y te entrega el valor directamente.

Así, make([]int, 3) devuelve un []int normal, no un *[]int, y lo usas sin desreferenciar nada. make(map[string]int) devuelve un map en el que puedes escribir de inmediato. La función integrada que sí devuelve un puntero es new, que veremos más adelante.

var x Tmake(T, ...)
Sirve paracualquier tiposolo slices, maps y channels
Te dael valor ceroun valor inicializado y utilizable
Resultado con un sliceslice nil (len 0, cap 0)slice no nil con array subyacente
Resultado con un mapmap nil (escribir provoca un panic)map en el que se puede escribir
Resultado con un channelchannel nil (se bloquea para siempre)channel utilizable
Devuelveel valorel valor, nunca un puntero

¿Cuándo usar var?

Usa var con cualquier tipo cuyo valor cero ya sea útil: números, booleanos, cadenas, structs, punteros e interfaces. Go está diseñado para que el valor cero sea un estado inicial válido, así que pocas veces tienes que inicializar algo antes.

bytes.Buffer es un buen ejemplo. No tiene constructor. Lo declaras con var y empiezas a escribir:

example.gogo
package main

import (
	"bytes"
	"fmt"
)

func main() {
	var buf bytes.Buffer            // zero value is ready to use
	buf.WriteString("status=")
	buf.WriteString("ok")
	fmt.Println(buf.String())      // status=ok
}

sync.Mutex funciona igual. Su valor cero es un mutex desbloqueado, y no existe un constructor NewMutex porque no hace falta.

var también sirve para un slice, siempre que solo lo hagas crecer con append. Un slice nil es un slice vacío válido con longitud 0 y capacidad 0, y append reserva un array subyacente cuando añades el primer elemento. Eso sí, no puedes acceder a él por índice. ids[0] = 3 provoca un panic de índice fuera de rango porque la longitud sigue siendo 0. Si necesitas asignar por índice, usa make([]int, n) para que el slice tenga una longitud real desde el principio. Para ir acumulando elementos con append, un slice nil va bien:

example.gogo
package main

import "fmt"

func main() {
	var ids []int                  // nil slice, but append-ready
	for i := 1; i <= 3; i++ {
		ids = append(ids, i*10)
	}
	fmt.Println(ids)               // [10 20 30]
	fmt.Println(ids == nil)        // false, append allocated it
}

Escribir ids := make([]int, 0) aquí no aporta nada. El slice nil es más corto y se comporta igual en cuanto se ejecuta el primer append. Si quieres practicar esto, el curso de tipos compuestos tiene una serie completa de ejercicios sobre el comportamiento de slices y maps.

¿Cuándo usar make?

Usa make cuando un slice, un map o un channel tenga que contener datos antes de que se le añada nada con append. En la práctica, eso significa un map en el que vas a escribir claves, un channel por el que van a enviar goroutines o un slice que quieres dimensionar de antemano para que su array subyacente exista desde el principio.

Los maps son el caso más frecuente, porque no hay nada equivalente a append que reserve uno por ti. Si piensas asignar cualquier clave, primero tienes que crear el map con make. Contar peticiones por ruta en un handler HTTP es un caso típico:

example.gogo
package main

import "fmt"

func main() {
	hits := make(map[string]int)   // must initialize before writing

	for _, path := range []string{"/api", "/api", "/health"} {
		hits[path]++                // safe: map is initialized
	}
	fmt.Println(hits["/api"])      // 2
	fmt.Println(hits["/health"])   // 1
}

Con los channels pasa algo parecido. Un channel por el que envías tiene que crearse con make, porque un channel nil se bloquea para siempre (lo vemos más abajo). Un reparto de trabajo entre workers (fan-out) empieza con un channel creado con make:

example.gogo
package main

import "fmt"

func main() {
	jobs := make(chan int, 3)      // buffered channel, capacity 3
	jobs <- 1
	jobs <- 2
	close(jobs)

	for j := range jobs {
		fmt.Println("processing job", j)
	}
}

Con los slices, make tiene dos formas. make([]T, n) te da un slice de longitud n lleno de valores cero, al que accedes directamente por índice. make([]T, 0, n) te da un slice vacío con espacio para n elementos, al que añades elementos con append sin realocar. Usa la primera cuando vayas a rellenar cada posición por índice, por ejemplo al leer un número fijo de bytes:

example.gogo
package main

import "fmt"

func main() {
	// length 4, index into it directly
	buf := make([]byte, 4)
	copy(buf, []byte("data"))
	fmt.Println(string(buf))       // data

	// length 0, capacity 8, append without regrowing
	sums := make([]int, 0, 8)
	for i := 0; i < 8; i++ {
		sums = append(sums, i*i)
	}
	fmt.Println(sums)              // [0 1 4 9 16 25 36 49]
}

La segunda forma tiene que ver con el rendimiento. A medida que un slice crece, append lo va copiando a arrays cada vez más grandes. Si sabes más o menos cuántos elementos van a llegar, fijar la capacidad de antemano te ahorra esas copias. El comportamiento del programa no cambia, solo su velocidad.

¿Por qué escribir en un map nil provoca un panic pero hacer append a un slice nil funciona?

Los dos se diseñaron de forma distinta. append sabe manejar un slice nil. En la primera llamada reserva un array subyacente y devuelve un slice real, así que escribir mediante append siempre funciona. Los maps no tienen nada parecido. Una escritura va directa a una tabla hash, un map nil no tiene ninguna y el runtime provoca un panic en lugar de adivinar lo que querías.

A casi todo el mundo le pasa en su primera semana con Go. Esto es lo que hace cada tipo nil, empezando por el slice nil, en el que puedes leer y hacer append sin problemas:

example.gogo
package main

import "fmt"

func main() {
	var s []int                    // nil slice
	fmt.Println(len(s), cap(s))    // 0 0
	fmt.Println(s == nil)          // true

	s = append(s, 42)              // works: append allocates
	fmt.Println(s)                 // [42]
}

Un map nil es distinto. Leer de él devuelve el valor cero del tipo de los valores y recorrerlo con range no hace nada, pero la primera escritura provoca un panic:

example.gogo
package main

import "fmt"

func main() {
	var m map[string]int           // nil map
	fmt.Println(m["missing"])      // 0, reading nil map is safe
	fmt.Println(len(m))            // 0, also safe

	m["key"] = 1                   // panic: assignment to entry in nil map
	fmt.Println(m)
}

Esa división entre lecturas seguras y escrituras que provocan un panic es lo que hace que el error sea fácil de pasar por alto. Un map nil supera todos los tests que solo leen de él y luego provoca un panic la primera vez que llegan datos reales. La solución no cambia: inicialízalo con make antes de la primera escritura o acepta un map que el llamador ya haya creado.

Los channels nil son los que se comportan de forma más rara de los tres. No provocan un panic. Un envío o una recepción en un channel nil simplemente nunca avanza, lo que en una sola goroutine es un deadlock:

example.gogo
package main

func main() {
	var ch chan int                // nil channel
	ch <- 1                        // blocks forever: fatal deadlock
}

Suena a trampa, pero es útil dentro de un select. Un channel nil nunca está listo, así que asignar nil a una variable de channel desactiva su case, y de esa forma puedes apagar una rama en tiempo de ejecución. Cuidado con close, eso sí: cerrar un channel nil provoca un panic, así que cierra solo los channels que hayas creado tú. El curso de fundamentos de concurrencia trata a fondo el patrón de select con channels nil. La pareja de map nil y slice nil también está en nuestra lista de errores comunes en Go que debes evitar, porque también pilla a desarrolladores con experiencia.

¿Y new? (var vs make vs new)

new es la tercera función integrada de reserva de memoria, y hace un trabajo distinto al de make. new(T) reserva memoria puesta a cero para un T y devuelve un *T. make solo trabaja con slices, maps y channels y devuelve el valor. new funciona con cualquier tipo y siempre devuelve un puntero.

El motivo habitual para llamarlo es obtener un puntero a un valor cero nuevo sin declarar antes una variable con nombre:

example.gogo
package main

import "fmt"

func main() {
	p := new(int)                  // *int pointing at a zeroed int
	fmt.Println(*p)                // 0
	*p = 7
	fmt.Println(*p)                // 7
}

Desde Go 1.26, new también acepta una expresión. new(expr) reserva espacio, guarda ahí el valor de la expresión y devuelve un puntero a él. Eso hace innecesarias las antiguas funciones auxiliares al estilo de intPtr, que solo existían para obtener un puntero a un literal:

example.gogo
package main

import "fmt"

func main() {
	port := new(8080)              // *int pointing at 8080 (Go 1.26)
	enabled := new(true)           // *bool pointing at true

	fmt.Println(*port)             // 8080
	fmt.Println(*enabled)          // true
}

Esto es útil para campos opcionales de structs, donde un puntero nil significa «sin definir» y uno no nil significa que alguien eligió un valor. El curso de punteros y memoria tiene un ejercicio de valores por defecto de configuración basado en este patrón. Las tres funciones integradas, una al lado de la otra:

Función integradaSirve paraDevuelve¿El resultado es utilizable?
var x Tcualquier tipoel valor cerosi el valor cero lo es (no para escribir en maps)
make(T, ...)slice, map, channelel valor Tsí, inicializado
new(T)cualquier tipo*T, un punteroapunta a un T puesto a cero
new(expr) (Go 1.26)cualquier expresiónun puntero a ese valorapunta al valor indicado

Para un slice, un map o un channel que vas a llenar, casi siempre quieres make. Usa new cuando necesites específicamente un puntero, casi siempre un puntero a un literal para un campo opcional.

Una regla rápida para decidir

Si no tienes claro cuál usar, recorre esta lista y detente en la primera línea que encaje:

  1. ¿Un map en el que vas a escribir claves? Usa make(map[K]V). Un map nil provoca un panic en la primera escritura.
  2. ¿Un channel por el que van a enviar goroutines? Usa make(chan T) o make(chan T, n). Un channel nil se bloquea para siempre.
  3. ¿Un slice que quieres dimensionar de antemano, por velocidad o para asignar por índice? Usa make([]T, n) o make([]T, 0, n).
  4. ¿Un slice que solo vas a hacer crecer con append? Usa var s []T. Un slice nil funciona con append.
  5. ¿Un puntero a un valor nuevo? Usa new(T), o new(expr) en Go 1.26 para apuntar a un literal.
  6. ¿Cualquier otra cosa (int, string, bool, struct, la mayoría de los tipos de la biblioteca estándar)? Usa var, porque el valor cero ya se puede usar.

Si vas a recordar una sola cosa, que sea esta: antes de escribir una clave en un map, comprueba de dónde viene el map. Un map que llega como parámetro se supone que lo inicializó el llamador. Un map que declaraste tú con var es nil, y la primera escritura provocará un panic.

Preguntas frecuentes

¿Puedo usar var para un map?

Puedes declarar un map con var m map[string]int. Leer de él, recorrerlo con range y llamar a len sobre él es seguro. Escribir una clave no lo es: m["x"] = 1 provoca un panic con assignment to entry in nil map. Si solo lees, var te sirve. Si escribes cualquier clave, inicializa el map antes con make o recibe uno que el llamador ya haya construido.

¿Es make([]int, 0) lo mismo que var s []int?

Ambos tienen longitud 0 y se comportan igual una vez que haces append. La diferencia aparece al comparar con nil. var s []int es nil, mientras que make([]int, 0) es un slice vacío no nil. Eso importa si tu código o un test comprueba s == nil, o si codificas a JSON, donde nil se convierte en null y el slice vacío en []. Para añadir elementos sin más, prefiere var.

¿Sirve make para structs?

No. make solo acepta slices, maps y channels, así que make(MyStruct) da un error de compilación. Un struct no necesita la preparación en tiempo de ejecución que hace make. Créalo con un literal compuesto como MyStruct{}, decláralo con var s MyStruct para obtener el valor cero o usa new(MyStruct) cuando quieras un *MyStruct.

make vs new: ¿cuál usar para obtener un puntero?

Usa new cuando quieras un puntero. new(T) devuelve un *T que apunta a un valor puesto a cero, y en Go 1.26 new(expr) devuelve un puntero al valor de esa expresión. make nunca devuelve un puntero. Devuelve un slice, un map o un channel inicializado. Las dos no se solapan: make te da una colección utilizable y new te da un puntero.

Fuentes

Referencias principales citadas en este artículo (última verificación: 3 de julio de 2026):

Escribe Go como un ingeniero sénior

Lecciones interactivas en tu navegador. Las primeras son gratis.

Prueba una lección gratisO crea una cuenta gratuita