Looked at using Go ... nil/SEGV really bothered me .. Go2 Proposal?

379 views
Skip to first unread message

Michael Toy

unread,
Mar 23, 2022, 5:01:36 PM3/23/22
to golang-nuts
I barely understand Go, so this is likely a stupid idea. Since Go uses nil for a lot of things, lots of things can be nil.

I am not a huge fan of the null safe accessor. ( https://github.com/golang/go/issues/42847 )

I am a huge fan of the compiler telling me the places where I have not checked for nil ... It took me a while to get used to languages which do this, but now I can't imagine living without it.

Is it crazy to wish for ...

if x == nil {
  // this does not produce an error because x is known not to be nil
  x.interfaceFunc()
}
// this DOES produce an error/warning if y is possibly nil
y.interfaceFunc()


Axel Wagner

unread,
Mar 23, 2022, 5:15:13 PM3/23/22
to Michael Toy, golang-nuts
Personally, I think this leads to very unreadable code, for no real benefit.
If a nil-pointer dereference happens unexpectedly, that's always a bug. A panic is the correct signal to tell me about that bug, so I can go ahead and fix it. So, making my code less readable to get less robust code seems like a lose-lose proposition to me.

Of course, people can always choose to write/use whatever static checker they want. I'm not opposed to this existing. I just don't think it should be in the compiler or in `go vet`.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/5a700cd9-9274-4756-80a6-9d322232afebn%40googlegroups.com.

Sam Hughes

unread,
Mar 24, 2022, 4:39:39 AM3/24/22
to golang-nuts
Totally get where @Michael%20Troy is coming from. TypeScript is aware of logical elements as part of ancestry for the current expression, and provides some helpful warnings in cases where the expression can still be visited with incompatible predicates.

@axel, it my feel counter-intuitive, but a possible runtime panic converted to a compiler error is an increase in how robust the code is. The weird part, at least, why it might feel weird, is that you might never encounter the issue as a runtime panic, but as a compiler error, it will hit you every time. This will make exploring and experimenting with unfamiliar imports, features, and patterns more complicated, but it is the entire point, to my mind, of having a type-system.

P.S. Bug in the snippet given by OP? I expected to see "x != nil" instead of "x == nil", or else change comments around.


Axel Wagner

unread,
Mar 24, 2022, 5:41:23 AM3/24/22
to golang-nuts
One thing to be clear: It is very different if we are talking about "emit a warning if a value is known to be nil" and "emit a warning unless a warning is known not to be nil". The former seems fine to me - it is IMO fine for this code to cause a vet-failure:

var x *int
y := *x

What I'm opposing is the original idea, for this code to cause a vet-failure:

func (x *int) { *x }

Importantly, whether or not a value is `nil` is *always* going to be a heuristic.
If we complain about "known to be nil", every finding will always be a bug. I don't think it's objectionable to find them statically.
If we complain about "not known not to be nil", a significant number of findings will be non-bugs, leading to changes as OP suggested. So, I'm assuming that's the situation we are talking about.

On Thu, Mar 24, 2022 at 9:40 AM Sam Hughes <sam.a....@gmail.com> wrote:
@axel, it my feel counter-intuitive, but a possible runtime panic converted to a compiler error is an increase in how robust the code is.

Of course. But that's not what we are talking about. We are converting *some* runtime bugs into compiler errors (really, vet checks, we can't fail to compile because of backwards compatibility).
But most of them, where it's not clear from the code that a particular pointer is going to be nil, will get the treatment suggested by OP. Which ends up exploding the state-space of possible behaviors of a program, making it exponentially harder to know what it's doing.

That's IMO the less intuitive thing. People tend to think "crashing code is unreliable". But really, crashing is quite a safe and easy to reason about behavior. But having to constantly program against any possible bug leads to unmaintainable, brittle, impossible to reason about code. If every index-expression, every pointer-dereference and every method call needs to be wrapped in a conditional, it becomes impossible to really understand where a failure is coming from and how a program behaves in different failure modes.

