-
Notifications
You must be signed in to change notification settings - Fork 17.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: add mechanism to remove API as a function of caller's go.mod "go 1.xx" version #30639
Comments
I don't think this is a problem that actually needs to be solved for package-level identifiers (types, constants, and functions). For those, we still have to provide an implementation anyway, and given that we have that implementation, we may as well just add a The interesting problem, then, is how to remove methods, because they aren't necessarily visible at the point where they are used. Suppose I have: package mine
type Thing struct{ […] }
// Deprecated: use NewThing instead.
func (t *Thing) OldThing() {}
func (t *Thing) NewThing() {} package mediator
import (
[…]
)
func […] {
// This looks like it avoids the deprecated OldThing method...
var t theirs.NewThinger = new(mine.Thing)
theirs.Oops(t)
} package theirs
type NewThinger interface { NewThing() }
func Oops(t NewThinger) {
if x, ok := t.(interface { OldThing() }); ok {
// ...but we would need whole-program analysis
// to realize that OldThing is still in use.
x.OldThing()
} else {
t.NewThing()
}
} |
I have some ideas about how to address this, but I don't have the time to write them up this week. 😕 |
@bcmills Any ideas on mechanism? Thanks. |
If we use our standard fallback of a magic comment, then it could look like:
This means that
The type The magic comment could be used with package scope types, consts, variables, functions, and methods, and also with fields of struct types and methods of interface types. It could be used to cover an entire set of types, consts, or variables.
In this example |
Yes, that's the thing that works for everything but methods — but it's also mostly redundant with The problem is, what happens if you do a type-assertion that checks for the removed method in a pre- |
Deprecated comments have no machine-readable structure, though. Parsing English sentences sounds super gross. |
Why is machine-readable structure important? Either way, the content is merely informational: it's nearly trivial to construct a “backdated” forwarding module to access the removed definitions. |
That's not true. The whole point of this bug is to remove access to symbols from people. It's not to write English at them. We can already write English at them in comments. |
The suggestion is that, if |
The Given that we don't want to take the breaking-change approach, I don't see why the defeat mechanism of “ignoring the linter” is all that much worse than “using a backdated trampoline module” — and the comment-and-lint approach has the advantage of already being in wide use. |
Thinking about the linting approach some more: we do have one interesting option, at least. We run For example: could be a |
Yes, but that's fine. We wouldn't do this until modules were on by default for enough time that most the community has go.mod files with a
We're talking about the language version, not the Go version, though, which that comment doesn't make clear, if that was your intention. That's why the directive Ian wrote above said |
Right; my point is that it also wouldn't affect new code written with a backdated |
That's fine. That's the whole point, actually. This mechanism says: "You want to to play with the new toys? (e.g. generics) Then first stop using the old deprecated stuff. You can't have both. Choose." It's not about finding some fool-proof mechanism to stop people from accessing the symbols. (If so, we'd just delete them.) It's a casual nudge, and a way to let us remove godoc clutter. (godoc would stop showing these things, at least on golang.org's default view.) |
The point of this proposal is to permit breaking changes, without actually breaking existing code. It's adapting the same mechanism that we've decided to use to permit breaking changes in the language. |
Right. The problem is that — unlike language changes — the API check doesn't actually make them choose. A library cannot provide a language feature by definition, but it can easily provide access to a deprecated/removed part of a regular library API. If it's just a casual nudge either way, then it really seems like a refinement of the comment convention is the way to go — compiler support isn't needed. We can have |
In order to have vet warn about usage of names that we want to remove, we need a machine parseable comment that clearly says the language versions that should and should not support the name. I don't think we can just use our loosely specified "Deprecated" convention for this. We could use |
The // ClientConn is an artifact of Go's early HTTP implementation.
// It is low-level, old, and unused by Go's current HTTP stack.
// We should have deleted it before Go 1.
//
// Deprecated: Use Client or Transport in package net/http instead.
//go:lang < 1.14
type ClientConn struct { ... } Or would the The above is not compatible with the current godoc; it doesn't know to not include the All of the
I agree we can't just use it as it's currently defined:
But perhaps we can use it if it's modified. For example, the equivalent of // ClientConn is an artifact of Go's early HTTP implementation.
// It is low-level, old, and unused by Go's current HTTP stack.
// We should have deleted it before Go 1.
//
// Deprecated in 1.14: Use Client or Transport in package net/http instead.
type ClientConn struct { ... } There are existing tools that parse the current "Deprecated:" convention, so I suspect parsing "Deprecated in x.y:" may also be viable. Having just one place that to tell both humans and machines that an identifier is deprecated has the advantage that they can't fall out of sync. Edit: Something I realized just now is that this proposed convention is about removing an identifier, something that may happen after it's been deprecated, not at the same time. An identifier may be deprecated in 1.8, and removed in 1.14. In that case, "Deprecated in 1.14:" phrasing won't work. Maybe "Deprecated; removed in 1.14:" then. But that's not as clean. |
A thought: comments like |
Can't you add a v2 folder for that package and just let modules handle it? |
@docmerlin, the whole point of this proposal is to give us a lighter weight mechanism than the v2 hammer. Especially for the standard library, where v2 packages are proving to be very complicated. |
Thanks, @bradfitz |
If we provide a machine readable comment, we don't need an expression. We just need to specify the Go version that will no longer provide the functionality:
|
When compiling a program, the go tool can in principle know the language version of every module used in the program. If we let the comment, or whatever mechanism we use, affect the compiler, then we can pass a "minimally support version" to the compiler. In that case we don't need to compile the exported function/variable/type at all. This seems like a reason to use this in the tool rather than just in vet. We might have to always compile methods, even if they are marked. New packages wouldn't be able to refer to the methods directly, but perhaps they could via a type assertion. It's hard to see how to remove methods from the program while still keeping a consistent view of the type independent of language version of the modules in the program. |
It is not clear to me from the docs, is the module language version private to the module or subject to minimal version selection in the whole program (i.e., all modules are compiled with the minimal language version required by all packages)? From the discussion it seems it is the former, in which case it does seem that methods must remain. e.g., if a standard library type removes a method, but it is then accessed from a different module with a different version it could still be accessed via an interface. If it is the latter, then I wonder if keeping a consistent view of the type is something we need to require? There, this feature looks almost equivalent to syntactic sugar for redefining the entire type without removed fields/methods in a // +build go1.1n file. With the primary difference that this is based on the minimum version selection from go.mod rather than the toolchain version as used in the build tag. Since the whole program has the same view of the type, I'm not seeing where we'd run into problems. |
See #28221. IIRC, the module |
@prattmic The language version that appears in the go.mod file is specific to the module. Yes, this feature is similar to using a build tag with the Go release version. The difference is that the comment name is available for modules using older language versions, but not for modules using newer ones. |
Checking: does this mean that a name can only ever be used once, across all versions? Would this ever be valid (or desired)? package http
// Server is the completely overhauled net/http.Server.
type Server struct { ... }
// Server is the original net/http.Server.
//go:removedin 1.15
type Server struct { ... } This issue is mainly about removing/hiding things, but the description does say "let a package have different exports depending on who was using it." So perhaps there should be consideration for adding new things with previously removed/hidden names? |
@danp, see https://golang.org/design/28221-go2-transitions#language-redefinitions:
|
@bcmills, I think Dan is asking about stdlib redefinitions. The doc you linked pertains to the language spec. |
The same reasoning applies to both. If we are going to break the behavior of an existing program, we should surface that break as an error at compile time, not as unexpected behavior at run time. |
But there are a lot more public symbols in the stdlib than keywords & operators in the language spec. And most programs invoke a small fraction of the former. Reserving all of them for all time might be detrimental. |
Backward compatibility for a package means that we are in effect reserving any given name for as long as that version of the package exists. This doesn't seem so terrible to me; there are an infinite number of names available. If this becomes burdensome, it's likely time to start thinking about a v2 of the package. |
New syntax suggestion mostly from @bradfitz. Use a For example: //go:hide Function 1.15
//go:hide Type 1.15
//go:hide Constant 1.15
//go:hide Var 1.15
//go:hide Struct.Field 1.15
//go:hide Type.Method 1.15
//go:hide InterfaceType.Method 1.15 These comments do not remove the symbol. In particular, they do not remove methods, and types continue to work as before. What they remove is specific to the export data: if a package being compiled at language version 1.15 or earlier imports a package These comments do not change any aspect of compiling package This rule will not apply to test code within We could also add an optional additional argument that is a string that is included in the compiler error message. //go:hide Function 1.15 "Use NewFunction"
//go:hide Type 1.15 "Use package p2 instead"
//go:hide InsecureFunction 1.15 "InsecureFunction is a security hole" |
Nice, but maybe leading with the version is more readable over time...
|
I guess that depends on whether you'd like to group the "hide" directives by Go version, or by exported name. I think I'd do the latter, since they should be right next to the declaration itself. |
This does not seem to follow semver, specifically section 8. We are defining that removing a symbol and a go version upgrade do not constitute a backwards incompatible change. I find the implication that there is no compatibility guarantee between go versions deeply problematic and urge against this change. I already see people holding off on go version upgrades for other reasons, and this will enforce that preference. Technically Go and the stdlib do not follow semver today, so this may be fine. However I would ask, why is deprecation not enough? What is wrong with using v2 and following semver? If this impacts modules too, we are redefining their compatibility guarantees, and breaking semver. When would I ever need a v2, if I can just wait six months and sunset symbols with go 1.N+1? This will be confusing and seems to break diamond dependencies. What should I do if one module requires 1.N and another 1.N+1? Furthermore this is quite cancerous and will impair readability, as it will force dependent libraries to segment features with both these comments and build tags. Since, if i depend on a library, and they remove a future, I have to remove anything that was using it, but only for go 1.N+1. |
In a discussion on the Gophers Slack, @rogpeppe and @jayconrod pointed out that we already do have a machine-readable index of the API changes in each Go version, in Perhaps we could expand that format to also list deprecations? |
They also pointed out that it would probably make sense for That seems especially useful if the point-of-use is not itself guarded with a |
When reviewing a number of the Go2 issues, we keep finding that we're basically unable to clean up past mistakes.
On the language side, the go.mod file's "go 1.xxx" declaration lets users declare their expected Go language semantics, which lets us remove Go language features over time, but we have nothing equivalent for removing standard library symbols, short of simply making new major versions of all packages, which gets contagiously invasive very quickly with the dependencies between the std packages. For redesigns, new major versions works, but it doesn't work well for cleanups.
It would be nice to have a mechanism that's similar (but different) to
+build go1.x
build tags, but are added implicitly by the caller module module's expected language version.That'd let a package have different exports depending on who was using it.
Rather than propose an exact solution, this bug is more generally about tracking how (and whether) we can remove things from a package over time.
/cc @ianlancetaylor @griesemer @bcmills
The text was updated successfully, but these errors were encountered: