Skip to Content
Course content

169: Semantic Import Versioning Revisited

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this happen to plenty of developers who are used to how NPM or Cargo handle versions. They assume that once they run a "get" or "update" command, the project is now using the new version of the library. In Go, specifically with major versions, that's not how it works. If you don't change the import path, you're often still talking to the old version of the code, even if your go.mod says something different.

The phantom v1 dependency

Imagine you're using a hypothetical library called github.com/rob/calculator. You've been using v1 for months. The author releases v2, which changes the Add function from taking int to taking float64 to support decimals. You want that feature, so you run go get github.com/rob/calculator@v2.0.0.

Your go.mod now looks like this:

module my-app

go 1.21

require github.com/rob/calculator v2.0.0

But when you try to use the new functionality in your code, you hit a wall:

package main

import (
	"fmt"
	"github.com/rob/calculator" // Note the path here
)

func main() {
	// This should work in v2, but the compiler is screaming
	result := calculator.Add(10.5, 20.7) 
	fmt.Println(result)
}

The compiler gives you a confusing error: cannot use 10.5 (untyped float constant) as int value in argument to calculator.Add. You're thinking, "I just updated to v2! Why is it still expecting an integer?"

The reason is that Go treats github.com/rob/calculator and github.com/rob/calculator/v2 as two entirely different packages. By keeping the import path as the base URL, you've told Go you want the v1 (or v0) compatible version. Because of how Go's Minimal Version Selection (MVS) works, it's essentially ignoring the v2 code you downloaded because your source code isn't asking for it by name.

Explicitly versioning the import path

To actually use v2, you have to change the import path in every single file where that package is used. This is the "Semantic Import Versioning" rule: any major version from v2 onwards must include the version suffix in the path.

Here is the fix. We change the import to include /v2:

package main

import (
	"fmt"
	"github.com/rob/calculator/v2" // Added /v2 here
)

func main() {
	// Now the compiler sees the v2 signature: Add(float64, float64)
	result := calculator.Add(10.5, 20.7) 
	fmt.Println(result)
}

Once you change that import, you'll likely need to run go mod tidy. Go will realize that you are now explicitly requesting the v2 module, and it will align your go.mod and your imports.

I know it feels redundant to have the version in both the go.mod and the import statement, but this is a deliberate design choice. It allows a single binary to actually import both v1 and v2 of the same library simultaneously if you're in the middle of a complex migration. You can't do that if the import path is the same; the namespace would collide. By making the version part of the path, Go turns a versioning conflict into a simple naming difference.




📋 Practical Task

Migrating a Payment Processor to v3

You are maintaining a project that uses a payment library: github.com/finance/paygate. Currently, the project is using v2, and the code uses a function paygate.Process(amount int). The library has just been updated to v3, which introduces paygate.Process(amount float64, currency string).

Your Task:

  1. Assume your go.mod has already been updated to require github.com/finance/paygate v3.0.0.
  2. Below is a broken main.go file. Modify the import path and the function call to correctly utilize the v3 version of the library.
package main

import (
	"fmt"
	"github.com/finance/paygate"
)

func main() {
	// This needs to be updated for v3: Process(float64, string)
	err := paygate.Process(100) 
	if err != nil {
		fmt.Println("Payment failed")
	}
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.