Let’s face it. Global variables are mostly bad and you should not use them.
BUT they are cases where it’s better to use them. This post will explore these.
Sentinel errors
In Go, you don’t throw and catch errors.
You wrap and return them, and check them with errors.Is(err, errSomething)
.
You may wonder what errSomething
is? It’s a sentinel error.
The error check errors.Is(err, errSomething)
will return true
only if errSomething
is the same error variable as err
or as the one wrapped in err
.
That means in we would get in this code example:
errTest := errors.New("test error")
err := fmt.Errorf("some context: %w", errTest)
fmt.Println(errors.Is(err, errTest)) // true
fmt.Println(errors.Is(err, errors.New("test error"))) // false
fmt.Println(errors.Is(err, errors.New("some context: test error"))) // false
As you can see, you need the actual same variable for errors.Is
to work.
And… that’s why we need sentinel errors defined as global variables.
On top of this, unless you handle the error within a package (log it out or take some other action), you should export the sentinel errors. A more concrete example would be:
package store
import (
"errors"
"fmt"
)
var ErrUserIDNotFound = errors.New("user id not found")
func (s *Store) GetUser(userID string) (user User, err error) {
user, ok := s.idToUser[userID]
if !ok {
return user, fmt.Errorf("%w: %s", ErrUserIDNotFound, userID)
}
return user, nil
}
And a user of this package would use:
package main
import (
"errors"
"fmt"
"os"
)
func main() {
store := store.New()
const userID = "123"
user, err := store.GetUser(userID)
if errors.Is(err, store.ErrUserIDNotFound) { // <- we need the error exported at global scope!
fmt.Println("id not in here dawg")
os.Exit(1)
} else if err != nil {
fmt.Println("error:", err)
os.Exit(1)
}
fmt.Println("user found:", user)
}
Note errors.Is(err, store.ErrUserIDNotFound)
which is the important code piece here, highlighting why sentinel errors have to be exported and defined at global scope.
If you are interested into errors wrapping, you can check out my other post Wrap Go errors like it’s Christmas!.
Regex
The standard library regexp
package can compile regular expressions using two ways:
regexp.Compile(str string) (r *regexp.Regexp, err error)
regexp.MustCompile(str string) *regexp.Regexp
Now two additional facts to consider:
- Compiling a regular expression takes some time
- A compiled regular expression is immutable
Therefore, as soon as you have a constant regular expression string, you should have its compiled regular expression as a global variable (unexported by default). For example:
package main
import (
"regexp"
)
var regexAlphaNumeric = regexp.MustCompile(`^[a-zA-Z0-9_]+$`)
func isAlphaNumeric(s string) (ok bool) {
return regexAlphaNumeric.MatchString(s)
}
This allows to compile the regex only once, and also to detect any regular expression issue at program start time (or by running a test importing this package).
However, do not define regular expressions with a variable string expression as global variables, and use regexp.Compile
instead, checking its error.
Variables set by the build pipeline
You can set global variable values from the go build
command. For example:
main.go
:
package main
var version = "unknown"
using the build command:
go build -ldflags="-X 'main.version=test-1'" main.go
will set the global variable version
to "test-1"
.
Unfortunately, there is no way around using global variables in this case.
Beware of these
Caser
Since Go 1.18, strings.Title
is now deprecated.
It was tempting to have the newer *caser.Caser
at global scope:
package main
import (
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
var titleCaser = cases.Title(language.English)
func process(s string) string {
return titleCaser.String(s)
}
To avoid re-creating a Title cases.Caser
on every call to process
.
However, caser.Caser
is stateful and NOT thread safe. Therefore it gets messy quicky, even running parallel subtest will make it panic your code.
So don’t have it as a global variable!
High performance byte slices
You might think that having a global scope byte slice would yield higher performance? Benchmark, unless it’s megabytes, it won’t make a difference with a locally defined byte slice.
If you want a globally accessibly ‘byte slice’, have the byte slice data as a constant string and convert it where needed to a byte slice. In example:
package main
import "bytes"
const globalConstant = "global constant"
func main() {
_ = bytes.Count([]byte(globalConstant), []byte{1})
}
Conclusion
Global variables are bad. But a necessary evil for sentinel errors There are use cases for immutable variables (like regex) where it makes sense to use as well. But otherwise, don’t use them 😉
Comments