Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: Go 2: spec: make fewer types nillable #28133

Open
zenhack opened this issue Oct 10, 2018 · 24 comments

Comments

Projects
None yet
@zenhack
Copy link

commented Oct 10, 2018

Apologies if this has been suggested before; I've been unable to find an issue.

Currently go has many basic types where the zero value is nil. nil is a huge source of bugs, and a potential Go 2, where backwards-incompatible changes are on the table, offers an opportunity to make the language more robust by reducing its scope.

Specifically, in some cases there is an obvious zero-value that makes more sense than nil. Off the top of my head:

  • The zero value for slices could be a slice with length and capacity of zero.
  • The zero value for maps could be an empty map.
  • The zero value for channels could be an unbuffered channel.

This is more in keeping with the design principle of making the zero value useful.

There isn't an obvious good zero value that I can think of for pointers and interfaces, but even this partial reduction would make it easier to build reliable software.

@bcmills bcmills changed the title Go 2: make fewer types nillable proposal: Go 2: spec: make fewer types nillable Oct 10, 2018

@gopherbot gopherbot added this to the Proposal milestone Oct 10, 2018

@gopherbot gopherbot added the Proposal label Oct 10, 2018

@bcmills

This comment has been minimized.

Copy link
Member

commented Oct 10, 2018

nil slices and channels are, in my experience, not usually a significant problem.

nil maps and nil pointers, on the other hand...

@bcmills bcmills modified the milestones: Proposal, Go2 Oct 10, 2018

@zenhack

This comment has been minimized.

Copy link
Author

commented Oct 10, 2018

I've definitely run into bugs from nil slices (less so channels, but it has happened, and it seems like a straightforward fix). Agree that maps and (especially) pointers are a much bigger problem. Killing nil pointers would be huge but I don't know how to do it while still having zero values.

@networkimprov

This comment has been minimized.

Copy link

commented Oct 10, 2018

Related: #22729

@magical

This comment has been minimized.

Copy link
Contributor

commented Oct 10, 2018

It's worth noting that nil channels have useful properties in select statements: sending or receiving on a nil channel always blocks, so nil-ing out a channel before doing a select effectively removes cases involving that channel from consideration, allowing one to dynamically enable or disable cases at runtime.

That said, it's definitely annoying to have to explicitly initialize channels.

@deanveloper

This comment has been minimized.

Copy link

commented Oct 10, 2018

The zero value for slices could be a slice with length and capacity of zero.

That's already what it does :) A nil slice has no behavior difference from a zero-length-zero-capacity slice, except that s == nil is true.

I love this though. Zero values should be useful, begone with nil for things that don't need it!

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

commented Oct 10, 2018

One of the advantages of the current approach is that the zero values for all types is the value with all bits zero. This makes it easy to initialize variables. Of course that is not an absolute requirement; we could change that with some loss of efficiency. But it's the main reason that nil channels are not useful.

@robpike

This comment has been minimized.

Copy link
Contributor

commented Oct 10, 2018

But nil channels are useful: They signal that the communication cannot happen and thereby provide the ability to control individual cases in select statements.

@zenhack

This comment has been minimized.

Copy link
Author

commented Oct 10, 2018

I don't feel as strongly about channels, and apparently I was just plain mistaken re: slices (though it would probably be more intuitive for them to be non-nillable; most of the code I've seen in the wild that cares about nil slices seems to be written under the assumption that the same problems crop up as with pointers).

Making maps non-nillable seems like it is a very big win for robustness though.

@networkimprov

This comment has been minimized.

Copy link

commented Oct 10, 2018

Currently map is a covert pointer, whereas slice is a covert struct.

A Go2 default-empty map probably shouldn't allocate a map object until necessary, but then every m[k]=v incurs a test. Today every append(s, v) incurs a test, so maybe that's fine?

@zenhack

This comment has been minimized.

Copy link
Author

commented Oct 10, 2018

m[k]=v already has to do a nil check, but right now it panics instead of initializing the map.

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

commented Oct 10, 2018

Note that m[k] = v can not initialize the map if it is nil. Because maps have reference semantics, if we are going to initialize them automatically, we must do so at the point of declaration. Consider

func Add(m map[int]int) { m[0] = 1 }
func F() {
    var m map[int]int
    Add(m)
    fmt.Println(m[0])
}
@zenhack

This comment has been minimized.

Copy link
Author

commented Oct 10, 2018

@networkimprov, that doesn't fix the problem; if it's an unboxed struct and you assign it, then use one copy or the other, they end up being different maps, since initializing one pointer doesn't initialize the other.

Doing up-front allocation would at least have the upside of allowing the nil check to always be omitted. Probably still a loss overall wrt perf.

@networkimprov

This comment has been minimized.

Copy link

commented Oct 10, 2018

Sorry, just deleted the comment you refer to (re making map a covert struct like slice), as I realized that slice has the same issue @ianlancetaylor described :-)

