How To Separate Library Packages in Go
I’ve often seen, and have been responsible for, throwing code into packages without much thought. I’ve quickly drawn a line in the sand and started putting code into different folders (which in Go are different packages by definition) just for the sake of findability. Learning to properly build small and reusable packages can take your Go career to the next level.
What is a Package?
In Go, code is organized into packages. Every folder that contains Go code is a package. Runnable programs must have a package called “main” which acts as an entry point to the program. All other packages can be named (almost) anything, and they export code that can be used in other packages and runnable programs. These kinds of non-runnable packages we call “library” packages by convention.
Library packages allow developers to export code so it can be used by the outside world. Packages are essentially APIs where exported functions are user-facing and unexported functions are only for internal use.
Rules Of Thumb
Now that we’ve gone over the basics of what a package is let’s talk about how to write good packages. The rest of this article will focus on some good rules of thumb to know when, how, and why to separate code into a new package.
1. Hide Internal Functions
Oftentimes an application will have complex logic that requires a lot of code. In almost every case the logic that the application cares about can be exposed via an API, and most of the dirty work can be kept within a package. For example, imagine we are building an application that needs to classify images. We could build a package:
package classifier
// ClassifyImage classifies images as "hotdog" or "not hotdog"
func ClassifyImage(image []byte) (imageType string) {
return hasHotdogColors(image) && hasHotdogShape(image)
}
func hasHotdogShape(image []byte) bool {
// internal logic that the application doesn't need to know about
return true
}
func hasHotdogColors(image []byte) bool {
// internal logic that the application doesn't need to know about
return true
}
We create an API by only exposing the function(s) that the application-level needs to know about. All other logic is unexported to keep a clean separation of concerns. The application doesn’t need to know how to classify an image, just the result of the classification.
2. Don’t Change a Package’s API
The unexported functions within a package can and should change often for testing, refactoring, and bug fixing.
A well-designed library will have a stable API so that users aren’t receiving breaking changes each time they update the package version. In Go, this means not changing exported function’s signatures.
3. Don’t Export Functions From Main
Any capitalized function in Go is exported, which means that other programs can import and call those functions. Main packages can contain exported functions, but as a general rule don’t do it. It is confusing to future readers of the code, and in most cases accomplishes nothing.
4. Packages Should Have No Knowledge of Dependents
Perhaps one of the most important and most broken rules is that a package shouldn’t know anything about its dependents. In other words, a package should never have specific knowledge about a particular application that uses it. For example:
package classifier
// ClassifyImage uses a slightly different algorithm if
// the image comes from boot.dev
func ClassifyImage(image []byte, isBootdotdevImage bool) (imageType string) {
return hasHotdogColors(image) && hasHotdogShape(image)
}
Here is an example of a clear violation of this rule. An image classifier shouldn’t have knowledge of a “boot.dev image”, which we can infer is an application that happens to depend on this package. The author should have made different types of classifiers for general use, and then the dependents of the package would be able to choose the correct one. Two apps that depend on the same package needn’t know about each other.
Related Articles
I Wrote Go-TinyDate, The Missing Golang Date Package
Mar 23, 2020 by Lane Wagner - Boot.dev co-founder and backend engineer
time.Time makes dealing with dates and times in Go a breeze, and it even comes bundled in the standard library! However, a time.Time{} struct uses more than 24 bytes of memory under most conditions, and I’ve run into situations where I need to store millions of them in memory, but all I really needed was a UTC date! Go-TinyDate solves this with just 4 bytes of memory.
How to Use Mutexes in Go
Mar 19, 2020 by Lane Wagner - Boot.dev co-founder and backend engineer
Golang is King when it comes to concurrency. No other language has so many tools right out of the box, and one of those tools is the standard library’s sync.Mutex{}. Mutexes let us safely control access to data across multiple goroutines.
Best Practices for Interfaces in Go
Mar 15, 2020 by Lane Wagner - Boot.dev co-founder and backend engineer
Interfaces in Go allow us to treat different types as the same data type temporarily because both types implement the same kind of behavior. They’re central to a Go programmer’s toolbelt and are often used improperly by new Go developers, which leads to unreadable and often buggy code.
Wrapping Errors in Go - How to Handle Nested Errors
Mar 09, 2020 by Lane Wagner - Boot.dev co-founder and backend engineer
Errors in Go are a hot topic. Many newcomers to the language immediately level their first criticism, “errors in go are clunky! Let me just use try/catch!” This criticism is well-meaning but misguided.