proposal: spec: add untyped builtin zero

I propose to add a new predeclared identifier zero that is an untyped zero value. While nil is an untyped zero value restricted to chan/func/interface/map/slice/pointer types, zero would be an untyped zero value with no such restrictions.
The specific rules for zero mimic nil with fewer restrictions:

zero is assignable to any variable of any type T, including when T is any type parameter.
Any value of any type T may be compared to zero, including when T is any type parameter (not just comparable ones).

That's it. That's all the rules.
Note that assignability includes function arguments and return values: f(zero) and return zero, err are valid.
Note also that type switches must still say case nil (not case zero), and zero is not an untyped constant: you cannot write 1+zero. However, you can write 1+int(zero) if you insist (1+0 or 1+int(0) would be more idiomatic.)
See CL 509995 for exact spec changes.

This proposal addresses at least three important needs:

Referring to a zero value in generic code. Today people suggest *new(T), which I find embarrasingly clunky to explain to new users. This comes up fairly often, and we need something cleaner.

Comparing to a zero value in generic code, even for non-comparable type parameters. This comes up less often, but it did just come up in cmp.Or (#60204).

Shortening error returns: return zero, err is nicer than return time.Time{}, err.

More generally, the zero value is an important concept in Go that programs currently have no name for. Now they would: zero.
As far as idiomatic discussion and naming, I expect that zero will only be used for these general uses and will not displace nil as a more specific kind of zero value. In particular, we will keep using terms like nil pointer and nil interface; we will not switch to saying zero pointer, zero interface, and so on.
There is confusion today between nil pointers and nil interfaces containing nil pointers. Adding zero as an alias for nil in those two contexts will not improve but also should not worsen that confusion: the source of the confusion is a (somewhat unavoidable) detail of interfaces, not a property of the names.

I'm not claiming any originality in this proposal. Others have certainly suggested variants in the past, in quite long discussions. I'm not aware of any precise statement of the exact rules above, but I won't be surprised if one exists.
A brief comparison with earlier proposals:

#61129 proposes a builtin iszero(x). This proposal uses x == zero instead.
#60695 proposes a generic zero function: zero(T) is a zero of type T. This proposal uses plain zero which enables use in comparison and avoids a usually unnecessary (T). In non-assignment, non-conversion contexts where a type must be written, this proposal uses T(zero), which seems more idiomatic to me at least.
#35966 proposes an untyped zero value spelled {}. Later discussion in also considered zero.
#19642 proposes an untyped zero value spelled _.

Assuming that you prefer zero over _ in terms of the syntax for a zero value or predeclared identifier, could you expand on your reasoning?
I personally find zero to be clearer and consistent with nil, although I admit that return _, _, err is shorter and feels nicer than return zero, zero, err. It also mirrors _, _, err := foo(), for example.

As far as idiomatic discussion and naming, I expect that zero will only be used for these general uses and will not displace nil as a more specific kind of zero value. In particular, we will keep using terms like nil pointer and nil interface; we will not switch to saying zero pointer, zero interface, and so on.

I would really like for the spec and builtin change to include guidance on this. That is, I assume we want "idiomatic Go" to not replace most uses of nil with zero, except perhaps where it helps with consistency, like rewriting return nil, time.Time{}, err into return zero, zero, err.

What are the advantages of adding zero over removing the restrictions for nil?

except perhaps where it helps with consistency, like rewriting return nil, time.Time{}, err into return zero, zero, err

FWIW, I would still prefer https://github.com/golang/go/issues/21182 here (return ..., err). I think that it is my single favorite open proposal. (Although I'm also partial to 128 bit ints. :P)

I am not a super smart guy but, I do think explaining *new(T) = *&struct{} is useful and not awkward at all, I come from strong C background, I think everyone who is new should learn what "*" or "&" stands for the avoid incorrect usage, some codebases actually pass **type or ***type without realizing because of this lack of knowledge.
Secondly, I think standard cmp package shouldn't be reasoning behind any change because we as developers now going to bump into same package names with 1.21 (because we use go-cmp primarly for our testing and IDEs will show up now, did you mean this or that etc), I am personally not happy with 1.21's cmp package
Thirdly, I think explaining zero as a concept over struct{} will be harder for everyone and in most cases, we never return allocated struct and error at the same time, it should be the dev who is assigning to a default when error occurs, not the other way around. I always never liked returning struct{}, err or nil,nil
apologies for the noise from me

I prefer zero to _ because it would be weird to say if f == _.

FWIW I don't have strong opinions about how to spell a universal zero value - I'm fine with any color for that bikeshed.
I do think this proposal (or this proposal with s/zero/_/g) has the advantage of addressing several semi-related issues with a single, easily understood mechanism.
@mrwormhole

Secondly, I think standard cmp package shouldn't be reasoning behind any change […]

The justification isn't the cmp package, it's that we need a mechanism to compare values against their zero value, even if their constraint is not comparable. That is always possible, but there currently is no way to implement that. The cmp package is one consumer of such mechanism, but not the only one. Personally, I ran into this with a container type wrapping map, where I would have preferred not to store zero-values, as they take up memory without carrying any semantic benefit for my use case, where "not stored" and "the zero value" where semantically equivalent.
I think it's fair to criticize arguments like the explainability or readability of *new(T) for their subjectivity. But this particular problem has no solution without a language change.

this proposal would also satisfy #26842.
@rsc would accepting this proposal also involve changing cmp.Or to accept any type and use == zero or would that need to be a separate proposal?
@josharian #21182 could be additionally accepted. It would be less needed than it is now but there's still an argument to its utility and if it were accepted instead of this proposal there'd still be a need for the additional functionality contained in this proposal.

I like this a lot. I also like _ for the universal-zero.
What I almost want is for zero to be a valid value for every non-pointer value, but not for pointers, which need nil. I say "almost" because in some contexts, especially generics, I don't know whether a value happens to be pointer-ish.
What I really want is "zero is a universal zero that you can use except when you know you are thinking about a thing in pointer terms, in which case you want nil". Although thinking about it more, at least two cases where I currently use nil (slices, maps), I think "zero" would be comparably/similarly expressive.
I am now very conflicted on whether I think it's more consistent to call the zero value for a slice zero or nil in such a case. I definitely prefer nil for pointers, though.

Okay but thinking about it more, I have concluded:
I would also be fine with just extending nil to all types, including non-pointer types. I'm not actually going to be particularly confused by seeing return nil, err in a function returning a non-pointer type for any length of time, we already have the word, and it's good at expressing "i don't actually want/need a value here".

Could it be just 0 instead of an identifier? (return time.Time{}, err -> return 0, err)

@seebs I think it would be interesting to allow null for pointers and no other types. But I don’t think that addresses a pressing problem in the same way. It’s more of a “if I wrote Go 2” idea.

A linter to complain about zero where nil could be used instead seems like it would help preserve existing idioms.

would accepting this proposal also involve changing cmp.Or to accept any type and use == zero or would that need to be a separate proposal?

cmp.Or hasn’t been merged yet (it’s on hold until 1.21 is released), so I think it could just change to T any without a discussion.

Could it be just 0 instead of an identifier? (return time.Time{}, err -> return 0, err)

It might be funny business if 1-1 and 0 behaved differently :)

Why won't case zero work?
What guidance do you propose to give for when 0 vs. zero and nil vs. zero should be used? In other words, what would be idiomatic? For example, for func F() (int, string, time.Time, error), should it be return 0, "", zero, nil or return zero, zero, zero, zero?
I'm concerned by the direction the language is evolving. I see non-orthogonal features like this being added instead of existing features being generalized. There is already a zero value in Go: nil. If you lump all the built-in number types together, most built-in types have a nil value. Numbers, strings, arrays, and structs are the exception. Instead of adding something new to solve this one problem, let's generalize what we already have: make nil work for all built-in types. This has been proposed many times by many people. I'm disappointed that this proposal didn't address why this obvious solution won't work. My vote is no until it's changed to do so.

FWIW I don't have strong opinions about how to spell a universal zero value - I'm fine with any color for that bikeshed.

As an aside, I don't agree that coming up with a good name is bikeshedding. Naming is often characterized as one of the two hard things in computer science. Whether or not you agree with that quotation, naming is indeed important, and entirely relevant to the quality of a software design, as opposed to a nuclear power plant design committee getting derailed by the paint color for a bike shed.

I would write return 0, "", zero, nil unless it became idiomatic to do something else

@willfaught

Why won't case zero work?

