Refactoring my first Go code
David Kröll
Posted on December 3, 2020
My first Golang code was published on 25th April 2018. You may have a look at it on Github. It's a SHA256 hash checker done in 43 lines.
davidkroell / shariff
SHA(riff) - Fingerprint checking with Go
Today I am going to review and refactor it. Start by analyzing the main
func. Well, it's the only func in this project.
func main() {
args := os.Args[1:]
inFile := args[0]
inHash := args[1]
// keep the check case insensitive
inHash = strings.ToLower(inHash)
file, err := os.Open(inFile)
if err != nil {
log.Fatal(err)
}
defer file.Close()
// review: maybe change the name to hasher here
hash := sha256.New()
if _, err := io.Copy(hash, file); err != nil {
log.Fatal(err)
}
hashToCheck := hex.EncodeToString(hash.Sum(nil))
// review: introduce string formatting here
if inHash == hashToCheck {
color.Green(inFile + " has SHA256 " + inHash)
} else {
color.Red(inFile + " doesn't match with " + inHash)
color.Red("The correct hash is: " + hashToCheck)
}
}
So we found already some problems. Furthermore I'd like to extract the checking logic out and write tests for it.
Add parameter check
At the top of the main func I'll add an parameter count check for improved error handling. I'm also adding an non-zero exit code here so we can check in our command line if the command ran succesfully or not.
func main() {
// check for missing parameters
if len(os.Args) != 3 {
fmt.Println("Error occured: missing parameters")
// emit non-zero exit code
os.Exit(1)
}
// continue
}
Add string formatting
It's pretty bad doing a string concatenation for printing on the command line. It's of course preferred to use string formatting to output something.
So go from this:
color.Green(inFile + " has SHA256 " + inHash)
to that:
color.Green("%s has SHA256 %s", inFile, inHash)
Pull out the code
I'd like to create a func which checks hashes for arbitrary streams, not only files. One could think of checking a hash when receiving files in an HTTP server - so the file would already be in memory. Therefore I am using the io.Reader
interface. It will also make the code suitable for unit tests.
// checkHash calculates the hash using and io.Reader, so it is now testable
func checkHash(reader io.Reader, hash string) (isValid bool, calculatedHash string, err error) {
// keep the check case-insensitive
hash = strings.ToLower(hash)
hasher := sha256.New()
if _, err := io.Copy(hasher, reader); err != nil {
return false, "", err
}
calculatedHash = hex.EncodeToString(hasher.Sum(nil))
return hash == calculatedHash, calculatedHash, nil
}
Write test
Now since the code is testable, we have to write tests for it.
func TestCheckHash(t *testing.T) {
b := []byte{0xbe, 0xef, 0x10, 0x10, 0xca, 0xfe}
r := bytes.NewReader(b)
isValid, calculatedHash, err := checkHash(r, "77efeeff80507604bbd4c32b32ce44306879154f83f46afe8c15223932a6a4cb")
if err != nil {
t.Error(err)
} else if !isValid {
t.Error("hashes do not match", calculatedHash)
}
}
So in the code above, we create a byte slice with the hex codes
0xbeef1010cafe
, create an io.Reader
from it and calculate it's hash.
Conclusion
My first published Go code has now a test coverage of incredibly 22.7%. Wow!! After all, I'd say the code from 2018 wasn't that bad as expected beforehand but of course, there are always improvements possible, even if it's only some name changes or added exit codes.
Posted on December 3, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.