-
Notifications
You must be signed in to change notification settings - Fork 17.8k
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
proposal: spec: expression to create pointer to simple types #45624
Comments
How much would it break things to let I think I dislike the implicit allocation on taking the address of non-addressible things, because I think basically all this means is that we will finally be able to replace the "loop variable shadowing and goroutines" thing with "i took the address of a thing in a map but writes to it aren't changing it" as the most frequently asked question about Go. If it only happens with &conversion(), though, that seems significantly more clear; conversion is clearly logically creating a new object, even if you convert a thing to exactly the type it already is. So far as I can tell, object names and type names are the same namespace, it's not like C's struct-tag madness, but at any given time a given identifier refers only to one or the other. |
Alternatively, what's the problem with adding composite literals of simple types, like |
If we have this new I think I like the Adding |
What about generalizing the second approach and simply allow taking the address of a function result? p := &f(...) // for any f Conversions are just special functions, so this would cover them. |
I like the i := new(int, 42) ...much shorter than package prepended codes like this: i := stdutil.NewInt(42) Consequently... i := new(int, func() int {
r := rand.New(rand.NewSource(99))
return r.Int()
}()) |
I prefer As far as I can tell, this would make it possible to express all valid Go programs without using the
|
@peterbourgon Probably we shouldn't derail this to try to get rid of
I used to be in favor of |
@benhoyt If you only restrict |
When supporting taking the address of return values, the question on whether returning makes a copy of the return value obtains. For example, consider code like this:
@benhoy Not really in favour of the |
I also kinda wonder why the obvious |
Rob, don't tell me about such a gap. I was implementing Bliss interface for so long. |
@clausecker Because why add a new construct ( |
@robpike Compound literals too are an existing construct and taking the address of them is already legal. So it's as much “adding a new construct” as the |
The As far as I understand, only numeric literals have a problem with unambiguous type inference. With the above in mind, Examples: _ = &int(42)
_ = &true
_ = &"brains"
type Name string
_ = &Name("what's my name?")
type Count int64
_ =&Count(100500) |
The Go 2 playground permits the function:
If I break this issue up into cases, I end up with either "I need this zero times in a module" (by far the dominant case), "I need this once or twice" in which case I would just take the couple of extra lines, and "I need this all over the place" in which case, either define that function or pull it in from somewhere once the generics are out. If one is using this a lot one may prefer a shorter name than I'd suggest just waiting for generics to drop and writing/providing that function. |
But you can't omit the type. The description clearly says that type conversion will be addressable, not the values themselves. It would have to be: _ = &bool(true)
_ = &string("brains") And TBH I'm fine with that. Having something like But either way, having the Option 2 would be very cool IMO. |
While it's true that generics would allow you to write the |
If I can add voice here, I would much prefer option 2 than 1. |
Do we have any data on I'm definitely preferential to option 2 ( |
Why not |
Indeed, I understand the difference between conversion and function calls, but I feel like people learning Go will be confused by Function calls have defined types, so I can't think of any issue with taking their pointer, and I definitely had to be reminded by the compiler that it wasn't allowed a few times. I never use |
I like that Jerf's |
All of the proposed options seem better than the status quo, but still have the downside of requiring types to be written out explicitly even when they are obvious from the value. Compare: d := time.Millisecond
p1 := &d // No noise from types! vs. p1 := new(time.Duration, time.Millisecond)
p2 := &time.Duration(time.Millisecond) In contrast, the generic approach (#45624 (comment)) does not stutter on types, but requires the introduction of a new name for the generic function. So I wonder if it would be preferable to add a generic builtin instead: d := ptrTo[time.Duration](time.Millisecond) or d := ptrTo(time.Millisecond) I don't feel strongly about the specific name, but I think the ergonomics of a generic function are much nicer than the proposed ergonomics of |
When initially @chai2010 made the first proposal, it was considered as "adding a third syntax seems not a good plan" now that rob pike propose it, it is wonderful !!! so go maintainers you can do whatever you like.... |
Several people (@rogpeppe, @robpike and others) have commented that rather than both It's true that this makes the expression more verbose in some cases. However, it seems reasonable to guess that most cases where a long type is used are structs, and for which we have the &S{} literal notation. It seems less likely that people will want to write
As discussed above, there are several reasons why using an |
@ianlancetaylor That's a slight mischaracterisation of my comment. To repeat, my preference is still to choose a new spelling for a function takes a value only. I'm not keen on shoehorning the functionality into |
I don't particularly like That being said, |
It's better if I already know that |
Another point: the e.g.
We have to mention the type where there's otherwise no need for it and it might not be obvious, making the code a little more brittle. With a "pointerTo"-style function, it's nicer IMHO:
|
I don’t like the new(v) form because it depends on the reader knowing if v is a value or a type. Between new(T, v) and ref(v), I could live with either, and it really just depends on if you think it’s better to not add another predeclared identifier or better to not add an overloaded form for an existing identifier. |
@rogpeppe Apologies for the mischaracterization. |
FWIW I agree that But as I said, if we can't agree on any color of bikeshed for a type-argument-less version of this builtin, |
If func ptr[T any](v T) *T { return &v } in random places anyways to avoid the hassle, though admittedly less often as |
We could lean into the syntax we already have: &&value
&&time.Second
var p *int = &&123 |
One of the subproposals discussed in #34515 is to omit the type in |
apropos male(): Still havent understood why map fields always need to be explicitly initialized by make(), instead of directly working off the zero value. This makes using maps in structs much more complicated. |
Every type's zero value is literally the memory all set to zero. A map is a pointer to a struct internally, so its zero value is literally just a nil pointer. Trying to make the zero value behave differently would fail in the following situation, among others: func addThing(m map[string]string) {
// This would allocate an hmap, but only this m would get set to its address.
m["example"] = "This is an example."
}
func main() {
// Remember, this is a *hmap.
var m map[string]string
// addThing() gets a copy of the address, currently nil.
addThing(m)
// No matter what addThing() does, the local m is still nil at this point.
} |
Ah, so a map can be directly passed as value (instead of reference/pointer to it) while still Indeed, now an on-demand allocation would cause this kind of trouble. If we'd ever go that But what would happen (besides extra compiler complexity) if we'd let it implicitly emit an Am I missing something ? By the way, still haven't fully understood how code generation and runtime code really work thx. |
Summarizing some comments:
Given that it is trivial to write the helper function, a language change would add marginal value. |
@adonovan There is certainly no need, but I still find the imbalance troubling: it's easier to build a pointer to a complex thing than to a simple one. None of the bullet points in your list seem fatal to me. The first one is irrelevant to what I suggested, the middle two are true but not clearly problems, while the ease of writing that function doesn't touch the fundamental asymmetry. |
Leaving open until someone brings this discussion to a consensus. |
Judging from the past 1.5 years, I appear to be writing this function about once every second month, when I need it in a new package. The need especially arise with pointers to strings in unit test files, I've noticed. Admittedly, I work a lot with code generated from API specifications. That code tend to use It's not very annoying, but does feel a bit like I'm littering my packages with this function, so not having to write it would be welcome. I do realise I can put it in a package I import, but that also seems overkill for a one-liner. |
As far as being explicit about allocation is concerned, I think Go is well past that point. Whether or not an object is on the stack or the heap is entirely predicated by escape analysis. If anything, |
The helper function can easily be called with a pointer value which would typically be a programming error. Having a language construct could prevent this, either by rule or by removing a layer of indirection. |
If the type inference worked well enough (perhaps by just hard coding this) that you could write |
This notion was addressed in #9097, which was shut down rather summarily. Rather than reopen it, let me take another approach.
When
&S{}
was added to the language as a way to construct a pointer to a composite literal, it didn't quite feel right to me. The allocation was semi-hidden, magical. But I have gotten used to it, and of course now use it often.But it still bothers me some, because it is a special case. Why is it only valid for composite literals? There are reasons for this, which we'll get back to, but it still feels wrong that it's easier to create a pointer to a struct:
than to create a pointer to a simple type:
I would like to propose two different solutions to this inconsistency.
Now it has been repeatedly suggested that we allow pointers to constants, as in
but that has the nasty problem that 3 does not have a type, so that just won't work.
There are two ways forward that could work, though.
Option 1: new
We can add an optional argument to
new
. If you think about it,can be considered to be shorthand for
or
That's two steps either way. If we focus first on the
new
version, we could reduce it to one line by allowing a second, optional argument to the builtin:That of course doesn't add much, and the stuttering is annoying, but it enables this form, making a number of previously clumsy pointer builds easy:
Seen in this light, this construct redresses the fact that it's harder to build a pointer to a simple type than to a compound one.
This construct creates an addressible form from a non-addressible one by explicitly allocating the storage for the expression.
It could be applied to lots of places, including function returns:
Moreover, although we could leave out this step (but see Option 2) we could now redefine the
&
operator applied to a non-addressible typed expression to be,That is,
where
expr
is not an existing memory location is now just defined to be shorthand forOption 2
I am more of a fan of the
new
builtin than most. It's regular and easy to use, just a little verbose.But a lot of people don't like it, for some reason.
So here's an approach that doesn't change new.
Instead, we define that conversions (and perhaps type assertions, but let's not worry about them here) are addressible.
This gives us another mechanism to define the type of that constant 3:
This works because a conversion must always create new storage.
By definition, a conversion changes the type of the result, so it must create a location of that type to hold the value.
We cannot say
&3
because there is no type there, but by making the operation apply to a conversion, there is always a defined type.Here are the examples above, rewritten in this form:
Discussion
Personally, I find both of these mechanisms attractive, although either one would scratch the itch.
I propose therefore that we do both, but of course the discussion may end up selecting only one.
Template
Would you consider yourself a novice, intermediate, or experienced Go programmer?
I have some experience.
What other languages do you have experience with?
Fortran, C, Forth, Basic, C, C++, Java, Python, and probably more. Just not JavaScript
Would this change make Go easier or harder to learn, and why?
Perhaps a little easier, but it's a niche problem.
Has this idea, or one like it, been proposed before?
Yes, in issue #9097 and probably elsewhere.
If so, how does this proposal differ?
A different justification and a new approach, with an extension of
new
.Who does this proposal help, and why?
People annoyed by the difficulty of allocating pointers to simple values.
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
new
operator would get an optional second argument, and/or conversions would become addressible.Please also describe the change informally, as in a class teaching Go.
See above.
Is this change backward compatible? Breaking the Go 1 compatibility guarantee is a large cost and requires a large benefit.
Yes. Don't worry.
Show example code before and after the change.
See above.
What is the cost of this proposal? (Every language change has a cost).
Fairly small compiler update compared to some others underway. Will need to touch documentation, spec, perhaps some examples.
How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?
Perhaps none? Not sure.
What is the compile time cost?
Nothing measurable.
What is the run time cost?
Nothing measurable.
Can you describe a possible implementation?
Yes.
Do you have a prototype? (This is not required.)
No.
How would the language spec change?
Answered above. Why is this question here twice?
Orthogonality: how does this change interact or overlap with existing features?
It is orthogonal.
Is the goal of this change a performance improvement?
No.
If so, what quantifiable improvement should we expect?
More regularity for this case, removing a restriction and making some (not terribly common, but irritating) constructs shorter.
How would we measure it?
Eyeballing.
Does this affect error handling?
No.
If so, how does this differ from previous error handling proposals?
N/A
Is this about generics?
No.
If so, how does this differ from the the current design draft and the previous generics proposals?
N/A
The text was updated successfully, but these errors were encountered: