Slice vacío en Go: cómo no equivocarse desde el principio
\n\nTodo desarrollador de Go, tarde o temprano, se enfrenta a la pregunta: "¿cómo inicializar correctamente un slice vacío?". Parece una tarea simple, pero no todo es tan claro. En el FAQ oficial de Go y en Stack Overflow, este tema tiene cientos de comentarios y miles de votos. ¿Por qué?
\n\nPorque en Go hay dos formas fundamentalmente diferentes de crear un slice vacío, y la elección entre ellas afecta el rendimiento, la legibilidad del código e incluso el comportamiento durante la serialización. Si programas en Go y quieres escribir código limpio y eficiente sin sorpresas, este artículo es para ti.
\n\nAnalizaremos:
\n- \n
- En qué se diferencia
var s []intdes := make([]int, 0)\n - Cuándo usar un nil-slice y cuándo un slice vacío \n
- Cómo afecta esto a JSON, la memoria y el rendimiento \n
- Qué dicen los gurús de Go y los autores de la biblioteca estándar \n
Dos hermanos gemelos: nil slice vs empty slice
\n\nEn Go, un slice es una estructura de tres campos: puntero a un array, longitud y capacidad. Cuando hablamos de un slice vacío, nos referimos a un slice con longitud 0. Pero la forma en que lo creamos sí importa.
\n\nForma 1: Declaración con var (nil slice)
\n\nvar s []int // s == nil, len(s) == 0, cap(s) == 0\n\nEste es un nil-slice. No apunta a ningún array en memoria. Sin embargo, puedes llamar a append sin problemas: Go asignará memoria automáticamente.
Forma 2: Inicialización con make (empty slice)
\n\ns := make([]int, 0) // s != nil, len(s) == 0, cap(s) == 0\n\nEste es un slice vacío. Ya apunta a un array cero (un zerobase especial en el runtime de Go).
\n\nA primera vista, la diferencia es imperceptible. Pero profundicemos.
\n\nSalida en consola y JSON — la primera trampa
\n\nPrueba ejecutar este código:
\n\npackage main\n\nimport (\n "encoding/json"\n "fmt"\n)\n\nfunc main() {\n var nilSlice []int\n emptySlice := make([]int, 0)\n\n fmt.Printf("nilSlice: %v, is nil: %v\\n", nilSlice, nilSlice == nil)\n fmt.Printf("emptySlice: %v, is nil: %v\\n", emptySlice, emptySlice == nil)\n\n // Serialización JSON\n nilJSON, _ := json.Marshal(nilSlice)\n emptyJSON, _ := json.Marshal(emptySlice)\n\n fmt.Printf("nilSlice JSON: %s\\n", nilJSON)\n fmt.Printf("emptySlice JSON: %s\\n", emptyJSON)\n}\n\nEl resultado te sorprenderá:
\n\nnilSlice: [], is nil: true\nemptySlice: [], is nil: false\nnilSlice JSON: null\nemptySlice JSON: []\n\n¡Ahí está la gran sorpresa! La serialización JSON se comporta de manera diferente. Si tu API espera [] en la respuesta y pasas un nil-slice, el cliente recibirá null. Y esto rompe los contratos.
Muchos clientes REST (especialmente en JavaScript) verifican estrictamente: si se espera un array en la respuesta y llega null, puede producirse un error o romperse la lógica en el frontend.
Rendimiento: ¿hay alguna diferencia?
\n\nLa respuesta corta es: sí, pero en el 99% de los casos es insignificante.
\n\nCuando usas make([]int, 0), Go asigna inmediatamente memoria para la estructura del slice (24 bytes en sistemas de 64 bits) y lo vincula con zerobase. Al usar var, la asignación ocurre solo en el primer append.
Si creas un slice que probablemente permanecerá vacío, el nil-slice ahorra una asignación de memoria. Si sabes con certeza que agregarás elementos, make especificando la capacidad dará un mejor rendimiento:
// Si conoces el tamaño aproximado, especifica la capacidad\ns := make([]int, 0, 100) // capacidad 100, longitud 0\nfor i := 0; i < 100; i++ {\n s = append(s, i) // ¡sin reasignaciones!\n}\n\nPero para la mayoría de las aplicaciones prácticas, la diferencia es