skip to content

Why do Go programmers use map[string]struct{} rather than map[string]bool for a set?

level: middleimportance: should knowfreq 46%

answer

  1. what is the value slot even for
  2. how big is a struct with no fields
  3. the keys carry all the information
  4. no stored false to misread

basics

~20 s

struct{} is a type with no fields and a size of zero, so the map stores keys and no value payload at all. Membership is then read from the comma-ok form, with no ambiguous stored false to misread.

solid answer

~40 s

`struct{}` is the struct type with no fields, and its size is zero bytes, so a `map[string]struct{}` carries only the keys — with `map[string]bool` every entry also reserves a value slot. The saving is modest but real on large sets, and the stronger argument is meaning: with a bool map, `m[k]` yields `false` both for an absent key and for a key deliberately stored as `false`, so a careful caller has to use `v, ok := m[k]` anyway. Writing `m[k] = struct{}{}` and testing `_, ok := m[k]` makes membership the only question the type can answer. The same zero-size idea powers `chan struct{}`, used purely to signal — usually by closing it — where no value needs to travel.

code

go · 6 lines
go
seen := make(map[string]struct{}, 8)
seen["schema-v1"] = struct{}{}

if _, ok := seen["schema-v2"]; !ok {
	// "schema-v2" is not a member
}

go deeper

for a junior

Know how to write it: insert with m[k] = struct{}{} and test with the two-value form _, ok := m[k]. Be able to say struct{} is simply a struct type that declares no fields.

for a middle

Explain that the type's size is zero so the value slot costs nothing, and contrast it with map[string]bool, where a returned false is ambiguous between absent and deliberately stored.

for a senior

Judge when the awkward syntax earns its place: very large in-memory sets and pure signalling channels, versus a bool map when explicitly-false must stay distinguishable from missing.

for a principal

Decide how a set crosses a package boundary. Exposing map[string]struct{} in an exported signature commits every caller to the idiom, while a small named set type keeps you free to change the representation later.

## What struct{} actually is `struct{}` is not a keyword or a special case. It is an ordinary anonymous struct type that happens to declare no fields, and `struct{}{}` is a composite literal of that type — the first pair of braces is the type, the second is the empty literal. Because it has no fields, it has nothing to store: `unsafe.Sizeof(struct{}{})` is `0`. Every value of the type is identical to every other, so the type carries exactly zero bits of information. That is the whole basis of the idiom. If a map's value carries no information, the value type should be one that *cannot* carry any. ## The set idiom ```go seen := make(map[string]struct{}, 8) seen["schema-v1"] = struct{}{} if _, ok := seen["schema-v2"]; !ok { // not a member } ``` Two things are being bought here. **Space.** A map entry in `map[string]bool` reserves storage for the bool value in addition to the key; in `map[string]struct{}` the value contributes nothing. For a set of a handful of names this is irrelevant. For a set of several million interned strings it is measurable, though the key storage still dominates — the value slot is one byte plus whatever the entry layout rounds it to, not a pointer-sized word. Do not oversell the saving: lookup speed is the same, and the hash work is the same, because the key does all of it. **Meaning.** This is the argument that actually decides most code reviews. With `map[string]bool` the expression `m[k]` returns the zero value `false` for a key that was never inserted, which is indistinguishable from a key explicitly stored as `false`. That ambiguity is fine when the map is genuinely a set (nothing is ever stored as false), and it is a bug waiting to happen when someone later starts storing false deliberately. With `struct{}` there is no second state to confuse: the only way to ask the question is the comma-ok form, and the type documents that presence is all the map knows. Note that `seen[k] == struct{}{}` compiles — struct{} is comparable — but is *always* true, because a missing key yields the zero value, which for a type with no fields is the same value as any other. Comparing the value is meaningless here; only `ok` reports membership. ## When a bool map is the right answer When you need three states. A feature-override map where a key present and set to `false` means "explicitly disabled" and an absent key means "use the default" genuinely needs the bool: the value carries information. Reaching for `struct{}` there loses data. Similarly, if code is being read far more often than it is being run, `map[string]bool` with an honest comment is easier for a newcomer than `struct{}{}` syntax, and the memory difference for a small set is nil. ## The signalling channel The other place zero-size values show up is `chan struct{}`: ```go ready := make(chan struct{}) go func() { close(ready) // every receiver unblocks }() <-ready ``` The element type says out loud that no data travels — the *event* is the message. Closing the channel broadcasts to every waiting receiver at once, while sending a value would release one receiver per send. Using `chan bool` here would invite a reader to wonder what `true` versus `false` means, when the answer is "nothing". ## Details worth knowing - All zero-size values may share one address; the language explicitly says two distinct zero-size variables may have the same address, so never rely on pointer identity of `*struct{}` values. - A `[]struct{}` of any length also allocates no element storage, which is occasionally used to express a count-only slice, though a plain integer is usually clearer. - If a struct's *final* field is zero-sized, the compiler pads the struct so that a pointer to that field does not point one byte past the allocation. So embedding a trailing `struct{}` field can make the enclosing struct larger, not free — an implementation detail, but a surprising one if you are counting bytes. - There is no set type in the standard library. `map[K]struct{}` is the idiom, and wrapping it in a small named type with `Add`, `Has` and `Len` methods is a reasonable move when a set crosses a package boundary, because it leaves you free to change the representation later.

  • How much memory does the empty-struct value type actually save per map entry?
    The value contributes zero bytes, against one byte plus alignment for a bool. On a set of a few million keys that adds up to megabytes, but key storage and the map's own overhead still dominate, and lookup speed is identical. Pitch it as a small win plus a clarity win, not as an optimisation.
  • When is map[string]bool the better choice?
    When you need three distinguishable states: absent, present-and-true, present-and-false. An override map where an explicit false means disabled and a missing key means use the default genuinely needs the bool, because the value carries information that struct{} would throw away.
  • Why is chan struct{} preferred over chan bool for a done signal?
    Because no value is ever meant to be read, and the element type should say so. Closing the channel broadcasts to every waiting receiver at once, while sending values releases one receiver per send. A bool element invites readers to look for meaning in true versus false when there is none.

saying these in an interview costs you the question

  • Claims the empty-struct value makes map lookups faster
  • Says struct{}{} allocates a fresh object per insert
  • Reads m[k] on a bool map and treats false as absent
  • Thinks struct{} is nil or an empty interface
  • Calls struct{} a compiler keyword rather than a type