Fix

"ambiguous import" in Go

Fix ambiguous import errors in Go by running go mod tidy to resolve conflicting package versions in your dependency tree.

The ghost in the dependency tree

You are building a command line tool. You add a new library for HTTP retries. You run go build and the compiler immediately stops. The terminal prints an error pointing to a package you never imported yourself. The message says the import is ambiguous. Two different versions of the same package path exist in your dependency tree, and Go refuses to guess which one you actually want.

This is not a bug in your code. It is a collision in the module graph. Go's build system enforces a strict rule: every import path must resolve to exactly one version in a given build. When the dependency resolver finds two versions for the same path, it halts compilation and asks you to make an explicit choice. The language prioritizes reproducible builds over convenience. If the graph is inconsistent, the build fails.

Go refuses to guess. You pick the version.

How Go resolves packages

Go modules use a deterministic algorithm called Minimal Version Selection. The resolver starts with your direct dependencies listed in go.mod. It then walks down the tree, collecting every transitive dependency. When two packages require different versions of the same third party, the algorithm picks the highest version that satisfies both. This guarantees that your project uses the newest compatible code without pulling in breaking changes unnecessarily.

Think of it like a construction site with a single blueprint. Every subcontractor must follow the same set of measurements. If one subcontractor brings a ruler marked in inches and another brings a ruler marked in centimeters, the foreman stops work. Go acts as that foreman. It checks every package path against the module graph and ensures only one version survives.

The resolver writes the final list of versions to go.mod. It also writes cryptographic hashes of every downloaded module to go.sum. The go.mod file declares what you want. The go.sum file proves what you actually got. Both files are committed to version control. This convention keeps builds identical across machines, CI runners, and team members.

The compiler trusts the graph, not your intentions.

A minimal conflict

Here is a simple scenario that triggers the error. You have two direct dependencies. Each one pulls a different version of a shared utility package.

// main.go
package main

import (
    "fmt"
    "github.com/example/client"
    "github.com/example/server"
)

// Run starts a basic client-server demo.
func Run() {
    // client requires github.com/example/shared v1.2.0
    // server requires github.com/example/shared v1.3.0
    fmt.Println("Starting demo")
}

When you run go mod tidy, the tool downloads both versions of github.com/example/shared. The resolver tries to pick the highest version that satisfies both constraints. If client explicitly requires v1.2.0 and server requires v1.3.0, the algorithm normally picks v1.3.0 because it is newer. But if client pins an exact version using @v1.2.0 in its own go.mod, or if you manually added conflicting require lines, the resolver cannot merge them.

The compiler rejects the program with ambiguous import: found package github.com/example/shared in multiple modules. The error message lists the two conflicting versions and their locations in the dependency tree. It does not suggest a fix. It expects you to adjust the graph.

You resolve this by running go mod tidy again after removing manual overrides, or by explicitly pinning the version you want in your own go.mod. The toolchain rebuilds the graph and updates go.sum. If the conflict remains, you have a direct contradiction in your requirements that must be edited by hand.

When the toolchain leaves you hanging

Manual edits are the most common source of lingering conflicts. Developers sometimes open go.mod and add a replace directive to point a dependency at a local fork. Later, they remove the fork but forget to delete the replace line. The module system now sees two paths for the same package: the remote version from the registry and the local path from the directive.

// go.mod
module example.com/myapp

go 1.21

require (
    github.com/example/shared v1.3.0
)

// This directive was added for local testing but never removed.
// It forces the resolver to treat the local path as a separate module.
replace github.com/example/shared => ../forks/shared

When the compiler builds the package list, it sees github.com/example/shared coming from the proxy and the same path coming from the replace directive. The resolver cannot merge them because they have different module roots. The build fails with the ambiguous import error.

The fix is straightforward. Remove the stale replace line. Run go mod tidy. The toolchain drops the local override, fetches the correct remote version, and rebuilds the graph. If you actually need the fork, keep the replace directive but ensure no other dependency pulls a different version of the same path. You can force alignment by adding an explicit require line that matches the fork's version.

Replace directives are surgical tools, not crutches.

Common traps and compiler feedback

Manual edits break the chain. Let the tool rebuild it.

The ambiguous import error usually appears alongside other module warnings. If you edit go.mod without running go mod tidy, the go.sum file falls out of sync. The compiler may complain with verifying module: checksum mismatch or go.sum: missing go.sum entry. These messages mean the cryptographic hashes no longer match the downloaded files. Run go mod tidy to regenerate the hashes. Never commit a go.mod file that does not match its go.sum file.

Another common trap is mixing go get commands with manual edits. go get package@version updates go.mod and go.sum automatically. If you then open the file and delete a line, the toolchain loses track of why that dependency was there. The next go mod tidy may drop it entirely, or it may leave a dangling reference that triggers the ambiguous import error. Stick to the command line for dependency changes. Use go mod why -m package to trace why a module exists in your graph. Use go list -m all to print the full resolved tree. These commands show you exactly which direct dependency pulled in a conflicting version.

The compiler also rejects builds when two modules claim the same package path but different major versions. Go modules use semantic versioning. A v2 release must change its import path to github.com/example/shared/v2. If a maintainer forgets to update the path, you get two modules with the same path but different version tags. The resolver treats them as the same package and throws the ambiguous import error. The fix here is to wait for the upstream maintainer to correct the path, or to use a replace directive pointing to a fixed fork.

Manual edits break the chain. Let the tool rebuild it.

Choosing your resolution path

Use go mod tidy when you want the tool to automatically prune unused dependencies and resolve version conflicts. Use go get package@version when you need to pin a specific release because a newer version broke your tests. Use a replace directive when you are actively developing a fork or need to apply a patch that has not been released yet. Use go mod why -m package when you need to trace why a specific module ended up in your dependency tree. Keep your go.mod clean and let the toolchain manage the graph.

Trust the module toolchain. Pin versions only when you have a reason.

Where to go next