How to Deprecate a Function or Package in Go

Deprecate Go functions or packages by adding a // Deprecated: comment before the declaration to signal users to migrate to a newer alternative.

When the old way stops being the right way

You released a library six months ago. Developers are using ParseConfig to read their settings. You just realized ParseConfig silently drops unknown keys, which causes subtle bugs in production. You wrote a better function, ParseConfigStrict, but you can't just delete the old one. Your users' code will break. You need to tell them to switch without blowing up their builds.

Deprecation is how you warn the world that a piece of code is on its way out. It doesn't remove the code. It doesn't stop the code from compiling. It adds a loud, visible warning that says, "This still works, but you should stop using it." Go treats deprecation as a documentation convention that tooling respects. The mechanism is simple: a specific comment format. The impact is ecosystem-wide.

The syntax is a comment, not a keyword

Go doesn't have a @deprecated annotation or a special keyword. You deprecate a symbol by adding a comment immediately before its declaration. The comment must start with // Deprecated: followed by a space and a message. The message should explain why the symbol is deprecated and what to use instead.

Here's the simplest function deprecation:

// ParseConfig reads a config file and returns the settings.
//
// Deprecated: Use ParseConfigStrict instead. ParseConfig silently ignores unknown keys.
func ParseConfig(path string) (Config, error) {
    // Implementation remains unchanged.
    // Users can still call this function.
    return readFile(path)
}

The // Deprecated: prefix triggers the tooling. The rest of the comment is for humans. ParseConfigStrict is the replacement. The implementation stays exactly the same. The function runs identically. The only change is the signal you send to anyone reading the code or using the package.

What happens when you add the comment

Nothing changes in the binary. The compiler ignores the comment. The function generates the same machine code. The change happens in the developer experience.

go doc picks up the comment and displays it prominently. If you run go doc ParseConfig, the output includes the deprecation notice. Linters like staticcheck detect the comment and warn when code calls the deprecated function. The gopls language server shows a strikethrough on the symbol name in your editor and surfaces the warning in the diagnostics panel.

The compiler never rejects code that uses a deprecated function. Deprecation is a warning, not an error. This design choice is intentional. It allows libraries to signal changes without breaking downstream builds. Users can migrate at their own pace.

Convention aside: the Deprecated: prefix is case-sensitive. It must be capitalized. If you write // deprecated:, the tooling ignores it. The comment becomes invisible to go doc and linters. Always use the capital D.

Deprecating fields, variables, and constants

You can deprecate anything that has a declaration. Struct fields, variables, constants, and types all support the same comment format. The rule is the same: the comment must sit immediately before the declaration with no blank lines.

Here's how you deprecate a struct field and a constant:

// Config holds application settings.
type Config struct {
    // Timeout is the request timeout in seconds.
    //
    // Deprecated: Use TimeoutMs instead. Milliseconds are clearer and avoid ambiguity.
    Timeout int `json:"timeout"`

    // TimeoutMs is the request timeout in milliseconds.
    TimeoutMs int `json:"timeout_ms"`
}

// Deprecated: Use TypeReg instead. TypeRegA was a typo.
const TypeRegA = '\x00'

The struct field deprecation works exactly like the function. go doc Config shows the warning on the field. The JSON tag stays intact so existing serialization doesn't break. The constant deprecation follows the same pattern. The value remains accessible, but the tooling warns anyone who references it.

Deprecating an entire package

You can deprecate a whole package by adding the comment to the package documentation. The package doc comment is the first comment in the package, usually in doc.go or the main file. The // Deprecated: line goes inside that comment block.

Here's a package deprecation:

// Package oldutils contains legacy helpers for config parsing.
//
// Deprecated: Use the new utils package. oldutils will be removed in v2.0.0.
package oldutils

When users import oldutils, go doc shows the deprecation notice. Linters warn about the import. The package still compiles and runs. This is useful when you split a large package into smaller ones or rename a package to match a new naming convention.

Pitfalls and silent failures

The deprecation mechanism is fragile if you miss the formatting rules. The most common mistake is a blank line between the comment and the declaration.

// Deprecated: Use NewFunc instead.

func OldFunc() {
    // This deprecation is invisible to tooling.
}

If you put a blank line, go doc treats the comment as a regular paragraph, not a deprecation signal. The warning vanishes. Linters don't fire. Users never see the message. The compiler doesn't error on this. You have to catch it by reviewing the go doc output or running a linter that checks comment formatting.

Another pitfall is forgetting to mention the replacement. A comment that says // Deprecated: This is slow. gives users a reason to stop, but not a path forward. The community standard is to always name the replacement. // Deprecated: Use FastFunc instead. is the expected pattern.

Deprecation also requires discipline around versioning. You can deprecate a symbol in a minor version bump. Removing the symbol is a breaking change that requires a major version bump. If you deprecate in v1.2.0 and remove in v1.3.0, you've broken the contract. Users expect a grace period. The standard practice is to deprecate in one release cycle and remove in the next major version.

Tracking usage with a wrapper

Sometimes you need to know how many users are still calling the deprecated function. You can wrap the deprecated function to log usage or emit metrics. The wrapper keeps the old API stable while giving you visibility.

Here's a wrapper that logs calls:

// OldFunc calls NewFunc and logs a warning for metrics.
//
// Deprecated: Use NewFunc instead.
func OldFunc() {
    // Log usage to track migration progress.
    log.Println("OldFunc called: migration needed")
    
    // Delegate to the new implementation.
    NewFunc()
}

The wrapper preserves the deprecation comment. Users still see the warning. The log statement helps you measure adoption. You can remove the wrapper once the metrics drop to zero. This pattern is common in large codebases where you need data before you delete code.

Decision matrix

Use a // Deprecated: comment when you have a replacement ready and want to guide users to the new API over time.

Use a wrapper function when you need to track usage metrics or add runtime warnings while keeping the old API functional.

Use a blank identifier assignment when you are refactoring a package and need to keep a symbol exported to avoid breaking imports, even if the symbol does nothing.

Use a build tag to hide the deprecated code when you are preparing for a major version bump and want to clean up the surface area for internal testing.

Use a major version bump when the deprecation period is over and you are ready to remove the code entirely.

Where to go next

Deprecation is a promise. Keep the code working until you remove it. Tooling reads the comment. Users read the replacement.