One of my favourite aspects of Go is the lively and growing ecosystem for third-party libraries, and I’ve ended up using absolutely dozens and dozens for little projects here and there. Compared to other popular languages, Go has made it easy to write packages in a consistent style that are easy to read, use and contribute to, by virtue of standard tools like go doc, go fmt and go lint.

However, the tools don’t (or can’t) pick up on everything, so here are some additional tips from my experiences working with Go packages:

Make your package discoverable

GoDoc is probably my favourite single website in the Go world, as it’s the best source for finding libraries or looking up documentation. So, if you want people to find and use your library, you need to make sure it’s listed here.

Ideally, host your package on a platform that it supports, such as GitHub or BitBucket - and of course keep the repository public! Once you’ve published your library, go to GoDoc and enter your library’s name into the search box to ensure that it is indexed and searchable. Do the same at GoWalker if you can.

Of course, hosting your package like this makes it easier for others track development and improve it collaboratively, so it’s a no-brainer, really.

Add a package comment

Go types and values are commented by adding a comment directly above their declaration - the same applies to package declarations too.

To make your package more discoverable, and to better describe its intended purpose, include a package comment, usually in a lone file like doc.go or package.go:

1
2
3
4
5
6
/* 
Performs amazing things using the power of
Foo, Bar, and the deeply mystical aspects
of Qux. 
*/
package "github.com/example/foobar"

This will then be indexed and appear at the top of results in the various Go documentation engines.

Export your exposed types

By convention, types, values and methods in your package that start with an uppercase letter are ‘exported’ and can be called by other Go packages. They also appear in the documentation generated by go doc, so that users can see exactly what your package exposes and how to use it. Everything else is hidden by default (unless you look at the source files directly, of course.) So make sure the functionality you want to expose is named correctly.

However, there’s a subtle caveat - if a type is not visible (i.e. it starts with a lowercase letter), none of its methods will appear in documentation - even if they do start with an uppercase letter. So in the following example, the Baz() method will not be documented because the fooBar type leaked by NewFooBar is not actually exported:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package "github.com/example/foobar"

type fooBar int

// Baz returns a string that is critical to life, 
// the universe, and everything.
func (f fooBar) Baz() string {
    return "You all still have Zoidberg"
}

// NewFooBar constructs and returns a fresh fooBar.
func NewFooBar() fooBar {
    return &fooBar{}
}

As a loose rule, if your package returns any custom types to callers (such as fooBar), make sure those types are public. In this case, renaming the fooBar type to FooBar is enough to ensure that Baz() appears in documentation.

The same applies for referenced types too, for example:

1
2
3
type Qux struct {
    Thing fooBar
}

In this case, the Qux type is public, and the Thing member is public, but the fooBar type is not public, so documentation for Qux.Thing won’t be readily available. Again, the solution is to either conceal it as an internal detail by renaming the member to thing, or - if you intend users to use it - rename the type to FooBar.

Make errors distinct and discernable

Ensure users can discern between common errors without needing to resort to string comparisons. Declare error values in your package so simple comparisons like if err == foobar.ErrBazzed can be used to recover from distinct errors gracefully (see errors like io.EOF or http.ErrNotSupported in the io and http packages for more examples.)

In practise, it means replacing errors like this, which convey information that the calling program can probably recover from:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// SporkEgg attempts to spork an egg.
func SporkEgg() error {
    if sporks == 0 {
        return fmt.Errorf("not enough sporks")
    }
    if eggs == 0 {
        return fmt.Errorf("not enough eggs")
    }
    return spork("egg")
}

…with errors like this, which are declared in one place, exported, conventionally-named, and clearly documented:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// ErrNoSporks indicates we ran out of sporks. If this
// occurs, try again later.
var ErrNoSporks = errors.New("not enough sporks")

// ErrNoEggs indicates there are no available eggs.
var ErrNoEggs = errors.New("not enough eggs")

// SporkEgg attempts to spork an egg.
func SporkEgg() error {
    if sporks == 0 {
        return ErrNoSporks
    }
    if eggs == 0 {
        return ErrNoEggs
    }
    return spork("egg")
}

If this isn’t possible, expose methods that allow users to identify common errors, e.g. func IsBazzError(err error) bool.

Keep imports lean