The weird part, at least, why it might feel weird, is that you might never encounter the issue as a runtime panic, but as a compiler error, it will hit you every time. This will make exploring and experimenting with unfamiliar imports, features, and patterns more complicated, but it is the entire point, to my mind, of having a type-system.

P.S. Bug in the snippet given by OP? I expected to see "x != nil" instead of "x == nil", or else change comments around.


On Wednesday, March 23, 2022 at 4:15:13 PM UTC-5 axel.wa...@googlemail.com wrote:
Personally, I think this leads to very unreadable code, for no real benefit.
If a nil-pointer dereference happens unexpectedly, that's always a bug. A panic is the correct signal to tell me about that bug, so I can go ahead and fix it. So, making my code less readable to get less robust code seems like a lose-lose proposition to me.

Of course, people can always choose to write/use whatever static checker they want. I'm not opposed to this existing. I just don't think it should be in the compiler or in `go vet`.

On Wed, Mar 23, 2022 at 10:01 PM 'Michael Toy' via golang-nuts <golan...@googlegroups.com> wrote:
I barely understand Go, so this is likely a stupid idea. Since Go uses nil for a lot of things, lots of things can be nil.

I am not a huge fan of the null safe accessor. ( https://github.com/golang/go/issues/42847 )

I am a huge fan of the compiler telling me the places where I have not checked for nil ... It took me a while to get used to languages which do this, but now I can't imagine living without it.

Is it crazy to wish for ...

if x == nil {
  // this does not produce an error because x is known not to be nil
  x.interfaceFunc()
}
// this DOES produce an error/warning if y is possibly nil
y.interfaceFunc()


--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/5a700cd9-9274-4756-80a6-9d322232afebn%40googlegroups.com.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.

Brian Candler

unread,
Mar 24, 2022, 6:22:44 AM3/24/22
to golang-nuts
The OP hasn't said specifically which language or feature they're comparing with, but I wonder if they're asking for a pointer type which is never allowed to be nil, enforced at compile time.  If so, a normal pointer-which-may-be-nil would have to be represented as a Maybe[*T] or union { *T | nil }. To use such a pointer value at runtime you'd have to deconstruct it via a case statement or similar, with separate branches for where the value is nil or not-nil. I am sure there have been proposals along those lines floated here before.

I don't think this would negatively affect code readability, because a function which takes *T as an argument can be sure that the value passed in can never be nil (the compiler would not allow a value of type Maybe[*T] to be passed).  Conversely, a function which accepts Maybe[*T] as an argument is explicitly saying that the value passed may legitimately be nil, and hence it needs to check for this.

I like this idea in principle, but in the context of Go it would mean that *T does not have any valid zero value, so you would not be able to use it in any variable or struct which is not explicitly initialized.  This would definitely not be Go any more.

type T ....
var t T  // ok

var p1 Maybe[*T]  // ok
var p2 *T = &t  // ok
var p3 *T  // fails to compile (no zero value is available)

type A struct {
    a Maybe[*T]
}
var q1 A // ok

type B struct {
    b *T
}
var q2 B = B{b: &t} // ok
var q3 B  // fails to compile (cannot create zero-valued instance of this struct, because it includes a member which cannot have a zero value)
 

Ian Lance Taylor

unread,
Mar 24, 2022, 9:36:30 PM3/24/22
to Brian Candler, golang-nuts
On Thu, Mar 24, 2022 at 3:23 AM Brian Candler <b.ca...@pobox.com> wrote:
>
> The OP hasn't said specifically which language or feature they're comparing with, but I wonder if they're asking for a pointer type which is never allowed to be nil, enforced at compile time. If so, a normal pointer-which-may-be-nil would have to be represented as a Maybe[*T] or union { *T | nil }. To use such a pointer value at runtime you'd have to deconstruct it via a case statement or similar, with separate branches for where the value is nil or not-nil. I am sure there have been proposals along those lines floated here before.

Some proposals in this space:

https://go.dev/issue/30177
https://go.dev/issue/33078
https://go.dev/issue/42847
https://go.dev/issue/49202

Ian

Henry

unread,
Mar 24, 2022, 11:24:57 PM3/24/22
to golang-nuts
It is good practice for a function to validate the values it receives from outside, whether they are function arguments or return values from other functions. It is not just a nil check, but a proper validation. I don't think that automated nil check can replace proper validation.

Sometimes for things that shouldn't happen, it is often more practical to just let it crash and burn. Often the problem is not with the function itself, but somewhere higher up the hierarchy. By letting it fail spectacularly, it is easier to identify the bug.

Michael Toy

unread,
Mar 25, 2022, 2:41:07 PM3/25/22
to golang-nuts
The discussion is quite informative for me to read, thanks for responding. Go uses nil in a way which I don't quite yet grok, and so I had no idea if it was even a reasonable thing to wish for. Today I am writing in Typescript, and the way null is integrated into the type system now (after a while) feels natural and helpful to me.

Sam is correct, there is bug in my Go snippet in the post. For humor value only, I would like to point out that the imaginary Go compiler I was wishing for would have found that bug!

I think Brian gets to the heart of my question, which is "If I really understood Go, would I want something like this". I am hearing, "No, you would not"

I think if I were to have a long conversation with Axel about "what is it that makes programs robust and maintainable" we'd go round in circles a bit, as should happen any time you talk about something complex and important. I think I disagree with some statements, but even the disagreement is super helpful.

Thanks for the discussion!

-Michael Toy
Message has been deleted

Stephan Lukits

unread,
Mar 26, 2022, 2:59:35 PM3/26/22
to Michael Toy, golang-nuts


Stephan Lukits

On 25 Mar 2022, at 20:41, 'Michael Toy' via golang-nuts <golan...@googlegroups.com> wrote:

The discussion is quite informative

Axel Wagner

unread,
Mar 28, 2022, 2:37:08 AM3/28/22
to golang-nuts
On Mon, Mar 28, 2022 at 12:39 AM Sam Hughes <sam.a....@gmail.com> wrote:
@Axel, I really did mean what I said.

So did I.

FTR, if OP would have asked for changes to the type-system to be able to represent non-nilable types, my response would have been different. I didn't read their original message as asking for that.

I didn't say "it's impossible" or "it's a bad idea" to have a programming language which is more strict about nil-references. I said that it's impossible to have the Go compiler force you to check for nil-pointers and a bad idea to have vet do it. I consider every discussion on this list as starting from the language as it is today.

The more convenient approach is to implement a type like below. If you disagree? So help me....I'll.... I'll disagree with you?

```Go
type Box[T any] *T

func (ptr Box[T]) Raw() *T {
  return (*T)(ptr)
}

func (ptr Box[T]) IsNil() bool {
  return ptr.Raw() == nil
}

func (ptr Box[T]) Value() (checked T) {
  if blind := ptr.Raw(),  ok := !IsNil() bool; ok {
    checked = *blind
  }
  return checked
}

To me, this seems like it reduces robustness for most programs (assuming you meant to write `Box[T any] struct { p *T }`, otherwise it's at best the same). For one, if you have a pointer, you usually want to do something with that value - getting a new value every time it is dereferenced would make it really easy to write broken programs. It's also easy for functions to have non-sensical behavior when applied to the zero value of some type.

In short, the semantics of "nil-pointers dereference to a new zero-value of the type" seems extremely brittle and a bad idea to me.
 

I recently saw a talk called "It's all about Tradeoffs". This is an excellent example of that. Maybe the above could be improved by static checking and optimization, but it's never as cheap as just trusting there's something there, so long as there actually is something there.
Reply all
Reply to author
Forward
Message has been deleted
0 new messages