Home > OS >  How to elegantly map to methods returning variable-length arrays in Go?
How to elegantly map to methods returning variable-length arrays in Go?

Time:07-02

I'm somewhat new to Go. A school assignment has me writing a wrapper in a language of our choice around various built-in hashing algorithms. I decided to use Go, and wrote up the following method (only including two of the algorithms for brevity):

package hash

import (
    "crypto/md5"
    "crypto/sha256"
    "errors"
)

func Hash(data string, algorithm string) ([]byte, error) {
    bytes := []byte(data)
    switch algorithm {
    case "md5":
        hash := md5.Sum(bytes) // As far as I know, I need a temporary variable to convert to a slice.
        return hash[:], nil
    case "sha256":
        hash := sha256.Sum256(bytes)
        return hash[:], nil
    default:
        return []byte{}, errors.New("Unsupported Hash Algorithm: "   algorithm)
    }
}

This method works and is sufficient for the assignment, but I'm wondering if it can be written better.

In particular, because the different hashing algorithms return variable-length arrays, I need to convert to slices, so the return hash[:], nil is repetitive. Additionally, almost all the real logic is in retrieving the specific method.

I tried to make a variable before the switch like var hash []byte but this still has the length problem.

I also tried to put all of the functions in a map where I would define pairs like hashes["md5"] := md5.Sum:

var (
    hashes map[string]func([]byte) []byte
)

But this actually just makes creating the functions a mess, because I end up having to convert to a slice anyways.

hashes["md5"] := func(data []byte) []byte {
    hash := md5.Sum(data)
    return hash[:]
}

I recognize that the whole method is kind-of pointless since the need to make the wrapper is artificial, but it feels like my methodology is off and I'm somehow misunderstanding something about how arrays and slices are supposed to be treated in Go. Can I do better?

CodePudding user response:

The two methods you specified are good ways of doing what you want. There is no problem with repeating the hash[:] piece.

Different hashing algorithms return different size arrays. An array is quite different from a slice: when a function returns an array, it is a fixed sized object, and it is copied. Thus, [n]byte is a different type than [m]byte if n is not equal to m. However, when a function returns a slice, it returns a slice header, which is essentially a reference to an array. So, if you need to return different sized arrays, a good way to do it is actually to return a slice.

Converting an array to a slice is not an expensive operation: it simply involves setting three words: the pointer to the array, size, and cap. So, what you are doing is efficient, and neat.

A different implementation might attempt to return interface{}, which allows you to return arrays without creating slices, however you pay a heavy price when you have to type-assert them to extract the correct-sized array from it. I don't recommend that approach.

CodePudding user response:

In Go an array is a numbered sequence of elements of a specific length. and a slice are typed only by the elements they contain (not the number of elements).

The functions that you are calling from the crypto packages are returning arrays of an specific length ([16]byte or [32]byte) and your function return type is a slice []byte and an error. So you need those values returned by the functions to match the return value that you specified.

Converting an array into a slice is not that expensive memory wise, so I would not change anything about your code. Sometimes in go we can get confused because it has different ways to deal with stuff that traditional languages do not use.

I do not recommend this but you can also try returning an interface, but it might prove difficult to know which to return. You can read about them in the link below. https://gobyexample.com/interfaces

Happy Coding!

CodePudding user response:

Package hash provides a common interface for hashes.

Try this

var ErrUnsupportedHashAlgorithm = errors.New("unsupported hash algorithm")

func Hash(data string, algorithm string) ([]byte, error) {
    // Select a hash factory function based on the algorithm name.
    // If not defined, it will be nil
    var makeHash = map[string]func() hash.Hash{
        "md5": md5.New,
        "sha256": sha256.New,
    }[algorithm]

    // Algorithm not found in the map
    if algorithm == nil {
        return nil, ErrUnsupportedHashAlgorithm
    }

    var (
        hash = makeHash()
        err error
    )

    // Write string data into the hash
    _, err = io.Copy(hash, strings.NewReader(data))

    // If there's an error while reading data or writing the hash
    if err != nil {
        return nil, err
    }

    // Return the current hash and no error
    return hash.Sum(nil), nil
}
  • Related