Currently, _ only has behaviour on the LHS.
I would like to propose giving it meaning on the RHS as meaning "zero value". This has two main areas of effect.
First, when returning from a function with multiple return values, you can omit the zero-value allocation. For example,
func GetString() (string, error) { return _, errors.New("nostring") }
Understandably you can use named returns as well, but that is less than ideal if you use the named return value as a "scratch space" before deciding to return zero anyway. For example, when building some struct, such as a Request, you might use a named return to build it and, later in the function, come across an error which invalidates your scratch space, in which case you would want to return a zero-value instead of a half-initialized struct with some garbage inside.
Second, when you want to reset a struct, such as when using a pool, you could use _ to restore it to a zero-value.
func Pool() func() (User, func()) {
var pool User
// in reality, use some pooling mechanics
return func() (u User, reset func()) {
return pool, func() {
// zero out and return this copy to the pool
pool = _
}
}
}
nil would remain the correct way to create the zero-value for reference types.
This has been proposed and rejected in the past. I'll try to dig up references.
Previously:
@nigeltao's in Jan 2012:
https://groups.google.com/forum/#!msg/golang-dev/iAysKGpniLw/qSbtBUx4-sMJ
(@robpike had said "not now")
@derekchiang in Sep 2013:
https://groups.google.com/forum/#!topic/golang-nuts/kBOnfs3a2tU
@kr in Aug 2010:
https://groups.google.com/d/msg/golang-nuts/VXWNJQFCgts/V2ED_W_tCdEJ
As a consideration, a different language proprosal (#12854) for stronger type elliding may solve what this proposal is trying to do.
Returning the zero value is only painful for structs. The proposal in #12854 would allow the following:
func MyFunc() (otherpkg.ReallyLongStructName, error) {
return {}, errors.New("not implemented")
}
dsnet, the {} syntax would more clearly differentiate it's use from nil, plus it builds on existing syntax. I love it, a definitely improvement over _.
Returning the zero value is only painful for structs, slices, and maps
nil
is a fine zero value for slices and maps. "" is fine for strings. 0 is fine for numbers. I think structs are the only types that would benefit from a more succinct zero.
Doesn't seem worth it to me. (But if #12854 solved this as a side effect, that would be ok.)
It's interesting to note that we can get an expression for the zero value for any type by using reflect.Zero
, but there is no corresponding language construct. The closest we can come in the language is to declare a variable of that type.
That is interesting, but changing _ to address that disparity feels wrong to me. Not a very scientific comment, I admit.
I agree, the {} syntax blows overloading _ out of the water in hindsight.
Personally, I'm an advocate for a new "zero" identifier, similar to nil
apart from that it can be used for values of any type, not just pointer types.
Then propose it, @rogpeppe.
FWIW, I'm still in favor of the original proposal: a new "zero" identifier, just spelled "_".
Just recently, in https://go-review.googlesource.com/c/38280/2/font/sfnt/sfnt.go#867 I changed e.g.
return nil, ErrNotFound
to
return nil, 0, 0, ErrNotFound
and having to distinguish "nil" versus "0" here seems like unnecessary, and unimportant, detail. I'd rather write:
return _, _, _, ErrNotFound
Off-topic, but while I think of it, alternative sugar for this particular line could be
return ... ErrNotFound
but I'm not sure if even I like that idea.
Then propose it, @rogpeppe.
I'm not sure another proposal would add much value. Perhaps the title of this one could be changed to something like "Proposal: define an identifier that means the zero value for all types", because there are a number of possibilities for spelling, but the gist is the same regardless.
@nigeltao I agree that can be a pain, particularly when there are many return statements and even more so when returning struct types by value. I've come to like this idiom:
func (f *Font) viewGlyphData(b *Buffer, x GlyphIndex) (buf []byte, offset, length uint32, err error) {
fail := func(err error) ([]byte, int, int, error) {
return nil, 0, 0, err
}
xx := int(x)
if f.NumGlyphs() <= xx {
return fail(ErrNotFound)
}
i := f.cached.locations[xx+0]
j := f.cached.locations[xx+1]
if j < i {
return fail(errInvalidGlyphDataLength)
}
if j-i > maxGlyphDataLength {
return fail(errUnsupportedGlyphDataLength)
}
buf, err = b.view(&f.src, int(i), int(j-i))
return buf, i, j - i, err
}
That idiom also makes it easier to add and remove return values.
Currently the compiler's not clever enough to make it cost-free,
(well, it wasn't the last time I checked) but I don't see why it shouldn't.
@ianlancetaylor
It's interesting to note that we can get an expression for the zero value for any type by using reflect.Zero, but there is no corresponding language construct.
Is there any type T
for which
var x = *new(T)
doesn't work?
@rogpeppe Another option might be to make nil
a valid identifier for the zero-value of all types, since it already represents the zero-value of many types.
I don't actually like that option — nil
is confusing enough as it is, and the potential for "I thought that was a pointer but it's not" bugs is high — but it's the zero-value name that would add the least new surface area to the language.
I like the zero predeclared identifier idea.
And I still like the "use _ to mute identifiers" idea: https://github.com/golang/go/issues/17389
:)
@rogpeppe
I'm not sure if it needs to be a new keyword here. There is iota
, and (if to push it) we can reuse default
With iota
type S struct { }
var v1 S = iota // equivalent v1 := S{}
var v2 *S = iota // var v2 *S = nil
var v3 int32 = iota // 0
func f() (S, int, error) {
return iota, iota, iota // equivalent to return S{}, 0, nil
}
With default
type S struct { }
var v1 S = default // equivalent v1 := S{}
var v2 *S = default // var v2 *S = nil
var v3 int32 = default // 0
func f() (S, int, error) {
return default, default, default // equivalent to return S{}, 0, nil
}
Personally I like default
more :)
I was going to file an experience report, but found this issue first. I'm definitely in favour of allowing _
as a zero return. Having to return values makes the code less readable and inconsistent with interface/pointer nil
returns (i've seen this cause a bunch of bugs). It may also allow the compiler/runtime to detect when a zero value is used when it shouldn't be. e.g.
func getFoo() (foo, error) {
if noFoo {
return _, errors.New("no foo")
}
return foo{}, nil
}
f, err := getFoo()
if err != nil {
// oops forgot to handle
}
Save(f) // This could panic
I agree with @nigeltao, let's allow _ to be a standin for a constant or literal zero value in a return statement.
I was leery of this proposal because I could only see use for it in return values and for composite literals of non-pointer structs.
But I thought of another good use case: generated code.
Currently, if you need a zero value in generated code where you are generating code using an arbitrary type, you need to either
var v T
The first is complicated for little benefit and the second can make the code feel awkward.
With a universal zero value, you can just use that.
If Go2 gets generics, the same argument applies except that you would have to use var v T
.
Another possibility might be to extend the composite literal syntax to cover non-composite types as well.
x := int32{0} // equivalent to x := int32(0)
x := int32{} // omit the initializer for the zero value
x := int32{1, 2} // error
x := &int32{} // equivalent to x := new(int32)
x := &int32{1} // obviates proto.Int32
var x int32 = {} // lightweight zero-values with https://github.com/golang/go/issues/12854
var ch = (chan int){} // equivalent to make(chan int), perhaps?
type T struct {
ch chan struct{
s string
err error
}
}
t := T{
ch: {}, // No need to repeat the channel type (assumes #12854 or #21496).
}
I don't think this pulls its weight by itself, but might be of more use with hypothetical Go 2 generics. It could also serve as a replacement for both make
and new
, which I find intriguing.
I saw @davecheney was in favour of this in his comment so I'm wondering if this is still being discussed someplace else? If so, a link would be useful.
TL;DR: A representation of zero-values would be useful regardless of the syntax used.
My take: I have experienced what @dantoye was "complaining" about multiple times, and without even knowing about this thread, I imagined that a good token that could solve this problem would be _
. I don't really care about what token would be used instead of _
(maybe a community-wide poll would help with this, as long as we won't get to _yMc_face
), but it would be very helpful to have this "shorthand".
Also I don't see a problem with it given that it doesn't break compatibility with Go 1 (given that the previous token usage was LHS, although this might cause a little confusion). The only downside is the possible confusion when you have code like:
_, err := foo(42)
if err != nil {
return _, err
}
But I think go developers are smart enough to understand this, and won't make any pythonish assumptions.
In addition, things like:
if duration == _ {
doThat()
}
could be useful (again, not necessarily suggesting the _
construct, but the concept behind, i.e. a RHS representation of a zero value)
Per the comment above about an expression that produces the zero value of a type (which can be done using *new(T)
), if we ever support some form of generics we are going to want some way to zero out a variable of a generic type. That is, given
var v T
for some generic type T
, it will sometimes be desirable to write
v = 0
but of course T
might actually be string
, in which case that won't compile, so one would have to write
v = *new(T)
but that seems absurd. So one advantage of this proposal, if we get generics, is the ability to write
v = _
(or any other notation for a general zero value).
A few different alternatives.
// analogous to tagged struct literals.
func f() (X: int, Y: float64, E: error) {
return X: 42
return X: 12, Y: 3.14
return E: errors.New("bar")
}
// _ as zero value of type (i.e. reflect.Zero).
func g() (int, float64, error) {
return 42, _, _
return 12, 3.14, _
return _, _, errors.New("bar")
}
// `zero` predeclared identifier as zero value of type (i.e. reflect.Zero).
func h() (int, float64, error) {
return 42, zero, zero
return 12, 3.14, zero
return zero, zero, errors.New("bar")
}
// i special case error return value to use zero value for excluded return
// arguments.
//
// Note, functions with multiple error values have to be handled specifically in
// the specification; either use only last error, force all error values to be
// returned.
//
// func j() (error, int, error) {
// // only last error
// return errors.New("bar")
// // force all error values
// return errors.New("foo"), errors.New("bar")
// }
//
// Note, this example is not one that we would endorce, however it was included
// as it was part of our discussion and helped us iterate upon different
// alternatives.
func i() (int, float64, error) {
return 42, 0, nil
return 12, 3.14, nil
return errors.New("bar")
}
As a follow up to the example analogous to tagged struct literals, it would be interesting to extend this concept to function parameters as well.
func j() {
k(A: 13)
k(B: 3.14)
k(B: 3.14, A: 42)
k(42, 3.14)
}
// analogous to tagged struct literals.
func k(A: int B: float64) (X: int, Y: float64, E: error) {
return X: 42
return X: 12, Y: 3.14
return E: errors.New("bar")
}
Would _
be permitted only in an assignment or return statement, or would it be permitted in other cases? Would it be OK to write
if v == _ { fmt.Println("zero") }
If we permit this kind of operation, then _
has some of the same confusions as nil
.
Another comment. If we use _
, then we can see code like
_ = x
return _
That seems potentially confusing.
@ianlancetaylor Most simply, _ only has behavior on the LHS right now.
This proposal simply defines RHS behavior as an untyped const roughly translating to "zero value". Identical to how an untyped const 123 has different meaning to different numeric Kinds, _ would have different meaning to all Kinds, and always mean the 0 value of that type.
There is no ambiguity or conflict. if v == _
is using _ as a RHS. _ = x
is being used as LHS. return _
is being used as a RHS.
I tend to think about Go expressions (including variables) in terms of the sets of values they can take. (In mathematical terms, Go expressions are a _join-semilattice_.) At the moment, the expression _
can take any value at all: it is the _top_ of the value lattice. In contrast, zero-values are very near the bottom: for any given T
, there is only one possible value for *new(T)
.
This proposal would add a second meaning for _
: as an lvalue it means “the top of the expression lattice”, and as an rvalue it means “the top of the zero-value lattice”. Those are very different meanings.
With the meaning of “top of the expression lattice”, then @ianlancetaylor's example of v == _
should mean “v
is equal to any value”: that is, it should mean true
. On the other hand, with the meaning of “top of the zero lattice”, then it should mean “v
is equal to the meet of the type of v
and the top of the zero-value lattice”: that is, it should mean v == 0
or v == nil
or similar.
So, under this proposal, in order to determine what _
means, you would have to first determine whether it is an lvalue or an rvalue. I am aware of only one other place in the language where the meaning of an expression changes depending upon whether it is an lvalue or an rvalue (https://github.com/golang/go/issues/20660#issuecomment-367781440), and I think it's confusing there too.
If we're going to choose some identifier to mean “any zero-value”, I don't think we should use one that already means “any value at all”.
@dantoye I did not mean to suggest that there is any ambiguity or conflict. I meant to say that if we permit if v == _ { }
while also permitting v = _
then we are creating another instance of https://golang.org/doc/faq#nil_error, with _
taking the place of nil
.
The only place where this will really be used is in a return statement of a function with multiple results. In that particular case, it may be more convenient for the writer of the code to be able to write just '_'`s, but readability is definitively not improved. It gets worse if there are many return values. If there are few return values, it is often better to write out the result values.
Finally, there is still the option to use a naked return which is actually better if there are many return values because of the documentation of the return values in the signature.
@griesemer
If there are few return values, it is often better to write out the result values.
If you use a return type from a different package it might not be worth to investigate that certain type
For example:
func foo() somepackage.Type
In this case, the developers of the current package should not be concerned with what that type represents. It might be a type definition like type Type string
with some exported consts; as a developer you would have to check that type (or an arbitrary number of type definitions that lead you to the true type) and determine that final type's zero-value to finally write your code.
Finally, there is still the option to use a naked return which is actually better if there are many return values because of the documentation of the return values in the signature.
What happens if you actually assign a value to a return variable?
func foo() (i int) {
i, err := bar()
if err != nil {
return
}
return
}
Maybe some 3rd party library bar
(incorrectly) sets i
on error. As a developer I would be inclined to write return 0, err
on error, but that leads us to my first point.
(opionion) Regarding redability, if this kind of a feature would be accepted and people would know about it, I think it wouldn't be very hard for us to understand when we see return _
(or any other marker instead of _
).
Nevertheless, I see no significant difference of readability between say return 0, nil, false
and return _, _, _
. If you have a path where you know you have to return zero-values for certain variables/positional-returns, does it matter that much what type they were?
P.S. Why was this issue closed?
@bogatuadrian The issue was closed because we decided against adopting this proposal.
Most helpful comment
As a consideration, a different language proprosal (#12854) for stronger type elliding may solve what this proposal is trying to do.
Returning the zero value is only painful for structs. The proposal in #12854 would allow the following: