-
Notifications
You must be signed in to change notification settings - Fork 18k
proposal: Go 2: Ban typed nil interfaces through banning nil method receivers #27890
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
Comments
EDIT |
I don't like this mainly because I like thinking of methods in Go as syntactic sugar over functions, which really is what they are. A method really should just be a function, which allows arguments to be nil. Something else that may become an issue is the following playground: |
@deanveloper the example in your playground is interesting. |
You are exclusively talking about pointers. But there are more types that can be assigned (and compared to) nil, namely In the end, I don't even think this proposal really helps or reduces complexity. Say, I have a function
Under your proposal, to make this correct we'd have to either
Whereas to make it correct now, you'd have to
In the end, you'll still have to have nil-checks everywhere. You either have to have them in the receiver, or you have them in the caller, or you have them in the callee. At least right now, if Personally, I find the current contract pretty clear: If I take an interface, I expect that to be a valid value implementing that. I don't care what that value is and I don't want to. The implementer of the interface knows best what values are valid. Ultimately, if a message call panics, that just means the implementation wasn't valid (and note that it will always be possible for methods to panic, so you can't ever include that). With this change, the contract becomes much harder, because it isn't clear whether I have to check that it's a valid interface-value (i.e. not nil) or the caller has to check that it's a valid value of the type implementing the interface (which might be nil, if that's a pointer). |
@Merovius thank you for the feedback. Like you said I hadn't considered other nulable types, but the same changes do apply to them. In fact it is easier for these than for pointers as none of these types ( As your feedback has highlighted, type definitions are also nilable and will be cast to untyped nil when used in an interface. The better patter is a struct wrapping the nilable: To emphasize, typed nil pointers do implement interfaces, which makes them more complex as defined in my original post. But these other nilable types don't implement (non- var x func()
var i interface{}
i = x
_ = i.(func()) // this used to be OK, now panics as the nil has become untyped But I don't think relying on this property significant (and if you are using it you should probably not anyways). If not convinced just note the below code: s := sort.Interface(sort.IntSlice(nil))
switch s.(type) {
// case []int: // this doesn't even compile! "impossible type switch case: s (type sort.Interface) cannot have dynamic type []int (missing Len method)"
case sort.IntSlice: // this case holds
} |
Something to consider is that this would be pretty hard to implement in some kind of go1to2 transpiler... Although it is possible, so I don't think it should be a barrier to add a language feature, but it is something to think about. Also, remember that nil maps and slices are significant and can be used. Consider the following: Sorry for any spelling mistakes, I'm on mobile right now |
No, it is not. It is a defined type with underlying type
Not really, they are far less complex, TBQH. nil-maps allow reading-accesses without panic, which pointers don't. nil-slices allow ISTM that the rules for pointers are far simpler than for most of the other types - the only operation that is special about pointers is dereferencing and that works if and only if they are not nil. The other types allow more special operations that each have their own peculiar semantics when working on a nil-value.
But as I pointed out - it is. Currently, code doesn't need to check for nil when passing well-behaved implementations as interfaces and code doesn't need to check for nil when being passed well-behaved implementations. Under your proposal, at least one (if not both) needs to happen. i.e. pretty much all Go code out there is relying on this property, by not cluttering everything with nil-checks.
Type assertions on concrete types check whether the dynamic type is identical to the asserted one. |
@Merovius regarding the Not only there are no more nil checks, but they are almost certainly fewer. Say The exception I do acknowledge is when |
@Merovius I stand corrected, what I meant is for |
@deanveloper yes the re-writes may be significant and non-trivial. I would favor discussing the merits of the proposed change first though, only if we (the go community!) see benefits in it is the discussion about the cost of backwards incompatibility changes worth having. See the exchange with @Merovius on other nilable types. |
@Merovius slice read/write, map read/write, channel send/receive, len, cap, (...?) are all functions in the runtime, not methods on these types. My proposal changes nothing on functions, these remain unchanged. |
Yes, and my point was exactly that currently there is also only one place that nil has to be checked - in the method. Except that currently, that check is only needed if nil is not a valid value - and the person needing to do the checking is exactly the person implementing the type. In your proposal, the check (whether at the caller or callee site) always has to happen. You are strictly increasing the amount of nil-checks needed. For every nil-check in a method you can leave out, you have to add a nil-check at all usage sites.
"typed nil interfaces" are not a thing. You mean interfaces with dynamic value nil. I'm not trying to just nitpick - being reasonably precise is important to ensure we talk about the same things and don't unnecessarily talk past each other.
I don't think this argument holds. If think checking the receiver for nil in a method is required for safety, then it's also required for safety to check an interface value for nil before calling any methods on it. In practice, we do neither - we simply accept a certain level of lack of type-safety (also, we usually can't do anything but panic anyway). But if you want that type-safety, you have to actually enforce it everywhere.
Only if you assume any concrete pointer type is only used exactly once. In practice, that's certainly not the case. For example, there are many assignments of I don't believe there is a plausible scenario for a concrete type to have more methods than usages.
Slice, map and channel types all allow accessing a nil-receiver just fine.
Also, It is my impression that your thinking is heavily influenced by classes in OO-languages, in that methods are closely associated with pointers to struct. But that's not how the Go type system works. TBQH I think it's easier to just accept that - because then it also becomes obvious why thinking about "typed nil interfaces" is such a fallacy. An interface value is defined by the methods it implements. It does not make sense to compare it to nil and expect that to have any relationship to the dynamic type (whether pointer, slice, map…). If you intentionally forget anything about the dynamic type of an interface and purely concentrate on the method set, the question of "is the dynamic value nil" is about as natural as the question "is the dynamic value an integer type storing 23". It's simply an unfortunate accident of history that the zero-value of interface-types, the zero-value of pointers, and of maps, slices, channels and funcs are all denoted by the same identifier. But they really are different values. |
The argument is, that the behavior of non-pointer types for nil-values is far more complicated to reason about than the behavior of pointer-types for nil-values. Thus it doesn't make sense to exclusively focus on the pointer case, as the non-pointer case has far more questions to answer. I find |
@Merovius just to verify we are on the same page, by 'nil check' I mean a runtime check that panics with nilPointer on nil. That is what you mean too, right? As in the same kind of check that go currently does when trying to access a pointer that the compiler can't guarantee is non-nil.
The nil check in my proposal happens exactly once for all cases (as you acknowledge). The nil check does not happen in the current go for methods were nil is a valid value (as you highlight). But for methods where nil is invalid, the check will happen at least once, but may happen many times! Consider the below: type X struct {
value bool
}
func (x *X) LoopFoo() {
for i := 0; i < 100; i++ {
x.Foo()
}
}
func (x *X) Foo() {
// do something that requires x.value
} This will not be inlined (and if unconvinced simply assume that
Usages of |
yes. As I said before sort.IntSlice would have to be modified for sorting to still work. Alternatively and much easier simply change (in sort package) func Sort(data Interface) {
if data == nil {return} // this is new
// the rest is unchanged
}
Calling any method in |
Unlikely because I come from Maths not CS, I barely did any formal OO classes 😆. But who knows where the influence lies... Thanks for the great feedback today, @Merovius . Looking forward to resume this very soon |
No, I'm talking about I'm concerned with actual code - that is, how would the way we write Go have to change to maintain correctness. I don't see any reason to touch how nil/pointers/interfaces interact at all, except to reduce the number of comparisons to nil in correct code.
As I said, these checks don't exist. But FTR, even if they would - I highly doubt it would matter in practice. CPU branch predictors are very good and with modern architectures this wouldn't actually incur any cost except in extreme edge cases.
Yes, that's the second half of my b) :) But it's good to get a definitive "yes" from you that you intend to also have this apply to non-pointers. :)
FTR: Me too ;) |
Oh. I was under the wrong assumption on that, and it does mean the runtime nil-check reductions my proposal promised are not real (as there are no such thing as runtime nil checks! 😞). That reduces the advantages of this proposal only to removing the interfaces with dynamic value nil issue, but makes some code trickier (see Overall I think it is time to acknowledge this introduces more problems than it solves. Thanks for the feedback @Merovius . 👋 |
Overview
Methods with pointer receivers have no restrictions on nil receivers. For example the below is valid:
This extends to interfaces, creating the issue of typed nil interfaces. See typed-nils-in-go-2 for further context. I would like to see this situation removed in Go2 as typed nil interfaces are a source of bugs and offer no meaningful additional functionality.
Proposed Changes
I propose some changes to the Go2 language spec that would mean banning nil receivers, resulting in banning typed nil interfaces.
Make every method with a pointer receiver nil-pointer panic (at the beginning of the method) on a nil receiver. Implicitly that means
if this == nil {panic NilPointer}
is added at the beginning of every method. Note this is already implicitly added to most methods (those that use the receiver), just further down the method on the first use the receiver. Compared to the current specs, this would result in:Follow this up with another change, when assigning a (typed) pointer to an interface, check its value. If nil, set the interface value as the untyped nil. Note this means the specific type of the pointer unrecoverable. As an example:
This means if an interface is a typed pointer, it doesn't have to check if the pointer is nil or not, it is non-nil by design, hence the typed nil interface problem is gone.
Other considerations
I imagine others gophers will identify many others issues, these are some I am aware and would like to share upfront:
Optional nil receivers
Methods optionally allowing nil receivers are no longer allowed:
Instead a pointer type would be required,
type XPtr struct {*X}
, which would do the nil check.Type assertions in nil types
By design, type assertions from nil pointer types are no longer supported. I don't think it is a common (or advisable) pattern, but it will still break the existing libraries using it. Where genuinely needed a pointer type could be used to wrap the pointer:
type XPtr struct {*X}
.Methods not using the receiver
Methods do not have to use the receiver. The current spec does not distinguish between the two ways these may be defined (with/without named receiver):
func (x *X) ...
orfunc (*X) ...
. I suggest using this ambiguity and defining them differently in Go2, while the named receiver is just a normal method (and can't be called on a nil typed pointer) the unnamed one may be called on typed nil and does not have the implicit nil check. Note however that because interfaces are never typed nils, once called from an interface it will always be called with a non-nil receiver.Methods as functions
Consider the below situation:
A method on a nil typed pointer is being assigned to a function variable. Calling that function will always panic, making it useless. For this reason I think it is better to change the language spec to have this panic on the assignment.
Compiler considerations
Note these considerations are not affecting the language spec, but I think they may make this changes more appealing.
This proposal means interfaces holding pointers do not have to check if the pointer they hold is nil valued. But that is largely useless if the first thing all methods do is check if the value of the pointer is nil. It would be much better if the nil check where performed by the caller (and still NilPointer panic if nil), improving performance by reducing the number of nil checks (among other things, methods would not need to make that check when calling other methods on themselves).
Also related, when assigning a pointer to an interface the value has to be checked for nil (which is not required currently). While this makes assigning to an interface more expensive, it is likely the compiler can optimize away many of these checks as the nil-ness of the pointer may be known at compile time.
The text was updated successfully, but these errors were encountered: