Skip to content

Instantly share code, notes, and snippets.

@haleyrc
Created March 22, 2019 14:27
Show Gist options
  • Save haleyrc/02358a4476ecab34e0d60449e7ded551 to your computer and use it in GitHub Desktop.
Save haleyrc/02358a4476ecab34e0d60449e7ded551 to your computer and use it in GitHub Desktop.
A reminder that complex map keys do exist
// This gist is a reminder to don't be like me and forget that you can
// use complex keys for maps.
//
// https://play.golang.org/p/KCJ58WH2jU-
package main
import "fmt"
func main() {
// Before continuing to the details, confirm for yourself that
// the client code is identical here.
cid := "18292"
{
ndb := make(naiiveDatabase)
id := ndb.AddVehicle(
cid,
Vehicle{Year: "2016",
Make: "Jeep",
Model: "Patriot"},
)
fmt.Println("ID: ", id)
v, err := ndb.GetVehicle(cid, id)
if err != nil {
fmt.Println(err)
} else {
fmt.Printf("%#v\n", v)
}
}
{
sdb := make(smartDatabase)
id := sdb.AddVehicle(
cid,
Vehicle{Year: "2016",
Make: "Jeep",
Model: "Patriot"},
)
fmt.Println("ID: ", id)
v, err := sdb.GetVehicle(cid, id)
if err != nil {
fmt.Println(err)
} else {
fmt.Printf("%#v\n", v)
}
}
}
// When standing up a mock database for testing, I was naiively just putting
// everything into complicated series of maps to simulate a composite key of
// cid and entity id. This works, but it can get difficult to manage
// especially when entities have additional constraints.
type naiiveDatabase map[string]map[int]Vehicle
// AddVehicle is fairly straightforward, except that we first have to make
// sure the sub-map for the given cid exists since this isn't handled by
// the call to make that created the initial database.
func (db naiiveDatabase) AddVehicle(cid string, v Vehicle) int {
if _, ok := db[cid]; !ok {
db[cid] = make(map[int]Vehicle)
}
id := uuid.New()
v.ID = id
db[cid][id] = v
return id
}
// GetVehicle is even more complex because we have to do our existence check
// twice. What's more, this doesn't really reflect the type of thing you would
// see in a database lookup, since the first error corresponds to no entries
// with the cid, and the second corresponds to no entries with the id. In reality
// though, we really just want to know if there is *an* entry with the full
// composite key.
func (db naiiveDatabase) GetVehicle(cid string, id int) (Vehicle, error) {
sub, ok := db[cid]
if !ok {
return Vehicle{}, ErrNotFound
}
v, ok := sub[id]
if !ok {
return Vehicle{}, ErrNotFound
}
return v, nil
}
// The smarter version of this makes use of the fact that keys to maps don't have
// to be primitive types. We still have the top level map, but now we key into it
// with a composite key.
type smartDatabase map[vehicleKey]Vehicle
// Our composite key type can now match the structure of the database index more
// naturally and can be extended for larger composites.
type vehicleKey struct {
cid string
id int
}
// AddVehicle is now simpler because the top level map is all we have to worry
// about and that's taken care of by the initial call to make.
func (db smartDatabase) AddVehicle(cid string, v Vehicle) int {
id := uuid.New()
v.ID = id
key := vehicleKey{cid, id}
db[key] = v
return id
}
// GetVehicle is even more simplified and the error we return corresponds directly
// to what we would expect: the vehicle identified by the composite index (cid, id)
// does not exist.
func (db smartDatabase) GetVehicle(cid string, id int) (Vehicle, error) {
key := vehicleKey{cid, id}
v, ok := db[key]
if !ok {
return Vehicle{}, ErrNotFound
}
return v, nil
}
// Utility stuff not important to the demo
var ErrNotFound = fmt.Errorf("not found")
type Vehicle struct {
ID int
Year, Make, Model string
}
type idgen struct {
last int
}
func (g *idgen) New() int {
g.last++
return g.last
}
var uuid = &idgen{}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment