golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
123.51k stars 17.6k forks source link

proposal: Go 2: explicit nil and pointer param #36884

Closed atishpatel closed 4 years ago

atishpatel commented 4 years ago

Summary

At compile time, there should be a way to specify you can't pass nil into the function call but it's still a pointer.

Pointers are awesome and there are many reason to use a pointer such as not having to copy a param, being able to mutate a param, etc. But, using pointer is makes you prone to nil pointers, and people who use your functions often try to pass nil and you have to handle this. I genuinely think there is potential here for a way to improve the developer experience by providing compile time errors for invalid nil pointers.

Side note: One of the main reasons I love Golang is because it's an opinionated statically typed language that provides a great developer experience. I get lint warnings if i don't add proper comments and that is wonderful.

Current options - landmine runtime error

Check all params for nil and panic or return error.

func example(param1 *Type1, param2 *Type2) (*Result, error) {
  if (param1 == nil) {
  // or perhaps better to panic so a developer isn't caught by a runtime error in prod
   return nil, fmt.Errorf("Param1 cannot be nil")
 }
  if (param2 == nil) {
  // or perhaps better to panic so a developer isn't caught by a runtime error in prod
   return nil, fmt.Errorf("Param2 cannot be nil")
 }

// actual function logic
} 

Goal

func foo(a <- chan int) *Type1{
    if <-a < 0 {
        return nil
    }
    return newType1(a)
}

func example(a #Type1) {
    ...
}

go func(){
    // read from network
    ch <- fromNetwork()
}
// Compile error: cannot use  untype nil as type *Type1 in argument to example
example(foo(ch)) 

v  := foo(ch)
if v != nil {
  // compiles successfully
  example(v)
}

var newVar *Type1
// Compile error: cannot use newVar (untype nil) as type *Type1 in argument to example
example(newVar)

newVar = &Type1{}
// compiles successfully
example(newVar)

func call(t *Type1) {
 if t == nil {
   return
 }
  // compiles successfully
  example(t)
}

Proposal 1 - non-nil pointer character - backward compatible

Introduce a character that implies a non-nil-able pointer. In this example, the character is #.

