DEV Community

Charles Francoise
Charles Francoise

Posted on

Go, I love you, but you're bringing me down.

#go

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{})
}
Enter fullscreen mode Exit fullscreen mode

We could then use the [] operator as a shorthand to these functions.

foo := keyVal["bar"]
Enter fullscreen mode Exit fullscreen mode

would be equivalent to

foo := keyVal.Value("bar")
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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 {
Enter fullscreen mode Exit fullscreen mode

would be equivalent to

it.Reset()
for !done {
    i, done := it.Next()
    if done {
        break
    }
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

This was fixed by adding the initialization.

    h := Handler{FooHandler: &Foo{}, BarHandler: &Bar{}}
Enter fullscreen mode Exit fullscreen mode

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
            }
        }
    }
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
mdrollette profile image
Matt Drollette

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 if ca had been closed the value of ok would be false

Collapse
 
loderunner profile image
Charles Francoise

Thanks!

Sameer Ajmani pointed it out on Twitter. I updated the post.

That's one problem down!

Collapse
 
opennota profile image
opennota

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?

Collapse
 
loderunner profile image
Charles Francoise

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.

Collapse
 
opennota profile image
opennota

@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?

Thread Thread
 
loderunner profile image
Charles Francoise

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 a nil member, or the instance being nil 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.

Collapse
 
legolord208 profile image
jD91mZM2

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

Collapse
 
loderunner profile image
Charles Francoise

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.

Collapse
 
legolord208 profile image
jD91mZM2

Right, I see