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: default initializers for types #32076

Closed
deanveloper opened this issue May 16, 2019 · 6 comments
Closed

proposal: Go 2: default initializers for types #32076

deanveloper opened this issue May 16, 2019 · 6 comments
Labels
FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Milestone

Comments

@deanveloper
Copy link

Default Initializers

I'm not sure what reminded me of this idea. This is an offshoot of #28939 which was closed, however there was still discussion about default initializers which shortly died off. I figured I would create a quick proposal about how this would work.

Goal

The goal of default initializers is to allow types to have default state when initialized. This helps reduce boilerplate code at both the declaration site and the call site.

Declaration Syntax

There are two syntaxes that will be brought up.

Syntax 1:

type Foo struct {
    X int = 5
    Y float64 = 7
    unexported time.Duration = 5 * time.Second
}

Syntax 2:

type Foo struct {
    X int
    Y float64

    unexported time.Duration
} default {
    X: 5
    Y: 7
    unexported: 5 * time.Second
}
  • Syntax 1
    • A bit more intiuitive, less boilerplate
  • Syntax 2
    • Closer to struct creation syntax
    • Allows for shorter lines when declaring default values for variables with long type signatures
    • type Bar uint default 5 Allows for default values for non-struct values

Call Site

var f Foo // should definitely use default initializer

f := Foo{} // Foo{5,7}
f := Foo{Y: 10} // Foo{5,10}

The reason that Foo{Y:10} resolves to Foo{5,10} and not Foo{0,10} is for consistent behavior with unexported variables. Also this prevents people from trying to be "clever" by writing code that is less readable, and prevents subtle bugs from changing Foo{} to Foo{Y:10}.

Note - the following two functions remain equivalent, just as they are in current Go:

func NewFoo1() Foo {
    return Foo{Y: 10203}
}

func NewFoo2() Foo {
    var f Foo
    f.Y = 10203
    return f
}

Footnote

I'm personally not sure what to think about this proposal. I mainly made it since it's been discussed before but there was no proposal made for it yet. I know some people would like it since it reduces boilerplate code, while others would say it adds bloat to the language and makes the call-site less predictable.

@gopherbot gopherbot added this to the Proposal milestone May 16, 2019
@randall77
Copy link
Contributor

randall77 commented May 16, 2019

There are lots of cases that would have to be handled to make this consistent.
new(Foo) and make([]Foo, 10) need handling.
What's returned by

    m := map[int]Foo{}
    x := m[0]

?
or the same question for channel receive, type assertion, etc.

At an even deeper level, what happens in this case:

var g *Foo
// goroutine 1:
g = new(Foo)
// goroutine 2:
p := g
if p != nil { println(p.X) }

This is a data race, of course, so as far as the spec goes we're in uncharted territory. But goroutine 2 might very well observe p.X == 0, a mysterious out-of-the-blue value that objects of type Foo should never contain. This isn't a problem today because all objects are zero-initialized. The GC can zero everything at sweep time and synchronize with all threads so they are guaranteed to see that zeroing. No reader can ever see the contents of the previous life of a recycled object. In contrast, in this proposal there's no cheap way to do that global synchronization because the contents required to initialize the memory are not known at sweep time.

@beoran
Copy link

beoran commented May 16, 2019

I think that the fact that everything is zero-initialised in Go is a useful feature, which leads to the Go idiom of making sure that the zero value of your struct is actually useful. That's an important idiom we should stick to, so that's why I don't like this proposal very much. Also, it does nothing that can't be done using a constructor function. Therefore I don't think we need a new language feature for this.

@jimmyfrasche jimmyfrasche added v2 A language change or incompatible library change LanguageChange labels May 16, 2019
@jtarchie
Copy link

@randall77 and @beoran, the proposal doesn't nullify the zero-value aspect of golang. This seems to be a proposal on extending initialization for a default value.

If that is the case, @randall77, you would not see any difference in the new or make setup.
When it comes to initializers, a constructor function could be made sure, but the proposal is for having uniformity to it. The constructor function also can have more behaviour than just initial value, so this is not a complete cross over.

@deanveloper
Copy link
Author

deanveloper commented May 20, 2019

To address issues about consistency:

The proposal ideally would change what the "zero value" (I guess now "default value"?) of a type would be.

type Foo int default 5
slice := make([]Foo, 2) // [5, 5]
var foo [5]Foo // [5,5,5,5,5]
newFoo := new(Foo) // *Foo(5)

var fooMap map[string]Foo
fooMap[""] // 5

var fooChan chan Foo
close(fooChan)
<-fooChan // 5

That's fairly intuitive. I don't have opinions one way or another about this.

Unfortunately it allows for this...

type Bar int
var b = 5

// this is gross
type BarPtr *Bar default &b

var x, y *Bar

*x = 10
// x = *Bar(10), y = *Bar(10)

This is gross. However, the 1st syntax in the proposal would not allow for this since only structs can have default values in that scenario, but it would also mean that all non-structs would not be able to have default values.

Regarding data races, I unfortunately can't think of a way to make it thread-safe off the top of my head. The only way I could think of is building mutexes into certain variable accesses, and that's just... bad.


I think that the fact that everything is zero-initialised in Go is a useful feature, which leads to the Go idiom of making sure that the zero value of your struct is actually useful

Really all this proposal does is make the "zero value" (aka default value) easier to use. What this proposal does is make it much easier to make the default value of a type to be useful, rather than making developers jump through hoops to make the zero value of their type useful.

However, it does make the default value of a type less predictable, it forces you to look at the documentation rather than being able to assume what it is, which is not good.

Also, it does nothing that can't be done using a constructor function.

I was about to say that this was factually correct but missing the point, but it actually isn't factually correct. Constructor functions are unable to control what is emitted from fooMap["unknownKey"] or <-closedChan. Ideally, default values would be able to control this.


I'm not super fond of this proposal myself, and the data race issue doesn't help my take on it. It's just an offshoot of another proposal, and I wouldn't personally mind it in the language

@bcmills
Copy link
Contributor

bcmills commented May 20, 2019

@randall77, racy Go programs can already observe invalid values, such as interfaces whose values are not actually of the associated type.

And, of course, nothing prevents the allocator or collector from zeroing unused memory and anticipating zeroed values in normally-nonzero fields of allocated objects.

@griesemer
Copy link
Contributor

Given the problems pointed out by @randall77 and the fact that we rely heavily on zero initialization in Go programs, this proposal seems to introduce more problems than it solves. For instance, a reader of a program could not rely anymore on the fact that a composite literal T{} represents the zero value for that T without inspecting the definition of T. This seems like a fundamental property that we rely on in Go code that would be invalidated with this proposal. The conventional way to get a default value for a T is to call an initialization function, such as newT or makeT.

@golang golang locked and limited conversation to collaborators May 27, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

8 participants