(Bad) examples of valid composite literals

October 11, 2023

Today we round out the spec’s discussion of composite literals, with a few examples. Including some bad ones. 🤷

Composite literals

…

Examples of valid array, slice, and map literals:

// list of prime numbers
primes := []int{2, 3, 5, 7, 9, 2147483647}

// vowels[ch] is true if ch is a vowel
vowels := [128]bool{'a': true, 'e': true, 'i': true, 'o': true, 'u': true, 'y': true}

// the array [10]float32{-1, 0, 0, 0, -0.1, -0.1, 0, 0, 0, -1}
filter := [10]float32{-1, 4: -0.1, -0.1, 9: -1}

// frequencies in Hz for equal-tempered scale (A4 = 440Hz)
noteFrequency := map[string]float32{
	"C0": 16.35, "D0": 18.35, "E0": 20.60, "F0": 21.83,
	"G0": 24.50, "A0": 27.50, "B0": 30.87,
}

There’s one in particular I want to call out as “bad” form.

// vowels[ch] is true if ch is a vowel
vowels := [128]bool{'a': true, 'e': true, 'i': true, 'o': true, 'u': true, 'y': true}

Of course, the spec isn’t trying to teach us good form, so much as correct form. So I’ll forgive the spec in this case. But let’s look at some better alternatives.

Let’s first get it out of the way that there are much more efficient ways to store a list of all vowels in the English language, than a 128-byte long array of boolean values. We could potentially use two int64s for example, for 128 bits, then flip just the bits we care about. But we probably only care about letters, and there are only 26 of those, so really an int32 should suffice. But let’s talk about the more general case of tracking boolean flags for an arbitrary number of “things”.

As it is, we’re storing 128 values, 122 of which are the default value of false. Surely we can use a map, and store only the valuse we actually care about, right?

	vowelsM := map[int]bool{'a': true, 'e': true, 'i': true, 'o': true, 'u': true, 'y': true}

Is this a win? Let’s check:

	fmt.Println(unsafe.Sizeof(vowelsS), unsafe.Sizeof(vowelsM)) // 128 8

HUGE win, right? Not so fast. That’s only showing us the size of the map container, not its elements. Maps can be arbitrarily complex and recursive. To calculate its true memory usage, we need a more sophisticated tool. I found this one:

	fmt.Println(size.Of(vowelsS), size.Of(vowelsM)) // 128 126

Hmm. So the map is still a win, but barely. And if we grow the map, it will probably be a loss. But then this is for a fairly small potential data set: 128 values. Most maps like this have an arbitrary length, or aren’t of small integer types. For example:

func unique(in []string) []string {
	found := map[string]bool{}
	result := []string
	for _, str := range in {
		if !found[str] {
			result = append(result, str)
			found[str] = true
		}
	}
	return result
}

And it’s in these situations where we can really benefit from yet another iteration on the concept: A map of empty structs!

	vowelsM2 := map[int]struct{}{'a': struct{}{}, 'e': struct{}{}, 'i': struct{}{}, 'o': struct{}{}, 'u': struct{}{}, 'y': struct{}{}}

But that looks ugly. Why would we ever want that?

Two reasons:

  1. An empty struct requires no memory. So for very large maps, using struct{}{} will save memory allocations versus bool.
  2. More important in most cases, it’s more semantically clear what’s being done. If you have a map[string]bool, there’s always the question of what false indicates. By using an empty struct, you’re making it clear to any future readers of your code, that you’re simply indicating the existence of a thing, not a trinary true/false/none option.

Quotes from The Go Programming Language Specification Version of August 2, 2023


Share this

Direct to your inbox, daily. I respect your privacy .

Unsure? Browse the archive .