When (not) to call package-level APIs

Libraries must not call package-level APIs of other libraries.

What do I mean by that? Let me first define “package-level API”. Surely you know that some packages of the standard library provide a default object of a type, along with package-level functions.

Black Friday Special:
My course Master Go is $60 off until Dec 2nd.
Hop over to AppliedGo.com for more info.

Examples:

  • The flag package has a FlagSet type and a package-level flag variable CommandLine, with package-level functions that operate on this variable. Under the hood, these functions are only shallow wrappers around the methods of FlagSet .
  • The net/http package has default Client and ServeMux variables and package level functions that wrap the corresponding methods.

Such package-level variables and functions build a package-level API. The main purpose of this is convenience. If you want to write a quick CLI tool or a simple server, using the package-level API saves you a few lines of code.

The catch

Of course, there is a catch. I would not have written this Spotlight if there wasn't a catch.

Package-level variables are, by definition, not thread-safe. This is not a problem if the package-level functions do not modify the state of the variable. For example, http.Get() does not write anything to http.DefaultClient, hence, no data race can occur. But as soon as state-changing methods are called without synchronization mechanisms, the data race game begins.

Here is a practical example: The log package provides a package-level logger. Clients can set log flags to control the prefix of every log output through log.SetFlags(…) . This function is thread-safe. The Logger struct's flags field is an atomic.Int32 that can be updated only through atomic operations. So, technically, the following code contains no data race, but the result is still not the desired one:

package main

import (
	"log"
	"time"
)

func main() {

	done := make(chan struct{})

	go func(done chan<- struct{}) {
		for range 10 {
			log.SetFlags(log.Lshortfile)
			time.Sleep(time.Microsecond)
			log.Println("file")
		}
		done <- struct{}{}
	}(done)
	go func(done chan<- struct{}) {
		for range 10 {
			log.SetFlags(log.Ltime)
			time.Sleep(time.Microsecond)
			log.Println("time")
		}
		done <- struct{}{}
	}(done)

	// wait for both goroutines
	<-done
	<-done
}

Output:

prog.go:18: file
prog.go:26: time
23:00:00 file
23:00:00 time
23:00:00 time
23:00:00 file
prog.go:26: time
prog.go:18: file
prog.go:18: file
prog.go:26: time
23:00:00 time
23:00:00 file
prog.go:18: file
prog.go:26: time
23:00:00 time
23:00:00 file
prog.go:18: file
prog.go:26: time
23:00:00 time
23:00:00 file

Program exited.

(Playground link)

The prevention

Prevention is better than cure, they say, and prevention is actually pretty easy in this case.

If you write a binary, feel free to use package-level APIs but apply caution if your app uses goroutines.

If you write a library, make sure that your code only uses isolated instances of the package's types. After all, you have no control over the code of your library's clients. The Google Go Style Guide says, “Infrastructure libraries that can be imported by other packages must not rely on package-level state of the packages they import.” So make your code watertight.

For example, instead of calling the package-level logger, each goroutine can create its own Logger instance:

package main

import (
	"log"
	"os"
	"time"
)

var yes = struct{}{}

func main() {

	done := make(chan struct{})

	go func(done chan<- struct{}) {
		l := log.New(os.Stdout, "", 0)
		for range 10 {
			l.SetFlags(log.Lshortfile)
			time.Sleep(time.Microsecond)
			l.Println("file")
		}
		done <- yes
	}(done)
	go func(done chan<- struct{}) {
		l := log.New(os.Stdout, "", 0)
		for range 10 {
			l.SetFlags(log.Ltime)
			time.Sleep(time.Microsecond)
			l.Println("time")
		}
		done <- yes
	}(done)

	// wait for both goroutines
	<-done
	<-done
}

Output:

prog.go:20: file
23:00:00 time
prog.go:20: file
23:00:00 time
23:00:00 time
prog.go:20: file
prog.go:20: file
23:00:00 time
23:00:00 time
prog.go:20: file
prog.go:20: file
23:00:00 time
23:00:00 time
prog.go:20: file
prog.go:20: file
23:00:00 time
prog.go:20: file
23:00:00 time
23:00:00 time
prog.go:20: file

(Playground link)

So, while package-level APIs can be convenient, creating unique instances of package types is the safer bet.