Skip to content
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: explicit nil and pointer param #36884

Closed
atishpatel opened this issue Jan 30, 2020 · 16 comments
Closed

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

atishpatel opened this issue Jan 30, 2020 · 16 comments
Labels
Milestone

Comments

@atishpatel
Copy link

atishpatel commented Jan 30, 2020

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

  • Would you consider yourself a novice, intermediate, or experienced Go programmer?
    • intermediate
  • What other languages do you have experience with?
    • C++, Typescript, HTML, CSS, Python, SQL
  • Would this change make Go easier or harder to learn, and why?
    • A little harder because the would need to learn # is a non nil pointer.
    • Little easier because they could write more safe code.
  • Has this idea, or one like it, been proposed before?
    • An ultimate solution to change fundamentally how nils work in Golang has been suggested, but I didn't see anything that was limited in scope and would provide much value such as this proposal.
  • If so, how does this proposal differ?
    • This change is limited in scope and focuses on keeping Go 1 compact while delivering of a better developer experience.
  • Who does this proposal help, and why?
    • Developers. It moves runtime issues to compile time saving time and forcing them to handle nils better.
  • Is this change backward compatible?
    • Yes
  • Breaking the (Go 1 compatibility guarantee)[https://golang.org/doc/go1compat] is a large cost and requires a large benefit.
    • Does not break Go 1 compatibility / compat
  • Show example code before and after the change.
    • Shown above.
  • What is the cost of this proposal? (Every language change has a cost).
    • Compiler would need to understand if a variable can be nil or not based on if a nil check as been done beforehand.
  • How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?
    • My understanding of these tools are limited. From my understanding as long as the underlying AST parser is updated to support the character, it shouldn't be major impact for many tools. But, there would need to be changes to how the compiler works.
  • What is the compile time cost?
    • I don't know. Though this will increase compile time as a trade off to prevent compile time errors.
  • What is the run time cost?
    • none
  • Can you describe a possible implementation?
    • Described above.
  • Do you have a prototype? (This is not required.)
    • I do not.
  • How would the language spec change?
    • # would mean a pointer type that is not nil
  • Orthogonality: how does this change interact or overlap with existing features?
  • Is the goal of this change a performance improvement?
    • No
  • If so, what quantifiable improvement should we expect?
  • How would we measure it?
  • Does this affect error handling?
    • No
  • If so, how does this differ from previous error handling proposals?
  • Is this about generics?
    • No
  • If so, how does this differ from the the current design draft and the previous generics proposals?

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

@gopherbot gopherbot added this to the Proposal milestone Jan 30, 2020
@ianlancetaylor ianlancetaylor changed the title Proposal: Explicit nil and pointer param proposal: Go 2: explicit nil and pointer param Jan 30, 2020
@ianlancetaylor ianlancetaylor added v2 A language change or incompatible library change LanguageChange labels Jan 30, 2020
@chewxy
Copy link

chewxy commented Jan 30, 2020

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
Copy link

chewxy commented Jan 30, 2020

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

@ianlancetaylor
Copy link
Contributor

ianlancetaylor commented Jan 30, 2020

See also #28133 and #30177.

@ianlancetaylor
Copy link
Contributor

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!

@gopherbot gopherbot added the WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided. label Jan 30, 2020
@rfielding
Copy link

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
Copy link
Contributor

davecheney commented Jan 30, 2020

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
Copy link
Author

atishpatel commented Jan 30, 2020

@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
Copy link
Contributor

@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
Copy link
Contributor

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
Copy link
Author

@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
Copy link

chewxy commented Jan 30, 2020

// 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
Copy link
Author

atishpatel commented Jan 30, 2020

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
Copy link
Author

@gopherbot please remove label WaitingForInfo

@gopherbot gopherbot removed the WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided. label Jan 30, 2020
@beoran
Copy link

beoran commented Jan 30, 2020

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
Copy link
Contributor

As mentioned above in #36884 (comment), 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
Copy link
Contributor

No further comments. Closing.

@golang golang locked and limited conversation to collaborators Mar 10, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

7 participants