Skip to content

Commit c81cca2

Browse files
committed
getOrMake, first pass
1 parent 1aaa034 commit c81cca2

File tree

1 file changed

+174
-0
lines changed

1 file changed

+174
-0
lines changed
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
---
2+
title: "Tutorial: GetOrMake for maps."
3+
slug: tutorial-getormake
4+
date: 2025-01-08T22:06:58-08:00
5+
tags:
6+
- golang
7+
- tutorial
8+
categories:
9+
- Go
10+
- Tutorial
11+
---
12+
13+
Today I'm going to quickly go over one of my favourite convenience functions,
14+
made possible with Go's Generics. It's invaluable if you deal with maps of maps
15+
in Go.
16+
17+
<!--more-->
18+
19+
### Maps
20+
21+
Maps are one of the the original "generic" types available to Go users.
22+
If you aren't familiar with them, you can learn about them in [Effective Go](https://go.dev/doc/effective_go#maps).
23+
24+
The important bit is that the key type must be `comparable`, which is a special
25+
built in interface, that defines all comparable types.
26+
See https://pkg.go.dev/builtin#comparable for more details on that.
27+
28+
29+
### Nested Maps
30+
31+
However, the values of those maps can be any Go type at all, including any other map.
32+
33+
```go
34+
// nested maps, two deep.
35+
var mapOfMaps map[string]maps[string]string
36+
```
37+
38+
And the values for those *nested* maps, can also have maps.
39+
40+
```go
41+
// nested maps, two deep.
42+
var mapOfMapsOfMaps map[string]maps[string]map[string]string
43+
```
44+
45+
### Setting nested maps
46+
47+
But this nesting has a downside. What if you want to get to that final map, so
48+
you can set the value on it's key.
49+
50+
Say it's a field Map on a struct, and we have a method to set the inner most value.
51+
The code would then need to initialize all the maps along the way to setting the
52+
value.
53+
54+
```go
55+
func (w *mWrapper) Set(k1,k2,k3, value string) {
56+
if w.Map == nil {
57+
w.Map = make(map[string]maps[string]map[string]string)
58+
}
59+
v1, ok := w.Map[k1]
60+
if !ok {
61+
v1 = make(map[string]maps[string]string)
62+
w.Map[k1] = v1
63+
}
64+
v2, ok := v1[k2]
65+
if !ok {
66+
v2 = make(map[string]string)
67+
v1[k2] = v2
68+
}
69+
v2[k3] = value
70+
}
71+
```
72+
73+
What's happening here, is that for each level with an inner map, we need to
74+
check if that map exists. We use the "comma ok" idiom, where the value of OK
75+
indicates if the map as a value for the given key or not. If it doesn't exist,
76+
we make a new instance of the map type, and assign it both to the value variable,
77+
and to the key in the map itself.
78+
79+
As you can imagine, the more layers of nesting you have, the more tedious
80+
creating the earlier nested map types become.
81+
82+
### Generics to the rescue!
83+
84+
Using generics, we can avoid some of this!
85+
86+
```go
87+
func (w *mWrapper) Set(k1,k2,k3, value string) {
88+
if w.Map == nil {
89+
w.Map = make(map[string]maps[string]map[string]string)
90+
}
91+
v1 := getOrMake(w.Map, k1)
92+
v2 := getOrMake(v1, k2)
93+
v2[k3] = value
94+
}
95+
```
96+
97+
Now it's eight lines shorter! 4 per level we've removed, including the tedious
98+
repetition of the nested type.
99+
100+
Here's the code for `getOrMake`.
101+
102+
```go
103+
104+
func
105+
106+
// getOrMake is a generic helper function for extracting or initializing a sub map.
107+
func getOrMake[K, VK comparable, VV any, V map[VK]VV, M map[K]V](m M, key K) V {
108+
v, ok := m[key]
109+
if !ok {
110+
v = make(V)
111+
m[key] = v
112+
}
113+
return v
114+
}
115+
```
116+
117+
First we've declared our generic types.
118+
119+
* K is the key for the outermost map.
120+
* VK is the key for the value map.
121+
* VV is the value for the value map,
122+
* V is the type of the value map: `map[VK]VV`
123+
* M the type of the map being passed in, `map[K]V`
124+
125+
The function then takes in an instance `m` of type `M`, and the `key` of type `K`,
126+
returning an instance of the the value map.
127+
128+
The then the code is pretty straightforward.
129+
130+
We lookup the value of the key, and whether it exists or not. If the key has
131+
no value, then we do as we did before: create an instance of the value map, and
132+
assign that to the key. Then we return the value itself.
133+
134+
I wouldn't pull out `getOrMake` the first time I need it, and it only works for
135+
maps. But if I start repeating the pattern around in a package, or I start to
136+
play with the inner types, using `getOrMake` can reduce some effort around that
137+
refactoring.
138+
139+
### Aside: Top Level Maps
140+
141+
It also can't work for top level maps. For example.
142+
143+
```go
144+
func NoArgMakeMap[K comparable, V any, M map[K]V]() M {
145+
return make(M)
146+
}
147+
```
148+
149+
That doesn't work, since Go's type inference doesn't pull from the return value
150+
even if it would be helpful.
151+
152+
The closest you can get is to explicitly hint the type, by including an unused
153+
parameter.
154+
155+
```go
156+
func OneArgMakeMap[K comparable, V any, M map[K]V](_ M) M {
157+
return make(M)
158+
}
159+
```
160+
161+
That'll be enough to have the compiler do the right thing.
162+
163+
There is at least [one](https://github.com/golang/go/issues/50285) issue in the
164+
Go issue tracker for this feature request, but no real proposals at this time.
165+
166+
I don't think it's critical for Go to get this though, since the work around
167+
isn't terrible at this point.
168+
169+
### Conclusion
170+
171+
Generics in Go are a powerful tool to reduce boiler plate, even with small
172+
utility functions. Especially with Go's original generic types, like `map`!
173+
174+
Thanks for reading.

0 commit comments

Comments
 (0)