Currently, it works because case nil is special cased in a type-switch. If we'd want case zero to work, we'd have to similarly special case it (note that in a type switch, the regular "check for the zero value" logic doesn't work, because the cases are types, not values). There seems to be no compelling reason to do so, especially if we generally advice to use nil, if it works.

I feel like zero is a great name because it results in a bit pattern of just zeros following assignment, and zero'd bit patterns are already so fundamentally baked into the language. Occasionally it is just easier to think about the bits.

Some have favored removing the restrictions on nil instead of bringing in the zero to Go. I'd be curious on hearing arguments against it. So far comments in here and on the slack performance channel on it have all evolved from uncomfortable feeling to quiet acceptance.
So often have I refactored return myType{}, err to return nil, err when really there was no semantic difference in my program.

@willfaught

As an aside, I don't agree that coming up with a good name is bikeshedding. […]

FWIW there is more to the bikeshed analogy than just the importance of the question. But, in any case, I was merely trying to express that it doesn't matter to me. I want something to happen and I find iszero(T), zero, _, nil… all satisfying, personally. I would support any of them. I just strongly oppose doing none of them, because we can't agree on how to spell zero.

So, I think the messiest aspect of this is roughly the behavior in ambiguous contexts. If you're returning an interface value, then return concrete{} and return zero are not the same.
I could see a hypothetical benefit to a zero being distinct from nil if zero were restricted to needing a concrete type. So, it can be type-inferred, but if the inferred type is an interface, that's an error. Thus, you'd have to do something like return concrete(zero) to return an (interface wrapping) a zero-valued concrete type, and nil to return a nil interface.
Right now, if you are actually returning an interface type, and you have a lot of return &concrete{...} in the function, it's easy to miss that return nil is not the same thing as return (*concrete)(nil) in this context.
So I think zero could be worth it if it added that. Otherwise, just allow nil to be used as a generic zero even for not-at-all-pointery things, and everything's solved. No existing code breaks, no worries about namespace.

it's easy to miss that return nil is not the same thing as return (*concrete)(nil) in this context.

Returning (*concrete)(nil) is, in my experience, almost always a bug. It even has an FAQ entry: https://go.dev/doc/faq#nil_error. So making it easier to do that doesn't seem like a priority to me.

There is confusion today between nil pointers and nil interfaces containing nil pointers.

I think if zero does not address this, then I little point of having zero over simply relaxing nil. I in fact would prefer if err == zero to mean that I would like err to be a nil pointer of any interface, not just nil interface. But the proposal above does not propose that, so then why we just do not relax nil?

@mitar
relaxing nil causes confusion, for instance 0==nil . But 0==zero is acceptable.

relaxing nil causes confusion, for instance 0==nil . But 0==zero is acceptable.

I think I would find it easier to explain to novice developers why 0 == nil than why typed interface of nil pointer is not nil. And literally, nil means zero:

Nil means the same as zero.

So why 0 == nil is pretty clear. It is the same.

That's why I'm in favor of an iszero() builtin function and no zero otherwise. It fixes the one thing that is not possible today and avoids the other questions.

@fzipp if primary objective of this proposal is for comparison then iszero makes more sense as I find  iszero more readable that v==zero or v==nil.
Reqouting what's mentioned in the above threads, zero does cause ambiguity- err==zero , err==nil ;
Assigning zero values only makes sense for generic functions. Cuz for normal functions since the type is known the code looks cleaner and retraceable with current standards.
But in case of generic functions, we can still do this
func X[T any]() {
	var zero T
	return
}

Nil means the same as zero. So why 0 == nil is pretty clear. It is the same.

I would disagree. For these many years, the default value of int was 0. If u say now that its nil . I find it hard to accept.

Some have favored removing the restrictions on nil instead of bringing in the zero to Go. I'd be curious on hearing arguments against it. So far comments in here and on the slack performance channel on it have all evolved from uncomfortable feeling to quiet acceptance.

I think it would lead to more confusion in cases like np != nil && *np == nil. It's less confusing that np != zero && *np == zero might be true.

But in case of generic functions, we can still do this
func X[T any]() {
	var zero T //use this for assignment
	return
}

That works fine for return values, but it doesn't work for t == zero unless you have [T comparable].
I would almost be fine with saying you can't use zero outside of generics, except it's sort of an arbitrary restriction. I'd be happy though if something like go vet or a linter were to complain about it.

@carlmjohnson

I think it would lead to more confusion in cases like np != nil && *np == nil. It's less confusing that np != zero && *np == zero might be true.

Did you mean to write np != nil && *np == zero, which AIUI would be the suggested idiomatic code under this proposal? Because as written, both seem exactly the same level of confusing.

That works fine for return values, but it doesn't work for t == zero unless you have [T comparable].

@carlmjohnson I think we can agree to the  point that introducing zero is just for zero checks . So in that case why don't we handle it heads on with iszero() .

Maybe gofmt should rewrite zero to nil when it can. I think it doesn't break anything (all nil are zero, not all zero are nil)? If it doesn't break anything, I'd rather not think about it much.

Lots of feedback, thanks. A few responses:

Re expanding nil, I think accidents like var x int = nil, or writing f(nil) when f takes an integer, or writing x == nil when x is an integer, or writing *x == nil when x is a *int but *x is an int, are mistakes that are worth continuing to diagnose. So I am not in favor of allowing nil to mean any zero anywhere. In C, NULL is defined literally as 0, and it is easy to make mistakes like that. In Plan 9 C our standard headers said #define nil ((void*)0), and it caught plenty of mistakes where you wrote nil and should have written an integer; NULL would not catch those, and nor would an expanded Go nil. Types are good, and we should continue to take advantage of them.

Re gofmt, gofmt cannot rewrite zero to nil since it does not have type information. I don't believe this would be a great idea anyway: gofmt is not about enforcing that kind of semantic style.

The reason for zero instead of _ is that x == zero reads nicely while x == _ is confusing.

To people who like *new(T), all I can say is I'd personally be embarrassed to get up and tell people that's the best we can do. We can disagree of course.

Yes, we'd change cmp.Or to take T any not T comparable.

None of this precludes #21182 from also happening.

Regarding restrictions about "only use zero for certain types", it is important to allow zero for type parameters, to enable things like cmp.Or. And if we allow it for an arbitrary type parameter that might have type T, I have a hard time saying it's not allowed for concrete values of type T. You should be able to copy-paste a generic function body and substitute concrete, non-generic types and have the code both compile and preserve the meaning of the generic instantiated with that concrete type. That is, "hand instantiation" should work, as a matter of orthogonality and simplicity. Restricting zero but allowing it for type parameters breaks "hand instantiation".

I feel like zero is a great name because it results in a bit pattern of just zeros following assignment, and zero'd bit patterns are already so fundamentally baked into the language. Occasionally it is just easier to think about the bits.

Nil literally means zero, so in my opinion, nil is just as good a name in that respect; but it's even better in that it already exists for the majority of built-in types.

The only thing that is not possible today is the zero value comparison, at least not without reflect.

Returning a zero value with abbreviated notation hasn't been possible, which was a pain point, which was addressed in the proposal.

relaxing nil causes confusion, for instance 0==nil . But 0==zero is acceptable.

There's no confusion if you understand the language. Nil would be the zero value for the type, and through assignability, the type of nil in 0==nil would be int, and thus its value would be 0. The ship for avoiding the complexity of nil, assignability, and conversion sailed a decade+ ago. We might as well embrace it if we can get additional expressiveness out of it, in my opinion. We've embraced other parts of the language that are suboptimal just because they're already in the language, and the Go team didn't want to having feature overlap (e.g. interfaces and assertions vs. sum types).

I would disagree. For these many years, the default value of int was 0. If u say now that its nil . I find it hard to accept.

Nil would equal 0 for int types. In other words, it's a synonym, similar to how rune is a synonym for int32. When nil is converted to int, its meaning is 0, as in all-zero bits. It's simple, in my opinion.

I think it would lead to more confusion in cases like np != nil && *np == nil. It's less confusing that np != zero && *np == zero might be true.

nil would be synonymous with all-zero bits, so it seems the same to me. The meaning of zero would be just as dependent on context as nil in your example. In all contexts, the meaning of nil/zero depends on its type. It's unsurprising that p and *p would both be comparable to nil, if nil were the zero value for all types. Same for zero.

Did you mean to write np != nil && *np == zero, which AIUI would be the suggested idiomatic code under this proposal? Because as written, both seem exactly the same level of confusing.

If I understand correctly, he was demonstrating what it would look like if nil was relaxed, where np is a pointer type, which would be comparable to nil, and the pointer's element type would also be comparable to nil (since nil would be the zero value for all types). So his code is correct, if that's the case. If this proposal were adopted instead, his code would be np != zero && *np == zero, which is basically the same thing.

I think we can agree to the point that introducing zero is primarily for zero checks . So in that case why don't we handle it heads on with iszero() .

See above.

Re expanding nil, I think accidents like var x int = nil, or writing f(nil) when f takes an integer, or writing x == nil when x is an integer, or writing *x == nil when x is a *int but x is an int, are mistakes that are worth continuing to diagnose. So I am not in favor of allowing nil to mean any zero anywhere. In C, NULL is defined literally as 0, and it is easy to make mistakes like that. In Plan 9 C our standard headers said #define nil ((void)0), and it caught plenty of mistakes where you wrote nil and should have written an integer; NULL would not catch those, and nor would an expanded Go nil. Types are good, and we should continue to take advantage of them.

var x int = nil wouldn't be an accident, that's the point. Nil would be the zero value for all types. It's valid to use nil everywhere where the zero value is required. If you want to use special syntax for a particular type to refer to its zero value, like 0 or "", then great, but it's not required. Wherever nil appears, look at its type to understand which value it represents through assignability or conversion.
The rest of your Go examples aren't mistakes either. They would type-check and compile. There would be no problem to diagnose. x==nil for integers isn't currently valid, so new programs that use it within the new semantics would be valid, without a need to correct or diagnose.
Go isn't weakly typed, so I don't see how your C example applies. As shown above, x==nil for integers wouldn't be an error requiring a failure or diagnostic. If you don't want the zero value for int, then don't write nil. If you write nil with an int type, then you'll get 0. It's really simple.
I agree with the rest of your points.

accidents like var x int = nil, or writing f(nil) when f takes an integer, or writing x == nil when x is an integer, or writing *x == nil when x is a *int but *x is an int

Then I think nil and zero should be disjoint. So zero should be valid only for non-chan/func/interface/map/slice/pointer types. Otherwise you can have accidents like var x *int = zero or writing f(zero) where f takes a pointer, or writing x == zero when x is a pointer, or writing x == zero when x is a *int but *x is an int.
Or are the accidents you listed problematic but the accidents I have not?

Go isn't weakly typed, so I don't see how your C example applies. As shown above, x==nil for integers wouldn't be an error requiring a failure or diagnostic. If you don't want the zero value for int, then don't write nil. If you write nil with an int type, then you'll get 0.

I think he meant that you might write x = nil when you meant to write *x = nil (with claim being that *x = zero is clearer in the current proposal). You just made a null pointer and then the next time you do *x you get an error. That is an accident.
What I do not buy in that argument is that you could (with current proposal) easily do x = zero when you meant to do *x = zero and have the same accident. I think if we want to prevent such accidents, zero and nil should be disjoint. So that x = zero is invalid if x is a pointer.

@willfaught

The rest of your Go examples aren't mistakes either. They would type-check and compile. There would be no problem to diagnose.

The fact that those examples would type-check and compile is the problem, if they aren't what the programmer meant to write. That's the kind of mistake that strong type systems are usually good at catching. Extending nil to all types makes Go's type system less good at catching that – although exchanging zero for nil would have the same problems, and could at best be caught by convention rather than model checking.

Why not add a simple builtin function isZero(any) bool rather than a new identifier? Isn't that a cleaner way to satisfy the requirement and still keeping codebases consistent?

If the main issue with making zero and nil disjoint is that func F[T any](v T) bool { return v == zero } wouldn't be possible, then maybe it's best to introduce a new built-in function after all? Go 1.21 is introducing three new built-in functions primarily for dealing with NaNs correctly, so a built-in to check zero status of a value regardless of of it's zeroable or nilable doesn't seem so out of place to me anymore.

Go 1.21 has supported return type inference, so why not add a builtin func zero[T any] T function instead?
It does need two more characters in uses, but it has not the overlapping problem brought by a zero builtin identifier.
BTW, without considering the zero() way, I prefer the extended nil way over the new zero identifier way.

... or writing x == nil when x is an integer, or writing *x == nil when x is a *int but *x is an int, are mistakes that are worth continuing to diagnose ...

This is not a new problem brought by the extend nil. The problem has already existed for the current nil for containers, such as *ptrToNilSlice == nil and *ptrToNilMap == nil.

I do wonder if this needs to live in the stdlib? Implementing
func isZero[T any](val any)

and
func zero[T any]() T

feel trivial in userland.

Additionally, I am failing to understand why we are considering zero and nil to be disjoint? Isn't nil the zero value of pointer types(and a few built-in types) from a users perspective?

func isZero[T any](val T) bool and func zero[T any]() T feel trivial in userland

isZero[T any] can't use ==, and isZero[T comparable] can't accept non-comparable types. Similarly if val == zero[T]()... depends on whether T is comparable.

Maybe we should take a step back and look at the bigger picture. Why do some Go programmers want to compare against the zero value of a data type so badly? It means they want to assign a special meaning to it and treat it differently from other values. They are essentially using an in-band value of a data type as a sentinel value. Is this a good practice? Wouldn't it be better to have a way to express the non-existence of a value instead? I fear that this proposal or other suggested variants would encourage the practice of assigning special meaning to zero values.

@gophun I strongly disagree with that chain of argument. The example I brought up here didn't treat the zero value as a sentinel or a stand-in for "non-existent" at all. It was a bog-standard value.
I also don't understand how you imagine "a way to express the non-existence of a value", on an implementation level. ISTM there are three ways that can be implemented:

By using a pointer, with nil meaning "non-existence". That is very cache-inefficient.
By pairing it with a boolean to store validity (like sql.NullString etc). That is bad for alignment, adds significant storage overhead and is thus again bad for caches.
Designate one of the legal values of a type to mean "non-existence". That's pretty much this proposal. It essentially designates the zero value that way.

Also, no matter which of these you choose - in all three of them, you end up with the semantic of "the zero value means there is no value" and ISTM that any such design would have to have that semantic: You definitely want var x T to be the "non-existent" value, if there is one.
Obviously, there are types for which the zero value does not naturally imply "there is no such value". But those can always choose to go with one of the first two strategies, even under this proposal - and they will work fine with code that compares against the zero value.
So, really: Even under that chain of argument, ISTM that this proposal is the natural point to arrive at.

Thanks for the continued feedback. I am sensing maybe a little bit of urgency to get their points across from some posters, so let me try to defuse that a bit. This discussion is not going to resolve in the next couple days - it hasn't even appeared in the proposal minutes yet.
Here are some responses to recent comments.

@atdiar asked about F(zero) where f takes an unexported type. That will work, just like F(0) does where F takes an unexported integer type, or F(nil) does where f takes an unexported slice type. In general using unexported types in an API does not provide any kind of hard guarantee about users not being able to construct them. In particular users can already write:
func ZeroFor[T any](f func(T)) T {
	var zero T
	return zero
}

F(ZeroFor(F))

and a variety of other similar programs.

@willfaught replied about my "mistakes" being valid programs if we make nil be a universal zero, which is exactly my point. Being valid programs does not preclude them from being "mistakes" (i.e., not what the user intended). Google once had a significant data loss (covered by our redundant systems) because someone wrote something like (abstracting a bit) 'if(deleteEverything)' instead of 'if(*deleteEverything)' in a C++ program, where deleteEverything was a pointer-to-bool and the pointer was non-NULL but false, while the code interpreted it as true. "If you don't want the zero value for int then don't write nil" amounts to "don't make mistakes". Mistakes happen (at least in my experience), and type systems are useful for finding them. Expanding nil to scalar types would stop catching real mistakes, because they would no longer be type errors. (Thanks to @zephyrtronium for replying to that effect as well.)

@go101 pointed out that mistakes involving pointers to chan/func/map/pointer are already not caught, but that's not a good reason to catch even fewer mistakes.

@fzipp pointed out var zero T instead of *new(T), and yes I think that's the best we can do today (although some people clearly love *new(T)), but as soon as you have two different generic types to make zeros for now you have to write things like
var zeroK K
var zeroV V
return zeroK, zeroV, false

And to me at least that makes clear we have a problem. We can disagree of course, but most people I've spoken to about this believe there's a problem worth solving here: users shouldn't have to puzzle through one or more of these workarounds.

A few people, including @AyushG3112 and @DeedleFake, suggest looking again at iszero, but to me that's a non-starter. We already have a way to test for equality to a zero value for many types, and that's == 0 or == nil. To fit into the existing Go language, any solution here should look like those. @AyushG3112 also suggested writing isZero as a library, but that's not possible, at least if you want it to handle funcs, maps, slices, and structs containing those. (@AndrewHarrisSPU pointed this out too.)

@gophun asked why we care about the zero value at all, and @Merovius already answered. I will just repeat what I said above, which is that "the zero value is an important concept in Go that programs currently have no name for", at least in the general case. @gophun seems to be arguing that maybe it shouldn't be such an important concept. That's not a viable argument - we're not going to change this fundamental detail about Go 15 years in.

@mitar wrote a pair of posts about zero and nil being disjoint, and @AyushG3112 mentioned it too. Keeping zero and nil disjoint might well help catch mistakes. There is a tension between mistake-catching and the general rule that what's valid in generic code is what's valid in non-generic code with valid types substituted, and vice versa. The proposal as it stands today resolves that tension in favor of generality. I do see the arguments for resolving it the other way too, saying that zero is only applicable to arrays, structs, and type parameters. We will have to think more about that.
There is also a tension between mistake-catching and readability. Obviously having to write zero(T) or zero[T]() would also "catch mistakes", as would disallowing the use of := and the var x = expr forms (requiring explicit types in any variable declaration). There is a balance to strike, and in general we want the balance to be consistent with the parts of Go that have come before, so that Go still feels like a coherent whole. Omitting the T in zero(T) or zero[T]() seems consistent with untyped nil, with untyped constants, and with allowing the type to be elided in := and var x = expr.

FWIW it might be helpful (for the "adding zero" vs. "expanding nil" discussion) to emphasize that the intended outcome of this proposal is that people continue to use nil if they know something is a pointer/interface/map/channel, at least for the most part.
That is what I said here. It would be legal to write p != zero && *p == zero, but it would be idiomatic to write p != nil && *p == zero. So idiomatic code would be protected by the type-checker from at least some mistakes. If we expanded nil comparisons/assignments, it would be both legal and idiomatic to write p != nil && p == nil, which is not protected by the type-checker in the same way.
I think that's the primary difference between "adding zero" and "expanding nil" and arguments that rely on both of these making it legal to write traps miss that argument of convention.
We might be able to nudge/enforce these conventions with vet or other static checks, at least for some common cases.

@rsc

A few people, including @AyushG3112 and @DeedleFake, https://github.com/golang/go/issues/61372#issuecomment-1636993251 https://github.com/golang/go/issues/61372#issuecomment-1636994974 again at iszero, but to me that's a non-starter. We already have a way to test for equality to a zero value for many types, and that's == 0 or == nil. To fit into the existing Go language, any solution here should look like those.

Just to add another option to the mix: It would be possible to expand nil but only to comparisons. That is, we could say "x == nil is always allowed and is true, if x is a zero value". That would address the same problem as iszero and would look like the existing ways. It also would not introduce another predeclared identifier and it would avoid the type-checking pitfalls of making nil assignable to anything.
The downside is that you could then write x == nil even if x = nil would be invalid. It also does not address construction of zero values.
(Yet Another Compromise would be to expand comparison to nil, but only to type parameter typed variables, which is the only place where it is not yet possible to write anything)

Instead of focusing on the upsides/downsides of expanding nil, what are the downsides of adding zero? It’s a new predeclared identifier (con). It can be used in confusing ways where nil is more appropriate (con). Are there any other cons?

Rather than adding an identifier for zero value, does it make sense to add a "comparable to nil" constraint for slices, channels, maps etc?
That'll allow something like this to work
func isZero[T comparable | nilcomparable](val T) bool {
	nc, ok := val.(nilcomparable)

	if ok {
		return nc == nil
	}

	var zero T
	return val == zero
}

@AyushG3112 This is not going to work with custom struct types (with slices, maps etc. fields)

@AyushG3112 interface{ comparable | nilcomparable } is not legal, as you can not have comparable in a union (and nilcomparable would likely have the same problems comparable has for that). The reasons are complicated, but it's not something we're going to fix in this issue.

@Merovius @mateusz834 yes I know, what I'm saying is whether we can add support for this in the language over the zero identifier?

@AyushG3112 And what I responded is "no, we can't for a bunch of reasons, among them the fact that comparable can not happen in a union". Those reasons are complex. We won't be able to litigate them out of existence as a side-note in this issue.

I don't mind the idea of a "zero" identifier as long as it's constrained to comparison. I just don't like the idea of being able to return it or use it as an actual value because

It introduces 2 different ways of doing the same thing - for example return time.Time{} and return zero will now be equivalent with no proper language guidelines on what is idiomatic and why.
It makes it easy to gloss over what the value being returned or passed actually implied or means to the consumer over when you explicitly instantiated it.

I do find return time.Time{} easier to read and comprehend  that return zero.

Its more intuitive
Enforces strong typing. Less error prone to accidental type changes.

I don't think we can use zero for zero checks due to its conflict with nil resulting in ambiguity and confusion.
Perhaps we can limit zero to generic functions.

@Nasfame consider var x struct { f, g func() } No generics involved but there is currently no way to check if x is its zero value currently aside from x.f == nil && x.g == nil. Under the proposal you could simplify that to x == zero.
Also every case where this is useful in generic code holds for generated code where the code generated doesn't involve generics. Being able to always write return zero, zero lets a code generator not have to worry about a lot of special cases.

@fzipp

if v == zero reads better than if iszero(v)

I disagree. I use zero checks and non-zero checks very often that I occasionally messes up if err==zero and if err!=zero.
iszero does alleviate the pain a bit. For one its faster to type, and  easier to read , and extend that the 1st option without the parenthesis.

@jimmyfrasche var x struct { f, g func() }
is very niche. Its pretty unclear why a developer would consider zero checks for this custom type. At best, it makes sense to do a nil check before calling the methods f or g

Being able to always write return zero, zero lets a code generator not have to worry about a lot of special cases.

@jimmyfrasche
You can still accomplish this- return zero, zero
func F() (x int, y int) {
	return
}

@fzipp

Doesn't that mean that you have a general difficulty with equality / inequality in C-style programming languages?

I disagree on that front.

Unnecessary uses of iszero(x) or x == zero/return zero/etc. are all completely trivial things to lint. It would be very easy to mechanically enforce only using these where they are the only option.

consider var x struct { f, g func() } No generics involved but there is currently no way to check if x is its zero value currently aside from x.f == nil && x.g == nil. Under the proposal you could simplify that to x == zero.

Memoization or some similar things jumped to mind, it's narrow but in the right spots a huge win - I think x == zero would be really helpful for things like this.

if !iszero(err) instead of if err != nil in Go. This possibility is a reason against iszero() in my book.

what's wrong with that? Is it the flexibility ?

what's wrong with that? Is it the flexibility ?

All codebases use err != nil as an idiom for checking errors. Bringing in a different way is dissonant to long time gophers and at best confusing for new gophers learning the language as there would be two different ways of doing the same thing.
That said, I think it's very unlikely that people would start using !iszero(err) since most IDEs have static analyzers that could warn against this usage favoring the all too established err != nil.

I could write func no(err error) bool { return err != nil } and then use if no(err) { all over my code.

@soypat I agree. Even I am likely to use err!=nil as I have been using it all these years.

lol, oops. The dangers of Sunday-posting!

Can I kindly suggest to let this issue cool down for a couple of hours? I don't believe this back-and-forth is going to move many viewpoints and it is prone to sniping, repetitiveness and moving off-topic.

func isZero[T any](P T) (check bool) {
	return P==zero
}

This is good!, It does make sense.
Bad Code
The reason we can't permit usage of zero in non-generic function
func iszero(P any) (check bool) {
	return P == zero
}

func isZero[T any](P T) (check bool) {
	return P==zero
}

This is good!, It does make sense.
Confusing Code
The reason we can't permit usage of zero in non-generic function
The Ambiguity
func iszero(P any) (bool) {
	return P == zero
}

func iszero(P any) (bool) {
	return P == nil
}

func isZero[T any](P T) (check bool) {
	return P==zero
}

This is good!, It does make sense.
Bad Code
The reason we can't permit usage of zero in non-generic function
func iszero(P any) (check bool) {
	return P == zero
}

I don't understand the paranoid regarding the possibilities to do the same thing in slightly different manners. While it is slightly inconvenient in the presence of inconsistencies, the differences among x == 0, x == zero or x == nil is the least thing to be worried about. They are just different names to the exact same thing, they are exactly the same, just like I can declare a variable with any name I like and it would not in any way raise the question of why this variable is named indices and not indexSlice. Languages are powerful, there are always different ways to do the same thing in any programming language, or else it will be useless. In Go we have math.Min and built-in min to get the minimum of 2 floating point numbers with mostly-the-same-but-slightly-different semantics, var zero T, *new(T) or even a custom-made zero[T]() to get the zero value in a generics-compatible way, etc.
Regarding this specific proposal, the most important point being addressed is the ability to compare any variable against its type's default value in a generics-compatible manner. If the construct is not allowed to appear in non-generics code, then the restriction itself would be an inconsistency, and the simple mental model of generics being a copy-and-paste solution for parametric polymorphism is violated. On the other hand, if the construct is allowed in non-generics code, then it would inevitably lead to different ways to compare a variable against zero, regardless of the construct being a == nil, a == 0 or isZero(a).
In conclusion, my point is that there would inevitably be inconsistencies if we want the most important issue to be addressed, but different spelling of the default variable is the most minor thing that needs to be concerned with and it should not interfere with the decision of including this feature in the language.

I'm wholly favor of this proposal, @rsc is spot on when he says:

the zero value is an important concept in Go that programs currently have no name for. Now they would: zero.

Full disclosure, I'm the author of one of his linked related proposals (https://github.com/golang/go/issues/60695).

Given the choice between:

A new predeclared identifier zero that is a superset of nil
Relaxing the constraints for nil to be defined as the untyped zero value for any type

The second option makes more sense because it simplifies the language, which I’d argue is in the spirit of Go. Given that nil already means the zero value for non-pointer types, the author or reader would not have to consider whether the type is pointer-ish when assigning or comparing to the zero value.
Sure, var s string = nil feels weird today, but does it feel more weird than uppercase exported symbols or gofmt? Would it feel weird in 6 months or a year?

Would it make sense to try and distinguish zero from nil instead of making it a superset of nil? That zero is for zero-values of values types: structs, ints, floats, complex, strings, bools, and so on. Therefore if assigning it to a pointer type, it would create a new zero non-nil pointer?
Ie:
var x int = zero // x == 0
var x *int = zero // x == new(int)

It's not so much that I am convinced that this is a great idea, but that the only issue I see with this proposal is the overlap between zero and nil. This is just an attempt at preserving as much orthogonality in the language design as possible.
This would ensure that zero values are non-nil.
The big downside would be that the following would be confusing:
var x, y *int = zero, zero
x == y // false

Just food for thought.

"the zero value is an important concept in Go that programs currently have no name for", at least in the general case. @gophun seems to be arguing that maybe it shouldn't be such an important concept.

I didn't mean to dispute that the zero value is an important concept in Go. The zero value as a default for variables is great to avoid questions about uninitialized memory, and the rule to make the zero value useful is great, too. Using it as a throwaway value in return SomeStruct{}, err is fine, too.
But what bothers me is giving special meaning to the zero value in the form of conditions based on it. The signifier of meaning should be something else, such as the ok in the "comma ok" idiom, or the err in return SomeStruct{}, err, not the zero value.

I keep seeing statements that the primary goal is comparisons, and the suggestion to just add an iszero builtin. But can I ask, why then can't we consider allowing "truthy" tests to be a shorter acceptable equivalent? Instead of x == zero or  iszero(x), why can't we just allow all types to do if x (not zero value) and if !x (zero value).

I keep seeing statements that the primary goal is comparisons, and the suggestion to just add an iszero builtin. But can I ask, why then can't we consider allowing "truthy" tests to be a shorter acceptable equivalent? Instead of x == zero or iszero(x), why can't we just allow all types to do if x (not zero value) and if !x (zero value).

See  https://pkg.go.dev/github.com/carlmjohnson/truthy#example-Value 😃

See https://pkg.go.dev/github.com/carlmjohnson/truthy#example-Value 😃

Right, so that exists via library. And if we are talking about the option of adding zero or iszero() to achieve the same result of testing for the zero value, why can't we consider making the feature of your library a builtin language feature so we can directly test values without the library?

I might miss it in the thread or from the description.
But since zero value will be a universal value for any type, what will we see in this use case?
package main

import "fmt"

func main() {
	val, err := returnZero()
	fmt.Println(val == zero, val == nil, err == zero, err == nil)
}

func returnZero() (any, error) {
	return zero, nil
}

I would expect to see true for all of these conditions.
Could someone clarify please, what do we expect to see there?

Would it make sense to try and distinguish zero from nil instead of making it a superset of nil? That zero is for zero-values of non-pointer types: structs, ints, floats, complex, strings, bools, and so on. Therefore if assigning it to a pointer type, it would create a new zero non-nil pointer?
Ie:
var x int = zero // x == 0
var x *int = zero // x == new(int)

It's not so much that I am convinced that this is a great idea, but that the only issue I see with this proposal is the overlap between zero and nil. This is just an attempt at preserving as much orthogonality in the language design as possible.
This would ensure that zero values are non-nil.
The big downside would be that the following would be confusing:
var x, y *int = zero, zero
x == y // false

Just food for thought.

BTW I think adding zero identifier would break code like this:
	var zero int64 = 0
	objectMeta.SetDeletionGracePeriodSeconds(&zero)

k8s API objects have a lot of pointer fields and &zero is used for initialization

https://github.com/search?q=repo%3Akubernetes%2Fkubernetes+%26zero&type=code
https://github.com/search?q=%26zero+lang%3Ago&type=code

@AlexanderYastrebov thankfully a "predeclared identifier" is different in Go than a keyword, in that it is valid to shadow a predefined identifier.
Your example would therefore still compile and have the same meaning as before, but it would mean that for the remainder of this lexical scope zero has a different meaning than what's defined in the specification (it's now literally int64(0)), which could be confusing for maintainers of that particular function.
I wonder if the compiler could have a special diagnosis for a situation where assignment type checking fails for an identifier named zero that isn't the predeclared identifier, explicitly noting that zero was redefined with a definition location included in the error message.

@AlexanderYastrebov thankfully a "predeclared identifier" is different in Go than a keyword, in that it is valid to shadow a predefined identifier.
Your example would therefore still compile and have the same meaning as before, but it would mean that for the remainder of this lexical scope zero has a different meaning than what's defined in the specification (it's now literally int64(0)), which could be confusing for maintainers of that particular function.
I wonder if the compiler could have a special diagnosis for a situation where assignment type checking fails for an identifier named zero that isn't the predeclared identifier, explicitly noting that zero was redefined with a definition location included in the error message.