func example(param1 *Type1, param2 #Type2) (*Result, error) {

In this case, param1 could be nil but param2 would give a compile time error if someone passed in nil.

I'm not an expert at the language so perhaps someone else can tell me if there is a better way than this.

Alternative Proposal 2 - Not Nil Union Type - backward compatible

This is more elegant but it could break people's code on library updates. For example, if you are relying on a library that updated to use this, your code would give compile time errors saying you can't use nil here. But, perhaps it is good because if the developer updates the library and you get compile time errors, you shouldn't have been passing nil into the function anyway and it saved you from a runtime error. šŸ¤·ā€ā™‚

func example(param1 *Type1 , param2 *Type2 | !nil) (*Result, error) {

In this case, param1 could be nil but param2 would give a compile time error if someone passed in nil.


Template


At the core of this is a better developer experience by giving compile time error instead of runtime errors.

chewxy commented 4 years ago
func foo(a <- chan int) *Type1{
    if <-a < 0 {
        return nil
    }
    return newType1(a)
}

func example(a #Type1) {
    ...
}

go func(){
    // read from network
    ch <- fromNetwork()
}
example(foo(ch)) 

How would you propose to solve the issue of breaking composability as presented above.

This example shows you need to evaluate at compile time, leading to a twostage compilation thing.

chewxy commented 4 years ago

(p/s: null pointer detection is undecidable in the general case)

ianlancetaylor commented 4 years ago

See also #28133 and #30177.

ianlancetaylor commented 4 years ago

For language change proposals, please fill out the template at https://go.googlesource.com/proposal/+/bd3ac287ccbebb2d12a386f1f1447876dd74b54d/go2-language-changes.md .

When you are done, please reply to the issue with @gopherbot please remove label WaitingForInfo.

Thanks!

rfielding commented 4 years ago

you can't assign to a non-nil pointer, or dereference it until you have proven that it has been assigned a non-nil value? ie:

Prove it with a branch...

if p == nil {
  ...
} else {
  p.execute()
}

Or prove it with a declaration that it's not nil. But if that pointer exists in a struct, must it be assigned on struct construction?

davecheney commented 4 years ago

This could get quite confusing

type P struct { *q }

type q struct {}

func (x *q) execute()

func main() { var p P p.execute() }

is this permitted? if not, what check is required? what would happen if P and q were not in the same package as main, the expression if p.q == nil would not be allowed.

atishpatel commented 4 years ago

@ianlancetaylor I'm not suggesting a language change that would require breaking the Go 1 compat from my understanding. I know that Go 2 is code word for it'll never happen. What i'm suggesting is this is specifically for function parameters only. Would you still like me to fill out the template?

Again, i'm not an expert, and I do not know if we can do what i'm suggesting below is possible or not.


@chewxy Great question. I'm sorry i forgot to put this in the original issue. Here is your code with what would and working give compile errors.

func foo(a <- chan int) *Type1{
    if <-a < 0 {
        return nil
    }
    return newType1(a)
}

func example(a #Type1) {
    ...
}

go func(){
    // read from network
    ch <- fromNetwork()
}
// Compile error: cannot use  untype nil as type *Type1 in argument to example
example(foo(ch)) 
v  := foo(ch)
if v != nil {
  // compiles successfully
  example(v)
}

newVar := new(Type1)
// Compile error: cannot use newVar (untype nil) as type *Type1 in argument to example
example(newVar)

newVar = &Type1{}
// compiles successfully
example(newVar)

func call(t *Type1) {
 if t == nil {
   return
 }
  // compiles successfully
  example(t)
}

The goal would be to analyze the code to see if it is possible for the param to be nil. And if it is, at compile time, state this is invalid.

I hope you don't mind me adding this to my original comment. šŸ™

ianlancetaylor commented 4 years ago

@atishpatel It's not correct to say that "Go 2 is code word for it'll never happen.'' We use the Go 2 label for every language change, including the language changes that have in fact happened. For example, see https://golang.org/doc/go1.13#language; each of those changes has an associated issue marked "Go 2".

So, yes, I would still like you to fill out the template. Thanks.

ianlancetaylor commented 4 years ago

A language change that permits people to write

    if t == nil {
        return
    }
    // Here t can be assigned to pointers required to be non-nil.

requires very clear instructions for exactly when the compiler can assume that the pointer is not nil. There are multiple Go compilers, and they must all precisely agree as to which programs can be compiled.

atishpatel commented 4 years ago

@ianlancetaylor My apologizes. I remember Rob Pike announcing Go1.10 by saying there probably won't be a Go2, and we are here Go1.14. I didn't realize the Go2 label is used for language changes that still keep the Go 1 compact. I'll fill out the template and add it to the original comment. Thank you for your help. šŸ™

chewxy commented 4 years ago
// Compile error: cannot use  untype nil as type *Type1 in argument to example
example(foo(ch)) 
v  := foo(ch)
if v != nil {
  // compiles successfully
  example(v)
}

This is a language change. Also, not quite as useful given that you can write a program to check for trivial nil

newVar := new(Type1)
// Compile error: cannot use newVar (untype nil) as type *Type1 in argument to example
example(newVar)

This is confusing. newVar is not nil. it's a pointer to a value of type Type1 with the zero values of the components of Type1

atishpatel commented 4 years ago

Chewxy. Yes, this is a language change but i don't think it breaks go1 compat.

Yes. The goal isn't to check if something is nil or not. The goal is write and share code that is more clear with pointers.

I've written Go for 3+ years, and as my projects grow bigger and there is older code, i run into more issues with nil pointers. One could just deal with runtime errors to find out if there is a nil pointer issue. But, i'm trying to find a way where, if i know something shouldn't be nil, i can communicate that to everyone including my future self.


chewxy. You are correct. I meant var newVar *Type1 instead of newVar := new(Type1) I've updated it in the original issue comment, but left it in the middle one. Thank you again.

atishpatel commented 4 years ago

@gopherbot please remove label WaitingForInfo

beoran commented 4 years ago

The problem with this idea is that not all, or not even most nuls can be checked at compile time. The compiler would have to solve the halting problem for this. Rather, if you don't want to receive a nul, see if you can't pass the struct by value in stead of by pointer.

ianlancetaylor commented 4 years ago

As mentioned above in https://github.com/golang/go/issues/36884#issuecomment-580080773, in order to make code like this work

    if a != nil {
        F(a) // where F is defined using # 
    }

we need to add a notion of dataflow to the language, so that all compilers will analyze this code in the same way. That is a significant complexity that we want to avoid.

The suggested # syntax only supports pointer types, but of course there are other kinds of types that can be nil: slices, maps, channels, functions, interfaces.

For these reasons this is a likely decline. Leaving open for four weeks for final comments.

ianlancetaylor commented 4 years ago

No further comments. Closing.