Engineering Blog

                            

Creating utility packages

This blog is about how it is a bad practice to create shared packages such as utils, common and base. We will learn about the problems with such approach and how we can improve our code and project structure by avoiding such an approach.

Let’s look at an example inspired by the official Go blog. It’s about implementing a set data structure (a map where the value is ignored). The idiomatic way to do this in Go is to handle it via a map[K]struct{} type with K that can be any type allowed in a map as a key, whereas the value is a struct{} type. Indeed, a map whose value type is struct{} conveys that we aren’t interested in the value itself. Let’s expose two methods in a util package:

package util
func NewStringSet(…string) map[string]struct{} {
// Creates a string set
}
func SortStringSet(map[string]struct{}) []string {
// Returns a sorted list of keys
}

A client will use this package like this:

set := util.NewStringSet("c", "a", "b")
fmt.Println(util.SortStringSet(set))

The problem here is-

  • The util package name is meaningless as that name doesn’t provide any insight about what the package provides.

Instead of a utility package, we should create an expressive package name such as stringset. For example,

package stringset
func New(…string) map[string]struct{} { … }
func Sort(map[string]struct{}) []string { … }

In this example, we removed the suffixes for NewStringSet and SortStringSet, which respectively became New and Sort. On the client side, it now looks like this:

set := stringset.New("c", "a", "b")
fmt.Println(stringset.Sort(set))

Here we have created a nano package for stringset the idea itself of a nano package isn’t necessarily bad. If a small code group has high cohesion and doesn’t really belong somewhere else, it’s perfectly acceptable to organize it into a specific package. There isn’t a strict rule to apply, and often, the challenge is finding the right balance. We could even go a step further. Instead of exposing utility functions, we could create a specific type and expose Sort as a method this way:

package stringset
type Set map[string]struct{}
func New(…string) Set { … }
func (s Set) Sort() []string { … }

This change makes the client even simpler. There would only be one reference to the stringset package:

set := stringset.New("c", "a", "b")
fmt.Println(set.Sort())

With this small refactoring, we get rid of a meaningless package name to expose an expressive API. As Dave Cheney (a project member of Go) mentioned, we reasonably often find utility packages that handle common facilities.

We could use utility package in cases such as below:-

For example, if we decide to have a client and a server package, where should we put the common types? In this case, perhaps one solution is to combine the client, the server, and the common code into a single package.

Conclusion

Naming a package is a critical piece of application design, and we should be cautious about this as well. As a rule of thumb, creating shared packages without meaningful names isn’t a good idea; this includes utility packages such as utils, common, or base. Also, bear in mind that naming a package after what it provides and not what it contains can be an efficient way to increase its expressiveness.

References:-

  • 100 Go Mistakes and how to avoid them, Teiva Harsanyi, Manning Publications Co
Previous Post
Next Post