Go veg could probably mark this code, but the compiler doesn't do anything about redefining len or any, so I don't see any reason for doing something else here.

@aohorodnyk I agree that all of those conditions should be true.

@ianlancetaylor I think the behaviour of zero can be replicated with this function
func zero[T any]() (zero T) {
	return
}

CC: @aohorodnyk

@justinfx

I keep seeing statements that the primary goal is comparisons, and the suggestion to just add an iszero builtin. But can I ask, why then can't we consider allowing "truthy" tests to be a shorter acceptable equivalent? Instead of x == zero or iszero(x), why can't we just allow all types to do if x (not zero value) and if !x (zero value).

I'd say the same arguments as against expanding nil to be assignable/comparable to any type apply, just more.
Also, FWIW: I wouldn't say that allowing comparisons is the primary purpose of this proposal (though I agree, obviously, that many people say that). It's just the only thing that has an objective value, in that it allows to write code that is not currently possible to write. The other purposes - decrease in verbosity, having a clear and universal name to refer to an important concept - are subjective and harder to quantify, but that doesn't make them less important. I'd say.

will this work for zero as currently == is supported only for comparable types
func SomeGenericFunction[T any](P T) bool {
	var z1 T = zero
	return P == z1
}

will this work for zero as currently comparison operators are only supported for comparable types
func SomeGenericFunction[T any](P T) bool {
	var z1 T = zero
	return P == z1
}

