Menu

Golang Set: map[T]struct{}, Mengenoperationen und generisches Set

Go hat keinen eingebauten Set-Typ. Das übliche Idiom ist eine Map mit leeren Struct-Werten. Hinzufügen, prüfen und entfernen, Vereinigung, Schnittmenge und Differenz, und wie du ein kleines generisches Set schreibst.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Das Idiom: eine Map mit leeren Werten

Go hat kein set-Schlüsselwort und kein Set in der Standardbibliothek. Eine Map, deren Schlüssel die Elemente sind und deren Werte nichts tragen, erledigt die Aufgabe.

struct{} ist der leere Struct-Typ und struct{}{} sein einziger Wert. Er belegt null Bytes, also speichert die Map nur die Schlüssel.

Alle Map-Regeln gelten: Schlüssel müssen vergleichbar sein, die Iterationsreihenfolge ist zufällig, eine nil-Map löst beim Schreiben eine Panic aus, und nebenläufige Schreibzugriffe brauchen ein Lock. Die Seite zu Maps behandelt jeden Punkt.

map[T]struct{} oder map[T]bool

Die andere verbreitete Schreibweise ist map[T]bool. Sie liest sich besser, weil ein fehlender Schlüssel false liefert:

seen := map[string]bool{}
seen["a"] = true
if seen["a"] { ... }

Die Abwägungen:

map[T]struct{}map[T]bool
Größe des Werts0 Bytes1 Byte (plus Alignment)
Mitgliedschaftstest_, ok := s[k]s[k]
Mehrdeutigkeitkeines[k] = false ist ein dritter Zustand

Die dritte Zeile ist der eigentliche Grund, warum viele Codebasen den leeren Struct bevorzugen: Mit bool schreibt irgendwann jemand s[k] = false, und len(s) ist nicht mehr die Anzahl der Elemente. Bei kleinen Sets spielt der Speicherunterschied keine Rolle.

Duplikate aus einem Slice entfernen

Der häufigste Einsatz eines Sets ist Deduplizierung. Das hier behält das erste Vorkommen jedes Werts und erhält die Reihenfolge:

Ausgabe:

[b a c]
[3 1 2]
[1 2 3]

Vereinigung, Schnittmenge, Differenz

Mengenalgebra sind ein paar Schleifen. Durchlaufe beim Mitgliedschaftstest die kleinere Menge, da jeder Lookup im Mittel konstante Zeit braucht.

sorted gibt es nur, damit die Ausgabe stabil ist. Gibst du ein Set per range aus, kommt bei jedem Lauf eine andere Reihenfolge heraus. maps.Keys und slices.Sorted brauchen Go 1.23.

Ein benannter Typ wie type set map[string]struct{} bleibt eine Map: Du indizierst ihn, durchläufst ihn mit range und löschst mit delete genauso, und du kannst Methoden daran hängen.

Ein kleines generisches Set

Mit Generics (Go 1.18) deckt ein Typ jeden vergleichbaren Elementtyp ab. Für die meisten Programme reicht das:

Die Map in einem Struct zu verpacken versteckt das Rauschen von struct{}{} und garantiert, dass die Map vom Konstruktor erzeugt wird. Damit fällt die Panic bei einer nil-Map weg. Sorted kann keine Methode sein: Eine Methode kann keine eigenen Typparameter deklarieren und die comparable-Constraint des Typs nicht verschärfen, und Sortieren braucht cmp.Ordered. Constraints erklärt die Seite zu Generics.

Brauchst du ein Set mit vollem Funktionsumfang (threadsichere Varianten, viele Operationen), gibt es Pakete von Drittanbietern wie github.com/deckarep/golang-set. In den meisten Fällen nutzen Go-Programmierer das Map-Idiom oder einen Typ mit 30 Zeilen wie diesen.

Sets aus Structs

Jeder vergleichbare Typ kann ein Element sein, auch Structs mit vergleichbaren Feldern. Damit werden Prüfungen wie „habe ich dieses Paar schon gesehen“ direkt:

type edge struct{ from, to string }
visited := map[edge]struct{}{}
visited[edge{"a", "b"}] = struct{}{}

Slices und Maps können keine Set-Elemente sein. Um eindeutige Slices zu verfolgen, wandle jeden vorher in einen vergleichbaren Schlüssel um, zum Beispiel ein Array fester Größe oder einen mit fmt.Sprint gebauten String.

Häufige Fehler

  • Das Initialisieren vergessen. var s map[string]struct{} ist nil; das erste Hinzufügen löst eine Panic aus.
  • Ein Set ausgeben und eine stabile Ausgabe erwarten. Sortier die Elemente vorher.
  • map[T]bool verwenden und false speichern. Dann zählt len nicht mehr die Elemente. Entferne mit delete.

Häufig gestellte Fragen

Hat Go einen Set-Typ?

Nein. Die Standardbibliothek hat kein Set. Das Idiom ist eine Map, deren Werte keine Information tragen: map[string]struct{}. Hinzufügen ist s[k] = struct{}{}, die Mitgliedschaft prüfst du mit _, ok := s[k], Entfernen ist delete(s, k), und die Größe ist len(s).

Sollte ich für ein Set in Go map[T]bool oder map[T]struct{} nehmen?

map[T]struct{} macht die Absicht explizit, und seine Werte belegen null Bytes. map[T]bool liest sich natürlicher (if seen[x]), weil ein fehlender Schlüssel false liefert. Beides ist korrekt; der Speicherunterschied zählt nur bei sehr großen Sets. Entscheide dich für eins und bleib dabei.

Wie entferne ich Duplikate aus einem Slice in Go?

Um jeweils das erste Vorkommen in der Reihenfolge zu behalten, durchläufst du den Slice, merkst dir gesehene Werte in einer map[T]struct{} und hängst nur ungesehene an. Ist die Reihenfolge egal, sortier und entferne benachbarte Duplikate: slices.Sort(s); s = slices.Compact(s).

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S