This week:
Avoid using math/rand, use crypto/rand for keys instead.
Empty slice or, even better, NIL SLICE.
Error messages should not be capitalized or end with punctuation.
When to use Dot (.) Import and Blank (_) Import?
Avoid using math/rand, use crypto/rand for keys instead.
When you're working on projects that require generating keys, like for encryption or for creating unique identifiers, the quality and security of those keys are really important.
Why not math/rand?
The math/rand package generates pseudo-random numbers.
This means if you know how the numbers are generated (the seed), you can predict the numbers.
Even if you seed it with the current time (like time.Nanoseconds()), the amount of unpredictability (entropy) is low because there's not a lot of variation in the current time from one execution to the next.
Why crypto/rand?
crypto/rand provides a way to generate numbers that are cryptographically secure. It's designed to be unpredictable, using sources of randomness provided by your OS, which are much harder to predict.
crypto/rand is suitable for encryption, authentication, and other security-sensitive operations.
Empty slice or, even better, NIL SLICE.
When working with slices in Go, you have two approaches to start with what appears to be an empty slice:
Using var keyword: `var t []int`
This method declares a slice t of type int without initializing it. The slice is considered nil.
This means it doesn't actually point to any underlying array. Its length (len) and capacity (cap) are both 0.
Using slice literal: `t := []int{}`
Unlike the var declaration, this slice is not nil.
It's a slice that points to an underlying array, but that array has no elements.
So, which one is considered idiomatic?
A nil slice doesn't allocate any memory.
It's just a pointer to nowhere, while an empty slice ([]int{}) actually allocates a small amount of memory to point to an existing, but empty, array. In most cases, this difference is negligible, but for high-performance applications, it could be significant.
The Go community prefers the nil slice approach because it's considered more idiomatic to the language's philosophy of simplicity and zero values.
Of course, exceptions exist.
For example, when working with JSON, a nil slice and an empty slice behave differently.
A nil slice (var t []int) encodes to JSON as null, whereas an empty slice (t := []int{}) encodes to an empty JSON array ([]).
It's also idiomatic to design your code to treat a non-empty slice, an empty slice, and a nil slice similarly.
If you're familiar enough with Go, you may know that for range, len, append,... work without panic with a nil slice.
Error messages should not be capitalized or end with punctuation.
This might seem a bit unusual at first, but there's a practical reason behind it.
Why lowercase?
Error messages often get wrapped or combined with other messages. If an error string starts with a capital letter, it can look odd or out of place when it's in the middle of a sentence.
Starting with a lowercase letter helps it blend more naturally.
// application startup failed: failed to initialize module: could not open the database
Another point is the appearance of "..." at the end of the message.
This means, any text following %w in a formatted error string is intended to be appended at the end of the whole message.
Why no punctual?
It's to ensure that when one message is appended to another, the result looks like a coherent sentence rather than a jumble of phrases.
When to use Dot (.) Import and Blank (_) Import?
1. Blank import (import _ "package")
When you use the "Blank Import", you're bringing in a package not for direct access to its items (like functions or variables) but for its side effects.
But what're side effects?
Side effects are any operations that a package might perform when it's imported, such as initializing, registering somewhere, setting up the environment, etc.
This usually happens in the package's init() function, which runs automatically when the package is imported.
Even if the main function is empty, when you run this, the init() of the logger package runs upon import, printing "Logger package initialized" to the console.
When to use?
The main rule here is:
Use this form of import mostly in the main package.
In specific tests that need those side effects to run correctly.
A common example is importing a database driver package in a program that uses the database/sql package. The driver package is imported for its side effects (e.g. registering itself as a driver for database/sql).
2. Dot Import (import . "package").
Using the "Dot Import" syntax is a bit more special.
This means you can refer to those exported items as if they were defined in the current package.
See, we don't need to specify math.Abs or math.Pi.
Now, when to use it?
This form is particularly useful in tests.
Especially when dealing with circular dependencies that you can't resolve easily. Let me give you an example.
Imagine you have 2 packages:
mypackage: This is your main package that contains the functionality you're testing.
testhelpers: A separate package that provides helper functions for testing. One of the helper functions needs to use mypackage, creating a dependency from testhelpers on mypackage.
Now, you're writing tests for mypackage and you want to use the helpers from testhelpers.
But since testhelpers already imports mypackage, you can't simply import testhelpers into your mypackage_test.go without creating a circular dependency.
To work around this, the test file declares itself as package mypackagetest and uses dot import to access mypackage's identifiers directly, as if it were inside mypackage.
It's recommended to use these sparingly, as they can make code harder to read.