No - while P is variable that comparable with untyped zero,  z1 is an instantiated variable of type T and those are not comparable unless specifically marked as comparable.

@ianlancetaylor I think the behaviour of zero can be replicated with this
func zero[T any]() (zero T) {
	return
}

No - since this code will not work:
func compareToZero[T any](val T) bool {
    return val == zero[T]() // this will not compile
}

will this work for zero as currently == is supported only for comparable types
func SomeGenericFunction[T any](P T) bool {
	var z1 T = zero
	return P == z1
}

It related to my question.

I might miss it in the thread or from the description.
But since zero value will be a universal value for any type, what will we see in this use case?
package main

import "fmt"

func main() {
	val, err := returnZero()
	fmt.Println(val == zero, val == nil, err == zero, err == nil)
}

func returnZero() (any, error) {
	return zero, nil
}

I would expect to see true for all of these conditions.
Could someone clarify please, what do we expect to see there?

In the proposal we see:

Referring to a zero value in generic code. Today people suggest *new(T), which I find embarrasingly clunky to explain to new users. This comes up fairly often, and we need something cleaner.

If zero will return new(T) then in case of reference it won't match.
If zero will return nil it will match (see my question).

Looks like it's valid and open question about the behavioral.

If zero will return new(T) then in case of reference it won't match.
If zero will return nil it will match (see my question).

zero will return *new(T) and it will match, just like it does today https://go.dev/play/p/v93_g19Aj9p

will this work for zero as currently comparison operators are only supported for comparable types
func SomeGenericFunction[T any](P T) bool {
	var z1 T = zero
	return P == z1
}

No - while P is variable that comparable with untyped zero, z1 is an instantiated variable of type T and those are not comparable unless specifically marked as comparable.

@ianlancetaylor I think the behaviour of zero can be replicated with this
func zero[T any]() (zero T) {
	return
}

No - since this code will not work:
func compareToZero[T any](val T) bool {
    return val == zero[T]() // this will not compile
}

You stick with T any part where you are right. I thing the question should be focused on top this part of the proposal:

Referring to a zero value in generic code. Today people suggest *new(T), which I find embarrasingly clunky to explain to new users. This comes up fairly often, and we need something cleaner.

