Packages in Go serve the same purposes as libraries or modules
in other languages, supporting modularity, encapsulation, separate compilation,
and reuse. The source code for a package resides in one or more
.go
files, usually in a directory whose name ends with the
import path; for instance, the files of the gopl.io/ch1/helloworld
package are
stored in directory $GOPATH/src/gopl.io/ch1/helloworld
.
Each package serves as a separate name space for its declarations.
Within the image
package, for example, the identifier
Decode
refers to a different function than does the same identifier
in the unicode/utf16
package.
To refer to a function from
outside its package, we must qualify the identifier to make
explicit whether we mean image.Decode
or utf16.Decode
.
Packages also let us hide information by controlling which names are visible outside the package, or exported. In Go, a simple rule governs which identifiers are exported and which are not: exported identifiers start with an upper-case letter.
To illustrate the basics, suppose that our temperature conversion software has become popular and we want to make it available to the Go community as a new package. How do we do that?
Let’s create a package called gopl.io/ch2/tempconv
, a variation
on the previous example.
(Here we’ve made an exception to our usual rule of numbering examples
in sequence, so that the package path can be more realistic.)
The package itself is stored in two files to show how declarations in
separate files of a package are accessed; in real life, a tiny package like this
would need only one file.
We have put the declarations of the types, their constants, and their
methods in tempconv.go
:
// Package tempconv performs Celsius and Fahrenheit conversions. package tempconv import "fmt" type Celsius float64 type Fahrenheit float64 const ( AbsoluteZeroC Celsius = -273.15 FreezingC Celsius = 0 BoilingC Celsius = 100 ) func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) } func (f Fahrenheit) String() string { return fmt.Sprintf("%g°F", f) }
and the conversion functions in conv.go
:
package tempconv // CToF converts a Celsius temperature to Fahrenheit. func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) } // FToC converts a Fahrenheit temperature to Celsius. func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
Each file starts with a package
declaration
that defines the package name. When the package is imported,
its members are referred to as tempconv.CToF
and so on.
Package-level names like the types and constants declared in one file of a
package are visible to all the other files of the package, as if the
source code were all in a single file. Note that tempconv.go
imports
fmt
, but conv.go
does not, because it does not use anything
from fmt
.
Because the package-level const
names begin with upper-case letters, they too are
accessible with qualified names like tempconv.AbsoluteZeroC
:
fmt.Printf("Brrrr! %v\n", tempconv.AbsoluteZeroC) // "Brrrr! -273.15°C"
To convert a Celsius temperature to Fahrenheit in a package that
imports gopl.io/ch2/tempconv
, we can write the following code:
fmt.Println(tempconv.CToF(tempconv.BoilingC)) // "212°F"
The doc comment (§10.7.4) immediately
preceding the package
declaration documents the package as a whole. Conventionally, it
should start with a summary sentence in the style illustrated. Only
one file in each package should have a package doc comment. Extensive
doc comments are often placed in a file of their own, conventionally
called doc.go
.
Exercise 2.1:
Add types, constants, and functions to tempconv
for processing
temperatures in the Kelvin scale, where zero Kelvin is −273.15°C
and a difference of 1K has the same magnitude as 1°C.
Within a Go program, every package is identified by a unique string
called its import path. These are the strings that appear in an
import
declaration like "gopl.io/ch2/tempconv"
.
The language specification doesn’t define where these strings come
from or what they mean; it’s up to
the tools to interpret them.
When using the go
tool (Chapter 10),
an import path denotes a directory
containing one or more Go source files that together make up the
package.
In addition to its import path, each package has a package
name, which is the short (and not necessarily unique) name that
appears in its package
declaration.
By convention, a package’s name matches the last segment of its import
path, making it easy to predict that the package name of
gopl.io/ch2/tempconv
is tempconv
.
To use gopl.io/ch2/tempconv
, we must import it:
// Cf converts its numeric argument to Celsius and Fahrenheit. package main import ( "fmt" "os" "strconv" "gopl.io/ch2/tempconv" ) func main() { for _, arg := range os.Args[1:] { t, err := strconv.ParseFloat(arg, 64) if err != nil { fmt.Fprintf(os.Stderr, "cf: %v\n", err) os.Exit(1) } f := tempconv.Fahrenheit(t) c := tempconv.Celsius(t) fmt.Printf("%s = %s, %s = %s\n", f, tempconv.FToC(f), c, tempconv.CToF(c)) } }
The import declaration binds a short name to the imported package that
may be used to refer to its contents throughout the file.
The import
above lets us refer to names within gopl.io/ch2/tempconv
by using a qualified identifier like tempconv.CToF
.
By default, the short name is the package name—tempconv
in this
case—but an import declaration may specify an alternative name to avoid
a conflict (§10.3).
The cf
program converts a single numeric command-line argument
to its value in both Celsius and Fahrenheit:
$ go build gopl.io/ch2/cf $ ./cf 32 32°F = 0°C, 32°C = 89.6°F $ ./cf 212 212°F = 100°C, 212°C = 413.6°F $ ./cf -40 -40°F = -40°C, -40°C = -40°F
It is an error to import a package and then not refer to it.
This check helps eliminate dependencies that become unnecessary
as the code evolves, although it can be a
nuisance during debugging, since commenting out a line of code like
log.Print("got here!")
may remove the sole reference to the package
name log
, causing the compiler to emit an error.
In this situation, you need to comment out or delete the unnecessary
import
.
Better still, use the golang.org/x/tools/cmd/goimports
tool,
which automatically inserts and removes packages from the import
declaration as necessary; most editors can be configured to run
goimports
each time you save a file.
Like the gofmt
tool, it also
pretty-prints Go source files in the canonical format.
Exercise 2.2:
Write a general-purpose unit-conversion program analogous to cf
that reads
numbers from its command-line arguments or from the standard input
if there are no arguments, and converts each number into
units like temperature in Celsius and Fahrenheit,
length in feet and meters, weight in pounds and kilograms, and the like.
Package initialization begins by initializing package-level variables in the order in which they are declared, except that dependencies are resolved first:
var a = b + c // a initialized third, to 3 var b = f() // b initialized second, to 2, by calling f var c = 1 // c initialized first, to 1 func f() int { return c + 1 }
If the package has multiple .go
files, they
are initialized in the order in which the files are given to the
compiler; the go
tool sorts .go
files by name before
invoking the compiler.
Each variable declared at package level starts life with the value of
its initializer expression, if any, but for some variables, like
tables of data, an initializer expression may not be the simplest way
to set its initial value.
In that case, the init
function mechanism may be
simpler.
Any file may contain any number of functions
whose declaration is just
func init() { /* ... */ }
Such init
functions can’t be called or referenced,
but otherwise they are normal functions.
Within each file, init
functions are automatically executed when the
program starts, in the order in which they are declared.
One package is initialized at a time, in the order of imports in the
program, dependencies first, so a package p
importing
q
can be sure that q
is fully initialized before
p
’s initialization begins.
Initialization proceeds from the bottom up; the main
package
is the last to be initialized. In this manner, all packages are
fully initialized before the application’s main
function
begins.
The package below defines a function PopCount
that returns the
number of set bits, that is, bits whose value is 1, in a uint64
value, which is called its population count.
It uses an init
function to precompute a table of results,
pc
, for each possible 8-bit value so that the PopCount
function needn’t take 64 steps but can just return the sum of eight table
lookups.
(This is definitely not the fastest algorithm for counting bits,
but it’s convenient for illustrating init
functions, and for showing
how to precompute a table of values, which is often a useful
programming technique.)
package popcount // pc[i] is the population count of i. var pc [256]byte func init() { for i := range pc { pc[i] = pc[i/2] + byte(i&1) } } // PopCount returns the population count (number of set bits) of x. func PopCount(x uint64) int { return int(pc[byte(x>>(0*8))] + pc[byte(x>>(1*8))] + pc[byte(x>>(2*8))] + pc[byte(x>>(3*8))] + pc[byte(x>>(4*8))] + pc[byte(x>>(5*8))] + pc[byte(x>>(6*8))] + pc[byte(x>>(7*8))]) }
Note that the range
loop in init
uses only the index;
the value is unnecessary and thus need not be included.
The loop could also have been written as
for i, _ := range pc {
We’ll see other uses of init
functions in the next section and
in Section 10.5.
Exercise 2.3:
Rewrite PopCount
to use a loop instead of a single expression.
Compare the performance of the two versions.
(Section 11.4 shows how to compare the
performance of different implementations systematically.)
Exercise 2.4:
Write a version of PopCount
that counts bits by shifting
its argument through 64 bit positions, testing the rightmost bit
each time. Compare its performance to the table-lookup version.
Exercise 2.5:
The expression x&(x-1)
clears the rightmost non-zero bit of
x
. Write a version of PopCount
that counts bits by
using this fact, and assess its performance.