I've said it before, and I'll say it again: I really like Go. It's the most fun I've had learning a language since Python. It's statically typed and compiled – and as a systems-oriented programmer, I never found anything that could really move me away from C because of this – it has a great standard library, it handles code reuse without inheritance beautifully, and those concurrency paradigms have changed my life for ever.
That being said, I quickly had a few issues with some of the language's specifics. And while I've been trying to learn its patterns and conventions as best I can, I'm pretty certain that some of them could be fixed inside the language, and not by using a specific pattern or abstraction. For the sake of simplicity.
As I was reading Russ Cox's Gophercon 2017 talk about Go 2, I decided to take him up on this:
We need your help.
Today, what we need most is experience reports. Please tell us how Go is working for you, and more importantly not working for you. Write a blog post, include real examples, concrete detail, and real experience. That's how we'll start talking about what we, the Go community, might want to change about Go.
So here are, as a new Go programmer, my 2 cents about what's missing.
Operator overloading
Got your attention? Good. Because I hate operator overloading. I've been through too many C++ code bases where developers thought it was better to be clever than to write a clean interface. I've had my share of over-enthusiastic coders re-defining fundamental semantics such as the dot(.
), arrow(->
) or call(()
) operators to do very unexpected things. The cost in documentation and on-boarding new developers was huge, and the mental work of switching paradigms when coding made the whole thing quite error-prone. The fact that the stdlib does weird things pretty freely (std::cout << "Hello, World!"
, anyone?) doesn't really help.
But operators are also a great shorthand for some clearly defined semantics. In other words, if semantics precede the operator, there's a far lesser chance to abuse them. An example would be the []
operator for integer-based indexing (such as arrays or slices), or for key-value operations (such as maps). Or the usage of the range
keyword to iterate over arrays, slices, maps and channels.
One way to make sure these semantics are well respected would be to make the operators as syntactic sugar to well-defined interfaces.
Key-value operator:
struct KeyValueMap interface {
Value(interface{}) interface{}
SetValue(interface{}, interface{})
}
We could then use the []
operator as a shorthand to these functions.
foo := keyVal["bar"]
would be equivalent to
foo := keyVal.Value("bar")
The objects used as key would need to be hashable or comparable in some way. Perhaps the compiler could enforce the same rules it does with maps.
Range iteration could be done similarly:
type Iterator interface {
Next() (interface{}, bool)
Reset()
}
Next
would return the next value in the iteration, and a bool
that would be true
when the iterator is finished. Reset
would be used to initialize the iterator at the beginning of the for
loop.
for v := range it {
would be equivalent to
it.Reset()
for !done {
i, done := it.Next()
if done {
break
}
I think this would allow Go developers to implement much more go-ish structures and I can see many cases of memory-efficient iterators that would make use of this clean syntax.
Of course, this kind of breaks the strong typing in Go. Maybe a feature like this would go hand-in-hand with generics?
Initialization of embedded interfaces
We recently had a case of an issue that took us a while to debug. We were using type embedding to compose different services in one structure. At first we had just one handler.
type FooHandler interface {
// some `FooHandler` methods
}
type Handler struct {
FooHandler
}
func main() {
// Foo is a concrete type implementing FooHandler
h := Handler{FooHandler: &Foo{}}
RegisterHandler(h)
We embedded a second handler, but forgot to add it in the Handler
initialization. The error message was pretty cryptic.
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x20 pc=0x10871d6]
goroutine 1 [running]:
main.RegisterHandler(0x1108140, 0x1126ac8, 0x0, 0x0)
This was fixed by adding the initialization.
h := Handler{FooHandler: &Foo{}, BarHandler: &Bar{}}
I wish the error message would have been clearer. Such a simple omission shouldn't require 30 minutes and two programmers. Maybe we're just new at Go, but I wish the error would have been more explicit. That or the compiler could warn against using zero values for embedded types. I don't know if there's a case where you would want to embed a nil
value. This would probably be a breaking change for a few code bases, but the benefit in memory safety could be worth it.
Closed channels
EDIT: The issue I detailed here was only because of a feature of the language I didn't know of. It was pointed out in the comments and on Twitter (and here). I'll be showing Go way to avoid the problem I had.
In the following program, the final loop loops forever while receiving zero values from both channels, ca
and cb
.
func main() {
// Create a first channel
ca := make(chan int)
go func() {
// Send one integer over the channel every millisecond up to 9
for i := 0; i < 10; i++ {
ca <- i
time.Sleep(1 * time.Millisecond)
}
close(ca)
}()
// Create a second channel
cb := make(chan int)
go func() {
// Send one integer over the channel every millisecond up to 99
for i := 0; i < 100; i++ {
cb <- i
time.Sleep(1 * time.Millisecond)
}
close(cb)
}()
// Receive from the first available channel and loop
for {
select {
case n := <-ca:
fmt.Printf("a%d ", n)
case n := <-cb:
fmt.Printf("b%d ", n)
}
}
}
This problem can be fixed by reading two values from the channel. By using n, ok := <-ca
, ok
will be false
if the channel is closed. Once the channel has been closed, we set it to nil
as receiving from a nil
channel blocks.
// Read from first available channel and loop
// until both channels are nil
for ca != nil || cb != nil {
select {
case n, ok := <-ca:
if ok {
fmt.Printf("a%d ", n)
} else {
// If the channel has been closed, set it to nil
// Receiving from a nil channel blocks, so we know
// this select branch will be unreachable after this
ca = nil
}
case n, ok := <-cb:
if ok {
fmt.Printf("b%d ", n)
} else {
cb = nil
}
}
}
I'm still pretty new to Go, so I'm fairly certain these are either misconceptions on my part, or things that have been tried and abandoned because they didn't work. Feel free to give me any documentation or pointers in the comments.
Top comments (9)
On the closed channels point - reading from a channel returns a second boolean argument that indicates if the channel is closed or not. So, for your example you could do
n, ok := <-ca
and ifca
had been closed the value ofok
would befalse
Thanks!
Sameer Ajmani pointed it out on Twitter. I updated the post.
That's one problem down!
I've filed an issue about the unclear panic message with nil interfaces:
github.com/golang/go/issues/21152
Do you have anything to say?
Great! Nothing to add, you pretty much nailed it. Your example is clear and concise. Thumbs upped the issue.
My example is more of a "working case" where the lack of compiler checks or better panic messages can lead to long debugging before you get that forehead-slapping revelation.
@aclements asked:
Would it be an improvement simply for the message to say "nil value" instead of "nil pointer"? Did "nil pointer" lead you in the wrong direction?
I think the real issue is that the error message doesn't make it clear that it's the embedded interface that is
nil
. I looked for anil
member, or the instance beingnil
itself, but I didn't think of checking the embedded interfaces.I guess it's also part of being used to inheritance from C++ and Java (and generally learning to code in the 90s-00s), and I'll get used to double-checking embedded types. But it wouldn't hurt to have a more explicit error message, that would shave a few minutes off debugging the panic, even for experienced Go practitioners. And it would also ease the path for newcomers.
You should try Rust. I didn't fall for it at first, but I am now addicted to it. It has pretty much everything you want :P
I love Rust. I follow it closely and I hope to be doing some in the future.
Right now, I'm on a Go project, though. And while I think Rust is definitely a language for me, Go is really a lot of fun and I find it easy to share with other developers, from different backgrounds.
Right, I see