Code example (https://go.dev/play/p/CXGQVhBoGGb):
package main

import "fmt"

func main() {
	test[int]()
	test[*int]()
}

func zero[T comparable]() (res T) {
	return res
}

func test[T comparable]() (res T) {
	fmt.Println(zero[T]() == res)

	return res
}

Example:
package main

import "fmt"

func main() {
	test[int]()
	test[*int]()
}

func zero[T comparable]() (res T) {
	return res
}

func test[T comparable]() (res T) {
	fmt.Println(zero[T]() == res)

	return res
}

This example doesn't work for type mystruct struct{ slc []int } - it will simply not compile.

@Nasfame For your question about var z1 T = zero: I assume the spec-change will do the same as for comparison against nil - the spec will talk about one of the values in a comparison being "the predeclared identifier zero" and allow that.
As for

I thing the question should be focused on top of this part of the proposal

I'd refer to what I said here: The proposal addresses multiple things with a single mechanism, none of which is the "primary" or "only" thing it does. I think it's important to acknowledge that and that an alternative that only solves one of them - no matter which one it is - has this disadvantage over the proposal.
That being said, I also disagree that your code
func zero[T any]() (v T) { return }

really does replicate this part of the proposal. In particular, in the language as it is today, the type argument for zero is not being inferred, so you'd have to write return zero[time.Time](), err, which is worse than return time.Time{}, err.
Of course, we might at some point change the language to make type inference more powerful and infer type arguments from assignment context. At that point, yes, that function would replicate that part of the proposal.
Though note that you'd still be stuck on having to declare func zero[T any]() somewhere. If it is a predeclared function, then what's the benefit over the proposal? If it is in the standard library, users would have to write return somepkg.Zero(), err, which is again as clunky as return time.Time{}, err. If it isn't defined anywhere, users would have to declare it themselves, which is boilerplate.
Note that the primary argument behind the predeclared min/max functions was that they are commonly used, so shouldn't require an import. This applies even more so to zero, I'd argue.
So, in summary

Your suggestion does only replace part of this proposal.
It does require another language change (type inference) to even do that.
It then still needs to be defined somewhere, which leaves us no better, or worse, off than this proposal.

@Merovius @DmitriyMV
Thanks for your clarifications.
My thought was to replicate zero in the current version of go.
func zero[T any]() (zero T) {
	return
}

But as @DmitriyMV pointed out the code doesn't work for custom types.

type mystruct struct{ slc []int }

@Nasfame As I said above, the likely way this would be put into spec is "as a special case, any value may be compared against the predeclared identifier zero and evaluates to true if it is the zero value of its type". This is analogous to how nil works today:
func main() {
    var f func() = nil
    fmt.Println(f == nil) // works
    fmt.Println(f == f) // doesn't work
}

@Merovius Note that nil doesn't resolve into any concrete value per say. But zero does.
In other words,
x == zero resolves to x==mystruct{} which is not possible in Go.
x==nil doesn't resolve and is possible even if x is non-comparable.

@Merovius the polymorphic behaviour of zero makes it a bit more extraordinary than nil to me

@Nasfame x == zero does not resolve to x == MyStruct{}

If var x mystruct=zero //resolves to mystruct{}
Then x==zero //should as well

@Nasfame I disagree with the distinction you are making. In particular, your assertion that x == zero "resolves to x == mystruct{} is directly counter to what I said above how that comparison would likely be defined in the spec - which is exactly what x == nil does.
Either both "resolve to a value", or neither does. There is no basis for the distinction that you are making, except that you choose to think about it that way.

@Nasfame var x MyStruct = zero and var x MyStruct = MyStruct{} are 2 different ways to do the same thing, they do not necessarily resolve to each other.

@merykitty they kind have to mean the same thing. i.e zero should resolve to MyStruct as both the statements seems equivalent.

zero -  can be treated as a untyped constant. A nil value that is automatically converted to the default value of a type.

We can support == comparisons just like nil i.e with the restrictions of nil.
But even then this is a problem x == MyStruct{} ; x == zero . If we permit scenario-2, what exactly is zero then?

zero - can be treated as a untyped constant.

No, it can not. The proposal explicitly says so. You can not write 1+zero and you can not write const x = zero. zero is a special predeclared identifier, just like nil.

If we permit scenario-2, what exactly is zero then?

The predeclared identifier zero. What is nil if you write f == nil? It can't be a function variable with value nil, as function variables are not comparable.
Please try to understand how zero and nil are similar, instead of trying to force them to be understood to be different. The differences you are trying to ascribe to them are directly contradicting the intended semantics of the proposal.

There is also some confusion about zero vs nil. To try to be very clear, zero would be exactly like nil except that it can be used to denote and compare for the zero value of any type, whereas nil can only be used for chan/func/interface/map/slice values.

@rsc given this confusion—in your view, what’s the concise case against just using nil instead of a new predeclared identifier zero?
Thanks!

Hi @ydnar

@rsc given this confusion—in your view, what’s the concise case against just using nil instead of a new predeclared identifier zero?

If I understand your question, I think a concise response from Russ on that topic is in https://github.com/golang/go/issues/61372#issuecomment-1636929105:

Re expanding nil, I think accidents like var x int = nil, or writing f(nil) when f takes an integer, or writing x == nil when x is an integer, or writing *x == nil when x is a *int but x is an int, are mistakes that are worth continuing to diagnose. So I am not in favor of allowing nil to mean any zero anywhere. In C, NULL is defined literally as 0, and it is easy to make mistakes like that. In Plan 9 C our standard headers said #define nil ((void)0), and it caught plenty of mistakes where you wrote nil and should have written an integer; NULL would not catch those, and nor would an expanded Go nil. Types are good, and we should continue to take advantage of them.

It is impossible to actually have a discussion on github issues (We can't possibly read all the duplicated comments- leading to more duplicated comments). Can the discussion be moved to Github discussions?

If we add zero, now we can refer to the same things in two ways.

We don't create slices like slc := []string(nil), while it is possible. I don't think that much will change here.

We don't create slices like slc := []string(nil), while it is possible.

sometimes we do. However, I think most people would agree that var s []string is usually the right choice, especially if we are declaring only one variable.
However, at least for me, with zero it's much more hard to decide when to use what. For example
f(false) vs f(zero)
f(x, false, "") vs f(x, zero, zero)
if s == "" { vs if s == zero {
MyStruct{f: false} vs MyStruct{f: zero}
f(MyStruct{}) vs f(zero)
return 0, nil vs return zero, zero

Hi @gnojus

However, at least for me, with zero it's much more hard to decide when to use what.

As I understand it, if this proposal were to be accepted, there will be clear statements about what is considered idiomatic. Cherry picking one of your examples:

return 0, nil vs return zero, zero

Russ wrote in the opening comment:

As far as idiomatic discussion and naming, I expect that zero will only be used for these general uses and will not displace nil as a more specific kind of zero value. In particular, we will keep using terms like nil pointer and nil interface; we will not switch to saying zero pointer, zero interface, and so on.

If that ends up being what is considered idiomatic, then you would not replace return nil with return zero. In addition, very likely some of the common linters like vet, staticcheck and golangci-lint would help in pointing out non-idiomatic code in most common cases. (And if either vet or staticcheck start doing that, then it means it will be pointed out usually while you type if you are using one of the various editors using gopls, which I think is the majority of gophers at this point).

This proposal has been added to the active column of the proposals project
and will now be reviewed at the weekly proposal review meetings.
— rsc for the proposal review group

I’ve read portions of this discussion, but it is quite long, so there’s a good chance my comment has already been made. Sorry, if so.
I’m very fond of the name zero, and it gives me a bit of regret that nil exists at all, despite the mistake catching benefits.
my main concern is that there is a lot of overlap between where zero can be used and nil can be used, and I think people will use them inconsistently.
One way we might avoid this is with a new constraint on zero, it can only be used on non pointer types and type parameters. So it is invalid to say
var x map[a]b x = zero
then zero and nil are totally distinguished in their intended usages.
If this is not acceptable, I hope we come up with someway of discouraging inconsistent use of zero and nil.

Two good entry points into the discussion are this comment from July 15 and that comment from July 16. In both cases, Russ gave a set of replies to various things raised in the initial round of feedback. In fact, @rsc might want to edit the top-post to include a section at the end with some of these points - or even just links to those comments - for easier reference for new people.
@hherman1 your suggestion indeed was made and Russ addresses it in the last bullet point of the latter comment.

I tried writing rules where zero is only allowed for things that don't have a literal zero already - arrays, structs, and type parameters that can be arrays or structs (including type parameter 'any').
The diffs for that version are at https://go-review.googlesource.com/c/go/+/509995/6/doc/go_spec.html.

An advantage of doing it that way is you could start with allowing zero only in limited circumstances and loosen it later if it seems like it’s not a problem in practice or if it turns out not being able to “degenericize” by copy-pasting is a pain.

As a native German speaker, I find it a bit strange to read return zero, err as an English sentence (generously overlooking the comma), roughly meaning „the count of errors returned is zero“ when the statement idiomatically states the opposite. nil or _ IMO represents more the concept of „I have nothing to return“ and is less 0-ish.

“Return nil err” has the same natural language problem as “return zero err”.

I can't see return zero, err as any stranger than return 0, err.

I tried writing rules where zero is only allowed for things that don't have a literal zero already - arrays, structs, and type parameters that can be arrays or structs (including type parameter 'any').
The diffs for that version are at https://go-review.googlesource.com/c/go/+/509995/6/doc/go_spec.html.

Universality is simplicity, so I prefer universal zero and would reiterate your initial spec in the spirit of my proposal: https://github.com/golang/go/issues/35966

maybe default could be more indicative of an empty struct/reference than zero - C# also implemented this a few major versions ago

default is already a keyword in Go, not a predeclared identifier. Changing it to work like this would be backwards compatible, but it would make default work differently from every other thing in the language as no other keywords can be used as values like that.

@willfaught replied about my "mistakes" being valid programs if we make nil be a universal zero, which is exactly my point. Being valid programs does not preclude them from being "mistakes" (i.e., not what the user intended).

@rsc Let's look at those mistakes one at a time:

"var x int = nil"
"f(nil) when f takes an integer"
"x == nil when x is an integer"
Would nil be valid and meaningful in these contexts? Yes. Is someone more likely to mistakenly write "nil" instead of "zero" in these contexts? No. Is someone more likely to mistakenly write "nil" instead of "1234" in these contexts? No.
So I don't think using zero instead of nil saves people from mistakes in these cases.

"*x == nil when x is a *int but *x is an int"
Would nil be valid and meaningful in this context? Yes. Is someone more likely to mistakenly write "nil" instead of "zero" in this context? No. Is someone more likely to mistakenly write "nil" instead of "1234" in this context? No.
(You could make the same argument for iszero(x) vs. iszero(*x), by the way.)
This same argument could be made against how "x == nil" and "x() == nil" work currently, where x is func() error.
So I don't think using zero instead of nil saves people from mistakes in this case.

Using zero instead of nil will not allow us to diagnose these "problems" because zero could simply be used in its stead, with all the same "problems" you pointed out.

Google once had a significant data loss (covered by our redundant systems) because someone wrote something like (abstracting a bit) 'if(deleteEverything)' instead of 'if(*deleteEverything)' in a C++ program, where deleteEverything was a pointer-to-bool and the pointer was non-NULL but false, while the code interpreted it as true. "If you don't want the zero value for int then don't write nil" amounts to "don't make mistakes". Mistakes happen (at least in my experience), and type systems are useful for finding them. Expanding nil to scalar types would stop catching real mistakes, because they would no longer be type errors. (Thanks to @zephyrtronium for replying to that effect as well.)

I think I already addressed this argument with pointing out that Go is not weakly typed. This class of problem was likely to happen in C because integers and pointers are truthy in C. C has a bad type system. Go does not. Let's not let our Go design decisions be guided by flashbacks to worse languages.

I agree with @willfaught , the adoption of zero does have an overlap with nil resulting in confusions and runtime errors that were caught at compile time in previous version of Go.
I think a plausible solution that can address this problem is to draw a boundary between nil and zero.

nil as the default/zero value for any, slice,map, pointers
zero as default/zero value for the rest of the types struct{}, int, bool

@willfaught, I don't see how it is a flashback. I was pointing out a problem that happened with confusing whether a value was a scalar or a pointer. This absolutely happens. When it happens in Go programs today, the type system catches it. I might have a value f.foo that I think is a pointer but is actually an integer, and if I write 'f.foo == nil' today that's a compile error, which is helpful because it points out my confusion. If we make nil a universal zero value, then 'f.foo == nil' effectively silently rewrites to 'f.foo == 0'. Maybe that's what I meant, but probably not. I would rather the compiler tell me. Confusing scalars and pointers is a common mistake.
Another reason not to go down this road of universal nil is that many programmers come from languages without pointer types at all, in which every value can be nil/null/None/etc. in addition to its actual values. If you are used to writing in that language you could well be additionally confused.
Ultimately we can disagree on this, but I believe all the Go language designers feel strongly about not making it easier to confuse pointers and scalars. A universal zero named nil is off the table.

To recap, 'zero' is no longer a universal zero for all types. Instead, 'zero' is a zero for types without some other short way to spell zero:

zero is assignable to any variable of any type T that does not already have a short zero (0, "", nil), including when T is a type parameter with constraint any.
A value of any type T may be compared to zero when it cannot already be compared to a short zero (0, "", nil), again including when T is a type parameter with constraint any.

In practice this means zero can be used with arrays, structs, and type parameters that include arrays or structs or combinations of other types that have different zeros (for example T interface{string|int} can use zero with type T).
There is no longer any question about which zero form to prefer between 0, "", nil, and zero, because only one of those is allowed in any given context.
I believe this confusion about when not to use zero was the main objection. Now the answer is: use it when you can, don't use it when you can't.
Are there any remaining objections that I've missed? Thanks.

@willfaught, I don't see how it is a flashback. I was pointing out a problem that happened with confusing whether a value was a scalar or a pointer. This absolutely happens. When it happens in Go programs today, the type system catches it. I might have a value f.foo that I think is a pointer but is actually an integer, and if I write 'f.foo == nil' today that's a compile error, which is helpful because it points out my confusion. If we make nil a universal zero value, then 'f.foo == nil' effectively silently rewrites to 'f.foo == 0'. Maybe that's what I meant, but probably not. I would rather the compiler tell me. Confusing scalars and pointers is a common mistake.

@rsc I think I already addressed this argument with this point:

This same argument could be made against how "x == nil" and "x() == nil" work currently, where x is func() error.

Another reason not to go down this road of universal nil is that many programmers come from languages without pointer types at all, in which every value can be nil/null/None/etc. in addition to its actual values. If you are used to writing in that language you could well be additionally confused.

I would argue that people should just learn the language. There is no null in the JavaScript sense in Go. Education is the answer here. If we bend to trends in other languages, we will just wind up with those languages.

To recap, 'zero' is no longer a universal zero for all types. Instead, 'zero' is a zero for types without some other short way to spell zero:

At first I thought you meant that the proposal had been changed to this a while ago, but I think you meant that you're going to make that change. That does resolve my concern about generality. 👍

Perhaps it’s worth giving #12854 another look?

They also simplify code that returns a zero-valued struct and an error:
return time.Time{}, err
return {}, err // untyped composite literal

Always allowing zero makes generated code using zero values simpler. The first time I ran into this was with generated code long before generics. It would be a shame if code generators still needed to use var zero1 X to avoid having to figure out which zero to write for X. If generated code wants to know if something is nonzero it would be much simpler to write == zero than to have to generate different code for comparable/incomparable types.
If {} is allowed for arrays/structs in the future zero would still need to be allowed for backwards compat even though there's now a "better" zero value.
I certainly agree that you should not use zero when there's a better option, but it's very simple for a linter to enforce this.

@willfaught I understand that you want an expanded nil, but this issue is about adding zero. Can you think of reasons to avoid adding zero besides that doing something else might be better than doing that? My main objections to zero are (as stated above behind the LOAD MORE black hole) that it’s a new predeclared identifier and that it can be used in confusing ways where nil is more appropriate (but the changed rules by Russ would prevent that). Any more objections we should know?

@carlmjohnson Perhaps it wasn't clear, but

That does resolve my concern about generality. 👍

meant that I have no further objections.

@jimmyfrasche What is an example of a generator that is writing out code that it doesn't know the type of?

It was a generator similar to stringer in that it added boilerplate methods to a type specified on the command line, but the methods could fail so the generated code had
var zero T
// ...
if err != nil {
  return zero, err
}

If zero were allowed anywhere that wouldn't need the declaration.
I do not recall personally writing code generators that could have made use of the other properties of zero but it's simple to extrapolate from the generic use cases to the generated code use case for all of them.

If zero will not be universal, I agree with @ydnar that {} is a more intuitive spelling of the zero value for structs and arrays. It's already used in empty construction.

I think {} is not an intuitive spelling for the zero value of a type parameter though. It also requires parenthesis when used in a conditional, to disambiguate parsing, i.e. if x == ({}) { /* do thing */ }. That seems very unfortunate.

I think {} is not an intuitive spelling for the zero value of a type parameter though. It also requires parenthesis when used in a conditional, to disambiguate parsing, i.e. if x == ({}) { /* do thing */ }. That seems very unfortunate.

Fair points. I would vote for universal zero and allow idioms to evolve. Some overlap is not disqualifying. There's some overlap between any and interface{}, between if-else and switch, etc.

@Merovius I think there's no ambiguity in parsing if x == {, if {} were  a valid value, as the next character could only be } to make the code valid. The ambiguity arises currently with if x == T{} {} that must be written as if x == (T{}) {}.

@Merovius I agree with you, taken by itself, {} it seems the most obvious solution to represent the zero value of a struct, but in the context of if x == {} { the code appears rather strange to me.

zero semantically means one thing very important already a quantity of nothing. "" as a "zero value" has always pssed the #ActuallyAutistic me off. But I get it, a shorter name for "uninitialized default value" is desperately needed, but calling it zero is not the right one.UDV or any other original Gopher source manufactured term would be better than making zero a keyword and it not actually represent a numerical count of nothing.
Go so wants to NOT have the concept of null even though it absolutely does, and just calls it nil and only when referring to pointer types, should never have allowed uninitialized values; period.
Personally I NEVER use uninitialized primitives with their default values, and I only use nil because it is idiomatic and doing something else in the idiomatic cases would violate the Principle of Least Astonishment.

@rsc: Do you envision this working:
type Node[T any] struct {
        val T;
}

func (n *Node[T]) Reset() {
        n.val = zero;
}

@jarrodhroberson

But I get it, a shorter name for "uninitialized default value" is desperately needed, but calling it zero is not the right one.

"Zero value" is an established term in Go land for 14 years now, that's what it's called in the spec and in every documentation written during those years. Every Go programmer knows the term, so calling it zero would be absolutely the right thing to do.

should never have allowed uninitialized values

Go doesn't have uninitialized values in the sense that C does.
Anyway, we can't change how that works without doing Go 2, which is out of scope here.

@geraldss Yes. In that code, n.val has type parameter type, so assigning zero to that is pretty much one of the main points.

@Merovius That example has no restriction on the type. It can be any Go type. That's the focus of my question.

@geraldss I don't understand what that changes. On the contrary - that is even more what this proposal is about. If it was constrained on, say constraints.Integer, it might conceivably be invalid as you could use 0. But if it can be any type, you need a word for the zero value that doesn't exist yet.

@Merovius Yes, that's my point. I want to understand if @rsc envisions zero as universal or not, and if not, I want to understand if he envisions that example working or not.

@geraldss I answered your question. It's also answered directly in this comment by @rsc:

zero is assignable to any variable of any type T that does not already have a short zero (0, "", nil), including when T is a type parameter with constraint any.

So, again, yes, your example is intended to work.

Alright. If zero is universal in this context, I find the other restrictions on zero to be somewhat superfluous.

@jarrodhroberson
so calling it zero would be absolutely the right thing to do.

even if it does not represent "" or 0 or 0.0 or whatever the "zero value" of the type is?
zero is not the same thing as the term "zero value" ...
one bad decision in naming does not justify more, it could and should be called something more semantically rich and importantly CORRECT. because "" is not 0.

@rsc there have been a lot of replies, and admittedly I haven't been able to keep up with all the reading, but I wonder if you have thoughts about my reply at https://github.com/golang/go/issues/61372#issuecomment-1636773315. In a previous comment you did talk about why you don't want to restrict the usage of zero, but I really just want stronger guidance in the spec or builtin docs.
For example, when PRs and CLs get inevitably filed which perform a replace-all from nil to zero, I want to be able to say "please don't do this, see the guidance at ".

Could the utility of zero be expanded to cover all types (both literally T all and "every kind of type") in the future if it is determined that that is better? Or would that break comparability?  (I can't see why expanding zero's typing would, but I've missed things before)
To me the best usage of zero was in generics and code-gen, and restricting it makes it it fall close to/below the bar necessary to add a new builtin, but if it can be expanded in the future, I'm somewhat more OK with that.

@deefdragon I think expanding zero to work on all types would be backwards-compatible. It would be a strict addition, only allowing code that doesn't compile today to compile tomorrow.

@rsc I like the current proposal which makes zero be distinct from use cases with nil. Thank you for updating the proposal.
I do have one small edge case which might be possible to add to this new version of the proposal. And of course I am talking about the issue with the long-standing confusion in Go between a nil interface value and a non-nil interface value that holds a pointer type with a value of nil. I know there is a new proposal #61489 for it, but instead maybe comparing with zero could also be true when compared with a non-nil interface with nil pointer. In a way, that this is a zero value of that interface. Then one could write a (verbose) if err == nil || err == zero.

@mitar I still strongly believe that any code that tries to check if an interface value contains its dynamic type's zero value (without knowing that dynamic type) is fundamentally incorrect. Note that #61489 doesn't try to make that easier, it just tries to make it easier to tell that you did something wrong.

@mvdan The use of zero has been restricted, see https://github.com/golang/go/issues/61372#issuecomment-1644138501 and the updated issue description:

zero is assignable to any variable of any type T that does not already have a short zero (0, "", nil), including when T is a type parameter with constraint any.
A value of any type T may be compared to zero when it cannot already be compared to a short zero (0, "", nil), again including when T is a type parameter with constraint any.

Emphasis on "that does not already have a short zero (0, "", nil)" and "when it cannot already be compared to a short zero (0, "", nil)".

if so，why not consider the buildin function zero(T) T，its more concise to me. We don't have to learn a new concept, every one understand that function.

@hauntedness The proposal - even the modified one - allows to compare a type-parameter constrained on any to be compared against zero. That is not something that can be addressed with that function.

Perhaps a bit bikeshedding, but can we explore _ as the keyword for the zero value? Using zero means I can't use it as a variable name in my package or function anywhere without causing issues if someone starts using zero comparison later - leading to developer confusion.

@AyushG3112 Responded to here:

The reason for zero instead of _ is that x == zero reads nicely while x == _ is confusing.

Also proposed and declined in #19642

@Merovius can you share real use case, I dont see issues you mentioned. Why t == zero[T] doesn't work as long as t is [T comparable]

@Merovius can you share real use case, I dont see issues you mentioned. Why t == zero[T] doesn't work as long as t is [T comparable]

zero(T) won't work for anonymous struct types, e.g.
var s struct {
    Result int
    Err error
}

// ...

if s == zero {
    // ...
}

@Merovius can you share real use case, I dont see issues you mentioned. Why t == zero[T] doesn't work as long as t is [T comparable]

zero(T) won't work for anonymous struct types, e.g.
var s struct {
    Result int
    Err error
}

// ...

if s == zero {
    // ...
}

yes, but instead we can use a named struct right? Is it worth to change the language spec for such case? go is known as lacking many fetures. I would argue that nil-safe is much important than zero expression introduced here.

@hauntedness It is a feature of this proposal that one can write
func IsZero[T any](v T) bool { return v == zero }

even if T is not comparable.  That permits testing for cases like a nil slice or map.  That does not work if we only have zero[T].

@hauntedness For a concrete usecase, the proposal mentions #60204. This proposal would allow to write that function with type constraint any, instead of being restricted to type constraint comparable. Apart from that, I brought up a use case of using a map as the underlying container in a data structure and not storing zero values to save space - again, with the consequence of not having to constrain the data structure on comparable value types only.

For anyone coming into this issue, to avoid repetition, please:

Read the proposal text (the first comment)
Read the proposed spec changes.
Read this comment and this comment by @rsc, providing answers to a bunch of commonly asked questions.

@ianlancetaylor somehow this truely convinced me. But I still prefer buildin function, for those who merely define iszero(), but often use code snippet like var zero T.

@Merovius: I still strongly believe that any code that tries to check if an interface value contains its dynamic type's zero value (without knowing that dynamic type) is fundamentally incorrect.

Why? if err == nil is exactly that in 99% if not all cases. You do not care what the dynamic type is, just if it is an error or not.

@mitar This is becoming off-topic. But I disagree. if err == nil checks if err has no dynamic type, not that it contains a dynamic value that is a zero value. It is perfectly acceptable (if uncommon) for the zero value of a concrete type to represent an actual error happening. For example, I could write type sentinelError struct{} and return that.
If an error has a dynamic value that is not a valid error value (for example by being a nil pointer which then gets dereferenced in the Error method), the bug is for that to happen - you should only return valid error values, just like you should only return valid io.Readers. The fix for that bug - common as it may be - is to no longer return an invalid error value, not to make it easier to ignore it, introducing possible follow-up bugs in the process.
While I don't think we should do #61489, it does have the advantage of both making this common mistake harder to make, while also not making it easier to ignore it. It attacks the issue from the correct side - at the point where a nil-pointer is returned in an error interface, not at the point where it's checked.

I think that one of the simplest advantage to being able to assert that an interface holds a nil value (typed or untyped) is the possibility to implement custom error types including slices of error values. (even boxed, a nil slice would signal no error then)

And an empty, non-nil slice would signal an error. I don't see the advantage.

I don't see how you can take the position "people just shouldn't return a non-nil empty slice" while also taking the position "it's a huge problem that people return nil dynamic values". And any function that "prepopulates" a slice can also just return a nil error (just like errors.Join does). It makes no sense to sell slice-based error implementations as an advantage here.

How does this proposal compare with #12854? If #12854 ever gets implemented, does this then mean we will have two ways to express a zero value of a struct, zero and {}? (#12854 has a motivating example of using {} as a zero value.)

@mitar
#12854 doesn't allow you to compare values with{}. This proposal does, and works for any type, not just composite ones. IMHO I would prefer zero instead of {}, looks cleaner I think?

If both proposals were accepted you could write v == {} if the type is comparable but would need v == zero otherwise. You could write return {} or return zero.

Sorry, a bit late to the party!
It feels like the main motivation is to make zero values in generic code easier to handle.
For this proposal to be a success is it also necessary to make zero the zero value for structs and arrays?  I'm struggling to see how this extra use of zero is beneficial:

it complicates the definition of zero
it widens the impact of this proposal (beyond new generic code)
the improvement to return statements is marginal at best (and as noted in the comments there are other proposals which would have a lot more impact here)
(most importantly) we already have a more natural way to express the "short zero" value for these types which would give more context to the reader and be more consistent with the way these types are used in Go code today (yes, {})

var x time.Time
var y struct{}
var z [10]int

a := time.Time{}
b := struct{}{}
c := [10]int{}

fmt.Println(x == a, y == b, z == c)
// true true true

I have one very minor concern. I am not sure how nicely defining zero to be the complement of nil/0/""-assignability will interact with type parameters. Relevant spec:
Additionally, if x's type V or T are type parameters, x is assignable to a variable of type T if one of the following conditions applies:

x is the predeclared identifier nil, T is a type parameter, and x is assignable to each type in T's type set.
V is not a [named type](https://go.dev/ref/spec#Types), T is a type parameter, and x is assignable to each type in T's type set.

Whether a type param T's zero value is spelled zero or nil/0/"" becomes whether all types in the type set have a matching alternative zero value spelling. When one type in the type set switches from nil-assignable (for example) the spelling for a type-parameter's zero value can also change. It might be a bit confusing for an underlying type change to a named type p.N to change how type parameter code in a package q is written. This impacts non-type parameterized code too, but the extra distance for the type set feels spookier to me for some reason.
But this is admittedly way in the weeds so it is something I suspect folks can live with.

I agree with the concern raised by @timothy-king . If zero is not universal, it doesn't support the usage "zero out variables of some type parameters T0 .. Tn even if the type set of one of T0 .. Tn is modified in the future".

I don't think that I agree with what I'm about suggest, but just to throw it into the mix: What about adding two new predeclared identifiers? empty could be what zero is currently for, minus the usage with type parameters, while zero would be only for type parameters but would always work, regardless of the type set of thee constraints.

Proposal point 1

Referring to a zero value in generic code. Today people suggest *new(T), which I find embarrasingly clunky to explain to new users. This comes up fairly often, and we need something cleaner.

I don't know where "often" is supposed to come from.
Go was very careful to introduce new keywords for 14 years. And point 1 is really a special case as already stated (https://github.com/golang/go/issues/61372#issuecomment-1637085719):

Generic functions are only a fraction of all Go functions being written, and generic functions that need to create generic zero values are a fraction of generic functions, and those with more than one type parameter even more so.

Proposal point 2

Comparing to a zero value in generic code, even for non-comparable type parameters. This comes up less often, but it did just come up in cmp.Or (cmp: add Or #60204).

This is even less the case.
I don't see how little written code is the basis for introducing a keyword that has global impact.
Proposal point 3

Shortening error returns: return zero, err is nicer than return time.Time{}, err.

Here is the actual global impact, which relates to many, many more code than those in points 1 and 2.
return time.Time{}, err has worked for 14 years (as stated in https://github.com/golang/go/issues/61372#issuecomment-1636883069).
Code is read much more than it is written. Using an implicit keyword in the return value sounds like a second-saving writer who prefers implicitness to explicitness at the expense of the reader. time.Time{} shows explicitly what is returned. It matches the method signature 1:1 with same words.

For constraints that articulate a set of types that are exclusively stringy or numeric etc.,  the more narrow spellings "" or 0 seem way more readable to me.
The underlying types that support nil-assignability are weirder ... I think there are cases where it would a useful signal to ask for a different spelling, as well as cases where it would be just confusing. But really I suspect it'd be a very rare thing to run into, and worth doing carefully.

@meyermarcel First, a nit: You are using the word "keyword", but zero is not proposed to be a keyword, it's proposed to be a predeclared identifier. The difference matters. In particular, Go has not ever introduced new keywords, but we have introduced 5 new predeclared identifiers (three of which in Go 1.21): any, comparable, clear, min and max.
I also think that it's fairly understandable that we'll have to introduce a bunch of new language features to iron out rough edges in generic code and adding predeclared identifiers is the lowest-friction way to add capabilities to the language.
For the frequency of usage: I, personally, loath having to write time.Time{}. It suggests that there might be fields I could write into that struct literal instead - but there are none. It goes directly against my personal coding style guide to distinguish between using new(T) and using &T{}, which is to use the latter if there are field I could initialize (e.g. in http.Server) and use the former when there aren't (e.g. bytes.Buffer). So it's an interesting example to pick out as good, to me, as it's one of the few cases where I have actively missed having something like zero available (yes, specifically and actively, over the last ten years I've been using Go).
For *new(T): I would anecdotally estimate I use it around once a day that I write code. Admittedly, a good chunk of those are answering questions about generics in various Go forums, but a lot of them are production code.
Lastly, yes, comparison to zero for type parameters will be very rare. But it's something that's impossible to write right now. So this isn't just about making rarely written code more convenient. It's about adding power to the language.
And while none of these individually might justify adding a new predeclared identifier, we should look at all of these cases together.

For what it's worth, I think I preferred the unrestricted original version of zero to this new, restricted one. That being said, I also think we can lift the restrictions later, if the downsides turn out actually that noticeable - but we can't put them in later. And we did add the restrictions for a reason, I do think the overlap with nil is a significant downside (even if it's one I could live with).

@meyermarcel First, a nit: You are using the word "keyword", but zero is not proposed to be a keyword, it's proposed to be a predeclared identifier. The difference matters. In particular, Go has not ever introduced new keywords, but we have introduced 5 new predeclared identifiers (three of which in Go 1.21): any, comparable, clear, min and max.

Thanks for the clarification! :)

For the frequency of usage: I, personally, loath having to write time.Time{}. It suggests that there might be fields I could write into that struct literal instead - but there are none. It goes directly against my personal coding style guide to distinguish between using new(T) and using &T{}, which is to use the latter if there are field I could initialize (e.g. in http.Server) and use the former when there aren't (e.g. bytes.Buffer). So it's an interesting example to pick out as good, to me, as it's one of the few cases where I have actively missed having something like zero available (yes, specifically and actively, over the last ten years I've been using Go).

To be honest, I am also very reluctant to write time.Time{}. However, I like to read time.Time{} because it is the way to create an empty struct. I read the type time.Time and empty curly braces {} which tell me that it is an struct with zeroed values. Likewise, I don't have to look in the method signature to see what type it is.
When composite literal notation and zero exist in parallel we have fragmented the notation of code, taking away something familiar to the reader and replacing it with something implicit.
What happens to the composite literalnotation when, we introduce zero?
Will composite literal notation not be recommended for empty structs in a possible future?
Readability remains for me the most important feature in Go and in this issue you read a few times about the advantages that the writer of zero has but little about the impact for readability. I think we give up global readability here by allowing additional notations for existing constructs to allow special functionalities (which are rightly needed in rare cases) in generic code.

Note that you already don't always (or even usually, I'd argue) have a direct representation of  the type in a return statement. For example, you frequently do return a, b, c, nil, with local variables a, b, c. In fact, I suspect that most people will gravitate towards rather introducing local variables or named returns than re-typing the name in every return. I've certainly observed that in myself and in code reviews. So I'm honestly not convinced introducing zero will dramatically change things. On the contrary, I'd argue that it's far better for readability to write return zero, zero, err than to write return a, b, err and that people rarely write return Foo{}, Bar{}, err. The first makes immediately clear that zero values are returned, while the second requires you to check if those variables have been set.
Personally, I expect I will use zero similarly to what I outlined above as my rules for using new vs. a composite literal: If I want to emphasize that I might set fields but choose not to, I'd use T{}, whereas if I want to emphasize that I couldn't anyway and want "just the zero value" (for example because it's already valid, or because it's used as a sentinel as with time.Time) I'd use zero. To me, that seems a sensible policy that would enrich code by conveying semantics and improve readability. YMMV, of course, as always with matters of readability.

Another problem, with which zero will tremendously help is that you currently cannot write code like this
func transmuteSomething(val otherPackage.StructWithSlices) (anotherPackage.SomeType, error) {
	if val == otherPackage.StructWithSlices{} {
		return anotherPackage.SomeType{}, errors.New("incoming struct was empty")
	}
	// more code here
}

You are forced to resort to pointers, which do not carry your original intent and exist only because you cannot compare structs containing non comparable fields.
Compare with this:
func transmuteSomething(val otherPackage.StructWithSlices) (anotherPackage.SomeType, error) {
	if val == zero {
		return zero, errors.New("incoming struct was empty")
	}
	// more code here
}

You already know what are you working with, so a lot of cognitive load is removed. You can now also go without pointers.

I just wanted to sanity check how this handles negative zero.
Suppose we have:
negZero := math.Copysign(0, -1)
negStructZero := struct { F float64 }{negZero}
negArrayZero := [1]float64{negZero}

Today:
negZero := math.Copysign(0, -1)

negZero == 0                                         // true
struct{ F float64}{negZero} == struct{ F float64 }{} // true
[1]float64{negZero} == [1]float64{}                  // true

I assume, the equivalent with zero follows the same semantic?
negZero == zero                     // true
struct{ F float64}{negZero} == zero // true
[1]float64{negZero} == zero         // true

Also, is v == zero going to be identical to reflect.ValueOf(&v).Elem().IsZero()?

The expression v == zero should be the same as
    var vzero typeof(v)
    v == vzero

So, yes, negative floating-point zero will == zero.
I also think that v == zero is going to be the same as reflect.ValueOf(v).IsZero().  But this does lead to a different question that the proposal doesn't clearly address.  What precisely should happen for types that are not comparable?  As currently written, the proposal permits v == zero for struct and array types v that are not comparable.  And in general I don't see a problem with that, but we do need to be clear as to what it means.
Let's define the predicate IsZero(v) as

if v is comparable, then IsZero(v) istrue if v == vzero where vzero is the zero value of v's type;
otherwise, if v is a slice, map, function, or interface, IsZero(v) is true if v == nil;
otherwise, if v is a struct , IsZero(v) is true if for each non-blank field f of the struct type IsZero(v.f) is true;
otherwise, if v is an array, IsZero(v) is true if for each element of the array IsZero(v[i]) is true;
otherwise, v is a type parameter, and we evaluate IsZero as per the instantiating type argument.

Now v == zero is true if IsZero(v) is true.
Writing that out reveals a case where reflect.ValueOf(v).IsZero() may potentially differ from v == zero.  The reflect.Value.IsZero method, for a non-comparable struct type, currently checks the value of blank struct fields.  I am suggesting that v == zero should not check such fields, because we ignore them in ordinary == comparisons.  And perhaps we should change reflect.Value.IsZero to match that.

There was some confusion about the operational details, but @ianlancetaylor spelled it out and I think everyone agrees, so I don't think we need to leave this in likely accept for another week.

No change in consensus, so accepted. 🎉
This issue now tracks the work of implementing the proposal.
— rsc for the proposal review group

After a long think, too long I suppose, I've decided this proposal doesn't sit right. The peculiarity of being able to use the word "zero" for only those types for which a zero value cannot already be expressed is too fine-grained and clumsy. It will require thought each time to decide whether it can be used or not, complicating rather than simplifying the process of writing code.
What is the objection to making it work for any type? What harm does that do? Yes, it adds duplication, but it also simplifies the process of writing code.
https://github.com/golang/go/issues/61372#issuecomment-1668740652 explains IsZero well. I see no reason why
v == zero is true if IsZero(v)

should not be symmetric, with zero and IsZero being mirrored:
v == zero is true iff IsZero(v)
IsZero(v) is true iff v == zero

I agree with @robpike as per my proposal and previous comments. Simplicity.

It would be even greater if the zero identifiers of non-interfaces and interfaces could be made different. How about zero for non-interfaces and nil for interfaces. We may let future go fix to rewrite all nil uses for non-interfaces to zero.

To answer the question about harm: if we allow zero as currently proposed (i.e., not only in the context of type parameters), there's also a new choice to be made because zero is now redundant with pre-existing notations. One has to decide whether to write s = "" vs s == zero, if p == nil ... vs if p == zero ..., x = 0 vs x = zero etc. I am concerned that there will be pressure to standardize on zero everywhere. It seems to me that this would be a real loss of readability. We already see some of this loss with nil and interfaces which is confusing people.
The current proposal disallows the use of zero where there's a pre-existing notation, to avoid that loss of readability, and, perhaps to a lesser extent, to fight the urge of wholesale conversions of millions of lines of existing code to use zero everywhere (the latter could also be controlled with a tool).
Here are some other choices we could make for zero to be more "universal":

We could allow zero to be used whenever we need a zero value, but in comparisons only (this would make it equivalent to isZero). In generic code, to obtain zero value for other uses one would still have to write var zero P where P is a type parameter.

We could allow zero to be used whenever we need a zero value, but only if the type is a type parameter. This would make zero a universal zero value, but only in generic code.

There are probably others.
But should we decide that we just want a universal zero value without restrictions, then we should simply use nil for that. I don't see why we need a new identifier. var x int = nil looks about as odd to me as var s string = zero.
Do I love the current proposal? No. But it seems the most palatable choice among many unpalatable alternatives.

One has to decide whether to write s = "" vs s == zero, if p == nil ... vs if p == zero ..., x = 0 vs x = zero etc.

The current proposal is to not allow zero for any type with a pre-existing universal short-form zero value literal, so you can't use zero with string, as they have "", any number type, as they have 0, or anything nilable, as they have nil, unless it's via a type parameter. zero in the current proposal is only valid for structs and types obscured behind a type parameter.

The ideal would be to allow it everywhere in the language but lint it so you can use zero indiscriminately in generated code without having to worry about restrictions but you still get yelled at otherwise.

That doesn't seem like something that should be in go vet, though, as it's not a possible bug, just somewhat unidiomatic, and adding a language feature that has commonly-encountered unidiomatic usages and then relying on third-party linters to help avoid those feels really weird to me.

@DeedleFake Using nil in all places where we currently permit zero seems an interesting alternative. But perhaps in that case we'd disallow the use of nil with arrays and structs (as is currently proposed for zero) because there's alternative notations. So the changed proposal could be:
nil may be used as the zero value for a type T unless there's an existing (concrete) notation for that zero value.
I think that would mean we'd only see new uses of nil in generic functions and with type parameters. It does fit with the view that a (simple) implementation of generics could model type parameters as interfaces.

Big fan of not having a new builtin, it might be fine to add them but the recent additions of max/min have caused me a bit of anxiety

@griesemer arrays and structs may or may not be comparable so you'd need to be able to compare against nil for the incomparable ones, at least.

I see a subtlety in the difference between types where the zero value in some sense represents the absence of something (expressed as nil) vs. where it is just an alias for a "normal" value of the type (false, 0, ...), but I will confess I'm not sure yet if this is a meaningful difference or just some discomfort with change.
One notable difference I can see is that for types that gave a distinct nil zero value it's possible to write a function that returns that to represent "nothing", whereas for types whose zero value coincides with a "present" value we tend to need to return T, bool to indicate whether the T is valid.
But a counterpoint to that line of thinking is the set of types for which we can distinguish the zero value but conventionally don't, such as a nil slice vs. a zero-length slice.
So I'm still pondering this myself, but I'm curious what others think about whether the observations above suggest making zero distinct from nil or if I'm guilty of just bringing my intuitions about "nothing" values in other languages to a language that doesn't intend to represent that as a general rule.

For strings there are already two equivalent choices when writing an if statement: if s == "" and if len(s) == 0. Although I prefer the first, I see the second in code pretty often. This proposal adds a third choice: if s == nil/zero?
Since string and []byte values are often used in close proximity the subtle difference between if s == nil and if bs == nil (for var bs []byte) seems like an extra thing to think about now.

Somewhere along the line, I've forgotten the problem we are trying to solve by introducing "zero".
Somebody please inform and correct me, but it seems to me that the only true problem today that this proposal really helps is comparing to zero values in generic code?
Otherwise it is a little bit of sugar in a language that has long avoided sugar.
My feeling is that this proposal would be much less controversial if it aimed to solve one thing, and one thing only, well.
The fact that we need to debate style guides for future code if the proposal passes is raising red flags.
My recommendation would be to
start by constraining the use of the "zero" identifier to generic code, perhaps even as an experiment, and see what the community feedback is.

But should we decide that we just want a universal zero value without restrictions, then we should simply use nil for that.

This seems quite pleasant.
Perhaps it might even reduce the confusion regarding nil interface values and nil pointers slightly; the distinction between an interface value and a pointer is subtle, but the distinction between a pointer and a string is not. If nil is the universal zero value, its protean nature might be a bit more obvious.

arrays and structs may or may not be comparable so you'd need to be able to compare against nil for the incomparable ones, at least.

I think this is a good argument in favor of just re-using nil. Structs and arrays that aren't comparable are so because they contain things that aren't, but all things that aren't comparable are also nilable and can be compared to nil. If nil became a universal zero value for composite types, something like
type Example struct { F func() }
var ex Example
ex == nil // true

makes a lot of sense to me.

regarding

I am concerned that there will be pressure to standardize on zero everywhere

how would zero compare to the addition of any and are there things we can gleam from that? at a glance zero feels more complicated due to the readability concerns 0 and "" vs zero, but it also feels quite similar.
In general I'd prefer the simplicity of zero supporting 0. but you would have to add recommending users use 0 for readability where possible.
As a whole, I think the key is what a new go programmer would expect.
I actually asked my coworkers about this ("go has added a keyword zero. what do you expect it to be able to do?") and they had the initial reaction of expecting that it could be used in place of literal 0 in assignment or comparison. It's hearsay, but I think it leans towards the expectation of zero covering everything.

otherwise, if v is an array, IsZero(v) is true if for each element of the array IsZero(v[i]) is true;

that one is bugging me
This means that an array v with 10k elements each being a complex struct can be equal to zero  (or IsZero(v) can be true). This isn't very intuitive for beginners.
I understand why it's like that so the whole system can be sound but it doesn't feel right to use the word "zero" for a non empty array (or even a struct).
The issue is because the word "zero" here means "default  value". For numbers this happens to be "zero" ("0")  so that word "zero" was chosen for everything else too. Maybe choose another word ?
Also there will be no builtin equivalent for a slice to test if all its elements are "zero"  which will add confusion for beginners.
And like someone mentioned, I'd also prefer to use return time.Time{} then return zero because it can catch tricky errors when modifying a function (for instance changing the order of a return tuple).

This post-acceptance discussion is interesting, illustrating that much about the how and way remains unsettled. There are many complex and interacting issues here, and outright problems with the proposal as currently accepted. It doesn't seem wise to do anything for the next release, but instead to continue to develop the thinking around what is to be done and for what reason.

I feel like this discussion is going in circles. Russ came up with a workable compromise, but no one actually changed their pre-existing opinions, so once there was a ripple of activity, everyone came back to reassert whatever they thought before it all started.
Here’s what I thought before this started:

*new(T) is weird and ugly.
It’s annoying to require T comparable just so you can do an iszero test.
It’s inconsistent that the spec says values are comparable or incomparable, but actually some but not all incomparable values are comparable to their zero (e.g. a function) and if you have a struct containing a weird incomparable you have to write s.field == nil instead of s == nil.

Russ’s compromise isn’t perfect, but it solved my pre-existing issues, so I’m happy with it.

The recent discussion here seems to be entirely retreading (or ignoring) the hundreds of comments that preceded the acceptance of this proposal. Among the points made in that prior discussion, the one about confusing scalars and pointers seems like a particularly strong one that the new discussion seems to be ignoring.

I tried writing rules where zero is only allowed for things that don't have a literal zero already - arrays, structs, and type parameters that can be arrays or structs (including type parameter 'any').

What about the other preexisting—and rather lovely—concise way to specify the zero value for arrays and structs (including anonymous structs): {}?
It’s currently only valid when used in a map, but could be relaxed to other cases where the type of left-hand side is known. @neild and others raised this in #12854, and I think it’s relevant here.
package main

type M1 map[string]struct{ S string }

type M2 map[string][4]byte

type S1 struct {
	F struct{ S string }
}

type S2 struct {
	F [4]byte
}

func main() {
	_ = M1{
		"K": {},
	}

	_ = M2{
		"K": {},
	}

	_ = S1{
		F: struct{ S string }{},
	}

	_ = S2{
		F: [4]byte{},
	}
}

I agree with @carlmjohnson and @cespare. Since the Acceptance, there have been new comments, but there has not been any new information or really new ideas. To me, this indicates that this issue is 1. completely subjective and 2. the design space is pretty clear. So there isn't really any reason for anyone to change their mind and I don't think more time is really going to help.
Maybe that does mean we should do nothing, in absence of a consensus - even if that means we'll never do anything about it. But given that at least part of the problem is about being able to write some code that you can't write currently, not just convenience (namely the zero-check for type parameters), that would make me a bit sad.

I'm some scared of the pace of recent Go changes. IMHO, for each language change, it at least needs 4 release cycles to make the final decision.
Calm down, we really don't need to be so hurry.

I agree that this restart of the discussion is only reiterating what has already been discussed, including expanding nil instead of zero and including "not doing anything". Yes I would have prefered to not have to think about whether I can use zero or not in a context but at least it's a compromise. Not doing anything is not a solution.

Another possible solution which would result in minimal changes to existing code.
Permit:

x == T{}, even if T is non-comparable struct, array or type parameter
T{} to refer to the zero value of type parameter T

I don't think that would work. map[int]int{} != nil. func(){} != nil.

What would not work? I'm sorry, I can't see how your snippets relate to my proposed change.

I think that Russ's compromise, only using zero where a known literal shorthand cannot be used minimizes any feature and syntax overlap, whilst keeps zero around for its intended purpose.
I think allowing zero anywhere, makes it easier to use zero - but makes Go harder to write and read overall.

@gnojus
He's saying that maps and functions, and he doesn't mention it but slices, too, are nilable but have non-nil values that are already denoted using {}, so using T{} would not work if the constraint limits it to one of those types. Take the following function, for example:
func Example[K comparable, V any, M ~map[K]V]() M {
  return M{} // This is already valid and returns a non-nil, empty map.
}

@griesemer why doesn’t that same kind of concern around rewriting apply to any? Yet we allow the use of any well..anywhere an interface{} can be used. And any is much more pleasant to type and read so the desire to rewrite interface to any is high and would impact millions of lines of go code.

@jaloren any very naturally stands for an arbitrary type and really is just an alias for interface{} - they are identical types. any could be user-defined and some people have done so. And in fact we have rewritten the (most of the code) code in the std library to use any now. There's much less if any loss of readability.
It's a bit different with zero: it's not an alias, it's a somewhat magic built-in value. There's an obvious loss of readability: zero says less then say "" or 0.0 (floating-point vs integer constant), or zero vs false. We would not rewrite code to use zero instead of 0.

I'd argue the readability depends on the usage even for cases like "" or 0.0. In cases where return values are mutually exclusive (which is often the case with functions that return an error and value), zero makes the situation more clear. return zero, errors.New("some error") imo is more readable than return "", errors.New("some error") as to what the intent is, which is that one value is not intended to be read. That said, it seems like type unions would help more with mutually exclusive return values like that and avoid the need to ever write things like that in the first place?

If the change proposed by @robpike were accepted, which aims to make zero work with every type, it has been noted that zero could potentially become redundant with the current notation.
One needs to choose between writing x == 0 vs x == zero, or  if err == nil vs if err == zero, which could make the code less readable.
However, this concern could be alleviated by using a longer and more descriptive name for zero, such as anyzero.
There wouldn't be the same hesitation between x == 0 and x == anyzero, or between x = "" and x = anyzero. I wouldn't expect to encounter code using anyzero when 0, "", false, or nil can be used.

The proposal mentions three important needs, but the first two - referring to and comparing to zero in generic code - I think are far more pressing. No one disagrees that *new(T) is unsatisfying. I believe using nil for these purposes - only in generic code - would be satisfying.

The proposal mentions three important needs, but the first two - referring to and comparing to zero in generic code - I think are far more pressing. No one disagrees that *new(T) is unsatisfying. I believe using nil for these purposes - only in generic code - would be satisfying.

It would be weird that only in generic code nil and "" are the same.

It would be weird that only in generic code nil and "" are the same.

Is there is a conceptual difference between T and ~T? Something like: genuinely dependent types are rare, but logic that's more gradual in that direction is more common. Is an empty ~string valid to compute with, or what about enum-like ~int types derived for int?

@AndrewHarrisSPU I don't really understand your question. ~string is not a type. In case you are wondering if "" is a valid value for a type-parameter constrained to ~string, the answer is yes.

@AndrewHarrisSPU In case you are wondering if "" is a valid value for a type-parameter constrained to ~string, the answer is yes.

I think nil is another reasonable spelling, not that "" is unreasonable. If we have some spelling of zero that is universal for generics, I think we won't be confused if it's used when a more precise spelling could be demanded. Sometimes I think it would be preferred - sometimes 0x0 is a useful spelling of a rune value. In these cases, nil is abstract but I think also exact about everything we can infer - that's where I see some difference between string and some instance of type S interface { ~string }, the inferences we'd draw for the concrete instances can be in subtle disagreement.

There were concerns raised about allowing zero everywhere causing style discussions about when to write zero vs when to write nil vs when to write 0 vs when to write "". Disallowing zero when nil or 0 or "" is already possible cuts off those discussions. It also avoids any expansion of the "interface nil vs non-interface nil" ambiguity.
Another reason we started with the limitation is that it can always be removed, but it cannot be imposed later. It seems reasonable to me to continue with the limitation in place and use it and see. A similar thing happened with 3-index slice. We talked about generalizing the ability to leave off indexes in a slice expression to 3-index slices and decided to try the limitation that when you use a 3-index slice you have to specify both the middle and end indexes, instead of being allowed to omit one or the other or both. The limitation has turned out not to be a problem, and in fact I think it avoids bugs.
@robpike suggests that perhaps in practice the limitation will "require thought each time to decide whether it can be used or not, complicating rather than simplifying the process of writing code." I am more skeptical, since I have never noticed a problem deciding between nil, 0, and "" today. Either way, if we proceed with the limitation we can find out. If there is evidence to relax it, we can do that.
It seems to me that we should proceed with the limitation for now.

@AndrewHarrisSPU suggests an idea that I think we should consider here.  The original post suggests three issues that can be solved with zero.  We can solve the first two without introducing a new predeclared name by saying that nil works with type parameters.  Specifically we would say

we can assign nil to a variable whose type is a type parameter; it sets the variable to the zero value of its type;
we can compare a value whose type is a type parameter to nil; this is permitted even if the type parameter is not comparable, and == reports whether the value is equal to the zero value of its type.

This permits referring to a zero value in generic code and permits comparing to a zero value in generic code.
That said, there are some disadvantages.  This will permit different the zero value to be written in different ways in some cases:
func F[T int|uint](v T) T {
    if v == 0 {      // compare to 0 is OK because 0 is valid for both int and uint
        return nil // returning nil is OK per this proposal
    }
    if v == nil {  // comparing to nil is OK per this proposal
        return 0  // returning 0 is ok because 0 is valid for both int and uint
    }
    return v
}

It will not shorten error returns in non-generic functions.  People will still have to write things like return time.Time{}.
It will permit comparing to the zero value in cases that can't be written in the language, as when a type parameter is instantiated with a non-comparable struct.  But the same is true of zero.
It will mean that a generic function can't be trivially changed to a non-generic function, because in some cases some instances of nil will have to change to the appropriate zero value.  But the same is true of zero.

@ianlancetaylor so I wouldn't be able to write v == nil if v is type [1]func() but I could write isZero(v) where
func isZero[T any](v T) bool {
  return v == nil
}

is that correct?

@jimmyfrasche Yes.  And the same is true of the zero proposal, but you would write v == zero.

Yes, but I could write v == zero directly without needing to go through isZero

Just to add some color to @ianlancetaylor's comment. I have implemented the type checking for zero (go.dev/cl/520336). Deciding whether a type (parameter) has a zero value (i.e., whether zero can be used with a value of type parameter type) is not hard, but (in the implementation) a tad more complicated than (I) expected at first. For instance, given a constraint int | float64, zero cannot be used, one has to use 0. For a constraint int | float64 | string, zero has to be used (and 0 won't work).
In summary, zero will need to be used in a generic function when there's no other applicable constant and vice versa - i.e., the compiler will complain as needed. So in practice, the decision is not hard to make - the compiler will make it if in doubt.
I do have some sentiment for simply allowing nil as the generic zero value in a generic function only. But it's also true that with the current specification for zero, it's no different from the already existing zero values (0, "", false, nil, or a composite literal) - they also can only be used when there's no alternative. The exception with zero is that we also allow it for arrays and structs, for convenience.

@jimmyfrasche Good point.

@ianlancetaylor allowing nil in generic functions will not solve *new(T) problem, which is unfortunate. It will also complicate code in the future if we decide to introduce a way to return zero values.
The fact that you can compare anything to nil in generic functions but not in regular is also going to be a problem when teaching people about generics in Go - you'll have to explain that this is special case created to solve specific problem. I do not think that introducing new special case is a good idea.
zero on other hand is quite easy to explain - you can't start using it even before you get to generics.

@DeedleFake I really don't like cmp.IsZero solution because it will likely result in almost every package directly importing cmp package. At that point, there is no sense of not making it a predeclared function.
I also don't believe that explaining why you have use 0 instead of zero when working with numeric types is going to be a problem, for example. There are several restrictions in Go - this will be one of them, and it's there for clarity. As @rsc said - it can always be relaxed in the future if sufficient need arises.

I'm also pretty sure that if we are going with nil path and add IsZero function in the future, we are going to see most of the code using IsZero. Mostly because it easier for people to transform existing non-generic functions into generic ones. That is, one is likely to start with this code:
func forEachNotEmpty(arr []MyType, f func(MyType)) {
	for _, item := range arr {
		if cmp.IsZero(item) {
			f(item)
		}
	}
}

and then refactor it to
func forEachNotEmpty[T any](arr []T, f func(T)) {
	for _, item := range arr {
		if cmp.IsZero(item) {
			f(item)
		}
	}
}

Even tho you can use item == nil here, you are likely not going to, since nobody is forcing you and new generic solution looks and feels the same way as a non-generic variant.

@DmitriyMV