32

Suppose I have a struct type in Go that I want to use as a key in a map, but I don't want to use Go's builtin equality operation. What's the best way to build such a map?

For a concrete example, here is my key type and equality operation:

type Key struct {
    a *int
}

func Equal(x Key, y Key) bool {
    return *x.a == *y.a
}

How can I build a map that uses Equal for key comparison?

Arch D. Robison
  • 3,829
  • 2
  • 16
  • 26
  • But how would I state that map should use that method? Worse yet, in my real code, the field a is a slice of int, and as far as I know, there is no built-in equality operations for silces. – Arch D. Robison Apr 15 '15 at 22:45
  • Yeah in the FAQ https://golang.org/doc/faq there is a question for 'why don't maps allow slices as keys' and the answer is there is no built in equality operator. I proposed what I think is the most reasonable solution in my answer. – evanmcdonnal Apr 15 '15 at 22:46

2 Answers2

28

Go has strict comparable semantics for values used as map keys. As such, you cannot define your own hash code and equality functions for map keys as you can in many other languages.

However, consider the following workaround. Instead of using the struct instances directly as a keys, use a derived attribute of the struct which is intrinsically usable as a key and has the equality semantics you desire. Often it is simple to derive an integer or string value as a hash code which serves as the identity for an instance.

Importantly, the keys should only collide if they truly represent semantic identity of the value being stored. That is, corresponding values should be truly interchangeable.

For example:

type Key struct {
  a *int
}

func (k *Key) HashKey() int {
  return *(*k).a
}

k1, k2 := Key{intPtr(1)}, Key{intPtr(2)}
m := map[int]string{}
m[k1.HashKey()] = "one"
m[k2.HashKey()] = "two"
// m = map[int]string{1:"one", 2:"two"}
m[k1.HashKey()] // => "one"

Of course, immutability is a critical concern with this approach. In the example above, if you modify the field a then the instance can no longer be used as a hash key because its identity has changed.

maerics
  • 151,642
  • 46
  • 269
  • 291
  • Thanks. In my real code I have slices as the keys. I guess I'll have to use a very high quality hash to 128-bit values, or "hash" them to strings. – Arch D. Robison Apr 16 '15 at 14:58
  • @ArchD.Robison: sure, the builtin [`crypto/md5` package](http://golang.org/pkg/crypto/md5/) might be helpful for that, or even a simple string representation of the slice via [`fmt.Sprintf("%v",slc)`](http://golang.org/pkg/fmt/#Sprintf). – maerics Apr 16 '15 at 15:02
  • Super-dangerous advice, since https://en.wikipedia.org/wiki/Hash_collision may happen. With this approach it's required to use "buckets" as values, and make an additional "real" equality check, when there're multiple items. That's how Dictionary works in C# – astef Feb 17 '23 at 19:26
  • @astef the answer explains that the key must be an "identity", which means colliding keys are inherently unfit. I'll update it to be explicit on that subject. – maerics Feb 19 '23 at 23:13
9

This is not possible in Go. There is no operator overloading or 'Equality' method you can override (due to not inheriting from a common base class like in .NET which your example reminds me of). This answer has more information on equality comparisons if you're interested; Is it possible to define equality for named types/structs?

As mentioned in the comments if you want to make something like this work I would recommend using a property on the object as a key. You can define equality based on how you set the value of that property (like it could be a checksum of the objects bytes or something if you're looking for memberwise equality).

Ayman
  • 11,265
  • 16
  • 66
  • 92
evanmcdonnal
  • 46,131
  • 16
  • 104
  • 115