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: spec: allow taking address of value returned in function #45653

Open
leighmcculloch opened this issue Apr 20, 2021 · 11 comments
Open

proposal: spec: allow taking address of value returned in function #45653

leighmcculloch opened this issue Apr 20, 2021 · 11 comments

Comments

@leighmcculloch
Copy link
Contributor

@leighmcculloch leighmcculloch commented Apr 20, 2021

Similar to @griesemer's proposal #3117

The non-addressability requirement of values returned from functions makes sense by itself since we don't want a surviving pointer to a value returned on the stack.

But surely it should be ok to have an invisible temporary variable copying the respective value which is just used during the assignment. It seems that this doesn't introduce any more issues than what we have already when assigning that cannot be assigned to atomically.

This would be particularly convenient when assigning the return value of functions to pointer fields in struct types that are used for JSON serialization. For better or worse the way to indicate struct types are empty is to make them a pointer, with omitempty, but this makes it inconvenient to directly assign values from functions that aren't pointers to those types.

E.g., the following program should be legal:

package main

import "fmt"

type S struct {
    Time *time.Time `json:"time,omitempty"`
}

func main() {
    s := S{
        Time: &time.Now(), // cannot take address of time.Now() (value of type time.Time)
    }
    fmt.Println(s)
}

E.g., this is one example of the code required today to work around this limitation:

package main

import "fmt"

type S struct {
    Time *time.Time `json:"time,omitempty"`
}

func main() {
    s := S{
        Time: func () *time.Time { t := time.Now(); return &t }(),
    }
    fmt.Println(s)
}

In a simple example this doesn't seem terribly inconvenient, but in large APIs it becomes tedious.

This proposal is constrained to single-value return functions, and not return functions return multiple values because functions that return multiple-values can already only be assigned to an explicit list of variables.

Proposal template:

  • Would you consider yourself a novice, intermediate, or experienced Go programmer?
    Experienced

  • What other languages do you have experience with?
    Java, Ruby, C#, C, JavaScript

  • Would this change make Go easier or harder to learn, and why?
    A little easier.

  • Has this idea, or one like it, been proposed before?
    Similar to #45624.

    • If so, how does this proposal differ?
      It refers to making simple types addressable, where this proposal refers to making values from return functions addressable.
  • Who does this proposal help, and why?
    See above.

  • What is the proposed change?
    See above.

    • Please describe as precisely as possible the change to the language.
      See above.

    • What would change in the language spec?
      The specification of what can be addressed, otherwise nothing.

    • Please also describe the change informally, as in a class teaching Go.
      See above.

  • Is this change backward compatible?
    Yes

  • Show example code before and after the change.
    See above.

  • What is the cost of this proposal? (Every language change has a cost).

    • How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?
      I'm not sure.
    • What is the compile time cost?
      Little.
    • What is the run time cost?
      None.
  • Can you describe a possible implementation?
    See above.

    • Do you have a prototype? (This is not required.)
      No.
  • How would the language spec change?
    It wouldn't.

  • Orthogonality: how does this change interact or overlap with existing features?
    It expands existing patterns of using & to address items.

  • 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.

  • Is this about generics?
    No.

@gopherbot gopherbot added this to the Proposal milestone Apr 20, 2021
@seankhliao seankhliao changed the title proposal: allow taking address of value returned in function proposal: spec: allow taking address of value returned in function Apr 20, 2021
@seankhliao
Copy link
Contributor

@seankhliao seankhliao commented Apr 20, 2021

Please fill out https://github.com/golang/proposal/blob/master/go2-language-changes.md when proposing language changes

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Apr 20, 2021

If f returns a pointer to a struct, I think it would be fairly unfortunate that &f() and &f().x mean significantly different things. The first would allocate new heap memory, copy the result of f() into that memory, and then take its address. The second would simply take the address of the field x in the struct pointer returned by f, without allocating any new memory. This means that two operations that look very similar would behave significantly differently.

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Apr 20, 2021

This is related to #45624.

