1. 8
  1. 6

    This is a pretty well curated list of advice. However, it almost makes me dislike the language more after reading it. (disclaimer: I write Go professionally, but it’s only tolerable to me, and not something I’m obsessed with)

    My more general advice to new gophers is: don’t be afraid to write code. A lot of programmers have this mindset that everything and anything should be as abstract as possible; heaven forbid we write one more keystroke than is necessary.

    Coming from languages like Lisp, where programming “bottom up” is a great and useful strategy, this statement irks me. I’ve done this way too many times in Go, and it leads to so many more lines of code to maintain, for seemingly no good reason. The inability to share simple methods on a family of “related types” implementing an interface is the biggest monster here.

    Another area which newly fledged Gophers should study are the various blog entries on using concurrency. Go puts the emphasis on communication between tasks where many popular languages are concerned with the threads of execution within tasks. Deprogramming the hideous anti-patterns threading encourages can take some effort but it’s definitely worth it to be freed from the conceptual tyranny of locks and mutexes.

    I find it interesting that the above statement is contentious within the advice. Everything from “x/ssh is better off now that it removed all the channel magic” to “channels and goroutines are the best–use them.”

    In reality, I’ve found that I use locks way more often than what I think was originally intended by the Lead Gophers. I’ve written lots of code in an attempt to avoid locks and mutexes, and it almost always leads to something more complicated, harder to reason about, and slower than code that just adopts locks.

    1. 1

      As an anecdote point, while developing my Raft implemenation in Ocaml I read a lot of Raft implementations in Go. I was really surprised that many of them were using locks. I come from Erlang, though, where I would assume most Go developers would create things like gen_server to manage state, but it felt like Java multithreaded programming with channels floating around somewhere as well. I can’t speak for the quality of Go developers writing Raft implementations, though, so I don’t know if that was them using the language correctly or not.

      1. 4

        So, it’s interesting. On the one hand you have these really powerful concurrency primitives, but on the other hand, you have these really primitive concurrency primitives in mutexes and locks, and such.

        Channels and goroutines aren’t understood very well by most programmers, but everyone thinks they understand locks, because it, in practice, is deceivingly simple.

        In Go, you could protect a resource by doing something like this (this may not compile, I might’ve taken more liberties than I expected):

        type Request struct { Fn func(int) int, Result chan int }
        func (s *SharedResource) server(reqs chan Request) {
           for r := range reqs {
               s.protectedVariable = r.Fn(s.protectedVariable)
               resp <- struct{} // as a notification that we're done
           }
        }
        
        rc := make(chan Request)
        
        globalSharedResource := SharedResource{protectedVariable: 0}
        go globalSharedResource.server(rc, rsp)
        

        Now, if you want to act on the shared resource, you more or less send functions to the reqs channel and wait for a response:

        // Get the value of the protectedVariable
        req := Request{func(x int) int { return x }, make(chan int, 1)}
        rc <- req
        value := <- req.Result   
        
        // Set the value of the protectedVariable, atomically
        req := Request{func(x int) int { return newValue }, make(chan int, 1)}
        rc <- req
        newValue := <- req.Result
        

        And, this will work. Operations will be atomic on that variable (provided nothing else is touching it). You can do something else where instead of sending functions you send structs describing operations, or use an Interface to describe operations, or whatever it might be. I’ve done this before. I’ve tried to write code like this, and it’s a complete hassle, when the following code is just as safe (edit: And way more efficient, easier to read, easier to maintain…):

        globalSharedResource := SharedResource{protectedVariable: 0, pvm: new(sync.Mutex)}
        // Get the value of the protectedVariable
        globalSharedResource.pvm.Lock()
        value := globalSharedResource.protectedVariable
        globalSharedResource.pvm.Unlock()
        
        // Set the value of the protectedVariable
        globalSharedResource.pvm.Lock()
        value := globalSharedResource.protectedVariable + 1
        globalSharedResource.pvm.Unlock()
        

        And that’s why you see in all those Raft implementations, mutexes instead of insane uses of channels.

        1. 2

          Yeah, channels are not mandatory and I think some introductory Go materials confuse people about that. My Go install’s src/ has 73 non-test files matching /sync.(RW)?Mutex/, 105 using sync.anything. I think the message should be you definitely want channels in your toolkit, not that you should use them all the time.

          Where channels are a fit it’s usually not a rearrangement of code that’s straightforward to do with a lock. It’s more like you’re already thinking in terms of work queues or threads communicating/signaling events to each other, and if you didn’t have channels you’d have to half reinvent them.

          For two examples, the experimental HTTP/2 implementation used receiving/sending/coordinating goroutines that talk via channels (that was the description in a talk bradfitz gave a while back, haven’t read the code), and some parallel sort stuff I wrote has a worker pool type thing going on.

          1. 2

            Where channels are a fit it’s usually not a rearrangement of code that’s straightforward to do with a lock. It’s more like you’re already thinking in terms of work queues or threads communicating/signaling events to each other, and if you didn’t have channels you’d have to half reinvent them.

            Sure. Channels work wonderfully for problems where communication is key. But, atomic reads/writes to shared state is a communication problem, after all.

            An interesting exercise is to implement a “traditional” lock using channels.

      2. 1

        You can easily share related methods on related types by anonymous embedding. I find it works much better than inheritance.

      3. 2

        Meta-advice, nothing much to do with Go: learning stuff comes in handy. But also, try to write code as boring as the day is long when you’re writing for production. You’ll come across plenty of interesting problems organically, and there is so much else to think about than clever ways to express or optimize your code.

        1. -3

          Stop? :)