Practical advice for Go library authors
Writing better libraries
14 July 2016
Jack Lindamood
Software Engineer, Twitch
Jack Lindamood
Software Engineer, Twitch
Many ways to do common things
Which way is preferred and why
Common pitfalls of Go libraries
What I wish I knew
2Generally imports are not renamed
Standard practice is PACKAGE.FUNCTION()
Think of package as part of the name
// In Java, the package name is silent Java: new BufferedReader() // In Go, the package name is where it is used Go: bufio.NewReader()
Tips for structs
// bad. Too generic var c client.Client // stuttering, but accepted var ctx context.Context // optimal example var h http.Client
Tips for functions
// Ok. Reads as a background context context.Background() // Not ok. Context redundant context.NewContext()
No rigid constructor like other languages
Zero value sometimes works
Constructor function (NewXYZ) has ramifications
Using the struct or constructor function
// Using the struct
x := http.Client{}
// Constructor function
y := bytes.NewBuffer(...)// Examples of a zero value
var c http.Client
y := bytes.Buffer{}Read operations work well on nil map/slice/chan
Less useful if
Zero struct support is viral!!!!
Example for defaults
// net/http/server.go
func (srv *Server) ListenAndServe() error {
addr := srv.Addr
if addr == "" {
addr = ":http"
}
// ...
}Example for nil reads
// bytes/buffer.go
type Buffer struct {
buf []byte
off int
}
func (b *Buffer) Len() int { return len(b.buf) - b.off }Not as terse as making zero value work
Constructors can do anything. Struct initialization only does one thing.
Risk people use struct w/o getting it from New
Some libraries hide this with a private struct, public interface
// anti-pattern.go
type authImpl struct {
}
type Auth interface {
// Tends to be very large
}
func NewAuth() Auth {
}func directUse() {
x := grpc.Client{
Port: 123,
Hostname: "elaine82.sjc",
}
// ...
}
func constructor() {
x := grpc.NewClient(123, "elaine82.sjc")
// ...
}Does constructor() do the following
var std = New(os.Stderr, "", LstdFlags)
// Singleton functions in package. Same as struct functions
func Print(v ...interface{}) {
std.Print(v...)
}
func (l *Logger) Print(v ...interface{}) {
// ...
}Use NewXYZ(WithThing(), WithThing()) can be clunky
Has not gained adoption
type Server struct {
speed int
}
func Speed(rate int) func(*Server) {
return func(s *Server) {
s.speed = rate
}
}
func NewServer(options ...func(*Server)) {
}
x := NewServer(Speed(10))Gaining popularity
Easy to understand all available options
Does zero work for default config? Sometimes not
Important to document if config is usable after being passed in
Easier to work with config management
// Usually JSON in config management
type Config struct {
Speed int
}
func NewServer(conf *Config) {
}import "log"
type Server struct {}
func (s *Server) handleRequest(r *Request) {
if r.user == 0 {
log.Println("No user set")
return
}
// ...
}Is it needed?
Logging is a callback
Log to an interface
Don't assume log package
Respect stdout/stderr
No singleton!
18type Log interface {
Println(v ...interface{})
}
type Server struct {
Logger Log
}
func (s *Server) log(v ...interface{}) {
if s.Logger != nil {
s.Logger.Println(v...)
}
}
func (s *Server) handleRequest(r *Request) {
if r.user == 0 {
s.log("No user set")
return
}
}Can we do even better?
Accept interfaces, return structs
Some libraries only expose interfaces. Keep all structs private.
Usually no need to include interface from outside your package/stdlib
Means an API that uses standard objects is usually best
type Rand interface {
ExpFloat64() float64
Float32() float32
Float64() float64
Int() int
Int31() int32
Int31n(n int32) int32
Int63() int64
Int63n(n int64) int64
Intn(n int) int
NormFloat64() float64
Perm(n int) []int
Read(p []byte) (n int, err error)
Seed(seed int64)
Uint32() uint32
}There are lots of ways to generate random things
What is wrong with this?
22Lots of ways to get random things
rand.Source is an interface. Very small. The building block.
// A Source represents a source of uniformly-distributed
// pseudo-random int64 values in the range [0, 1<<63).
type Source interface {
Int63() int64
Seed(seed int64)
}
// A Rand is a source of random numbers.
type Rand struct {
src Source
}Never?
panic() in a spawned goroutine is the worst
Panic is usually ok if:
// MustXYZ calls XYZ, panics on error.
// Note: Users still have the option to call XYZ directly
func MustCompile(str string) *Regexp {
regexp, error := Compile(str)
if error != nil {
panic(`...`)
}
return regexp
}Check all errors on interface function calls
If you can't return it, always do something.
When are returning errors appropriate
// Counterexample HasPermission(userID) error // Better HasPermission(userID) (bool, error)
Especially complex libraries can have state that isn't clear to the user
Suggest exposing an expvar.Var{} as a function
Debug logging can help
Ideally expose Stat() function for atomic integers around tracking information
type Stats struct {
// atomic.AddInt64(&s.stats.TotalRequests, 1)
TotalRequests int64
}
type Server struct {
DebugLog Log
stats Stats
}
func (s *Server) Var() expvar.Var {
return expvar.Func(func() interface{} {
return struct{...}
}
}Complex libraries could benefit users with a simple test helper
Abstract away time/IO/os calls with interfaces
// Package stub. For trim structs
var Now = time.Now
// Inside struct for most control
type Scheduler struct {
Now func() time.Time
}
func (s *Scheduler) now() time.Time {
if s.Now != nil {
return s.Now()
}
return time.Now()
}Stdlib has very few channels in the public API
Push what you would use channels for up the stack
Channels are rarely parameters to functions
Callbacks vs channels
Mixing mutexes and channels can be dangerous
Honestly, channels rarely belong in a public API
30Some libraries use New() to spawn their goroutines.
Close() should end all daemon threads
Push goroutine creation up the stack
// counterexample
func NewServer(..) *Server {
s := &Server{...}
go s.Start()
return s
}
// ideal
func userLand() {
// ...
s := http.Server{}
go s.Serve(l)
// ...
}All blocking/long operations should be cancelable!
Generally an abuse to store context.Context
When to use context.Value()
Singletons and context.Value() obscure your program's state machine
Seriously though, try not to use context.Value()
32Great advice in library design, system design, and Dilbert life
The less hard things you try to do, the less likely you'll screw up whatever you're doing.
Hard things (threading/Mutexes/Deadlock/channels/encryption)
Push problems up the stack
Corollary: Try not to do things
33Correctness still trumps efficiency
Minimizing memory allocs is usually first priority
Avoid creating an API that forces memory allocations
func (s *Object) Encode() []byte {
// ....
}
func (s *Object) WriteTo(w io.Writer) {
// ...
}Package management is not ideal for go libraries
Don't use /vendor for libraries.
Don't expose vendored deps as part of the library's external API
Honestly, just don't use libraries in your library
Cross OS libraries
Writing libraries that are compatible with new versions of Go
Integration tests
Numerous static analysis tools for go
Build options (travis/circle/etc)