How to Import Packages in Go

Import Go packages using the import statement with the package path or module URL.

The missing piece in every Go file

You write a function that calculates a hash. It works perfectly in isolation. Then you try to call it from another file in the same folder. The compiler throws a fit. You realize Go does not automatically share code between files the way some languages do. You need to tell the compiler exactly where to look. That is what imports do. They are not just syntax. They are the contract between your code and the rest of the ecosystem.

How imports actually work

Go treats every directory as a potential package, but only directories explicitly declared as part of a module get shipped and resolved. An import path is a string that points to a location on disk or a remote repository. The Go toolchain uses that string to find the code, download it if necessary, compile it, and link it into your binary. Think of an import path like a postal address. You do not need to know who lives there or how they organize their house. You just need the exact street address to deliver your message. The toolchain handles the routing.

The compiler does not parse every file on your machine. It follows the import graph. When it sees an import statement, it resolves the path to a directory, reads the package declaration inside, and compiles only the exported symbols. Anything starting with a lowercase letter stays hidden. Anything starting with a capital letter becomes available to your code. This capitalization rule replaces traditional access modifiers. You do not write public or private. You just capitalize the first letter.

Standard library imports are free. Treat them as part of the language.

The minimal import

Here is the simplest import: one standard library package, one function call, one file.

package main

import "fmt" // standard library package for formatted I/O

func main() {
    fmt.Println("ready") // writes to stdout and adds a newline
}

The compiler reads the import "fmt" line and looks for the fmt package inside the standard library tree. It compiles fmt once, caches the compiled object, and links it into your main binary. You do not need to specify a version for standard library packages. They move with your Go installation. The go command automatically runs gofmt on your code if you use go fmt or your editor is configured to run it on save. gofmt will sort your imports alphabetically and group standard library, third-party, and local packages into distinct blocks. Do not argue about indentation or ordering. Let the tool decide.

Modules, paths, and the resolution pipeline

Real projects rarely stay in one file. You split code into packages, then you need external dependencies. The import string changes from a short name to a full URL-like path.

package main

import (
    "fmt"          // standard library for formatted output
    "net/http"     // standard library for HTTP servers
    "github.com/go-chi/chi/v5" // external router from GitHub
)

func main() {
    r := chi.NewMux() // creates a new router instance
    r.Get("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprint(w, "ok") // writes response body directly
    })
    http.ListenAndServe(":8080", r) // starts server on port 8080
}

The github.com/go-chi/chi/v5 path is not just a convention. It is the exact location the go command will fetch the code from. The v5 suffix tells the toolchain this package follows semantic versioning and requires Go modules. When you run go build, the toolchain checks your go.mod file. If the dependency is missing, it downloads it, verifies the checksum against go.sum, and compiles it. The import path in your source code must match the module path declared in the dependency's own go.mod. Mismatched paths cause immediate build failures.

The resolution pipeline runs in three steps. First, the toolchain checks the local module cache. If the exact version exists, it skips the network. Second, it queries the module proxy. The default proxy is proxy.golang.org, which caches every public module on GitHub, GitLab, and Bitbucket. Third, it falls back to direct VCS access if the proxy is disabled or the module is private. The go.sum file stores cryptographic hashes of every downloaded archive. The build refuses to proceed if a hash changes unexpectedly. This prevents supply chain attacks and guarantees reproducible builds.

The import path is a contract. Match it exactly or the build fails.

Common traps and compiler complaints

The compiler enforces strict rules around imports. Unused imports are not allowed. If you add "os" but never call os.Args, the build stops with imported and not used: os. This rule keeps binaries small and forces you to acknowledge every dependency. You can silence it by using the blank identifier _ when you only want side effects, like registering a database driver.

import _ "github.com/lib/pq" // registers the PostgreSQL driver without exposing symbols

Path mismatches are the most common build error. If you type github.com/user/repo but the repository's go.mod declares module github.com/user/repo/v2, the compiler rejects the file with no required module provides package github.com/user/repo. You must match the exact module path, including version suffixes. The toolchain does not guess. It reads the go.mod at the root of the repository and trusts it completely.

Another trap is assuming imports are lazy. Go compiles every imported package into your final binary. If you import a heavy analytics library, your executable grows even if you only call one function. The compiler does not strip unused code from third-party packages by default. You pay for what you import. If binary size matters, audit your import graph with go mod why to see exactly why each dependency exists.

Alias imports exist but should be used sparingly. You can rename a package at the import site when two packages export the same symbol or when the default name is confusing.

import (
    "fmt"
    json "encoding/json" // renames to avoid collision with a local json package
)

The community prefers keeping the original package name unless there is a direct conflict. Renaming breaks the mental model for other developers reading your code. Stick to the default name whenever possible.

Unused imports are not warnings. They are build blockers by design.

When to reach for what

Use a standard library import when the built-in package covers your need. Use a direct GitHub or GitLab path when you need a maintained third-party library. Use a blank identifier import when you only want package initialization side effects. Use a local relative path only during early prototyping, then switch to a proper module path before sharing the code. Use workspace imports when you are developing multiple related modules simultaneously. Use a module proxy when your team needs reproducible builds behind a corporate firewall. Use go mod tidy after changing imports to clean up stale dependencies.

Import paths are addresses, not suggestions. Verify them before you commit.

Where to go next