However that code works if map is a covert **MapType. But that incurs an extra dereference :-(

@zenhack

This comment has been minimized.

Copy link
Author

commented Oct 10, 2018

But then you still have to allocate the (outermost) pointer up front.

@networkimprov

This comment has been minimized.

Copy link

commented Oct 11, 2018

Given all that, and the need for reference semantics, maybe default-empty maps don't fly.

@bronze1man

This comment has been minimized.

Copy link
Contributor

commented Oct 12, 2018

@neganovalexey
I think slice is not a reference ,but map is a reference is also a source of bug.
How about add another map like smap which is not a reference, and make it default-empty, and leave the origin map as it is.
This way may even save some allocs in some cases.

@wsc1

This comment has been minimized.

Copy link

commented Oct 19, 2018

I think the most important thing to keep in mind for any changes to nil-able things is consistency.

One thing that is consistent about the current semantics is that all nil-able (atomic) things are in some sense a reference or pointer and vice versa: all reference or pointer (atomic) things are nil-able. If you start making exceptions to that, then there are more rules to keep in mind and more clutter of specifications.

@mateuszmmeteo

This comment has been minimized.

Copy link

commented Jan 29, 2019

I'm in opposite of the author - NIL is very good idea. If value is not set then it should not be set. That's required action in most of cases.

Think about all the int's not touched and set up as 0 by default (example). This will not prevent from the bugs but make them more.

Now you can simply check if the variable had been set (that was the intention of developer). With new solution proposed variable will always have some value even if not touched.

Language should not be responsible for fixing developers bugs - developers are.

@deanveloper

This comment has been minimized.

Copy link

commented Jan 29, 2019

@mateuszmmeteo I'd recommend giving this a read - https://blog.valbonne-consulting.com/2014/11/26/tony-hoare-invention-of-the-null-reference-a-billion-dollar-mistake/

There are of course several reasons to have and not to have nil. Sometimes nil really is needed, but at other times it is not, and I think that this proposal does a good job of illustrating a couple places where nil really isn't needed

@mateuszmmeteo

This comment has been minimized.

Copy link

commented Jan 29, 2019

@mateuszmmeteo I'd recommend giving this a read - https://blog.valbonne-consulting.com/2014/11/26/tony-hoare-invention-of-the-null-reference-a-billion-dollar-mistake/

There are of course several reasons to have and not to have nil. Sometimes nil really is needed, but at other times it is not, and I think that this proposal does a good job of illustrating a couple places where nil really isn't needed

Ok, and regarding memory use? If you for example define very BIG map of LARGE objects when you want to allocate memory? Already on definition (because it will be zeroed with this example) and you need to have CAP to put objects there or later on first insert, what will generate unnecessary delay to reserve memory and then put new object there?

What about garbage collector? When we should consider that this LARGE object (slice, map) is not needed? What if developer will declare it but will not use it? Once again shout that this has not been used but defined? With existing scenario impact will be very low. On zeroed map the memory needs to be reserved for inserting.

Finally what is faster: checking that slice, map and channels have a some length or they are NIL?

@zenhack

This comment has been minimized.

Copy link
Author

commented Jan 29, 2019

Others have pointed out the perf related issues with nil maps, and those are valid. The other cases are kindof moot; I'd misunderstood the semantics in the first place. Channels have useful behavior that I hadn't realized. I no longer feel strongly about this proposal either way.

Finally what is faster: checking that slice ... have a some length or they are NIL?

Per discussion above, slices already do what I suggested. The check should be exactly the same; either way you're pulling out an integer-sized field and comparing it to zero.

I think it would make sense to have nil not be something that can be compared/assigned to slices, just for clarity -- I know I'm not the only person who's misunderstood this.

@creker

This comment has been minimized.

Copy link

commented Jan 30, 2019

@mateuszmmeteo making nil map valid by itself does not remove the ability to make maps with required capacity. Just like with slices, you either use the default or make the one you want.

Performance is a question of implementation, not semantics. Maps could be lazily allocated to avoid unnecessary garbage when it's not used. Again, like slices. The problem is that with slices we have slice = append(slice, ...) and that maps really well to lazy allocation. With maps we have m[key] = value and it's not obvious how would you allocate new map here.

Finally what is faster: checking that slice, map and channels have a some length or they are NIL?

For slices - the same. Slice is a struct with three fields - pointer, length and capacity. Nil slice check extracts pointer and checks it for nil. Exactly the same as extracting the length and checking it for zero. Again, that's an implementation question and necessary optimization can be done there.

@taralx

This comment has been minimized.

Copy link

commented Feb 28, 2019

nil maps drive me nuts. They're the largest source of "constructor" functions in my code. If I really want reference semantics for a map, I could always use *map[K]V. 👍 to maps having useful zero values and, if necessary, losing their reference semantics.

@networkimprov

This comment has been minimized.

Copy link

commented Feb 28, 2019

Another way to get ready-to-wear maps is via default initializers, which run in lieu of zeroing a new value.

See #28366 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.