An introduction to Packages, Imports and Modules in Go
This tutorial is written for anyone who is new to Go. In it we'll explain what packages, import statements and modules are in Go, how they work and relate to each other and — hopefully — clear up any questions that you have. We'll start at a high level, then work down to the details later.
There's quite a lot of content in this tutorial, so I've broken it down into the following eight sections:
- Packages
- The main package
- Importing and using standard library packages
- Unused and missing imports
- Exported vs unexported
- Modules
- Using multiple packages in your code
- Importing and using third-party packages
- Organizing import statements
To help illustrate things throughout this post we'll build a small CLI (command-line interface) application which generates and prints out a random 'lucky number'. If you'd like to follow along, run the following commands:
Then add the following code to the main.go
file:
At this point you should be able to run the application and see some output like this:
Packages
A package in Go is essentially a named collection of one or more related .go
files. In Go, the primary purpose of packages is to help you isolate and reuse code.
Every .go
file that you write should begin with a package {name}
statement which indicates the name of the package that the file is a part of. For example, in the 'lucky number' code above, the package main
line declares that the main.go
file is part of a package named main
.
At the moment:
- Our 'lucky number' application consists of one package, with the package name
main
. - The
main
package is made up of one file, with the filenamemain.go
.
It's important to explain that code in a package can access and use all types, constants, variables and functions within that package — even if they are declared in a different .go
file.
Let's illustrate this by splitting our 'lucky number' code across two files. Go ahead and add an additional random.go
file:
Then update the two files so that the main()
function calls a new randomNumber()
function, like so:
So now:
- Our 'lucky number' application consists of two
.go
files. - Both files are part of the
main
package (because they both start with apackage main
statement).
If you re-run the application using the two files, you should see the same output.
This example is a bit contrived but it illustrates the point nicely — our main()
function is able to call our randomNumber()
function because they are part of the same package — despite being in separate .go
files.
It's totally OK to have quite a lot of .go
files in the same package. Having 5, 10 or even 20 files — and thousands of lines of code — in the same package is not uncommon or an anti-pattern in Go.
The main package
In Go, main
is actually a special package name which indicates that the package contains the code for an executable application. That is, it indicates that the package contains code that can be built into a binary and run.
Any package with the name main
must also contain a main()
function somewhere in the package which acts as the entry point for the program. If it doesn't, and you try to run it, you will get this error:
It's conventional for your main()
function to live in a file with the filename main.go
. Technically it doesn't have to, but following this convention makes the application entry point easier to find for anyone reading your code in the future.
As an aside, if you try to build or run a non-main
package it will also result in an error. For example, if you changed the 'lucky number' code so that the package name is foo
instead of main
and try to run it, you will get the following (somewhat confusing) error:
Importing and using standard library packages
I'm sure you know this already, but individual .go
files can import and use exported types, constants, variables and functions from other packages — including the packages in the Go standard library.
The complete tree of Go standard library packages is available here.
In our 'lucky number' code we've imported and used the math/rand
and fmt
packages from the standard library to help us generate a random number and print a message. For example, in the random.go
file:
When importing a package from the standard library you need to use the full path to the package in the standard library tree, not just the name of the package. For example:
Once imported, the package name becomes an accessor for the contents of that package. Conveniently, all the packages in the Go standard library have a package name which is the same as the final element of their import path.
That means we can use the Intn()
function from math/rand
by calling rand.Intn()
, or the Printf()
function from fmt
by calling fmt.Printf()
.
As well as importing packages from the standard library it's possible to import your own packages or third-party packages too. We'll get to that shortly.
Unused and missing imports
If you import a package but don't actually use it in your code, it will result in a compile-time error. For example, if you import the os
package but don't use it you will get an error like:
Similarly, you'll also get a compile-time error if a package is referenced in your code but not imported. For example, if you try to use the strconv
package without importing it you'll get an error like this:
When you're developing rapidly it can sometimes be annoying to keep editing your import statements, but ultimately this behavior helps to keep your code correct and your import list clean and accurate.
Exported vs unexported
Earlier in this tutorial I said:
Individual
.go
files can import and use exported types, constants, variables, functions and methods from other packages — including the packages in the Go standard library.
So what does exported mean?
Essentially, something in Go code is exported if its name starts with a capital letter. Otherwise it is unexported. For example:
The difference between them is:
- Unexported things are 'private' to the package that they are declared in. They are only visible to code in the same package.
- In contrast, exported things in a package are 'public' and are visible to any code that imports the package.
In other words: when you import a package you get to use its exported things, but not its unexported things.
Depending on your programming background, capitalization might seem like a funny way to control visibility. But once you get used to it, it has some positives. It's simple, doesn't require you to remember any additional syntax, and it's trivial to see at a glance whether something is exported or not — even when that thing is being used far away from where it is declared.
Modules
If you have a small application which only imports packages from the standard library, then what we've done so far works just fine. But if you want to import and use a third-party package — or structure your code so it's split into multiple packages — then you first need to turn your code into a Go module.
The Go Wiki defines modules like this:
A module is... a tree of Go source files with a
go.mod
file in the tree's root directory.
In our example the lucky-number
directory already contains our two .go
files, so all we need to do is add a valid go.mod
file to the directory to make it a module.
The easiest way to do this is by running the go mod init
command and passing in a module path as the final argument, like so:
Before we go further, let's talk about module paths.
The module path act as a canonical identifier for a module. Ideally it should be unique and something that is unlikely to be used by anyone else, in any other project. In the command above I've used lucky-number.alexedwards.net
as the module path, but it could be (almost) any string value.
In the Go community it's conventional to base your module path on a URL that you own or control. So, for this example, a good module path would be something like lucky-number.alexedwards.net
or github.com/alexedwards/lucky-number
.
OK, let's take a look at the go.mod
file that was generated for us:
We can see that (for now) all this does is declare the module path, along with the version of Go that you are using. We'll revisit this file again later when we talk about using third-party Go packages.
So at this point in the tutorial:
- The code in the
lucky-number
directory is now a Go module. - The Go module has the module path
lucky-number.alexedwards.net
. - The module contains one
main
package, which is made up of ourmain.go
andrandom.go
files.
Using multiple packages in your code
Let's make our 'lucky number' application structure a bit more complex and split up the code into two packages. Before we get started on this change there are a couple of rules and conventions to be aware of:
- In Go, one package == one directory. That is, all
.go
files for a package should be contained in the same directory, and a directory should contain the.go
files for one package only. You shouldn't ever have.go
files with different package names in the same directory. - For all non-
main
packages, the directory name that the code lives in should be the same as the package name. When choosing a name you should pick something that is short, descriptive, lower case and ideally one word. The Go blog has a helpful post with additional guidance and some examples of good and bad names.
With those things in mind, let's restructure our 'lucky number' application so that the code for generating the random number is isolated in a new, separate, package called random
.
The file tree for the lucky-number
directory should now look like this:
The important thing to point out is that all the .go
files are still part of the same module — they are all part of a file tree with a single go.mod
file in the root directory of the tree.
OK, let's go ahead and add the following code to the new random/number.go
file:
There are four things I'd like to quickly highlight and re-iterate here:
- The
number.go
file is part of therandom
package (notice the statement in the first line). - The
Number()
function is exported (i.e. its name begins with a capital letter). This means it will be visible to any code which imports therandom
package. - The directory name that the code lives in is exactly the same as the package name (
random
) . - The
random
package is part of thelucky-number.alexedwards.net
module.
Next let's update our main.go
file to import and use the new package. Like so:
The most interesting thing about this is the import path for our new package. When you are importing packages that are part of the same module as your current .go
file, the import statement should take the form:
So in this case, the module path is lucky-number.alexedwards.net
and the path within the module for the package is random
, giving us an import path of lucky-number.alexedwards.net/random
.
Before we go further, I'd like to point out that I'm making this code structure more complicated than it needs to be (just to illustrate things for teaching purposes). There's no real reason here to have split the code into two packages.
In fact, overusing packages is a common mistake that newcomers to Go make. Generally you should only split code into additional packages if you have a demonstrable reason to, such as:
- You want a convenient way to reuse it the code, or to make it available for reuse.
- You want to isolate or enforce some boundary between the package code and the rest of your codebase.
- You have some complex code that acts as a 'black box' and moving it to a standalone package will reduce cognitive overhead when working with the rest of your code.
A more complex structure
Let's tweak the directory structure of our 'lucky number' code a bit more. We'll:
- Move the
main
package files into a newcmd/cli
directory. - Move the
random
package files into a newinternal/random
directory.
(Again, this is just for teaching purposes. This structure isn't actually necessary for such a small and simple application.)
Once that's done, let's update the cmd/cli/main.go
file so that the random
package is imported from its new location. Like so:
You should now be able to run the application by calling go run
with the path to the main
package. Like this:
This change helps to illustrate a couple of things:
It's not necessary for a
main
package to live in the module root. It can be anywhere. In fact, it's totally OK for a module to contain multiplemain
packages. For example, in a larger project you could have acmd/cli
directory with themain
package for a CLI tool, and acmd/web
directory with themain
package for a web application in the same module.Your non-
main
packages don't need to be a direct child of the module root either. They can be anywhere in an arbitrarily deep directory structure within the module.
Importing and using third-party packages
Let's quickly explore how to import and use a third-party package. As an example, we'll import the github.com/fatih/color
package and use it to change the color of the message that our application prints out — but the general process is exactly the same for most other third-party packages too.
First you need to download the third-party code from its public repository to your local machine, which you can do with go get
:
Notice that go get
will recursively download any dependencies that the code has too.
Then using the third-party package in your code is fairly straightforward. You'll need to import the third-party package using its module path (which should normally be the same as the repository location that you used when running go get
), and then access its exported things via its package name (which in most cases should be the same as the final element of the import path… if it is not, the documentation for the package should make that clear).
Let's head to our main.go
file and update the code to print a colored message using github.com/fatih/color
.
If you run the application again now, you should see a colorized message similar to this:
The go.mod
file should have been updated to include the dependencies that the lucky-number.alexedwards.net
module has too, along with their exact version numbers. It should look similar to this:
We can see that github.com/fatih/color
is listed as a direct dependency of the lucky-number.alexedwards.net
module, and the other dependencies are indirect (that is, our code doesn't import them directly, but they are imported by a package that our code imports).
As an aside, if you are ever looking at a go.mod
file and wondering why something is listed as a dependency you can use the $ go mod why
command. For example, if you wanted to find out why golang.org/x/sys
is an indirect dependency for the lucky-number.alexedwards.net
module you could run:
We can see from the output that our cmd/cli
package imports github.com/fatih/color
, which in turn imports github.com/mattn/go-isatty
, which in turn imports golang.org/x/sys/unix
.
Version 2+ packages
Sometimes the third-party packages that you want to use will be in modules with a major version number greater than 1 (like v2.0.0
, v3.4.5
etc).
In Go, it is conventional for modules with a major version number greater than 1 to append the major version number to their module path. A few popular real-life examples are:
Typically you will need to go get
these version 2+ packages using the full module path including the version number. Like this:
And then you need to also import them in your .go
files using the full module path (including the version number), but reference their exported things using the package name (which will normally now be the second-to-last element in the import path). For example:
Organizing import statements
Lastly, there's no right or wrong way to organize your import statements in Go. No single convention has really emerged in the Go community, so I recommend just picking something that works for you and being consistent with it.
Personally, I like to separate imports into four groups separated by an empty line. Like this:
Within each group, go fmt
will automatically sort the imports alphabetically for you. I like having aliased imports as a final standalone group because it helps to draws attention to them and highlight to the reader that 'there is something a little bit unusual going on here' with them.
As an illustration, here's an example from the main.go
file of a web application I was recently working on: