Nillable basic types?

450 views
Skip to first unread message

Daniel Lepage

unread,
Mar 17, 2024, 11:42:12 PMMar 17
to golang-nuts
Hey everyone, I've been thinking for a while now about the advantages of nillable types, especially for basic types, and I'm interested in making an official language proposal. I'd love to know what the rest of the community thinks about it, and if this discussion has been had before (I found some vaguely similar past proposals but they were all specifically about making pointers easier to use, which is not the same thing).

Thanks,
Dan



# Proposal: Nillable types

## Summary

I propose that for any type T, the type `T | nil` should also be a valid type with the following properties:
1. The zero value of a `T | nil` is `nil`.
2. Any `T` or `nil` may be used as a value of `T | nil`
3. It is a compile-time error to use a `T | nil` as a `T` without first checking that it is non-nil.

So basically, you should be able to write code like

```go

func Foo(x int32 | nil) {
  if x != nil {
    fmt.Println("X squared is", x*x)
  } else {
    fmt.Println("X is undefined")
  }
}
```

## Motivation

There are loads of examples out there (I can dig up a bunch if it would be helpful) where various go programs need values that can take some "unset" value that is distinct from the zero value of that type. Examples include configuration objects, command line flags, and quite a lot of large structured data types (protobuf used to have every field be a pointer for this reason, though in v3 they got rid of this in their go implementation because it was so frustrating to do, and now go programs just can't distinguish unset values from zero values in protobufs).

### Alternatives and why they're bad

There are three common ways I've seen of dealing with this in Go, and they all have serious limitations:

#### Pointers

The variable `var x *int32` allows you to distinguish between `x == nil` and `*x == 0`. However, using a pointer has three major drawbacks:

1. No compile-time protection

Go does not force developers to put nil guards on pointers, so nothing will stop you from dereferencing `x` without checking if it's nil. I've lost count of the number of times I've seen code that panics in corner cases because someone assumed that some pointer would always be set.

2. Passed values are mutable

If I want a function that can take an integer or an unset marker, and I define it as `func foo(x *int32)`, then users of my library just have to trust me that calling `foo(&some_var)` won't change the value of `some_var`; this breaks an important encapsulation boundary.

3. No literals

You can't take the address of a literal, so instead of simply writing e.g. `x = 3`, you have to either use a temporary variable:
```
tmp := 3
x = &tmp
```
or write a helper function that does this for you. Numerous libraries have implemented these helper functions - for example, both https://pkg.go.dev/github.com/openconfig/ygot/ygot and https://pkg.go.dev/github.com/golang/protobuf/proto define helpers named `Bool`, `Float32`, `Float64`, `Int`, `Int32`, etc. just so that you can write
```
x = proto.Int32(3)
```
But this makes code a lot more cumbersome to read, and also requires the developer to restate the type of `x` every time (as compared to `x = 3`, where Go will infer that `3` means `int32(3)` based on the type of `x`).

[Sourcegraph finds more than 10k results for functions that just take an int and return a pointer to it](https://sourcegraph.com/search?q=context:global+/func+Int%5C%28%5Cw%2B+int%5C%29+%5C*int/+lang:go&patternType=keyword&sm=0) so this is coming up A LOT.

#### Sentinel values

Some code uses explicit "impossible" values to indicate unset. For example, a nonegative value might have type `int` and be set to -1 to indicate that it is unset. However, this fails the criteria that the zero value for the type should be unset. It also requires that every function using this value check for -1 before using the value, and the compiler cannot enforce that this check has been made.

Furthermore, this requires you to use a broader type than the type you actually care about, which may be impossible (e.g. if the value can be any float) or extremely unwieldy (e.g. if you have to use an integer to represent a bool).

#### An additional bool

You can also approximate this by using a struct like

```go
struct {
  X int32
  IsZero bool
}
```

The zero value for this struct has `IsZero=false`, so you can use that to determine that `X` is not explicitly 0, but is in fact unset. However, this is confusing (what does it mean if IsZero is true but X is not 0?) and awkward (you have to remember to set IsZero any time you set X to 0, and to check IsZero any time you want to read X), and again, the compiler will not complain if you fail to do these.

An example of using BOTH a sentinel value AND an additional bool in golang itself is: https://github.com/golang/go/blob/68d3a9e417344c11426f158c7a6f3197a0890ff1/src/crypto/x509/x509.go#L724 . The `MaxPathLen` value is considered "unset" if it's set to -1 OR if it's set to 0 and the bool `MaxPathLenZero` is false.

This is necessary if you want to be able to mark it unset in a single line (`cert.MaxPathLen=-1`) but also have it be unset on any zero-valued (i.e. uninitialized) certificate. But as a consequence, every single use of MaxPathLen has to be guarded by multiple checks and any attempt to set it has to be careful about setting the 0 indicator as well; forgetting to take both possibilities into account would break your handling of X.509 certificates (which could even be a security issue, if you're rolling your own certificate handler instead of using an existing one).

## Nillable Types

If Go supported nillable types, an example like `MaxPathLen` would be written simply as
`MaxPathLen uint32 | nil`. The zero value would be nil (unset), setting it to a specific value would be easy (`c.MaxPathLen=5`), and setting it to nil would also be easy (`c.MaxPathLen=nil`).

Moreover, the compiler could enforce at compile time that a developer can't forget the possibility of nil.

### Syntax

The syntax would simply be that any type can have `| nil` appended to it to make a new, nillable type. For the sake of sanity, it would be reasonable to generate syntax errors on redundant constructs like `int | nil | nil`.

This syntax is (in my opinion) more readable than some other languages' syntaxes for the same (e.g. `int? x` in C#) and more concise that most other languages (e.g. `x: typing.Optional[int]` in Python or `x :: Maybe Int` in haskell)

Types would be checked by type assertions:

```go
func foo2(x int | nil) {
  if i, ok := x.(int); ok {
    fmt.Println("x squared is", i*i)
  } else {
    fmt.Println("No value for x")
  }
}
```

As with other type assertions, you may omit `ok` if you're sure that the value will match, but it will panic if you're wrong:

```go

func foo3(x int | nil) {
  if x != nil {
    fmt.Println("x squared is", x.(int)*x.(int))
  } else {
    fmt.Println("No value for x")
  }
}
```

### Nice-to-have: Implicit type guards

Ideally, the compiler would also infer simple type guards so that we wouldn't need intermediate variables or unchecked type assertions:

```go
func foo(x int | nil) {
  if x != nil {
    fmt.Println("x squared is", x*x)
  } else {
    fmt.Println("No value for x")
  }
}
```
i.e. the compiler would infer that `x` cannot be nil inside the `if` block and therefore must be an int. Obviously this is impossible to do for arbitrary expressions, but typecheckers in numerous other languages (e.g. `mypy` and typescript) do recognize simple `if x != None` checks as type guards; I have no idea whether it would be difficult to add this to Go.


### Expressed in terms of pointers

You could think of `T | nil` as being like a `*T` except with easier syntax for using it and not passed by reference; you could implement it solely in terms of AST transformations if you wanted to by having the following statements correspond to each other:
 
`var x int32 | nil` -> `var _x *int`

`x = nil` -> `_x = nil`

`x = 3` -> `var _tmp int32 = 3; _x = &_tmp`

`y, ok := x.(int)` -> `var y int32, ok bool; if _x == nil { ok = false } else { y = *_x; ok = true}`

`y := x.(int)` -> `y := *_x`

`foo(x)` -> `if _x == nil { foo(nil) } else { _tmp := *_x; foo(&_tmp) }`

I doubt this would be the most efficient way to actually implement this feature (I am not an expert on the internal works of the go compiler), but the fact that it *could* be written this way makes me think it would not be difficult to add to the language.


### Other implications

This change would be entirely backward-compatible - no existing code would contain the `T | nil` syntax, so nothing would change in the compilation of any existing code.

I don't think this would make the language any harder to learn - the syntax for using it is the same as the syntax for other type assertions, and the use of | for union types is A) pretty common in other languages, and B) under discussion as a more general Go feature (#57644). Moreover, it would make a lot of code more readable: the `x509.go` example from earlier has 16 lines of comments around `MaxPathLen` and `MaxPathLenZero` just to explain how they interact, and additional comments when they're used explaining again how they work; none of that would necessary if it were a single value-or-nil.

Also, this syntax fits nicely with the proposal for more general sum types (https://github.com/golang/go/issues/57644).

Jan Mercl

unread,
Mar 18, 2024, 3:01:25 AMMar 18
to Daniel Lepage, golang-nuts
On Mon, Mar 18, 2024 at 4:41 AM Daniel Lepage <dple...@gmail.com> wrote:

> This change would be entirely backward-compatible ...

Let's consider, for example, the type uint8, aka byte. A variable of
type byte is specified* to occupy 8 bits of memory and has 256
possible values. To represent all those values and the new possibility
of the value being nil, we need 257 distinct values. But that does not
fit 8 in bits anymore. However, changing the size and/or the bit
representation of such a variable is observable, making it not
backwards-compatible.

----
*: From https://go.dev/ref/spec#Numeric_types:

""""
The value of an n-bit integer is n bits wide and represented using
two's complement arithmetic.
""""

Brian Candler

unread,
Mar 18, 2024, 4:42:32 AMMar 18
to golang-nuts
I like Go because it's relatively simple and low-level, like C, except with things like garbage collection and channels integrated.  If Go were to have "| nil" types then the internal representation of such variables would have to be something like this:

type Maybe[T any] struct {
Value T
IsSet bool
}

In which case, why not just use that form explicitly? The code you write is then clear and obvious. Taking one of your examples:

func foo3(x Maybe[int]) {
  if x.IsSet {
    fmt.Println("x squared is", x.Value*x.Value)

  } else {
    fmt.Println("No value for x")
  }
}


I also think that you need to find multiple compelling use cases (of which that example is not one), before fundamentally adding complexity to the language. Remember that "nil" is already overloaded in Go, so adding another (similar but different) meaning increases complexity for the reader.

Full union types are a different thing again, and I can see the attraction of those - except at that point, the language is no longer Go. Having "int32 | int64" as a union type, but also as a generic type constraint, would also be somewhat mind-boggling. How would you write a type constraint that allows a union type?

Jeremy French

unread,
Mar 19, 2024, 12:22:30 PMMar 19
to golang-nuts
I understand the problem you are trying to solve, and it's valid, I think.  But this solutions "feels" bad and very un-Go-like.  Usable zero values is a feature in Go. Whether you agree with that or not, it's a selling feature of the language and clearly something that was/is important to the Go Authors - as is explicit vs implicit logic.  So having a zero value that implicitly means "don't use this" a a feature that is built into the language seems like it could lead to confusion or at least a very muddled message about the nature and purpose of Go.

I think maybe where your opinion diverges from the "hive mind" here is that you want the compiler to enforce this notion.  You can already achieve your basic objective with your "Additional Bool" example, and the confusion you complain about there can be easily mitigated with a better named variable "IsSet" rather than "IsZero" like Brian Candler suggested.  But you still wouldn't have compile-time enforcement.  And that I think is where Go is going to disappoint you.  Go is a strongly typed language, at least on the scale of QBasic to Pascal, but the compiler does allow you to do a lot of things that you shouldn't, in the interest of letting you do things that might be a little crazy but that still work.  It seems to me that Go likes to leave "should" rules to the linter rather than the compiler, so maybe you could use a custom linter to achieve what you're looking for?  It's not a perfect solution, but it feels like it gets you 90% there.

Daniel Lepage

unread,
Mar 19, 2024, 2:44:41 PMMar 19
to golang-nuts
From Jan Mercl: 
To represent all those values and the new possibility
of the value being nil, we need 257 distinct values. But that does not
fit 8 in bits anymore. However, changing the size and/or the bit
representation of such a variable is observable, making it not
backwards-compatible.

I'm not proposing that *any* value be made nillable, I'm proposing the explicit syntax

var x uint8 | nil

that would create a nillable uint8. A variable of type `byte` would still only take up one byte; a variable of type `byte | nil` would be larger. Existing code, which obviously doesn't use the `| nil` syntax because it doesn't exist yet, would be completely unaffected by this change.

From Brian Candler:
I also think that you need to find multiple compelling use cases (of which that example is not one), before fundamentally adding complexity to the language. Remember that "nil" is already overloaded in Go, so adding another (similar but different) meaning increases complexity for the reader.

Back when I worked at google, this was the number one cause (at least in code that I worked with) of runtime errors in go (mostly in code using protobuf v2 or ygot). I've also seen a lot of random github discussions that boiled down to "how do we differentiate between 'x is unset' and 'x is explicitly 0'; if I have some time next weekend I'll see if I can make a big list of those to support the motivation for this.

From Jeremy French:
I understand the problem you are trying to solve, and it's valid, I think.  But this solutions "feels" bad and very un-Go-like.  Usable zero values is a feature in Go. Whether you agree with that or not, it's a selling feature of the language and clearly something that was/is important to the Go Authors - as is explicit vs implicit logic.

This would make zero values *more* usable - it provides a way to ensure that the zero value of a struct is a valid and consistent empty value of that type, without requiring that users remember to set extra bools or do nil checks. `Certificate` is a good example of this - you would expect a "zero" certificate to not have a path length specified, but since 0 is a possible value for MaxPathLen in this context, the type needs the extra bool because otherwise `Certificate{}` would give you a certificate that asserts that its subject is a CA and that this certificate cannot be followed by any other intermediate certificates.

And, to be clear, the developers of that library could have left the bool out - they already have the logic that -1 indicates an unset length, so they could've just insisted that anyone who created a Certificate must remember to explicitly set its MaxPathLen to -1. They added this extra boolean solely because they wanted the zero value `Certificate{}` to actually be an empty certificate, and the fact that they added a bunch of extra logic (and new ways to make a logically inconsistent certificate) in order to this is pretty strong evidence that this would be a useful thing for the language to support.

It would not be an extremely common thing to do: for most types, the zero value really is usable! But in cases where the zero value of a field has an actual important meaning, it would be really great if there were a standard way to make zero values of structs still be useful.

 So having a zero value that implicitly means "don't use this" a a feature that is built into the language seems like it could lead to confusion or at least a very muddled message about the nature and purpose of Go.

As far as I know, nobody has this issue with pointers, which very definitely have a zero value that means "don't use this"; I don't see how using 'nil' this way would confuse anyone about the nature of Go.

Indeed, the most common way I've seen to solve this problem is just to use pointers, and live with the fact that if a junior dev forgets a nil check then your code will compile just fine but panic at runtime; this seems antithetical to the nature of Go.

I think maybe where your opinion diverges from the "hive mind" here is that you want the compiler to enforce this notion.  You can already achieve your basic objective with your "Additional Bool" example, and the confusion you complain about there can be easily mitigated with a better named variable "IsSet" rather than "IsZero" like Brian Candler suggested.

This is a minor point, but in the example I gave, IsZero is only checked if the value is zero, so that users can write `cert.MaxPathLen = 5` without remembering to set the bool except in the less common case where you set it to 0. It obviously wouldn't be hard to have an IsSet, but both cases add operations or checks that users must remember to do to maintain consistency, with no compile-time mechanism to validate that they actually are doing this, and make it possible to construct inconsistent data (e.g. setting Value to something non-zero but also setting IsSet to false).
 
But you still wouldn't have compile-time enforcement.  And that I think is where Go is going to disappoint you.  Go is a strongly typed language, at least on the scale of QBasic to Pascal, but the compiler does allow you to do a lot of things that you shouldn't, in the interest of letting you do things that might be a little crazy but that still work.  It seems to me that Go likes to leave "should" rules to the linter rather than the compiler, so maybe you could use a custom linter to achieve what you're looking for?  It's not a perfect solution, but it feels like it gets you 90% there.

Being able to tell the compiler "tell me if this code uses a value that isn't set" is a lot more important than "tell me if this code declares a variable but doesn't use it", which as far as I know is the established bar for whether a linter-type check should be enforced by the compiler or not.

I'm also not sure how one would build a custom linter for this. I guess you could just use pointers and have your linter yell anytime you dereference a pointer without first nil-checking it, and just add a lot of linter-ignore comments in places where these checks aren't needed? That wouldn't help with the issues around assignment or mutability, though - that would require a preprocessor or something, which would be dramatically more un-go-like. Or you could go with a Maybe[T] and make a linter that checks that .Value is never accessed unless .IsSet is checked, and .IsSet is set any time .Value is? That sounds like it would be pretty error-prone. Is there a better way to achieve that?

Thanks,
Dan


--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/e45d298f-154d-49c1-9316-73df4f250543n%40googlegroups.com.

Patrick Smith

unread,
Mar 19, 2024, 8:54:05 PMMar 19
to Daniel Lepage, golang-nuts
On Tue, Mar 19, 2024 at 11:44 AM Daniel Lepage <dple...@gmail.com> wrote:
I'm not proposing that *any* value be made nillable, I'm proposing the explicit syntax

var x uint8 | nil

that would create a nillable uint8. A variable of type `byte` would still only take up one byte; a variable of type `byte | nil` would be larger. Existing code, which obviously doesn't use the `| nil` syntax because it doesn't exist yet, would be completely unaffected by this change.

Any such proposal would have to specify how this interacts with types that are already nillable. Consider

var p *int | nil
p = nil

Does this make p "unset"? Does it set p to *int(nil)? How can subsequent code distinguish between the cases where p is unset and p has the value *int(nil)?

Also, given

var a any | nil

are there  _three_ different ways in which a can be nil (a is unset, any(nil), or, say, *int(nil))? People are already confused by having two different ways for interface values to be nil.

Mike Schinkel

unread,
Mar 20, 2024, 3:34:10 AMMar 20
to Daniel Lepage, GoLang Nuts Mailing List
On Mar 19, 2024, at 2:43 PM, Daniel Lepage <dple...@gmail.com> wrote:

I'm not proposing that *any* value be made nillable, I'm proposing the explicit syntax

var x uint8 | nil

that would create a nillable uint8. A variable of type `byte` would still only take up one byte; a variable of type `byte | nil` would be larger. Existing code, which obviously doesn't use the `| nil` syntax because it doesn't exist yet, would be completely unaffected by this change.

Focusing the proposal like that was helpful, at least for me.  The original proposal? tldr; 

Question: Assuming the following was currently possible with type constraints, how would your proposal differ from the following?

type NillableUInt8 interface {
   uint8 | nil 
}
var x NillableUInt8

Also, if the above were possible in future Go, would that achieve the same objectives you are seeking, or not?  And if not, why not?

Finally, for the Go team, if that would be meet his objectives, would extending type constraints in this manner be a viable potential?

-Mike

Brian Candler

unread,
Mar 20, 2024, 4:14:27 AMMar 20
to golang-nuts
When you say "var x NillableUint8" then you've just declared a variable of an interface type, and interface types are already nilable, so there's no need for "| nil" in the type!

https://go.dev/play/p/Jmtlta0h9m9   // generic version

It's a perfectly valid way of specifying an optional value ("error" works this way, after all). However, I don't think this meets the OP's  objective number 3:

> 3. It is a compile-time error to use a `T | nil` as a `T` without first checking that it is non-nil.

Therefore I think the underlying request a completely different one: that you should never be able to use an interface (or a pointer or a channel, or insert into a map), without first checking that it's not nil, in a way that can be statically validated by the compiler. I'm sure that suggestion has come up before and been discussed to death - e.g. you end up with static types like "a pointer which can never be nil".

Mike Schinkel

unread,
Mar 20, 2024, 5:01:43 AMMar 20
to golang-nuts
On Wednesday, March 20, 2024 at 4:14:27 AM UTC-4 Brian Candler wrote:
When you say "var x NillableUint8" then you've just declared a variable of an interface type, and interface types are already nilable, so there's no need for "| nil" in the type!

Well, I was thinking  of 1.) explicitness of intent, and 2.) potentially a signal to the compiler for the use-case.  

But you probably have a point.
 
However, I don't think this meets the OP's  objective number 3:

> 3. It is a compile-time error to use a `T | nil` as a `T` without first checking that it is non-nil.

Hmm.  While I did not read the whole proposal at first, my initial takeaway was that his intent was more about allowing a way for a scalar value to have an invalid value so it would be possible to determine if a scalar had been set or not rather than being unsure if zero means "It was set to 0" vs "It was never set and defaulted to 0."

Your comments made me go back and read the whole thing, but I was unable to find a list of enumerated objectives, and I did not find the text you quoted.  Did I miss it somehow?

What I did see is his "Nice to have" section which talked about checking for non-nil.  Since he titled it "nice to have" I presume he did not consider that a primary objective of his proposal?

OTOH, even if it is an explicit call-out of `nil` in a type constraint could be the signal for the compiler to enforce that, if that was something the Go team agreed with that. Still, it is probably too obscure to be an appropriate signal. ¯\_(ツ)_/¯

Therefore I think the underlying request a completely different one: that you should never be able to use an interface (or a pointer or a channel, or insert into a map), without first checking that it's not nil, in a way that can be statically validated by the compiler. I'm sure that suggestion has come up before and been discussed to death - e.g. you end up with static types like "a pointer which can never be nil".

I definitely see there is an argument one could make for having the compiler guarantee against incorrectly using `nil`.  But I got the impression the proposal was motivated by scalars that did not currently allow `nil` values and not by reference types like pointers, channels and maps. Although he did not state that explicitly, his examples implied that to me.

For the compiler to guarantee that an `int` is properly set it needs as a prerequisite the potential for what it effectively a `nil` state. But ensuring against a misused `nil` feels to me to be orthogonal to first allowing an "unset" state for scalars.  

Or maybe I misread?  Maybe the best thing to do is let him tell us what he was thinking?
 
-Mike


On Wednesday 20 March 2024 at 07:34:10 UTC Mike Schinkel wrote:

Brian Candler

unread,
Mar 20, 2024, 5:31:08 AMMar 20
to golang-nuts
On Wednesday 20 March 2024 at 09:01:43 UTC Mike Schinkel wrote:
Your comments made me go back and read the whole thing, but I was unable to find a list of enumerated objectives, and I did not find the text you quoted.  Did I miss it somehow?

It's in the very first post that opened this thread, under the heading "## Summary". This link should take you to it:

Quoting directly:

# Proposal: Nillable types

## Summary

I propose that for any type T, the type `T | nil` should also be a valid type with the following properties:
1. The zero value of a `T | nil` is `nil`.
2. Any `T` or `nil` may be used as a value of `T | nil`

Brian Candler

unread,
Mar 20, 2024, 5:47:00 AMMar 20
to golang-nuts
I got the impression the proposal was motivated by scalars that did not currently allow `nil` values

Under the heading "Alternatives and why they're bad" he describes some ways this is currently dealt with - such as the common idiom of returning or passing a pointer to a value, instead of a plain value.  (He didn't mention using an interface, incidentally - which doesn't have some of the downsides he described. For example, a plain numeric value wrapped in an interface can't be mutated)

As has already been observed, if you're going to allow the full range of values of some type T (i.e. all possible bit patterns), *plus* the sentinel value of "unset", then you need an extra bit - which can be done using a struct, or by indirecting through a pointer or an interface. 

The problem that the OP has is that none of those options will force you to check the state of the extra bit, or that the pointer/interface is not nil - which could be argued is a problem with pointers/interfaces in general.

But I think this is not really worth arguing. If you change fundamental things like this in the language, then you'll suggesting turning Go into something that looks like Rust. In which case, you may as well just use Rust.

Mike Schinkel

unread,
Mar 20, 2024, 6:19:18 AMMar 20
to golang-nuts
On Wednesday, March 20, 2024 at 5:31:08 AM UTC-4 Brian Candler wrote:

It's in the very first post that opened this thread, under the heading "## Summary".

I did in-fact miss it. Thank you for pointing to it.

-Mike 
 

Mike Schinkel

unread,
Mar 20, 2024, 6:26:09 AMMar 20
to golang-nuts
On Wednesday, March 20, 2024 at 5:47:00 AM UTC-4 Brian Candler wrote:
If you change fundamental things like this in the language, then you'll suggesting turning Go into something that looks like Rust. In which case, you may as well just use Rust.

Agreed.  Which is why I was asking if using interfaces as type constraints would address the concern.

And as discussed, probably not.  

But it is an interesting thought exercise. If an interface-based solution could be found, it would address the concern without turning us effectively into Rust programmers. ¯\_(ツ)_/¯ 

-Mike

Axel Wagner

unread,
Mar 20, 2024, 10:29:20 AMMar 20
to golang-nuts
FWIW I believe (as Brian sort of points out) this proposal is fully subsumed under #57644. Under that proposal, the proposed type `int | nil` would be spelled `interface{ int }`. The other syntactical constructs are, as far as I can tell, identical - you'd have to use a type-assertion to use an `interface{ int }` as an integer (e.g. to do arithmetic), you can use it with type-switches, and any `int` as well as `nil` would be assignable to it.

I think the one difference would be that `x == 42` would work on `interface{ int }`, but would (presumably) not work on `int | nil`. I personally doubt that this difference would justify an extra construction, but I'm mentioning it for completeness sake.

The "nice to have" of type guards is, I think, an easy idea to mention, but not an easy idea to add to Go. Note that the other languages mentioned, that do that, use a function-scoped type-inference (as far as I know) - that is, they look at an identifiers use over the entire function and then infer the most general type it would have.
Go has so far tried to avoid doing anything like that, limiting any inference to the statement (or expression) a value appears in. And this idea effectively means an identifier would change its type over its lifetime (from `interface{ int }` - does not allow arithmetic - to `int` - does allow arithmetic), which would create numerous problems for existing tooling, as it violates assumptions made by the `go/*` packages.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.

Mike Schinkel

unread,
Mar 21, 2024, 11:04:35 PMMar 21
to golang-nuts
Hi Axel,

Thank you for that link. I had not seen it before, but it is rather insightful.

-Mike

On Wednesday, March 20, 2024 at 10:29:20 AM UTC-4 Axel Wagner wrote:
FWIW I believe (as Brian sort of points out) this proposal is fully subsumed under #57644. Under that proposal, the proposed type `int | nil` would be spelled `interface{ int }`. The other syntactical constructs are, as far as I can tell, identical - you'd have to use a type-assertion to use an `interface{ int }` as an integer (e.g. to do arithmetic), you can use it with type-switches, and any `int` as well as `nil` would be assignable to it.

I think the one difference would be that `x == 42` would work on `interface{ int }`, but would (presumably) not work on `int | nil`. I personally doubt that this difference would justify an extra construction, but I'm mentioning it for completeness sake.

The "nice to have" of type guards is, I think, an easy idea to mention, but not an easy idea to add to Go. Note that the other languages mentioned, that do that, use a function-scoped type-inference (as far as I know) - that is, they look at an identifiers use over the entire function and then infer the most general type it would have.
Go has so far tried to avoid doing anything like that, limiting any inference to the statement (or expression) a value appears in. And this idea effectively means an identifier would change its type over its lifetime (from `interface{ int }` - does not allow arithmetic - to `int` - does allow arithmetic), which would create numerous problems for existing tooling, as it violates assumptions made by the `go/*` packages.

Daniel Lepage

unread,
1:53 PM (2 hours ago) 1:53 PM
to Mike Schinkel, golang-nuts

-- 
Dan

Reply all
Reply to author
Forward
0 new messages