In-App packages are created as part of a greater application. These typically serve a specific purpose. They are often created to isolate functionality rather than address reuse.
Stand alone packages are created to share functionality across many applications. They should be independent of application implementation. Allowing users to call the package in many different ways.
There are many proposed structures for In-App packages. But there is no standard everyone follows.
$ find . -type d
./internal/app/
./internal/pkg/
./internal/pkg/nonreusablepkg2/
./internal/pkg/nonreusablepkg1/
./pkg/reusablepkg1/
./pkg/reusablepkg2/
There are recommendations such as creating a /pkg directory for public sharable packages. While creating an /internal/pkg directory for internal only packages.
$ find . -type d
./app/
./reusablepkg1/
./nonreusablepkg1/
./reusablepkg2/
I put all packages in the repository top level under their own package names. I like this method as it keeps all packages in the same place, a sane easy to find location.
For stand alone packages, there is a common pattern followed by most.
Stand alone packages should be in their own repository.
The code files should be located in the top level directory. Any sub-packages should be directories in the top level.
$ find . -type f
./go.mod
./drivers/redis/redis_test.go
./drivers/redis/redis.go
./drivers/cassandra/cassandra.go
./drivers/cassandra/cassandra_test.go
./LICENSE
./go.sum
./README.md
./CONTRIBUTING.md
./hord_test.go
./hord.go
Naming files within a package is a key part of keeping a package organized. The guidelines below apply to both in-app and stand-alone packages.
As important as naming and directory structure. How we structure the interface is how users work with our package. The functions, methods, and types we export from our package dictates how users use it.
Some packages export simple functions that hold no long term state. Other packages will export structures to hold data and provide methods of access. Many packages, will do both.
Many packages provide users with simple helper functions. These functions take data as input, and provide something new or modified as output.
package config
// Marshal will take in an interface and format it
// into config data.
func Marshal(in interface{}) ([]byte, error) {
// do stuff
}
// Unmarshal will take in raw config data and parse
// it into the provided interface.
func Unmarshal(in []byte, out interface{}) error {
// do stuff
}
Some packages store context, data, or control services. These packages usually export a pointer to a custom type with methods. Each pointer, is an individual instance a user can interact with.
By using the custom types with our getter and setter methods. Users can create many instances of Config each with different data.
When writing tests, each test should have its own instances of Config. Then, they can run in parallel.
Stick to using New() or Dial() to create new instances. Users would be calling config.New().
package config
import "sync"
// Config is a custom type for holding and changing configuration
// data.
type Config struct {
sync.RWMutex
item string
}
// New will create a new instance of configuration.
// Each instance is unique and can hold different information.
func New() *Config {
// setup the config object
return &Config{}
}
// Item is used to access Config.item
func (c *Config) Item() string {
c.RLock()
defer c.RUnlock()
return c.item
}
// SetItem is used to change Config.item
func (c *Config) SetItem(s string) {
c.Lock()
defer c.Unlock()
c.item = s
}
Sometimes, it is necessary for packages to load outside dependencies. This is common with Config, Logging, etc. It's tempting to re-initialize these on package load, but don't do that. Let the calling application push those dependencies to the package.
package database
import "github.com/some/logger"
// don't do this... just don't
func init() {
log = logger.New()
cfg = config.New()
}
func MyFunc() {
// do work
log.Printf("Tell someone about it")
}
package database
// imagine some imports here
type DB struct {
cfg *config.Config
log *logger.Logger
}
func Dial(log *logger.Logger, cfg *config.Config) (*DB, error) {
// create a new DB control object
}
func (db *DB) MyFunc() {
// do work
db.log.Printf("Tell someone about it")
}
In the example before, we pushed a config object into our database package. But is that what we want? Now our package is locked into using this specific config package. What did it buy us? Dynamically update our DB config? It, sounds nice, but doesn't work like you think it will.
In this case, the implementing app should create a new DB instance and shut the old one down. That's the whole point of this package structure.
package database
type DB struct {
server string
password string
}
func Dial(server, password string) (*DB, error) {
// create a new DB control object
}
When pushing dependencies, ask yourself. Is there a better way of doing this? A way that doesn't kill reusability?
Logging has no place in stand alone or in-app packages. It should only be executed within the implementing application.
While logging within packages is a pattern that is popular in other languages. That doesn't mean it is right for Go.
But if you can't log, how do you get errors to the user? That's where multi-value returns, and custom error types come into the picture.
As a package, it's preferable to pass any errors to the calling application. This allows the calling application to check the error and perform a specific action.
myVal, err := db.MyFunc()
if err == db.ErrMyCustomError {
// do something fancy
}
if err != db.ErrMyCustomError && err != nil {
// ok maybe log and do something less fancy
}
Sometimes a package must perform tasks in the background. When writing these types of packages it is still better to provide the user with an error. But rather than returning the error on a function, call back to the user with an error.
I like two ways of accomplishing this. Either through an error channel, or an error function. Error functions are user defined functions that the package can callback with errors.
type Setup struct {
// user defined error function
errFunc func(error)
}
func main() {
// on setup
err := New(Setup{
errFunc: func(err error) {
if err == db.ErrMyCustomError {
// do something fancy
}
if err != db.ErrMyCustomError && err != nil {
// ok maybe log and do something less fancy
}
},
})
}
Packages should be made asynchronous only when necessary. It is often easier to implement asynchronous execution within the implementing application itself. This includes error handling.
Earlier we discussed the differences between in-app and stand alone packages. While there is a often a difference in the locations of these packages. Functionally, and structurally, there should be no difference between the two.
A well structured in-app package, should be able to be moved to a stand alone package with minimal changes.
Twitter: @madflojo
LinkedIn: Benjamin Cane
Blog: BenCane.com
Like this? Check out my some of my other stuff:
Principal Engineer - American Express