Avoid importing more packages than absolutely necessary, especially if they only exist to fulfil uncommon use-cases. Don’t mandate a specific library implementation where multiple implementations exist.

For example, say your library uses map[string]string internally to keep data in-memory, but you want to now sometimes persist this to disk via a key-value store. One solution would be to import goleveldb and hook your library up to it, but suddenly you’ve added a hard third-party dependency and tied your users to a specific implementation when alternatives exist (such as levigo or BoltDB, or even the Redis client Redigo).

The preferable solution is to use to create a ‘pluggable’ system: create an interface, rework your library to use this interface, and then publish an implementation against this interface in a separate package. Your core package can then be used without any additional requirements, but users can optionally use your persistence package, or even write their own implementation for whatever backend they want to use use instead.

So, say your original package looked like this, using in-memory (ephemeral) storage for keys:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package "github.com/example/cabinet"

// Cabinet stores data for us
type Cabinet struct {
    storage map[string]string
}

// Put the value `v` at key `k`
func (cab *Cabinet) Put(k, v string) {
    cab.storage[k] = v
}

// NewCabinet returns a fresh Cabinet
func NewCabinet() *Cabinet {
    return &Cabinet{}
}

You could rework the backend storage out into its own interface, and provide an implementation that provides the existing functionality (warning: wall of code ahead):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package "github.com/example/cabinet"

// Backend should be implemented by backends
type Backend interface {
    Put(k, v string)
}

// MemBackend is a Backend that stores data in-memory
type MemBackend struct {
    data map[string]string
}

// Put assigns the value `v` to the key `k`
func (b *MemBackend) Put(k, v string) {
    b.data[k] = v
}

// Cabinet stores data for us
type Cabinet struct {
    backend Backend
}

// Put the value `v` at key `k`
func (cab *Cabinet) Put(k, v string) {
    cab.backend.Put(k, v)
}

// NewCabinet returns a Cabinet using in-memory
// storage as the backend.
func NewCabinet() *Cabinet {
    return NewBackedCabinet(&MemBackend{})
}

// NewBackedCabinet returns a Cabinet using the given
// Backend.
func NewBackedCabinet(b Backend) *Cabinet {
    return &Cabinet{b}
}

Then your persistent GoLevelDB backend implementation can exist in a separate package:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package "github.com/example/cabinet/leveldb"

import (
    "github.com/example/cabinet"
    "github.com/syndtr/goleveldb/leveldb"
)

// Backend should be implemented by backends
type Backend struct {
    db *leveldb.DB
}

// Put the value `v` at key `k`
func (b *Backend) Put(k, v string) {
    // FIXME: handle the error returned here
    b.db.Put([]byte(k), []byte(v), nil)
}

func NewBackend(path string) *Backend {
    // FIXME: handle the error returned here
    db, _ = leveldb.OpenFile(path, nil)
    return &Backend{db}
}

Finally, users opt-in to your LevelDB backend by utilising it:

1
2
3
4
5
6
7
import (
    "github.com/example/cabinet"
    "github.com/example/cabinet/leveldb"
)

backend := leveldb.NewBackend("thinger.db")
cabinet := foobar.NewBackedCabinet(backend)

For a more thorough example, see my go-kvq package which supports several different k/v backends, but doesn’t mandate a specific one upon the user, and the ‘core’ library does not depend on any one implementation.

As an alternative, you can also use the “registration” mechanic adopted by the sql library, where merely importing a package (e.g. the postgres driver github.com/lib/pq) registers it against a common package.

Use build constraints

When hooking into other libraries via CGo, there’s a good chance you’ll end up with some very platform-specific Go code. Be sure to constrain these sections of your code to their appropriate platform using the Build Constraints features of Go’s build tool, as this will produce cleaner error messages for users on supported platforms, and make it easier to add support for other platforms down the line.

The simplest way to do this is to put your OS-specific implementations in appropriately-named files, e.g. foobar_linux.go or foobar_windows.go, with the commonalities going into foobar.go. Then, when a Plan 9 developer attempts to build, they’ll get clean errors indicating a missing implementation, rather than confusing errors about missing headers. To fix it, they’ll just have to implement the appropriate platform code in foobar_plan9.go and it’ll all work again - and it’s super easy to contribute this implementation back!

(The source for os in the standard library is chock-full of examples demonstrating this concept.)