Future of Golang
I started working on this shortly after this post went up on reddit. And with the Go2.0 announcement, I thought it was a good time to post. This is what I would like to see in the future of Golang. I am not a developer of the language, so take this with a very large grain of salt.
Before I tell you what I want (and do not want) I will give you the lens through which I view these arguments. I do not care how easy or hard it is to write Go code. I do care how easy it is to read and reason about Go code. I care very much that it remains difficult to be mislead by Go code. I acknowledge that Go is not perfect in this regard, but it is better than any other language I've seen to date.
I would also prefer to keep the language spec small. There are features that could be added that would be consistent with Go, but provide little functionality.
Go 1.X
These are changes I would like to see that we could have without breaking backwards compatibility. They may make more sense in a 2.0 release, but they could feasibly done in the current version.
Packages Fulfill Interfaces
Packages already behave a lot like structs. They have methods and fields. The methods and fields are accessed with the dot operator. It would not be a stretch to be able to use a package to fulfill an interface. This would be fantastic for managing dependencies and greater decoupling.
Built-in Interface hasher
We have one built-in interface of "error", so there is precedent. Having another of "hasher" would be helpful. A value fulfills hasher if it can be hashed to a map key.
Non-Exported Embed
Say you want to use a Struct from another package and extend it:
type MyFoo struct{
*their.Foo
myInt int
}
This is great, except that if you need to rely on the embeded value not being altered, it's a problem. I would propose
type MyFoo struct{
. *their.Foo
myInt int
}
All fields and methods on Foo would be exposed, but myFoo.Foo is not.
Append buffered Channels
A common problem when using buffered channels is determining what the capacity should be. There are few cases where if you exceed the capacity of a channel, you risk deadlock. There are a number of solutions to the problem, but all require at least one additional go routine to baby-sit the channel.
I would suggest extending append to work on buffered channels
ch = make(chan int, 10) ch = append(ch, 1,2,3,4,5) fmt.Println(len(ch), cap(ch)) // 5,10 ch = append(ch, 6,7,8,9, 10) fmt.Println(len(ch), cap(ch)) // 10,10 ch = append(ch, 11,12,13) // at this point, the channel is overflows the buffer, so the capacity is doubled fmt.Println(len(ch), cap(ch)) // 13,20
Others
Here are some others I like, but I have nothing to add to the conversation.
Go 2.0
These are things that would break backwards compatibility.
Remove Stuff
Complex numbers
I don't think complex numbers should be a built-in type. I'm not going to make a long argument for this.
Remove Named Returns
Named returns save a few lines at most and can cause confusion. They are almost universally recommended against.
new()
It's better to do
foo := &Foo{}
than
foo := new(Foo)
so just get rid of it.
Better nil
Most of the rough edges around Go have to do with nil handling. There are a few things that I think would help with this.
No built in types should be nil. What is currently an nil slice would be a slice with length 0 and capacity 0. Same thing for a map. Under the hood it could still be represented as nil. As an extension of this, the zero value of a map would not fail when the first value is assigned.
The only thing that should have a nil value is a pointer. And a pointer should always have a type.
Negative Indicies
Given a slice s, s[-1] should return the last item in the list.
It has been discussed before, but I disagree with the logic. I believe the opportunity for subtle bugs is fairly small. All the cases where you could have those subtle bugs are currently cases that could panic. And I believe it makes code more readable.
Type type
I have to admit, I'm less sure of this one. It's more a vague idea than a full suggestion, but I think it has merit. In a switch statement, we have foo.(type). I would propose extending that. This gets some of the functionality the community feels is missing from the lack of generics while keeping the language mostly static.
We could do type comparisons
var someBool = foo.(type) == bar.(type)
or
func RestrictType(t Type) func(interface{}){
return func(i interface{}){
if i.(type) != t {
panic("Type Mismatch")
}
// continue
}
}
And often when using data-structures (like btrees) we want them to hold a consistent type. Having a Type type would let us write packages that enforced that, but with the same semantic rigor that interfaces give us.
It still wouldn't let you cast to a type because the type isn't know at compile time.
Others
I like these but have nothing to add
- https://github.com/golang/go/issues/20443
- https://github.com/golang/go/issues/19623
- https://github.com/golang/go/issues/20165
- https://github.com/golang/go/issues/8082
- https://github.com/golang/go/issues/20733
Half-Baked
I'm less sure of the following ideas, but they're worth consideration.
Range with functions, maybe?
Given a function
func(...) (..., bool)
It can be used in a range loop, and the loop will exit when the bool is false. As it stands, there are four ways to get this behavior but they all have significant drawbacks.
You can have a function return a slice and use range. But if the slice is long, that can waste memory. And if it is likely that you'll only consume part of the list, it wastes compute power as well. It also fails completely if the upper bound isn't know.
You can have a channel with a go routine on the other end. But if the loop wants to break it needs to send a signal to the go routine. Doing this correctly is just a bit bulky and doing it wrong causes leaks.
You can do it with some sort of iterator, but that's also a bit bulky and there are many variations on this so the reader has to decipher how a particular iterator works.
And finally, the one that mostly closely matches the original suggestion, you can do it with a for loop, but that can get really bulky
for a, b, keepGoing := someFunc(); keepGoing; a, b, keepGoing := someFunc(){
// do something with a and b
}
not too bad there, but this gets worse
for a, b, c, d, keepGoing := someFunc(x, y, z); keepGoing; a, b, c, d, keepGoing := someFunc(x, y, z){
// do something with a and b
}
And because the first and last portion of the loop must match, there's room for error.
mutex.Lock returns unlock func
It would be safer
lock := mux.Lock() // do stuff lock.Unlock
Adding a Locked() bool method would also make it easy to drop in a for loop to create a "with" type structure.
for lock := mux.Lock(); lock.Locked(); lock.Unlock(){
// do stuff
}
Allow fulfilled types on Interface definitions
This comes up when building object graphs. Often you will have something like
type Node interface {
Parent() Node
Children() int
Child(int) Node
}
But if you have
func (f Foo) Parent() Foo{ return f.parent }
That doesn't fulfill. If Foo.parent can fullfil Node, than f.Parent() should return Foo and Node(f).Parent() should return Node.
What I don't want
Generics
These are not necessary and they will not make code more readable. There are already lots of alternatives to generics and there's a good trade off that higher level abstractions require more work from the author to keep the code in a state that it can be reasoned about.
Generics are more helpful when writing code than when reading code. As I stated at the beginning, I'm far more concerned with the later.
While there is a lot of support for the idea of adding generics, that is only true in the abstract. If you ask the community why they need them, what problems it will solve and how they should be implemented, they seemingly large block breaks down into disagreement. I don't think generics should be added until a significant portion of the community (~70%) can reach an agreement on what problems they will solve and how they will be implemented.
However...
I would make a suggestion to the community; don't use {}interface in your packages, except as a type declaration. Here's a simple example I've seen a few places (and written myself a few time); it is useful to couple a mutex and map. We you put that in package along with methods like Insert, Get, Iterate and so on. If we declare
type MapKey {}interface
type MapValue {}interface
Then it becomes trivial to setup a build process that copies that package and replaces those two lines. Yes, it's part of the build process, not part of the compile process and yes, now we have a bunch of generated code. But the code only needs to be generated when it's added or changed so it doesn't slow down compile times. And seeing generated code means you can confirm it looks they way you think it should and you can reason about. Remember that I want code that's easy to read. Code that's generated at compile time is impossible to read.
Dependency Management Tool
Yes, Go has some issues with dependency management. I know it can get frustrating on larger projects. But much like generics, I think this is an idea that gets a lot of support in the abstract, but far less if you put forth any specific suggestion.
This is also a deceptively complex problem and one that Go cannot actually solve no matter what tooling they provide. What I think most people want when they talk about this is some sort of fuzzy matching on versions. I say this because if you want to include a specific version of a package in a project, there are already several simple ways to do that. But if you want the latest 1.x.x branch of some package, but not the 2.x.x version, that is not so simple.
But Go can only do so much here. It's up to package maintainers to correctly version their packages. Without that, Go can't provide any tooling.
However...
Package maintainers should provide versioned branches of their code. And many do. This allows for the community to experiment with different conventions and different tools (like dep) but allow Go to remain agnostic.