@leighmcculloch
Copy link
Contributor Author

@leighmcculloch leighmcculloch commented Apr 20, 2021

simply take the address of the field x in the struct pointer returned by f, without allocating any new memory

Interesting. If it is possible to take the address of a field of the value returned by f, what's the distinction between getting the address of the value vs a field of the value?

This means that two operations that look very similar would behave significantly differently.

Would they behave differently to the developer, or only to the runtime? My understanding is the behavior difference would be in the runtime, which seems less of an issue. What problems does this behavior difference create?

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Apr 20, 2021

In one case you have the only pointer to the value and in the other case you have a pointer that other code may also have. That's a pretty big difference when trying to understand the behavior of your code with multiple goroutines.

For example, you know that &f() gives you a unique pointer. Suppose you want a unique pointer. Then you write &f().x, but now you don't have a unique pointer. Or maybe you write &(&f()).x, and now you do have a unique pointer. To be clear, none of this is a fatal problem. But in general it's best if code that looks similar behaves the same way, and it's a problem if code that looks similar behaves significantly differently.

@mdempsky
Copy link
Member

@mdempsky mdempsky commented Apr 21, 2021

This is related to #45624.

To elaborate, if #45624 is accepted, instead of &time.Now(), you would write something like &time.Time(time.Now()), new(time.Time, time.Now()), or new(time.Now()), depending on what spelling(s) the accepted proposal allows.

@dolmen
Copy link

@dolmen dolmen commented Apr 21, 2021

Pointer expression to any value (including return of function) already exist. They are just ugly.

Example (see on the Go playground):

	nowPtr := &(&struct{ time.Time }{time.Now()}).Time

	fmt.Printf("%T %[1]v\n", nowPtr)
	// Output: *time.Time 2009-11-10 23:00:00 +0000 UTC m=+0.000000001
@mdempsky
Copy link
Member

@mdempsky mdempsky commented Apr 21, 2021

@dolmen FWIW, a more concise (but still ugly) spelling would be &[]time.Time{time.Now()}[0].

@dpifke
Copy link
Contributor

@dpifke dpifke commented Apr 21, 2021

Would this proposal affect slices as well? (I can file a separate proposal if this is best discussed on its own task.)

Right now, one must do:

h := sha256.Sum256(b)
x := SomeStruct{
    Algo: AlgoSHA256,
    Hash: h[:],
}

It'd be much more convenient to be able to write:

x := SomeStruct{
    Algo: AlgoSHA256,
    Hash: sha256.Sum256(b)[:],
}

In Go 1.16, the above results in invalid operation sha256.Sum256(b)[:] (slice of unaddressable value).

Behind the scenes, I would expect the compiler to automatically perform the copy of the return value to back the desired slice.

Looking at the proposed new syntax in #45624, I'd also be open to something like:

x := SomeStruct{
    Algo: AlgoSHA256,
    Hash: new([]byte, sha256.Sum256(b)),
}

...although that seems much less clear to me.

@mdempsky
Copy link
Member

@mdempsky mdempsky commented Apr 21, 2021

Would this proposal affect slices as well?

I would recommend that it does not. The fact that & can be used for composite literals to implicitly allocate variables is rather special, and I think we'd do best to keep it minimal to avoid confusion. For example, Go allows (&[7]int{})[:], but not [7]int{}[:]. Similarly, (&T{}).M() is allowed for pointer-received methods, but not T{}.M().

If the variant form new(value) (as opposed to new(type, value)) discussed in #45624 is accepted, you could write new(sha256.Sum256(b))[:]. I'd prefer to encourage that than to allow new([]byte, sha256.Sum256(b)) (i.e., where the specified type can differ from the value expression's type).

@leighmcculloch
Copy link
Contributor Author

@leighmcculloch leighmcculloch commented May 22, 2021

Please fill out https://github.com/golang/proposal/blob/master/go2-language-changes.md when proposing language changes

@seankhliao Thanks for the link. Done!

@gopherbot please remove label WaitingForInfo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
7 participants