Not sure how to structure your Go web application?

My new book guides you through the start-to-finish build of a real world web application in Go — covering topics like how to structure your code, manage dependencies, create dynamic database-driven pages, and how to authenticate and authorize users securely.

Take a look!

An introduction to Packages, Imports and Modules in Go

Published on:

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:

  1. Packages
  2. The main package
  3. Importing and using standard library packages
  4. Unused and missing imports
  5. Exported vs unexported
  6. Modules
  7. Using multiple packages in your code
  8. Importing and using third-party packages
  9. 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:

$ mkdir lucky-number
$ cd lucky-number
$ touch main.go

Then add the following code to the main.go file:

File: main.go
package main

import (
    "fmt"
    "math/rand"
)

func main() {
    // Get a random number between 0 and 99 inclusive.
    n := rand.Intn(100)

    // Print it out.
    fmt.Printf("Your lucky number is %d!\n", n)
}

At this point you should be able to run the application and see some output like this:

$ go run main.go
Your lucky number is 12!

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 filename main.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:

$ touch random.go

Then update the two files so that the main() function calls a new randomNumber() function, like so:

File: random.go
package main

import (
    "math/rand"
)

func randomNumber() int {
    return rand.Intn(100)
}
File: main.go
package main

import (
    "fmt"
)

func main() {
    fmt.Printf("Your lucky number is %d!\n", randomNumber())
}

So now:

  • Our 'lucky number' application consists of two .go files.
  • Both files are part of the main package (because they both start with a package main statement).

If you re-run the application using the two files, you should see the same output.

$ go run *.go
Your lucky number is 23!

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:

$ go run *.go
function main is undeclared in the main package

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:

$ go run *.go
package command-line-arguments is not a main package

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:

File: random.go
package main

import (
    "math/rand"  // Import the math/rand package.
)

func randomNumber() int {
    return rand.Intn(100) // Call the Intn() function from the math/rand package.
}

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:

import (
    "fmt"
    "math/rand"         // Not "rand"
    "net/http"          // Not "http"
    "net/http/httptest" // Not "httptest"
)

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:

"os" imported and not used

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:

undefined: strconv

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:

var fooBaz string // This is an unexported variable.
var FooBar string // This is an exported variable.

func fooBaz() {...} // This is an unexported function.
func FooBar() {...} // This is an exported function.

type fooBaz struct {...} // This is an unexported type.
type FooBar struct {...} // This is an exported type.

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:

$ go mod init lucky-number.alexedwards.net
go: creating new go.mod: module lucky-number.alexedwards.net
go: to add module requirements and sums:
    go mod tidy

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:

File: go.mod
module lucky-number.alexedwards.net

go 1.20

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 our main.go and random.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.

$ rm random.go
$ mkdir random
$ touch random/number.go

The file tree for the lucky-number directory should now look like this:

$ tree --dirsfirst
.
├── random
│   └── number.go
├── go.mod
└── main.go

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:

File: random/number.go
package random

import (
    "math/rand"
)

func Number() int {
    return rand.Intn(100)
}

There are four things I'd like to quickly highlight and re-iterate here:

  • The number.go file is part of the random 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 the random package.
  • The directory name that the code lives in is exactly the same as the package name (random) .
  • The random package is part of the lucky-number.alexedwards.net module.

Next let's update our main.go file to import and use the new package. Like so:

File: main.go
package main

import (
    "fmt"

    // Import the random package.
    "lucky-number.alexedwards.net/random"
)

func main() {
    // Call the random.Number() function to get the random number. Notice that
    // we use the package name as the accessor, just like we do for the standard 
    // library packages.
    fmt.Printf("Your lucky number is %d!\n", random.Number())
}

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:

import {module path}/{path to the package relative to your go.mod file}

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:

  1. You want a convenient way to reuse it the code, or to make it available for reuse.
  2. You want to isolate or enforce some boundary between the package code and the rest of your codebase.
  3. 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 new cmd/cli directory.
  • Move the random package files into a new internal/random directory.

(Again, this is just for teaching purposes. This structure isn't actually necessary for such a small and simple application.)

$ mkdir -p cmd/cli internal
$ mv main.go cmd/cli/
$ mv random internal/
$ tree --dirsfirst
.
├── cmd
│   └── cli
│       └── main.go
├── internal
│   └── random
│       └── number.go
└── go.mod

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:

File: cmd/cli/main.go
package main

import (
    "fmt"

    // Import the random package using the new location under the 
    // `internal` directory.
    "lucky-number.alexedwards.net/internal/random"
)

func main() {
    fmt.Printf("Your lucky number is %d!\n", random.Number())
}

You should now be able to run the application by calling go run with the path to the main package. Like this:

$ go run ./cmd/cli
Your lucky number is 51!

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 multiple main packages. For example, in a larger project you could have a cmd/cli directory with the main package for a CLI tool, and a cmd/web directory with the main 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:

$ go get github.com/fatih/color@latest
go: added github.com/fatih/color v1.14.1
go: added github.com/mattn/go-colorable v0.1.13
go: added github.com/mattn/go-isatty v0.0.17
go: added golang.org/x/sys v0.3.0

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.

File: cmd/cli/main.go
package main

import (  
    "lucky-number.alexedwards.net/internal/random"

    // Import the color package.
    "github.com/fatih/color"
)

func main() {
    // Use it to print the message in green.
    green := color.New(color.FgGreen)
    green.Printf("Your lucky number is %d!\n", random.Number())
}

If you run the application again now, you should see a colorized message similar to this:

$ go run ./cmd/cli
Your lucky number is 96!

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:

File: go.mod
module lucky-number.alexedwards.net

go 1.20

require github.com/fatih/color v1.14.1
            
require (
    github.com/mattn/go-colorable v0.1.13 // indirect
    github.com/mattn/go-isatty v0.0.17 // indirect
    golang.org/x/sys v0.3.0 // indirect
)

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:

$ go mod why -m golang.org/x/sys
# golang.org/x/sys
lucky-number.alexedwards.net/cmd/cli
github.com/fatih/color
github.com/mattn/go-isatty
golang.org/x/sys/unix

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:

github.com/go-chi/chi/v5
github.com/jackc/pgx/v5
github.com/go-playground/validator/v10

Typically you will need to go get these version 2+ packages using the full module path including the version number. Like this:

$ go get github.com/go-chi/chi/v5
go: added github.com/go-chi/chi/v5 v5.0.8

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:

import (
    "github.com/go-chi/chi/v5"
)

func main() {
    router := chi.NewRouter()
    ...
}

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:

import (
    {standard library packages}

    {packages from the current module}

    {third-party packages}

    {aliased packages}
)

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:

import (
    "fmt"
    "net/http"
    "os"
    "runtime/debug"
    "sync"

    "example.com/internal/logger"
    "example.com/internal/smtp"

    "github.com/go-playground/form/v4"
    "github.com/spf13/pflag"

    _ "github.com/mattn/go-sqlite3"
)