En Go, no existe un tipo de dato set nativo. La convención estándar para implementar uno es utilizar un map donde la clave sea el elemento que queremos almacenar y el valor sea un struct{} vacío. Usamos struct{} porque, a diferencia de un bool o un int, este tipo no ocupa bytes de memoria; es una estructura de tamaño cero que le indica al runtime que solo nos interesa la presencia de la clave.
Esta aproximación es la más eficiente en términos de memoria para colecciones de gran tamaño. Si el overhead de memoria no fuera una preocupación, usar map[T]bool es una alternativa válida y un poco más legible, pero en sistemas de alta carga, cada byte cuenta. Para manipular estas colecciones, usamos la sintaxis de mapas para añadir elementos (m[k] = struct{}{}), eliminarlos (delete(m, k)) y verificar membresía mediante la coma-ok idiom (_, ok := m[k]).
Cuando necesitas limpiar estas estructuras, el builtin clear [disponible desde Go 1.21] es tu mejor aliado. Si usas un bucle para hacer delete de cada elemento en un mapa, el trabajo recae en el código del usuario y el runtime debe realizar múltiples operaciones de borrado. clear está optimizado a nivel de runtime para vaciar el mapa (o un slice) de forma eficiente, permitiendo reutilizar la capacidad de la estructura ya asignada sin necesidad de crear una nueva instancia y generar presión en el Garbage Collector. Sin embargo, debes tener cuidado: si intentas usar clear en un slice pensando que lo “vaciará” como un s[:0], te llevarás una sorpresa.
package main
import (
"fmt"
)
func main() {
// Definimos un set de permisos usando map[string]struct{}
// struct{} asegura que el valor no consuma memoria.
permissions := map[string]struct{}{
"read": {},
"write": {},
"execute": {},
}
// Añadir un nuevo permiso (add)
permissions["admin"] = struct{}{}
// Verificar membresía (contains)
if _, ok := permissions["write"]; ok {
fmt.Println("Permiso 'write' concedido.")
}
// Un segundo set para calcular la intersección
otherPermissions := map[string]struct{}{
"read": {},
"execute": {},
"delete": {},
}
// Intersección: elementos presentes en ambos sets
intersection := make(map[string]struct{})
for k := range permissions {
if _, exists := otherPermissions[k]; exists {
intersection[k] = struct{}{}
}
}
fmt.Printf("Intersección: %v\n", intersection)
// Usamos clear para resetear el mapa de permisos sin reasignar memoria.
// Esto es ideal en bucles de larga duración para reutilizar el mapa.
clear(permissions)
fmt.Printf("Mapa 'permissions' tras clear: %v, len: %d\n", permissions, len(permissions))
// clear también funciona en slices, pero con un comportamiento distinto.
logs := []string{"log1", "log2", "log3"}
fmt.Printf("Slice antes de clear: %v, len: %d\n", logs, len(logs))
clear(logs)
// Nota: clear en un slice pone los elementos a su valor zero, pero NO cambia su len.
fmt.Printf("Slice después de clear: %v, len: %d\n", logs, len(logs))
}
En el código anterior, hemos implementado la lógica de un set mediante map[string]struct{}. Al usar permissions["admin"] = struct{}{}, estamos insertando una clave sin asignar memoria al valor. En la operación de intersección, recorremos permissions y comprobamos si cada clave existe en otherPermissions usando la sintaxis if _, exists := .... Esto es eficiente porque la búsqueda en un mapa es, en promedio, $O(1)$.
Cuando llamamos a clear(permissions), el runtime vacía el mapa. Lo importante aquí es que permissions sigue siendo el mismo objeto con la misma capacidad de buckets; no estamos creando un nuevo mapa, lo que evita que el Garbage Collector tenga que limpiar la estructura antigua y nosotros podamos seguir insertando elementos sin nuevas asignaciones de memoria pesadas.
Finalmente, observa el comportamiento de clear(logs). A diferencia de lo que uno podría esperar, la longitud (len) del slice permanece en 3, pero sus elementos han sido seteados al “zero value” (en este caso, strings vacíos "").
El error frecuente
Un error común al trabajar con clear ocurre cuando se utiliza en un slice buscando resetearlo para volver a usarlo, confundiendo su comportamiento con el reslicing.
// ERROR FRECUENTE
func errorWithClear() {
s := []int{10, 20, 30}
clear(s)
fmt.Println(len(s)) // Imprime 3, no 0
fmt.Println(s) // Imprime [0 0 0], no []
}
Si tu intención es resetear un slice para que su longitud sea cero pero mantener la capacidad para reutilizar el buffer, no uses clear. En su lugar, usa s = s[:0]. El uso de clear es apropiado únicamente cuando quieres “limpiar” los datos de las posiciones actuales sin alterar la estructura del slice, o cuando trabajas con mapas para vaciar sus entradas manteniendo los buckets asignados.
N° 56