Import declarations in Go

Michał Łowicki
golangspec
6 min readJan 15, 2018

--

Programs in Go are made up of packages. Usually package depends on other packages either those built-in into the standard library or 3rd parties. Package needs to be imported first to use its exported identifiers. It’s done with construct called import declaration:

package mainimport (
"fmt"
"math"
)
func main() {
fmt.Println(math.Exp2(10)) // 1024
}

Above we’ve one import declaration with two ImportSpecs. Each ImportSpec defines single package import.

Package called main is used to create executable binary. Program execution starts in package main by calling its function which also called main.

But… there are other, less known options which are useful in various scenarios:

import (
"math"
m "math"
. "math"
_ "math"
)

Each of these four import specifications behave differently and in this post we’ll analyse those differences.

Importing package can reference only exported identifiers from imported package. Exported identifiers are the ones started with Unicode upper case letter — https://golang.org/ref/spec#Exported_identifiers.

The basics

Anatomy of import declaration

ImportDeclaration = "import" ImportSpec
ImportSpec = [ "." | "_" | Identifier ] ImportPath
  • Identifier is any valid identifier which will be used in qualified identifiers
  • ImportPath is string literal (raw or interpreted)

Let’s see some examples:

import . "fmt"
import _ "io"
import log "github.com/sirupsen/logrus"
import m "math"

Factored import declaration

Importing two or more packages can be written in two ways. Either we can write multiple import declarations:

import "io"
import "bufio"

or we can use factored import declaration (using multiple ImportSpec within a single import declaration):

import (
"io"
"bufio"
)

2nd option is especially useful if package has many imports and then repeating import keyword many times decreases readability. It also saves some keystrokes if you don’t use tools like https://github.com/bradfitz/goimports which fix imports automatically.

(Short) import path

String literal used in import specification (each import declaration contains one or more import specification) tells what package to import. This string is called import path. According to language spec it depends on the implementation how import path (string) is interpreted but in real life it’s path relative package’s vendor directory or `go env GOPATH`/src (more about GOPATH).

Built-in packages are imported with short import paths like "math" or "fmt".

Anatomy of .go file

Structure of every .go file is the same. First is package clause optionally preceded with comments usually describing the purpose of the package. Then zero or more import declarations. 3rd section contains zero or more top-level declarations (source code):

// description...
package main // package clause
// zero or more import declarations
import (
"fmt"
"strings"
)
import "strconv"// top-level declarationsfunc main() {
fmt.Println(strings.Repeat(strconv.FormatInt(15, 16), 5))
}

Enforced organisation doesn’t allow to introduce unnecessary mess which simplifies parsing and basically navigation through the codebase (import declarations cannot be placed before package clause nor be interleaved with top-level declarations so it’s always easy to find).

Scope of import

The scope of import is the file block. It means that it’s reachable from the whole file but not within the entire package:

// github.com/mlowicki/a/main.go
package main
import "fmt"func main() {
fmt.Println(a)
}
// github.com/mlowicki/a/foo.go
package main
var a int = 1func hi() {
fmt.Println("Hi!")
}

Such program cannot be compiled:

> go build
# github.com/mlowicki/a
./foo.go:6:2: undefined: fmt

More on scopes in one of previous posts:

Types of imports

Custom package name

By convention last component of import path is also name of imported package. Of course nothing stops us from not following that convention:

# github.com/mlowicki/main.go
package main
import (
"fmt"
"github.com/mlowicki/b"
)
func main() {
fmt.Println(c.B)
}
# github.com/mlowicki/b/b.go
package c
var B = "b"

Output is simply b. Although possible, following the convention is usually better — various tools depend on it.

If custom package name is not specified in import specification then name from package clause is used to reference exported identifiers from imported package:

package mainimport "fmt"func main() {
fmt.Println("Hi!")
}

It’s possible to pass custom package name for import:

# github.com/mlowicki/b/b.go
package b
var B = "b"package mainimport (
"fmt"
c "github.com/mlowicki/b"
)
func main() {
fmt.Println(c.B)
}

And the result is the same as before. This form of import can be useful if we’ve package which has the same interface (exported identifiers) as other package. One such example is https://github.com/sirupsen/logrus which has an API compatible with log:

import log "github.com/sirupsen/logrus"

If we’ll use only API found in built-in log package then replacing such import with import "log" doesn’t require any changes in the source code. It’s also a bit shorter (but still meaningful) so might save some keystrokes.

All exported identifiers into importing block

With imports like:

import m "math"
import "fmt"

it’s possible to either reference exported identifier with package name passed in import specification (m.Exp) or with the name from the package clause of imported package (fmt.Println). There is another option which allows to access exported identifier without qualified identifier:

package mainimport (
"fmt"
. "math"
)
func main() {
fmt.Println(Exp2(6)) // 64
}

When might it be useful? In tests. Let’s suppose that that we’ve package a which is imported by package b. Now we want to add tests to package a. If tests will be also in package a and tests will also import package b (because then need something implemented there) then we’ll end up with circular dependency which is forbidden. One way to bypass that is to put tests into separate package like a_tests. Then we need to import package a and reference every exported identifier with qualified identifier. To make our life easier we can import package a with dot :

import . "a"

and then reference exported identifiers from package a without package name (just like when tests were in the same package but non-exported identifiers aren’t accessible).

It’s impossible to import two packages using dot as package name if they’ve at least one exported identifier in common:

# github.com/mlowicki/c
package c
var V = "c"# github.com/mlowkci/b
package b
var V = "b"# github.com/mlowicki/a
package main
import (
"fmt"
. "github.com/mlowicki/b"
. "github.com/mlowicki/c"
)
func main() {
fmt.Println(V)
}
> go run main.go
# command-line-arguments
./main.go:6:2: V redeclared during import "github.com/mlowicki/c"
previous declaration during import "github.com/mlowicki/b"
./main.go:6:2: imported and not used: "github.com/mlowicki/c"

Import with blank identifier

Golang’s compiler will yell if package is imported and not used (source code):

package mainimport "fmt"func main() {}

Import with dot where all exported identifiers are added directly into importing file block fails as well while building — source code. The only variant which works is the one with blank identifier. It’s required to know what init functions are in order to understand why do we need to have import with blank identifier. Throughout introduction to init functions has been covered in one of previous stories:

I encourage to read it from top to bottom but in essence import like:

import _ "math"

doesn’t require to use package math in importing file but init function(s) from imported package will be executed anyway (package and it dependencies will be initialized). It’s useful if we’re interested only in bootstrapping work done by imported package but we don’t reference any exported identifiers from it.

Compiling will fail if package is imported without blank identifier and not used at all.

Circular import

Go specification explicitly forbids circular imports — when package imports itself indirectly. The most obvious case is when package a imports package b and package b in turn imports package a:

# github.com/mlowicki/a/main.go
package a
import "github.com/mlowicki/b"var A = b.B# github.com/mlowicki/b/main.go
package b
import "github.com/mlowicki/a"var B = a.A

An attempt to build any of these two packages ends up with an error:

> go build
can't load package: import cycle not allowed
package github.com/mlowicki/a
imports github.com/mlowicki/b
imports github.com/mlowicki/a

Of course it can be more complex scenario like a → b → c → d → a where xy means that package x imports package y.

Package cannot import itself neither:

package mainimport (
"fmt"
"github.com/mlowicki/a"
)
var A = "a"func main() {
fmt.Println(a.A)
}

Compiling such package also gives an error: can’t load package: import cycle not allowed.

Clap 👏👏👏 below to help others discover this story. Please follow me here or on Twitter if you want to get updates about new posts or boost work on future stories.

--

--

Michał Łowicki
golangspec

Software engineer at Datadog, previously at Facebook and Opera, never satisfied.