-
Notifications
You must be signed in to change notification settings - Fork 17.8k
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: Go 2: require explicit type cast to create "nil receiver" interface #30294
Comments
Just to clarify what you mean, take the following code func main() {
var me *MyError // Implements error
var e error
e = me // Here e's type would still be converted to error(nil), rather than *MyError(nil)?
if e == nil {
// Under your proposal, this gets executed, and...
} else {
// ...this doesn't?
}
} This would work for me too. I can't really see a use of keeping track of the type of Anyone know of any cases where doing this sort of automatic type conversion would be problematic? Also, if we did this, and someone wanted the old behavior, how would you specify that? An advantage of this way is that it silently "fixes" most code to do what people expected anyway. A disadvantage is that if anyone was relying on the old behavior, this silently breaks it in a way that's 1) nearly impossible to detect programatically, and 2) difficult to restore to functional equivalence. As I said, I can't immediately see a use for the behavior so the last two points don't bother me. |
@target-san If you implement the
|
@gwd And yes, you're right in your example. Implicit conversion under this proposal will result in nil @go101 |
If this exists in a standard package, it must be a bug. Please report it. |
@go101 Is it? I may be not that knowledgeable in Go, but most other languages assume "receiver" (this, self etc.) is not nil unless type explicitly specifies otherwise. Could you please point to some recommendation like "always check receivers for nil inside methods"? |
@target-san Re nil receivers that don't need state: It seems like you could achieve the same effect by just having dummy state; passing back a type based on an int, for example, rather than a type with a nil pointer. @go101 The problem with your if display == nil {
// Render not-found template
} else {
// Pass 'display' to the appropriate template for rendering
} I'd changed one of the functions returning a display-compatible structure to return nil when I wanted that thing not visible to this particular user; but instead of showing the "not-found" template, it passed the typed nil pointer to the "display" template, resulting in the user seeing a template error. Nothing crashed, but it still did the wrong thing in a surprising way. |
@gwd Yes. This can be achieved by using some stateless static object. And that would be even better solution IMO. Though we have what we have. And what we have is implicit unchecked creation of broken interface pointers. |
@target-san @gwd The correct argument should be if you put a nil pointer in an interface value then call a method of the pointer, then you do it wrongly. This has no differences to calling the method on a the nil pointer directly. |
@go101 Unlike known type, you cannot check data pointer for nil reliably without arcane reflection incantations. Even if you do, you cannot be sure it's not a legit "nil receiver" interface without examining exact type and its implementation |
I think if you can't make sure whether or not an interface value is encapsulating a nil pointer, and you know calling a method of the nil pointer will panic, then you should never call the method on the interface value. How can this situation happen? |
That's exactly the problem I am talking about. You can get into this situation with a perfectly correct code. No arcane tricks are required. Just directly pass some result - if that result is a typed nil and destination is an interface which it supports. Check initial post and George's one for examples how you can get such "broken" pointer. Imagine you can create 0x1 pointer of some type without Unsafe package and even without explicit cast. You check it for nil - it's not. You invoke method on it - it panics. |
EDITED This is exactly what this issue is about. Suppose we have package Foo, which includes package Bar, and package Bar includes package Zot: type ZotError struct {
error string
}
func (ze *ZotError) Error() string {
return ze.error
}
func Zot(arg int) (*ZotError) {
// Do some stuff with arg, everything turns out fine, return success
return nil
}
func Bar() error {
if normal {
// Do some stuff
return nil
} else {
// Do some stuff for an excepitonal case that's hard to test
return Zot()
}
}
func Foo() error {
// Do thing 1
// Do thing 2
if err := Bar(); err == nil {
// Success
} else {
// Clean up
return fmt.Errorf("Error doing Bar: %v", err)
}
// Do thing 3
} In this situation, if the "excepitonal case" in If The only way for func Foo() {
if err := Bar(); reflect.ValueOf(err).Kind() == reflect.Ptr && reflect.ValueOf(err).IsNil() {
// Success
} else {
e := err.Error()
// Do something with the error string, dereference NULL
}
} But none of the guides for how to write good Go code say this; they all recommend |
The problem with the code is here:
It's generally bad practice to return a concrete error type. If you always return
I'd be more amenable to building a tool that detects this FAQ item than changing the language. |
Errors are just an example. Replace with any optional object which gets cast to interface. |
@randall77 Indeed that is bad practice given how nil interfaces work at the moment; but it's very far from obvious why. Humans are very poor at consistently following rules; every "do this for safety" rule we can get rid of will make Go a safer language. |
@randall77 Also, this statement isn't quite true:
The following is a pattern that a reasonable developer might use: // Tries all steps regardless of error; returns an error describing the first issue encountered
func Zot() error {
var ze *ZotError
// Do something
if error1 && ze != nil {
ze = &ZotError{error1}
}
if error2 && ze != nil {
ze = &ZotError{error2}
}
// and so on
return ze
} Here we've followed the advice to return |
@gwd |
@go101 |
@go101 You'll have to forgive me if that kind of argument makes me a bit angry; my main job involves writing in C, which is absolutely strewn with hidden landmines of undefined behavior that can make your perfectly normal-looking program suddenly full of security holes; and when you go to the compiler people and tell them that obvious-looking code results in bizarre behavior, they tell you you're just using the language wrong, you should fix your program. One of the things that made me so happy when I first learned Go was that there was no undefined behavior. All values are initialized by default; no conversions even from int32 to int64 without an explicit cast. The compiler even prevents you from using Go's behavior with regard to nil pointers and interfaces is a giant black mark on an otherwise very nice language. It's not only dangerous and unnecessary: it's completely out of character. |
There are really some interface related confusions when I started learning Go. However, when I realized that an interface value is just a box to encapsulate non-interface values which satisfy some requirements (have all the methods the interface specifies), I never get confused again. There are two categories of interfaces, blank ones and non-blank ones. A blank interface doesn't specify any methods, they are mainly used for reflection. A non-blank interface specifies several methods, they are mainly used for polymorphism.
I feel your frustration comes from some bad practices in using interfaces. If you want to return a nil pointer as a non-blank interface value (aka, encapsulate a nil value in an interface value), you should expect users of the return interface value will call the method specified by the interface and declared for the pointer type, then you should guarantee that calling the method on the nil pointer will not panic. This is your responsibility. Otherwise, just don't encapsulate the nil pointer in an interface value. I think the principle is simple and clear. BTW, Go does have some undefined behaviors, though much less than C. And I don't very understand your proposal.
Do you mean The designs of main Go design elements are very consistent, though there are many inconsistencies in detailed designs and concrete implementations. Nil values are just the zero values of some kinds of types. Except this, they are not special comparing to other non-zero values, so Go doesn't treat them specially. |
You're effectively saying "expect absolutely any method to be called with nil receiver through typed nil". My point is that interface pointer with valid vptr and nil dptr can be created through implicit cast. Again, I don't see "just write code better" as an argument. Compiler should help us, not force us to decrypt some riddles. |
Honestly, I still don't very understand your proposal. I think I can understand it better if you can show an example how to use the
Sometimes, encapsulating a nil pointer in an interface value is desired, sometimes it is not. How can compilers distinguish them? |
Example is very primitive but should illustrate idea
Cannot disagree with that. My point is, compilers should catch unsound scenarios. And implicit half-nil interface is a soundness hole to me. |
The difficulty I see with this proposal is converting back and forth. For instance:
Should the third line fail? What about
Currently the first example passes and the second one fails. If the conversion of a nil pointer to an interface makes a full nil interface, then we can't distinguish these two cases - either they both have to fail (a nil interface is convertible to no type, as it is now) or they both have to succeed (a nil interface is convertible to the nil value of every pointer type?). Either way, I don't like the consequences. The first example seems like the identity operation that should always work, and the second example seems like it could hide type errors (although it wouldn't actually break the type system, because type fluidity only works for nil). So although I agree your proposal solves the problem you're investigating, it's not without its drawbacks in other areas. |
Yes, it should fail. If you want current behavior:
Fails as well.
Whether I'm right or you is highly dependent on one simple question. What is runtime typed nil in Golang? I mean semantically. What meaning does it carry? I'll expand a bit on my understanding though will try to keep it short. First, what is a pointer? It's a special type which holds location of an object of some connected type. And it has special value And here we have first question. What's the value on the other side when pointer is Second question which arises here. What's the type of variable Then we come to such concept as interface. Unlike OOP languages, Go's interface is more like trait, a concept more common in FP world. We use interfaces in Go through interface pointers. Which are not the same as normal pointers. In fact, they consist of two pointers, one points to actual object and the other to method table necessary for interface to work properly. Due to Go's runtime, second pointer can be also treated as type information. If we treat this "fat pointer" as some ordinary type, we can have this rough table of its possible states
First and last cases are pretty much obvious. They represent normal Third case doesn't make much sense because it's a data pointer without type pointer. It cannot be created in normal Go unless one uses reflection or unsafe. It's unreachable, so let's leave it as-is. But there's the last possible state which is The problem is, we already have nil receiver interfaces, though used not so often. How are they used and why do they exist? I think they exist to have cheap stateless interfaces like the one I showed above. And they shouldn't be treated as nil. Who's right is highly dependent here on the semantical meaning of runtime typed nil (RTN) in Go. Based on the answer we have several ways to resolve this strange issue - except leave it as-is of course. If RTN is just an artifact of cheap stateless interfaces. It would mean that nil is just default value of pointer and does not carry type on its own - but pointer does. Consequently it would mean that nil receiver interfaces are also prohibited state, like nil vtable. And if one wants stateless interfaces, he can use hidden static object. If RTN is an important part of type system, we must preserve it. But we must also properly express it as nil. So checks against nil receiver will result in "it's empty", with ability to check for its type. Then cheap stateless interfaces must go the same way as above. A third way is the one described in this proposal. There may be more. |
I think this is the fundamental issue here. I understand that there's confusion here, and that confusion can lead to bugs. But I don't think this proposal is the solution. Interfaces with nil pointers in them aren't an aberration; they can happen all the time. For example:
I'm just using interfaces as a container, and I expect to get back the same thing I put in. I'm not calling methods on it or anything, an |
You says this proposal is for compiler, but is it also for runtime? var x *int
if (aRuntimeCondition) {
x = new(int)
}
// At compile, we don't know whether x is nil.
var y interface{} = x.(*int).(interface{}) // or some other syntax
z := y.(*int) BTW, you can't declare methods for |
Well, kind of yes. Going with this logic, interfaces shouldn't support
The proposal you mentioned has significant drawback, at least to my sight. It introduces a lot of complexity and different kinds of Another issue here would arise when/if generics land on table. We will have to introduce "generic nil" to be able to handle something like generic range loop over some collection of interface pointers.
The example presented shows that runtime typed nils are significant part of type system. I have absolutely no objections against this. Let's just decide - is it nil? Or not a nil? Or should interfaces support nil comparison at all? My short conclusion. There's a loophole in Go's type system because of attempt to "sit on two chairs", i.e. treat interface as both simple pointer and aggregate data structure at once. I'm leaning towards treating it like simple pointer and thus deciding whether it's nil based only on its data pointer. The proposal itself happened because of certain doubts that Go core team would change behavior so significantly.
I think yes. Though I'm in doubts now due to Keith's feedback on typed
My bad. |
If it is also for runtime, then the syntax is not much useful, for it can't prevent nil interfaces with concrete nil values from happening at run time. BTW, I think using the terminology |
I want to be clear that in my opinion there is no loophole in Go's type system. I find that to be a perplexing statement. What is happening here is that the builtin identifier People new to the language find it confusing that a pointer that is equal to |
The confusion here stems from the nil pointer suddenly becoming non-nil through implicit cast. That's all. Again, what would you suggest to resolve issue mentioned in opening post and this one by @gwd , taking into account this issue is long-range and cannot be always resolved by just "not doing that"? |
If you know the pointer type, assume its is
|
And if I don't know it? Or it causes abstraction leak? Or the set of possible types is open? |
If you don't know it, then you can only use its methods (through the interface proxy), |
We are talking about language semantics here, so I think it's important to be pedantic about what we are saying. A "nil pointer" is a value of pointer type that is equal to
First, let me say that this since this is indeed a long-standing issue that we should take the time to find a good solution. We are not in a hurry here. Second, I'm already on record as proposing #22729. I don't think that is the answer, but I'm hopeful that it may gesture toward the answer. |
Effectively it becomes :D. Because it no longes compares equal to nil.
You may consider making pointers non-nilable, then add "optional value" concept. Ta-da! No need to resolve half-nil values, as nil will always be "outside" the value :D. Mostly joking actually.
Read through the thread more carefully than the first time. I must say I'm more sided with Dave and the others who suggest making nil-comparison independent of runtime type pointer stored in interface. I see that these "runtime typed nils" are already a thing. Anyway, my proposal was aimed to be "least change" by fixing only ambiguous conversion and leaving everything else as-is. |
My apologies, but, again, I think it's necessary to be pedantic about language issues.
The pointer remains equal to
The conversion is not ambiguous. It is precisely defined. It is |
I think perhaps @target-san means "confusing" rather than "ambiguous". What happens when you assign a variable with an interface type to a pointer is, as you say, unambiguous and well-defined; but the result is still rather surprising, even to people who theoretically understand what's going on. |
"Confusing" comes from not well understanding what are interface values. I feel your confusion comes from not familiar with how type deduction works in Go. When you comparing a value
The type deduction rules are straightforward and intuitive. |
Thank you very much but I'm well aware what are "fat pointers" and how they are usually implemented. As well as usual vtable implementations in OOP languages. And one of sources of my confusion comes from the fact that in other languages with fat pointers both pieces of such "value" are kept coherent. They're either both null or both point to something. But Go has runtime-typed nils, and here's where things get complicated. |
@go101 We have very different attitudes on this. In my ergonomics class, they said that it's your job as a designer to design things that are easy to use; and that 95% of the time when people misuse your product, it's due to poor design. In my documentation class, they said that it's your job as a document writer to make the documentation understandable; 95% of the time when people are confused by the documentation it's due to poor design. In my music conducting class, they said that it's your job as a conductor to communicate when the orchestra should start; 95% of the time when your group doesn't come in together, it's because you as a conductor sent mixed signals. (And he videoed everyone conducting, so that he could prove that it was our fault.) I could go on and on -- I see this situation in parenting, in economics, in leadership, in school -- situations where people do the "wrong thing" because they were prompted to by the circumstances around them, and then blamed for doing the natural, obvious thing. Sometimes there's no way to change the design, and education / exhortation to do better is the only answer. But our first resort, when we find this sort of "anti-pattern" happening, should be to see if there's a way we can re-design the system to make it better. var x InterfaceType
var y *InterfaceInstance
y = nil
x = y
if x == nil {
// Not executed
} else {
// Executed
} The code above is confusing. It is not natural or normal. It can be comprehended, but not without extra effort, and it's prone to error. Rather than spend so much effort trying to make people understand it, and dealing with the consequences of making mistakes, wouldn't it be better to redesign the language so that it doesn't point you in the wrong direction? Or at least, so that the compiler would help you avoid this sort of situation? I mean, if "just be more careful" were an effective strategy, why bother using a typed language like Go at all? |
It is true that, many new Go programmers, including me when starting learning Go, with experiences of some other popular languages may view In Go, Go is Go, other languages are other languages. Why do you have to let Go become other languages?
No, for every @gwd |
@gwd var x InterfaceType
var y *InterfaceInstance
y = nil
x = y
if x == (*InterfaceInstance)(nil) {
// Not executed
} else {
// Executed
} otherwise, it is equivalent to var x InterfaceType
var y *InterfaceInstance
y = nil
x = y
if x == InterfaceType(nil) {
// Not executed
} else {
// Executed
} This is the last attempt to make an explanation in this thread. |
@go101 I hit something similar in C++ where certain type had |
@go101 You keep describing how Go currently works; we're talking about how humans respond to it, and how we can change Go to make them respond better. If I didn't know how Go actually works, then I wouldn't have been able to write that example. If you don't care about making Go more intuitive and safe to use, that's fine; but a lot of people do. |
Thanks for the suggestion. I don't think we've seen this one before. A significant drawback of this proposal is that it would subtly change the behavior of existing code. The same Go code would behave differently in different versions of Go. This goes against the plan described in #28221 for language changes. We can add language features, and we can (where necessary) remove language features, but we want to avoid changing language features such that some code is valid in two different language versions but has different behavior. |
@ianlancetaylor Well, that's a very, very strong argument against this proposal which I overlooked. I agree that "behaves exactly the same or does not compile at all" is a reasonably good way to introduce changes. I then leave it up to your decision whether to leave this proposal open or close it. Hope you will find good enough way to resolve this issue. And thanks for your patience and overall efforts on Go lang. |
OK, closing this specific proposal. Perhaps there is something else in this area that could work. |
Summary
Go's "fat" interface pointers can contain nil data pointer and proper method table pointer. While this property is useful, it may easily become source of undesired bugs - because typed nil is automatically converted to such "half-nil" interface, even if methods on source type do not support nil receivers. Such implicit conversion must result in nil interface pointer. Although, there should still be a way to create nil-receiver interface explicitly
Motivation
Simplest code snippet which demonstrates problem. Please note that error case is not specifically tied to this problem. Any typed nil to interface conversion may cause this
Proposal
Change existing behavior such that implicit conversion of typed nil to interface produces nil interface pointer. Allow create "nil receiver interface" pointer using explicit syntax like
nil.(MyError*).(error)
. This would make most expected behavior the default one, with fallback available.Drawbacks
Changes semantics, which may result in code breakage in some places where code relies on existing conversion rules.
Alternatives
References
Previous proposals which touched this topic:
typednil
keyword for checking whether an interface value is a typed nil. #24635The text was updated successfully, but these errors were encountered: