Go 2.0: Retain simplicity by trading features

Dave Chenny have written a post simplicity dept, where he explains how you can not add a language feature without adding complexity. He also entertains the idea of keeping Go simple when adding new language features, by reducing complexity elsewhere in the language:

We have to build up a bankroll to spend on the complexity generics and immutability would add, otherwise Go 2 will start its life in simplicity debt.

To give some context, most languages, such as C++, Python or Java, have become more complex over time, as new features and syntax is added without removing any of the old. If we should believe Rob Pike, one of the most important features of Go might be it’s simplicity. As Rob Pike points out in his slides, we are not talking about the simplicity of the implementation, but about how simple it is to understand and use the language.

Proposing a trade

As is highlighted by Dave Chenny’s mind experiment about go without package scoped variables, removing (or adding) features will have ripple effects on the standard library. So how do we then calculate these ripple effects and ensure a good way of handeling them? Could we retain Go 2.0 simplicity by trading a specific set of feature for another so that the impact on the standard library is well understood?

If so, I would like to trade package scoped public variables and the init() function against being allowed to make any value a const. I will use some time to explain why I think this might be a reasonable trade.

Why replace public package scoped variables?

Package scoped private variables might be used for preformance optimizaiton like global cache, or a global register. When used for these purposes, it makes sense to make the variabels private and manipulate them through package scoped functions only. You might still want to have a pointer to access them, but never allow direct assignment.

Package scoped public varialbes in the standard library, appear to be mostly used to define things that should be constant such as error values. When they are used to define somethnig that is not meant to be constant, it would never be thread-safe to actually assing a value to them!

Therefore, it seams sensible to trade public package scoped variables for a new feature that would allow declaring more values as const.

What about init()?

I remember using init() quite a bit back in 2012, when we used it to initalize hardware drivers for an autonomous robot. Coming from C, I thought it was quite cool that we could write initialization function in such a way that that they where always run when the pacakge was imported. Especially as non of the hardware would work if we didn’t intialize it correctly!

However, it would have been possible to do without them, and I have yet to see any case, neither in this project or in the standard library, where relying on init() functions is esential. Most of the time, they just causes confusion, such as when used in the main package.

As was also pointed out in Dave Chenny’s though experiment, init functions causes magic by imposing side-effects to package imports. Removing it would definantly simplify Go.

What do you mean make any value a constant?

Go’s const keyword have some limitations, and there is a series of things that it’s currently not possible to declare as const:

const Array = [2]int{1, 2}
const errCodeLookup = map[int]string{
1: "genric error",
2: "invalid usage",
}
const ErrSomething = erros.New("some error")
const DefaultServeMux = &defaultServeMux
const DefaultServeMux2 = defaultServeMux
type T struct{A string, B integer, C *T}
const C = T{"A", 42, &T{"C.A", 0, nil}}

The first two cases would have to invlove that the content of Array[i] and errCodeLookup[key] are not settable, and that the following would cause a compilation error, or if not that, a run-time panic:

Array[0] = 42
errCodeLookup[1] = "something else"

The next three cases might be a bit less clear. I think how this would have to work, would be that if the const value is a pointer value, as is the case for ErrSomething, and DefaultServeMux, you still cannot assign a value, but you can modify the value pointed to, if that type provides pointer methods. For DefaultServeMux2, which here is not set to a pointer value, we would only have access to non-pointer methods.

To summarize this, what does this mean for the final block with the const C? I think it would have to mean the following:

C.A = "a"   // compilation error (or panic)
C.B++ // compilation error (or panic)
C.C = &T{} // compilation error (or panic)
C.C.A = "a" // maybe we would have to allow this

Effect on standard library

I won’t list them all, but I think the effects of this trade can be calculated by the Go team without to much effort. I assmue the following changes would need to be made:

  • Most Public package scoped vars, including all errors and values like os.Stderr, are replaced with consts. It could be errors should be handled differently, but that’s not a part of this specific trade.
  • If there are any cases where it’s desirable to keep a package scoped variable that can be assigned from the outside, it needs thread-safe, package scoped getters and setters.
  • Packages that rely on init() are rewritten to expose an explicit API. In most cases, this probably involves a bit more than just adding a public Init() function, as we would like to avoid global states altogether.

Overall, this would make the standard-library safer, and harder to missuse. At least I can’t do this anymore:

package somelib
func init() {
http.DefaultServeMux = nil
http.ErrMissingBoundary = &ProtocolError{"trolloollollololl"}
fmt.Fprintln(os.Stderr, "So long, and thanks for all the fish!")
os.Stderr.Close()
}

Once the effect of the trade is fully calculated, real-world open source programs could be inspected to see how such a change would affect them.

What about Go 1 backwards compatibility?

It’s been a proposed constraint for Go 2.0 that it must “bring along all the existing Go 1 source code”. What that means, is that all Go 1.x (third-party) packages and libraries must continue to work in a Go 2.0 program. So how do we achieve this if removing public package vars and init?

I think the only way to really make it work reliably, is probably to retain the current functionality for public package vars and init functions reserved for Go 1.x packages.

Conclusion

Trading publicly scoped variables and the init() method for a more complex const feature, shows a possible way where a set of language features could be traded for another to keep Go 2.0 simple. The trade helps calculate the effect on the Go standard library, which can agin be used to evaluate how this change affects real code, and reason about how it could improve real programs.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.