Skip to content

Commit 5b068ff

Browse files
committed
editing
1 parent a17a6c8 commit 5b068ff

File tree

1 file changed

+76
-81
lines changed

1 file changed

+76
-81
lines changed

content/posts/2025-01-08-getOrMake.md

Lines changed: 76 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,20 @@ categories:
1010
- Tutorial
1111
---
1212

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.
13+
Today, I'm going to quickly go over one of my favorite convenience functions,
14+
made possible with Go's generics. It's invaluable if you deal with maps of maps in Go.
1615

1716
<!--more-->
1817

1918
### Maps
2019

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).
20+
Maps are one of the original "generic" types available to Go users. If you aren't
21+
familiar with them, you can learn about them in [Effective Go](https://go.dev/doc/effective_go#maps).
2322

2423
The important bit is that the key type must be `comparable`, which is a special
25-
built in interface, that defines all comparable types.
24+
built-in interface that defines all comparable types.
2625
See https://pkg.go.dev/builtin#comparable for more details on that.
2726

28-
2927
### Nested Maps
3028

3129
However, the values of those maps can be any Go type at all, including any other map.
@@ -35,75 +33,73 @@ However, the values of those maps can be any Go type at all, including any other
3533
var mapOfMaps map[string]map[string]string
3634
```
3735

38-
And the values for those *nested* maps, can also have maps.
36+
And the values for those nested maps can also be maps.
3937

4038
```go
41-
// nested maps, two deep.
39+
40+
// nested maps, three deep.
4241
var mapOfMapsOfMaps map[string]map[string]map[string]string
4342
```
4443

4544
### Setting nested maps
4645

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.
46+
But this nesting has a downside. What if you want to get to that final map so
47+
you can set the value on its key?
4948

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.
49+
Say it's a field Map on a struct, and we have a method to set the innermost value.
50+
The code would then need to initialize all the maps along the way to setting the value.
5351

5452
```go
55-
func (w *mWrapper) Set(k1,k2,k3, value string) {
56-
if w.Map == nil {
57-
w.Map = make(map[string]map[string]map[string]string)
58-
}
59-
v1, ok := w.Map[k1]
60-
if !ok {
61-
v1 = make(map[string]map[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-
}
53+
54+
func (w *mWrapper) Set(k1, k2, k3, value string) {
55+
if w.Map == nil {
56+
w.Map = make(map[string]map[string]map[string]string)
57+
}
58+
v1, ok := w.Map[k1]
59+
if !ok {
60+
v1 = make(map[string]map[string]string)
61+
w.Map[k1] = v1
62+
}
63+
v2, ok := v1[k2]
64+
if !ok {
65+
v2 = make(map[string]string)
66+
v1[k2] = v2
67+
}
68+
v2[k3] = value
69+
}
7170
```
7271

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.
72+
What's happening here is that for each level with an inner map,
73+
we need to check if that map exists. We use the "comma ok" idiom,
74+
where the value of ok indicates if the map has a value for the given key or not.
75+
If it doesn't exist, we make a new instance of the map type and assign it both
76+
to the value variable and to the key in the map itself.
7877

7978
As you can imagine, the more layers of nesting you have, the more tedious
80-
creating the earlier nested map types become.
79+
creating the earlier nested map types becomes.
8180

8281
### Generics to the rescue!
8382

8483
Using generics, we can avoid some of this!
8584

8685
```go
87-
func (w *mWrapper) Set(k1,k2,k3, value string) {
88-
if w.Map == nil {
89-
w.Map = make(map[string]map[string]map[string]string)
90-
}
91-
v1 := getOrMake(w.Map, k1)
92-
v2 := getOrMake(v1, k2)
93-
v2[k3] = value
94-
}
86+
func (w *mWrapper) Set(k1, k2, k3, value string) {
87+
if w.Map == nil {
88+
w.Map = make(map[string]map[string]map[string]string)
89+
}
90+
v1 := getOrMake(w.Map, k1)
91+
v2 := getOrMake(v1, k2)
92+
v2[k3] = value
93+
}
9594
```
9695

97-
Now it's eight lines shorter! 4 per level we've removed, including the tedious
98-
repetition of the nested type.
96+
Now it's eight lines shorter! Four per level we've removed, including the
97+
tedious repetition of the nested type.
9998

100-
Here's the code for `getOrMake`.
99+
Here's the code for getOrMake.
101100

102101
```go
103-
104-
func
105-
106-
// getOrMake is a generic helper function for extracting or initializing a sub map.
102+
// getOrMake is a generic helper function for extracting or initializing a sub-map.
107103
func getOrMake[K, VK comparable, VV any, V map[VK]VV, M map[K]V](m M, key K) V {
108104
v, ok := m[key]
109105
if !ok {
@@ -114,61 +110,60 @@ func getOrMake[K, VK comparable, VV any, V map[VK]VV, M map[K]V](m M, key K) V {
114110
}
115111
```
116112

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`
113+
First, we've declared our generic types:
124114

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.
115+
K is the key for the outermost map.
116+
VK is the key for the value map.
117+
VV is the value for the value map.
118+
V is the type of the value map: map[VK]VV
119+
M is the type of the map being passed in: map[K]V
120+
The function then takes in an instance m of type M and the key of type K,
121+
returning an instance of the value map.
127122

128-
The then the code is pretty straightforward.
123+
Then the code is pretty straightforward.
129124

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.
125+
We look up the value of the key and whether it exists or not.
126+
If the key has no value, then we do as we did before:
127+
create an instance of the value map and assign that to the key.
128+
Then we return the value itself.
133129

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.
130+
I wouldn't pull out getOrMake the first time I need it, and it only works for maps.
131+
But if I start repeating the pattern around in a package, or I start to play
132+
with the inner types, using getOrMake can reduce some effort around that refactoring.
138133

139-
### Aside: Top Level Maps
134+
### Aside: Top-Level Maps
140135

141-
It also can't work for top level maps. For example.
136+
It also can't work for top-level maps. For example:
142137

143138
```go
144139
func NoArgMakeMap[K comparable, V any, M map[K]V]() M {
145-
return make(M)
140+
return make(M)
146141
}
147142
```
148143

149-
That doesn't work, since Go's type inference doesn't pull from the return value
150-
even if it would be helpful.
144+
That doesn't work, since Go's type inference doesn't pull from the return value,
145+
even if it would be helpful.
151146

152-
The closest you can get is to explicitly hint the type, by including an unused
147+
The closest you can get is to explicitly hint at the type by including an unused
153148
parameter.
154149

155150
```go
156151
func OneArgMakeMap[K comparable, V any, M map[K]V](_ M) M {
157-
return make(M)
152+
return make(M)
158153
}
159154
```
160155

161-
That'll be enough to have the compiler do the right thing.
156+
That'll be enough to have the compiler do the right thing.
162157

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.
158+
There is at least one issue in the Go issue tracker for this feature request,
159+
but no real proposals at this time.
165160

166-
I don't think it's critical for Go to get this though, since the work around
161+
I don't think it's critical for Go to get this, though, since the workaround
167162
isn't terrible at this point.
168163

169164
### Conclusion
170165

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`!
166+
Generics in Go are a powerful tool to reduce boilerplate, even with small
167+
utility functions, especially with Go's original generic types, like map!
173168

174169
Thanks for reading.

0 commit comments

Comments
 (0)