Elegant code and Go
Writing elegant or readable code is a driving force for some programmers that have been around long enough to know that less code is usually better than more. We also know that less code is usually also less efficient than more, for various reasons. Depending on how you like your cat skinned, there are several ways to do things.
You can work with something as simple as a password generator. Let’s go with a hands on example of writing a function that might generate a password for us. A quick google for it leads me to a few different examples:
Runes
This password generator creates a rune slice of a specified length, and chooses each character (rune)
before converting it to string and returning it. This uses the math/rand
package.
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
func RandStringRunes(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = letterRunes[rand.Intn(len(letterRunes))]
}
return string(b)
}
With a SLOC of 6 and 4 function calls, it’s clear what it does. I’m just not sure that anybody really cares to type out the alphabet or even paste it from some example.
Bytes
The same example but with a byte slice.
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
func RandStringBytes(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.Intn(len(letterBytes))]
}
return string(b)
}
I literally see no improvement ofer the above. Still at the same SLOC 6/ function calls metric, we’re not improving anything over the rune example. We still read it as the same function.
Modulo / remainder of rand
This function attempts to optimize the way the random characters are chosen.
func RandStringBytesRmndr(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.Int63() % int64(len(letterBytes))]
}
return string(b)
}
Notably, this function resorts to rand.Int63
and a modulo operator that would return
the remainder of the division with the character set length. Supposedly, this is faster,
but the readability hasn’t improved even a little, staying at SLOC 6, and introducing
an additional function call (casting int to int64).
Inspecting crypto/rand
The standard library provides the crypto/rand package.
The package provides a Reader io.reader
which makes it possible to read a specific number
of random bytes. For example, we can read a fixed amount of them like this:
raw := make([]byte, length)
io.ReadFull(rand.Reader, raw)
This gets us half of the way there to generating a fixed length password. Unfortunately each byte is in the range from 0 to 255, out of which about half of the characters are unreachable without an extreme combination of keyboard presses. That doesn’t make good password material.
But wouldn’t it be great, if we didn’t need letterBytes
or something? In fact we don’t.
The package encoding/base64 already provides
RawURLEncoding
which would be good enough for our purposes. Our generator would look
like this:
func GenerateRandomString(length int) string {
raw := make([]byte, length)
io.ReadFull(rand.Reader, raw)
return base64.RawURLEncoding.EncodeToString(raw)[:length]
}
And the underlying set of characters used by base64.RawURLEncoding
is:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_
Obviously, there’s some overhead with base64 encoding and the reslice, but our SLOC just dropped to 4.
Our function calls just dropped from 4 to 3. The function is wonderfully easy to read, and there’s no noise
from letterBytes
. We know and rely on the implementation details of the base64 package to make this
code short and concise.
Conclusions
People should sometimes step back from their benchmarks, and also optimize code for readability. While I wouldn’t advocate one choice over another in the given examples, I do realize that there are clear differences and implications between them. Reading concise and non-tivial code improves the overall feeling about the elegance of the code base.
To be honest, a high amount of new
, make
and casting calls between types, especially bytes and
string types, are usually a code smell indicating further problems with memory allocation. Of course,
this is coming from somebody who just advocated about 25% of allocation overhead in the last
example above (and this can be improved as well if you recalculate the length of the byte slice).
Take my advice with a pinch of salt, and improve readability where you can. Less is more.
While I have you here...
It would be great if you buy one of my books:
- Go with Databases
- Advent of Go Microservices
- API Foundations in Go
- 12 Factor Apps with Docker and Go
Feel free to send me an email if you want to book my time for consultancy/freelance services. I'm great at APIs, Go, Docker, VueJS and scaling services, among many other things.
Want to stay up to date with new posts?
Stay up to date with new posts about Docker, Go, JavaScript and my thoughts on Technology. I post about twice per month, and notify you when I post. You can also follow me on my Twitter if you prefer.