API Diff provides tools for detecting and analyzing API compatibility changes in Go packages and modules. It automatically classifies changes as either backward-compatible or incompatible (breaking changes), making it essential for semantic versioning and release management.
go get golang.org/x/exp/apidiff and go get golang.org/x/exp/cmd/apidiffimport (
"golang.org/x/exp/apidiff"
"go/types"
)package main
import (
"fmt"
"go/types"
"golang.org/x/exp/apidiff"
)
// Assuming you have old and new versions of a package loaded as types.Package
func main() {
// Compare two versions of the same package
oldPkg := loadOldPackage() // *types.Package
newPkg := loadNewPackage() // *types.Package
// Get the compatibility report
report := apidiff.Changes(oldPkg, newPkg)
// Check if all changes are compatible
if hasBreakingChanges(report) {
fmt.Println("Breaking changes detected - requires major version bump")
report.TextIncompatible(os.Stdout, true)
} else {
fmt.Println("All changes are backward-compatible")
report.TextCompatible(os.Stdout)
}
}
func hasBreakingChanges(report apidiff.Report) bool {
for _, change := range report.Changes {
if !change.Compatible {
return true
}
}
return false
}The apidiff package provides two primary analysis paths:
Changes() function to compare individual Go packagesModuleChanges() function to compare entire modules with multiple packagesThe package uses the Go types package to analyze type information and detect changes in:
Compares two versions of a package and produces a detailed report of API changes, classifying each change as either compatible or incompatible with semantic versioning.
func Changes(old, new *types.Package) ReportThe Changes function analyzes the differences between two package versions:
Report describing all detected changesEach change is classified based on Go's semantic versioning requirements:
Compares two complete modules and their contained packages, reporting on package additions, removals, and API changes within each package.
func ModuleChanges(old, new *Module) ReportThe ModuleChanges function performs module-level analysis:
Report describing all detected changes at module and package levelsThis is particularly useful for multi-package modules where entire packages may be added or removed.
Outputs a text summary of all detected changes.
func (r Report) String() stringReturns a concise string representation of the report suitable for logging or simple display.
Writes a formatted report of all changes to an io.Writer.
func (r Report) Text(w io.Writer) errorOutputs both compatible and incompatible changes in a human-readable format.
Writes only the compatible changes to an io.Writer.
func (r Report) TextCompatible(w io.Writer) errorWrites only the incompatible (breaking) changes to an io.Writer.
func (r Report) TextIncompatible(w io.Writer, withHeader bool) errorThe withHeader parameter controls whether to include a header section describing the breaking nature of the changes.
Represents a single API change with compatibility classification.
type Change struct {
Message string
Compatible bool
}Fields:
Message: A descriptive message explaining the change (e.g., "removed function Foo", "added method Bar to interface Baz")Compatible: A boolean indicating whether this change is backward-compatible (true) or breaking (false)A Change describes a single API modification. Examples of compatible changes include:
Examples of incompatible changes include:
Aggregates all API changes detected between two packages or modules.
type Report struct {
Changes []Change
}Fields:
Changes: A slice of all detected API changesA Report contains the complete analysis result from Changes() or ModuleChanges(). It provides multiple methods for examining and outputting the results.
Represents a Go module with its path and contained packages.
type Module struct {
Path string
Packages []*types.Package
}Fields:
Path: The module import path (e.g., "github.com/example/mymodule")Packages: A slice of all packages contained in the moduleThe Module type is a convenience wrapper that allows analyzing multiple packages together as a single logical unit. This is useful when:
package main
import (
"fmt"
"golang.org/x/exp/apidiff"
"go/build"
"go/parser"
"go/token"
"go/types"
"os"
)
func loadPackage(dir string) (*types.Package, error) {
// Parse Go source files
fset := token.NewFileSet()
astPkgs, err := parser.ParseDir(fset, dir, nil, 0)
if err != nil {
return nil, err
}
// Type check the package
var cfg types.Config
pkg, err := cfg.Check("", fset, getASTFiles(astPkgs), nil)
if err != nil {
return nil, err
}
return pkg, nil
}
func main() {
oldPkg, _ := loadPackage("./v1.0.0")
newPkg, _ := loadPackage("./v1.1.0")
report := apidiff.Changes(oldPkg, newPkg)
fmt.Printf("Total changes: %d\n", len(report.Changes))
for _, change := range report.Changes {
status := "compatible"
if !change.Compatible {
status = "breaking"
}
fmt.Printf("[%s] %s\n", status, change.Message)
}
}package main
import (
"fmt"
"golang.org/x/exp/apidiff"
"go/types"
)
func suggestVersion(oldVersion string, report apidiff.Report) string {
hasBreaking := false
hasAdditions := false
for _, change := range report.Changes {
if !change.Compatible {
hasBreaking = true
} else if isAddition(change.Message) {
hasAdditions = true
}
}
// Semantic versioning logic
if hasBreaking {
return "major version bump (X.0.0) required"
} else if hasAdditions {
return "minor version bump (x.Y.0) allowed"
} else {
return "patch version bump (x.y.Z) allowed"
}
}
func isAddition(msg string) bool {
return len(msg) > 0 && msg[0:3] == "add"
}package main
import (
"fmt"
"golang.org/x/exp/apidiff"
"go/types"
)
func compareModules(oldModPkgs, newModPkgs []*types.Package) {
oldMod := &apidiff.Module{
Path: "github.com/example/mymod",
Packages: oldModPkgs,
}
newMod := &apidiff.Module{
Path: "github.com/example/mymod",
Packages: newModPkgs,
}
report := apidiff.ModuleChanges(oldMod, newMod)
breakingCount := 0
compatibleCount := 0
for _, change := range report.Changes {
if change.Compatible {
compatibleCount++
} else {
breakingCount++
fmt.Printf("BREAKING: %s\n", change.Message)
}
}
fmt.Printf("\nSummary: %d breaking changes, %d compatible changes\n",
breakingCount, compatibleCount)
}package main
import (
"golang.org/x/exp/apidiff"
"os"
)
func categorizeChanges(report apidiff.Report) {
removals := []apidiff.Change{}
additions := []apidiff.Change{}
modifications := []apidiff.Change{}
for _, change := range report.Changes {
if contains(change.Message, "removed") {
removals = append(removals, change)
} else if contains(change.Message, "added") {
additions = append(additions, change)
} else {
modifications = append(modifications, change)
}
}
// Output categorized results
report.TextIncompatible(os.Stdout, true)
report.TextCompatible(os.Stdout)
}
func contains(s, substr string) bool {
return len(s) >= len(substr)
}The cmd/apidiff tool provides a command-line interface for comparing package versions without writing Go code.
The apidiff command determines whether two versions of a package are compatible according to semantic versioning rules. It analyzes API changes and exits with different status codes based on compatibility.
go install golang.org/x/exp/cmd/apidiff@latest# Compare packages by import path
apidiff old.go new.go
# Compare using flags for old and new package versions
apidiff -old=github.com/example/pkg@v1.0.0 -new=github.com/example/pkg@v1.1.0The apidiff command accepts the following flags and arguments:
Positional arguments: Path(s) to Go source files or packages to compare
-old flag: Specify the old package (alternative to positional argument)
github.com/example/pkg@v1.0.0-new flag: Specify the new package (alternative to positional argument)
github.com/example/pkg@v1.1.0# Compare two local Go source files
apidiff old_package.go new_package.go
# Compare using go get for remote packages
apidiff -old=github.com/example/mylib@v1.0.0 \
-new=github.com/example/mylib@v1.1.0
# Check specific major versions
apidiff -old=github.com/example/mylib@v1.5.0 \
-new=github.com/example/mylib@v2.0.0
# Compare from git repository
apidiff -old=$(git show v1.0.0:package.go) \
-new=$(git show HEAD:package.go)The command outputs a human-readable report showing all detected changes:
Compatible:
added function NewHelper
added method Foo on type Bar
Incompatible:
removed function OldAPI
changed function signature: func Process(string) (int, error) -> func Process(ctx context.Context, string) (int, error)The apidiff command is commonly used in:
Example CI integration:
#!/bin/bash
# Check if changes are compatible with patch version
if apidiff -old=mylib@v1.2.3 -new=./; then
echo "Changes compatible with v1.2.4"
exit 0
else
echo "Breaking changes detected - cannot release patch version"
exit 1
fiThe package follows semantic versioning (semver) guidelines:
When using the apidiff package programmatically, errors may occur during:
The Report type never returns errors from comparison functions - compatibility analysis is always attempted. Use error handling when loading packages:
report := apidiff.Changes(oldPkg, newPkg) // No error return
// Use report.Changes regardless of what it containsThe apidiff package is often used alongside: