El idioma: un map con valores vacíos
Go no tiene la palabra clave set ni un set en la librería estándar. Un map cuyas claves son los elementos y cuyos valores no llevan nada hace el trabajo.
struct{} es el tipo struct vacío y struct{}{} es su único valor. Ocupa cero bytes, así que el map guarda las claves y nada más.
Se aplican todas las reglas de los maps: las claves tienen que ser comparables, el orden de iteración es aleatorio, un map nil provoca un panic al escribir y las escrituras concurrentes necesitan un lock. La página de maps las explica una a una.
map[T]struct{} o map[T]bool
La otra forma habitual es map[T]bool. Se lee mejor porque una clave ausente devuelve false:
seen := map[string]bool{}
seen["a"] = true
if seen["a"] { ... }
Las contrapartidas:
map[T]struct{} | map[T]bool | |
|---|---|---|
| Tamaño del valor | 0 bytes | 1 byte (más alineación) |
| Comprobar pertenencia | _, ok := s[k] | s[k] |
| Ambigüedad | ninguna | s[k] = false es un tercer estado |
La tercera fila es el verdadero motivo por el que muchas bases de código prefieren el struct vacío: con bool, alguien acaba escribiendo s[k] = false y len(s) deja de ser el número de miembros. En sets pequeños la diferencia de memoria no importa.
Eliminar duplicados de un slice
El uso más común de un set es quitar duplicados. Esto conserva la primera aparición de cada valor y mantiene el orden:
Salida:
[b a c]
[3 1 2]
[1 2 3]
Unión, intersección y diferencia
El álgebra de conjuntos son unos pocos bucles. Recorre el set más pequeño al comprobar la pertenencia en el otro, porque cada búsqueda tarda un tiempo constante de media.
sorted existe solo para que la salida sea estable. Imprimir un set recorriéndolo da un orden distinto en cada ejecución. maps.Keys y slices.Sorted necesitan Go 1.23.
Un tipo con nombre como type set map[string]struct{} sigue siendo un map: lo indexas, lo recorres con range y haces delete de la misma forma, y puedes colgarle métodos.
Un pequeño Set genérico
Con genéricos (Go 1.18), un solo tipo sirve para cualquier tipo de elemento comparable. Esto basta para la mayoría de programas:
Envolver el map en un struct esconde el ruido de struct{}{} y garantiza que el constructor crea el map, lo que elimina el panic del map nil. Sorted no puede ser un método: un método no puede declarar sus propios parámetros de tipo ni endurecer la restricción comparable del tipo, y ordenar necesita cmp.Ordered. La página de genéricos explica las restricciones.
Si necesitas un set completo (variantes seguras para concurrencia, muchas operaciones), existen paquetes de terceros como github.com/deckarep/golang-set. Para la mayor parte del código, el idioma del map o un tipo de 30 líneas como este es lo que usan los programadores de Go.
Sets de structs
Cualquier tipo comparable puede ser un elemento, incluidos los structs con campos comparables. Eso hace directas las comprobaciones de "¿he visto ya este par?":
type edge struct{ from, to string }
visited := map[edge]struct{}{}
visited[edge{"a", "b"}] = struct{}{}
Los slices y los maps no pueden ser elementos de un set. Para registrar slices únicos, convierte antes cada uno en una clave comparable, por ejemplo un array de tamaño fijo o un string construido con fmt.Sprint.
Errores comunes
- Olvidar inicializar.
var s map[string]struct{}es nil; la primera inserción provoca un panic. - Imprimir un set y esperar una salida estable. Ordena antes los miembros.
- Usar
map[T]booly guardarfalse. Entonceslenya no cuenta los miembros. Usadeletepara quitar.
Preguntas frecuentes
¿Tiene Go un tipo set?
No. La librería estándar no tiene sets. El idioma es un map cuyos valores no aportan información: map[string]struct{}. Añadir es s[k] = struct{}{}, comprobar la pertenencia es _, ok := s[k], quitar es delete(s, k) y el tamaño es len(s).
¿Debo usar map[T]bool o map[T]struct{} para un set en Go?
map[T]struct{} deja clara la intención y sus valores ocupan cero bytes. map[T]bool se lee de forma más natural (if seen[x]) porque una clave ausente devuelve false. Los dos son correctos; la diferencia de memoria solo importa en sets muy grandes. Elige uno y sé coherente.
¿Cómo elimino los duplicados de un slice en Go?
Para conservar la primera aparición y el orden, recorre el slice apuntando los valores vistos en un map[T]struct{} y añade solo los que no has visto. Si el orden no importa, ordena y quita los duplicados contiguos: slices.Sort(s); s = slices.Compact(s).