-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
174 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
--- | ||
title: "Tutorial: GetOrMake for maps." | ||
slug: tutorial-getormake | ||
date: 2025-01-08T22:06:58-08:00 | ||
tags: | ||
- golang | ||
- tutorial | ||
categories: | ||
- Go | ||
- Tutorial | ||
--- | ||
|
||
Today I'm going to quickly go over one of my favourite convenience functions, | ||
made possible with Go's Generics. It's invaluable if you deal with maps of maps | ||
in Go. | ||
|
||
<!--more--> | ||
|
||
### Maps | ||
|
||
Maps are one of the the original "generic" types available to Go users. | ||
If you aren't familiar with them, you can learn about them in [Effective Go](https://go.dev/doc/effective_go#maps). | ||
|
||
The important bit is that the key type must be `comparable`, which is a special | ||
built in interface, that defines all comparable types. | ||
See https://pkg.go.dev/builtin#comparable for more details on that. | ||
|
||
|
||
### Nested Maps | ||
|
||
However, the values of those maps can be any Go type at all, including any other map. | ||
|
||
```go | ||
// nested maps, two deep. | ||
var mapOfMaps map[string]maps[string]string | ||
``` | ||
|
||
And the values for those *nested* maps, can also have maps. | ||
|
||
```go | ||
// nested maps, two deep. | ||
var mapOfMapsOfMaps map[string]maps[string]map[string]string | ||
``` | ||
|
||
### Setting nested maps | ||
|
||
But this nesting has a downside. What if you want to get to that final map, so | ||
you can set the value on it's key. | ||
|
||
Say it's a field Map on a struct, and we have a method to set the inner most value. | ||
The code would then need to initialize all the maps along the way to setting the | ||
value. | ||
|
||
```go | ||
func (w *mWrapper) Set(k1,k2,k3, value string) { | ||
if w.Map == nil { | ||
w.Map = make(map[string]maps[string]map[string]string) | ||
} | ||
v1, ok := w.Map[k1] | ||
if !ok { | ||
v1 = make(map[string]maps[string]string) | ||
w.Map[k1] = v1 | ||
} | ||
v2, ok := v1[k2] | ||
if !ok { | ||
v2 = make(map[string]string) | ||
v1[k2] = v2 | ||
} | ||
v2[k3] = value | ||
} | ||
``` | ||
|
||
What's happening here, is that for each level with an inner map, we need to | ||
check if that map exists. We use the "comma ok" idiom, where the value of OK | ||
indicates if the map as a value for the given key or not. If it doesn't exist, | ||
we make a new instance of the map type, and assign it both to the value variable, | ||
and to the key in the map itself. | ||
|
||
As you can imagine, the more layers of nesting you have, the more tedious | ||
creating the earlier nested map types become. | ||
|
||
### Generics to the rescue! | ||
|
||
Using generics, we can avoid some of this! | ||
|
||
```go | ||
func (w *mWrapper) Set(k1,k2,k3, value string) { | ||
if w.Map == nil { | ||
w.Map = make(map[string]maps[string]map[string]string) | ||
} | ||
v1 := getOrMake(w.Map, k1) | ||
v2 := getOrMake(v1, k2) | ||
v2[k3] = value | ||
} | ||
``` | ||
|
||
Now it's eight lines shorter! 4 per level we've removed, including the tedious | ||
repetition of the nested type. | ||
|
||
Here's the code for `getOrMake`. | ||
|
||
```go | ||
|
||
func | ||
|
||
// getOrMake is a generic helper function for extracting or initializing a sub map. | ||
func getOrMake[K, VK comparable, VV any, V map[VK]VV, M map[K]V](m M, key K) V { | ||
v, ok := m[key] | ||
if !ok { | ||
v = make(V) | ||
m[key] = v | ||
} | ||
return v | ||
} | ||
``` | ||
|
||
First we've declared our generic types. | ||
|
||
* K is the key for the outermost map. | ||
* VK is the key for the value map. | ||
* VV is the value for the value map, | ||
* V is the type of the value map: `map[VK]VV` | ||
* M the type of the map being passed in, `map[K]V` | ||
|
||
The function then takes in an instance `m` of type `M`, and the `key` of type `K`, | ||
returning an instance of the the value map. | ||
|
||
The then the code is pretty straightforward. | ||
|
||
We lookup the value of the key, and whether it exists or not. If the key has | ||
no value, then we do as we did before: create an instance of the value map, and | ||
assign that to the key. Then we return the value itself. | ||
|
||
I wouldn't pull out `getOrMake` the first time I need it, and it only works for | ||
maps. But if I start repeating the pattern around in a package, or I start to | ||
play with the inner types, using `getOrMake` can reduce some effort around that | ||
refactoring. | ||
|
||
### Aside: Top Level Maps | ||
|
||
It also can't work for top level maps. For example. | ||
|
||
```go | ||
func NoArgMakeMap[K comparable, V any, M map[K]V]() M { | ||
return make(M) | ||
} | ||
``` | ||
|
||
That doesn't work, since Go's type inference doesn't pull from the return value | ||
even if it would be helpful. | ||
|
||
The closest you can get is to explicitly hint the type, by including an unused | ||
parameter. | ||
|
||
```go | ||
func OneArgMakeMap[K comparable, V any, M map[K]V](_ M) M { | ||
return make(M) | ||
} | ||
``` | ||
|
||
That'll be enough to have the compiler do the right thing. | ||
|
||
There is at least [one](https://github.com/golang/go/issues/50285) issue in the | ||
Go issue tracker for this feature request, but no real proposals at this time. | ||
|
||
I don't think it's critical for Go to get this though, since the work around | ||
isn't terrible at this point. | ||
|
||
### Conclusion | ||
|
||
Generics in Go are a powerful tool to reduce boiler plate, even with small | ||
utility functions. Especially with Go's original generic types, like `map`! | ||
|
||
Thanks for reading. |