-
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
types2: Generics allow creating values of unexported or internal types #56669
Comments
CC @golang/compiler |
You can also do this workaround using reflect:
|
True, the call still works using reflect. Hm. |
The handling of internal packages is "external" to the language, it's really the build system that prevents invalid internal imports. So I'd leave that out from the discussion. But as you say, it doesn't require an internal package. Here's a playground link for the "trick" and reflection-based scenario simply using an unexported type. It's always been possible to create invalid values of unexported types, without using generics, and without reflection, at least for basic types. Here's an (admittedly pathological) example which creates a zero value for an unexported numeric type because operations such as The upcoming I wonder if the zero value for a type is the only value that can be created this way (ignoring numeric types, where one probably can create a 1, and then all the values, using suitable operations).
I think we should probably fix the type inference issue: it should not be possible to infer a type that is not otherwise accessible in a package. We have carved out a provision in our backward-compatibility guarantees for errors in the generics implementation. |
If |
Good point about maps: one simply needs to access an element with a key that doesn't exist, using comma-ok form, and one gets a zero value. I don't follow your slices example: if the slice is not empty, all we can append are the element (or other) values that already exist. What am I missing? There's also the case of a channel with elements of unexported type. Once the channel is closed, one receives zero values. But in all cases a package defining the unexported type can chose to not export a map, channel, slice, or array with that unexported type as element type. |
We can append the values that already exist until for len(s) == cap(s) {
s = append(s[:cap(s)], s[0])
}
fmt.Printf("%#v\n", s[:cap(s)][cap(s)-1]) https://go.dev/play/p/QrXkeXqvMfn
I agree. But then |
Tricky! It seems that the spec doesn't even say that the values past the length of a slice are zero values in case of growth. Hm. (Filed #56684.) But yes - it looks like |
As a concrete impact, this does make it quite a bit easier to evade the constant string checks in github.com/google/safehtml.
There are other, less convenient, ways to elude these checks, so this isn't a tremendous problem; the goal of the package is to avoid accidental error, not defend against malice on the part of the programmer. Still, it might support a change to avoid inferring types that the instantiating package can't name. |
Will this code https://go.dev/play/p/ASrjalg4xdr continue to work? I mean such usage of type aliases was not exactly correct, but it worked even before generics. |
@DmitriyMV I think under a realistic interpretation of what we are talking about, no, that would be forbidden. What do you mean by "it worked even before generics"? The part that is in question (calling |
I guess you mean this? That's a good point and it may speak against this change. |
Sorry, I wasn't clear enough - what I meant is that you could have private types as part of the public one and currently you can work with them, provided that you can find a way to create their instances. You link is actually what I wanted to express, with a bit of modification. |
There is also a question how with this proposal generic code will handle this where you use exported alias for the private types. |
I think ideally, the same restriction would have applied since Go 1 to any inference mechanism. It didn't and it seems too late to change that. So, I guess either we 1. stay consistent and allow to infer with every inference mechanism, 2. stay consistent which might break more than we are willing to, or 3. are not consistent and allow inference with some mechanisms and forbid it with others (where "inference" means "of unexported/internal types"). FWIW for my specific use case I've now added another layer of abstraction: -- a/internal/internal.go --
func Unwrap[W ~struct{ e E }, E any](w W) E {
return struct{ e E }(w).e
}
type Storage struct {
e StorageInterface
}
func WrapStorage(e StorageInterface) Storage {
return Storage{e}
}
-- a/storage/sqlite.go --
func New(/*…*/) internal.Storage {
return internal.WrapStorage(&storage{/*…*/})
}
-- a/a.go --
type Server struct {
// Storage backend to use. Defaults to something kinda sensible.
Storage internal.Storage
}
func (s *Server) DoThing() {
storage := internal.Unwrap(s.Storage)
if storage == nil {
storage = defaultStorage()
}
} I think this should guarantee that only approved implementations can exist, foreign packages can't use any methods on it and that even if someone is trying to circumvent the mechanism, they can at best get a zero value, which means to use default. So, with this I feel comfortable using an interface that can be changed without breaking compat. |
Hm, I was almost expecting something like this to come up. TBQH, I'm feeling more and more that this is just a lost cause. I wasn't totally sure it's a problem we should solve from the beginning. |
Change https://go.dev/cl/451220 mentions this issue: |
@DmitriyMV At the moment, your example would infer the unexported type |
I believe this a bug - it was never the intent that type inference would provide this new capability to the language. From the original Type Parameters Proposal, which outlines the generic design:
In other words, any code that we can write using type inference, we should be able to write without, by explicitly providing all type arguments. |
In general, exportedness of an identifier is only relevant to type-checking to restrict what identifiers a user can directly type in Go source. For example, today we allow:
In contrast, the only cases where we care about whether an identifier is exported today is in selector expressions (including qualified identifiers) and struct literals (where initializing an unexported field is disallowed, even in key-less literal notation). Also, as @randall77 pointed out earlier, the main motivating example for this (google/safehtml) can be trivially circumvented with reflection too. So I'm at least not convinced that restricting inferred type arguments is necessary. I think it's more consistent with the rest of the language that we continue to allow inferring unexported types, even when users can't spell them directly as type arguments. Finally, CL 451220 doesn't address the original issue report here: that an exported type from an internal package can be inferred. So there are still type arguments that can be inferred, but cannot be directly written in source. |
Agreed. Combine this with the explicitly stated intent that type inference is a convenience feature, and that means that we should not rely on type inference to find such unexported types. If we can't write the code w/o type inference, we shouldn't be able to call type inference to the rescue. Yes, reflection may be used in some cases, but I don't think that's a sufficiently strong argument. Reflection can be used to circumvent various language protections (and so is the package Internal packages are a language-external concept. There's simply not much we can do about them here. The language doesn't talk about the build system. |
FWIW the more I play with CL, the more I don't think this change is worth it. We already have numerous ways to circumvent visibility rules, starting from Also, as I noted in CL, this change will break legitimate code with aliases. We export types using aliases in tests, and that would mean that we would have to rewrite them. |
Sorry, have been discussing this on the CL while somehow missing the discussion here.
I actually think that this is a significant problem that should at least block this change until the compiler has better support for aliases. With this change, the refactoring |
Fair enough. I agree that the alias issue is a problem - even though unrelated - but it compounds this issue. Still, if we had raised an error starting with Go 1.18 when unexported types were inferred, such code would not have been written. And we can renege on the backward compatibility promise for design errors with generics. In contrast to the other features @mdempsky is reporting, type inference explicitly was considered a convenience feature. |
But if we allow type inference to continue inferring types from internal packages, then this issue remains present. As you mention, internal packages are technically a build system detail, not a language-level detail; but the majority of Go programmers only know one build system. As I see it, there are 3 options here:
Personally, I think (1) is the most pragmatic decision. It's the least effort to implement, the easiest to explain, and it doesn't risk breaking any working code. |
We should make a final decision here - most likely that we can't change things. But not urgent for 1.22. Moving along. |
I'm not sure if this is a bug or should be a proposal, or if it is just a curiosity. But I noticed a thing and I think it's weird.
Say
package a
wants to make sure that of some type, only well-controlled values are ever used (in my use case, its an interface which I want to share between packages but be able to change without breaking compatibility). It uses an internal package to do so:As
a/internal
cannot be imported, there is no way to get aninternal.X
that isn't sanctioned bya
.reflect
can be used to construct anany
with dynamic typeinternal.X
, but that can't be type-asserted, so it can't be passed toa.F
.However, using generics, we can do this trick:
The same also applies if the type is not from an internal package, but an unexported type. I suspect a similar issue would apply to #21498 (there is currently no way for a different package to write a
func(unexported)
, but #21498 would allow it by spelling it(x) => { … }
).I'm not sure how important it is to prevent this. But I don't think this is how it should work. I think if an inferred type argument is from an internal package or is a defined type with unexported name from a different package, it should fail. But doing that would technically be a breaking change.
Just thought I'd bring this up, at least.
The text was updated successfully, but these errors were encountered: