TLDR

Since map[[]byte]_ are not possible in go, use map[string]_ and index your byte slices using string(byteSlice).

Need for byte slice map keys

Go’s default hash.Hash interface returns hashes as slices of bytes.

type Hash interface {
    // ...
    Sum(b []byte) []byte
    // ...
}

As we often use cryptographic hashes to identify complex objects, associative arrays with hashes as keys are needed to provide efficient insertion and retrieval of those objects.

Problem

In go, associative arrays are provided by the language through the map construct.

However, byte slices cannot be used as a map key, as explained in the language specification. Operators == and != must be defined for the map key type. Since slices are descriptors of an underlying array, equality is ambiguous different as multiple definitions can exist. This explains the lack of a standard definition in the language.

Comparisons of possible solutions

So how can we still use byte slices as indexes to our maps ? Possibilities are:

  • avoid slices, use arrays if possible
  • convert the byte slices to a key type using a one-to-one function, such as string(), or hex.EncodeToString()
  • implement a custom map

Here is a benchmark of some possibilities, testing insertions, retrievals and deletions:

  • BenchmarkArrayKeyed: use byte array as a key
  • BenchmarkStringKeyed: convert byte slice to a key using string()
  • BenchmarkOptimizedStringKeyed: convert byte slice to a key using string(), and leverage an optimization described below
  • BenchmarkHexgKeyed: convert byte slice to a string using hex.EncodeToString().
BenchmarkArrayKeyed-8             	19557058	        60.6 ns/op
BenchmarkOptimizedStringKeyed-8   	17753274	        67.7 ns/op
BenchmarkStringKeyed-8            	11834193	        102 ns/op
BenchmarkHexKeyed-8               	 6212077	        192 ns/op

Benchmark code is available here.

As one can see, using the built-in string() conversion with the optimization leads to a very small performance loss compared to using byte arrays. Since we expect to use the Hash interface, the optimized string() conversion is to prefer.

Below I explain what is the optimized string() conversion.

Optimization

Cases of v := m[string(byteSlice)] can be optimized by the go compiler. The optimization was introduced here.

In short, internally the compiler can construct a temporary unsafe string from the byte slice, because it knows it will only be used as an index:

func slicebytetostringtmp(b Slice) (s String) {
    // ...
	s.str = b.array;
	s.len = b.len;
} 

This explains the difference in running times of:

// string only used as an index
v := m[string(byteSlice)]
// string may be used later
key := string(byteSlice)
v := m[key]