or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

apidiff.mdconstraints.mdebnf.mderrors.mdevent.mdgorelease.mdindex.mdio-i2c.mdio-spi.mdjsonrpc2.mdmaps.mdmmap.mdmodgraphviz.mdrand.mdshiny.mdslices.mdslog.mdstats.mdsumdb.mdtrace.mdtxtar.mdtypeparams.mdutf8string.md
tile.json

apidiff.mddocs/

API Diff

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.

Package Information

  • Package Name: apidiff
  • Package Type: Go
  • Language: Go
  • Import Path: golang.org/x/exp/apidiff
  • Installation: go get golang.org/x/exp/apidiff and go get golang.org/x/exp/cmd/apidiff

Core Imports

import (
    "golang.org/x/exp/apidiff"
    "go/types"
)

Basic Usage

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
}

Architecture

The apidiff package provides two primary analysis paths:

  1. Package-level analysis - Using Changes() function to compare individual Go packages
  2. Module-level analysis - Using ModuleChanges() function to compare entire modules with multiple packages

The package uses the Go types package to analyze type information and detect changes in:

  • Function signatures
  • Type definitions
  • Interface methods
  • Exported symbols
  • Package structure

Capabilities

Analyzing Package Compatibility

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) Report

The Changes function analyzes the differences between two package versions:

  • old: The baseline package version (typically a previous release)
  • new: The current package version to compare
  • Returns: A Report describing all detected changes

Each change is classified based on Go's semantic versioning requirements:

  • Compatible changes (minor version bump allowed)
  • Incompatible changes (major version bump required)

Analyzing Module Compatibility

Compares two complete modules and their contained packages, reporting on package additions, removals, and API changes within each package.

func ModuleChanges(old, new *Module) Report

The ModuleChanges function performs module-level analysis:

  • old: The baseline module version
  • new: The current module version to compare
  • Returns: A Report describing all detected changes at module and package levels

This is particularly useful for multi-package modules where entire packages may be added or removed.

Generating Text Output

Outputs a text summary of all detected changes.

func (r Report) String() string

Returns a concise string representation of the report suitable for logging or simple display.

Writing Formatted Change Report

Writes a formatted report of all changes to an io.Writer.

func (r Report) Text(w io.Writer) error

Outputs both compatible and incompatible changes in a human-readable format.

Writing Compatible Changes Only

Writes only the compatible changes to an io.Writer.

func (r Report) TextCompatible(w io.Writer) error

Writing Incompatible Changes with Optional Header

Writes only the incompatible (breaking) changes to an io.Writer.

func (r Report) TextIncompatible(w io.Writer, withHeader bool) error

The withHeader parameter controls whether to include a header section describing the breaking nature of the changes.

Types

Change

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:

  • Adding a new exported function or type
  • Adding a new method to a type (if not part of an interface)
  • Adding a new field to a struct (if not embedded)

Examples of incompatible changes include:

  • Removing an exported function or type
  • Changing a function signature
  • Removing a method
  • Changing a type definition in a breaking way

Report

Aggregates all API changes detected between two packages or modules.

type Report struct {
    Changes []Change
}

Fields:

  • Changes: A slice of all detected API changes

A Report contains the complete analysis result from Changes() or ModuleChanges(). It provides multiple methods for examining and outputting the results.

Module

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 module

The Module type is a convenience wrapper that allows analyzing multiple packages together as a single logical unit. This is useful when:

  • Checking for package additions or removals
  • Analyzing interdependencies between packages in a module
  • Generating comprehensive release notes for a module

Usage Examples

Basic Package Comparison

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)
    }
}

Determining Release Version

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"
}

Module-Level Analysis

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)
}

Filtering and Categorizing Changes

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)
}

Command-Line Tool: apidiff

The cmd/apidiff tool provides a command-line interface for comparing package versions without writing Go code.

Overview

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.

Installation

go install golang.org/x/exp/cmd/apidiff@latest

Basic Usage

# 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.0

Command-Line Options

The apidiff command accepts the following flags and arguments:

  • Positional arguments: Path(s) to Go source files or packages to compare

    • First argument: old/baseline version
    • Second argument: new/current version
  • -old flag: Specify the old package (alternative to positional argument)

    • Accepts import path with version: github.com/example/pkg@v1.0.0
    • Accepts local file or directory path
  • -new flag: Specify the new package (alternative to positional argument)

    • Accepts import path with version: github.com/example/pkg@v1.1.0
    • Accepts local file or directory path

Exit Codes

  • 0: All changes are backward-compatible (compatible release)
  • 1: Breaking changes detected (incompatible release)
  • 2: Error in processing (invalid input, cannot fetch version, etc.)

Example Usage

# 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)

Output

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)

Integration with Release Workflows

The apidiff command is commonly used in:

  1. CI/CD Pipelines: Automatically check if proposed version bumps are consistent with API changes
  2. Pull Request Checks: Verify that code changes are appropriately classified
  3. Release Management: Validate semantic versioning before publishing a release
  4. Breaking Change Detection: Prevent accidental breaking changes from being released

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
fi

Compatibility Considerations

What Constitutes a Compatible Change

  • Adding new exported functions or types
  • Adding new methods to types
  • Adding new fields to structs (in some contexts)
  • Extending an interface with new methods (not recommended for interface types)

What Constitutes a Breaking Change

  • Removing exported functions, types, or methods
  • Changing function signatures
  • Changing type definitions
  • Removing struct fields
  • Changing method receiver types
  • Modifying interface method signatures

Semantic Versioning Alignment

The package follows semantic versioning (semver) guidelines:

  • MAJOR version: Incompatible API changes
  • MINOR version: Backward-compatible functionality additions
  • PATCH version: Backward-compatible bug fixes

Error Handling

When using the apidiff package programmatically, errors may occur during:

  1. Package loading: Ensure packages are correctly formatted and accessible
  2. Type checking: Verify Go source files are syntactically valid
  3. Comparison: Packages must be of compatible types

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 contains

Related Tools

The apidiff package is often used alongside:

  • golang.org/x/exp/cmd/gorelease: Validates semantic versioning decisions
  • go test: Running compatibility tests before releases
  • go/types: Core type analysis for Go packages
  • golang.org/x/tools/go/loader: Loading and type-checking Go packages