As a package maintainer, how should deprecation be done? (interfaces)

Let’s say I have created a public library/package for use by anyone. In this library, I export a service like so.

package myLib

type LibService struct {
}

func (l LibService) GetThingName(id string) string {
  ... Some implementation ...
}

As a user of this library, I should define my own interfaces (as recommended here) to reduce coupling with the dependency, facilitate easier mocking for tests, etc.

import "codeplace.com/someone/myLib"
import "codeplace.com/someone/otherpackage"

package myProject

type ThingNameGetter interface {
  GetThingName(id string) string
}

func MyProjectFunction(getter ThingNameGetter) string {
  return getter.GetThingName("my id")
}

Now, after some time, the conceptual domain of the library changes in such a way that the GetThingName function is no longer sufficient. I would love to make the change below and have all of my library consumers know about the deprecation.

// Deprecated: Use GetNamespacedThingName
// This function will start panicking on Dec 1, 20XX
func (l LibService) GetThingName(id string) string {
  ... Some implementation ...
}

func (l LibService) GetNamespacedThingName(
  namespace string, id string,
) string {
  ... Some implementation ...
}

The problem is: because myProject has defined its own interfaces, they will not see the deprecation warning highlighted in their IDE (I don’t think lint will catch it either).

So they will not have time to react to the upcoming function removal. Instead, it will just suddenly “happen” on Dec 1, 20XX and they will have to scramble to update their usages.

What is the correct way to approach deprecation when consumers define their own interfaces?

Hi @Brad_Johnson,

Two thoughts:

1 - " I should define my own interfaces (as recommended here) to reduce coupling with the dependency"

The doc behind the link says that interfaces should be created by the client of a package and not by the provider of a package. It does not seem to say (correct me if I am wrong) that clients should always create interfaces when using a library.

You can create an interface when you need it, e.g. for mocking. (Not all tests need to mock a library’s API.)
Or when you have a reason for decoupling, for example, if there is a choice between similar packages that do the same thing, and you want to be able to switch easily.

Preemptively creating interfaces before a tangible need exists might be overkill because YAGNI.

Having said that, even when a client wraps an interface around a package function, the dependency from the package is not gone. At one point, the interface must be implemented, and if that implementation uses the deprecated package function, the deprecation message should not go unnoticed.

2 - Deprecation and breaking client code

When a function is removed from a package API, this is a breaking change and requires updating the major version, to stay in line with Semantic Versioning principles and the versioning rules of Go Modules.

Clients would have to intentionally update their dependencies (and also change the import paths accordingly) if they want to use a higher major version of a module. If they don’t, they continue to use the old version that still contains the deprecated function, and nothing would change for them.

Which raises an interesting question - do the semantic versioning rules for modules deprecate the //Deprecated: comment for functions? There would be little need for function-level deprecation messages if a breaking change requires a new major version anyway.

What Christoph Berger said: If your project is on, for example, version 3.5.2 and you want to deprecate a function, you can’t deprecate it until 4.0.0. If users depend on version 3, then they won’t lose that functionality. When they consciously upgrade their dependency on your project up to version 4, they’ll see the errors at that time.

I don’t know what the best practices are, but I think it would make sense to add the deprecated comment to your functions in, e.g., version 3 of your project as long as you don’t actually remove them until v4.

To confirm. Are you suggesting that deprecating a function (not removing it) is a breaking change?

I kind of like that approach, although it hinges on the person updating the library to look for usages of the function. On a multi-person project, they might just do the update and think “we can worry about that later” and then the function disappears later - albeit with yet-another major version update.

Deprecation is exactly done to inform users about a planned removal before the major/breaking release…

1.0.0 has a function Foo, then it is later considered that the usage of that function causes massive slowdowns and instead BetterFoo should be used, which requires an additional argument to even be able to do its work so mcuh quicker. So with introduction of BetterFoo Foo gets deprecated in 1.1.0. Still Foo is not allowed to get removed before 2.0.0 when we assume semver.

1 Like

What Norbert said: I think that adding the deprecation warning is OK in the same major version, but you can’t actually remove the deprecated function until the next major version.

This still means that if someone using your package just updates their version dependency to @latest or whatever the next major version is, their code will suddenly break without them ever knowing about it if they’re using that code through an interface like you said. That being said, that’s their fault. When a package’s major version is incremented it means that a breaking API change was introduced. I cannot depend on a package and then increment that dependency’s major version to the next major version of the package and expect things to just work.

It is my understanding that that is the same reason adding generics to Go just increments the minor version number from 1.17 to 1.18: Generics were added in a backwards-compatible way. If adding generics broke the existing pre-generics code, then it would be Go 2.0.

Yeah semantic versioning practises are not the problem I’m struggling with here. It’s the fact that dependency inversion (and library clients defining their own interfaces) seems to be encouraged by the language. Yet, using that feature and code style seems to render the deprecated tag pretty low-value.

One solution I’ve bumped into online is to deprecate the struct in the library and export a different struct that excludes the old functions that are no longer supported.

With this solution, client-defined interfaces can continue to be used and deprecations can be seen by the clients (where they instantiate the struct).

Only problem is that it’s a bit more tedious and… well… unusual.