One day at work, we were discussing the Go programming language in our work chatroom. At one point, I commented on a co-worker's slide, saying something along the lines of:
"I think that's like stage three in the seven stages of becoming a Go programmer."
Naturally, my co-workers wanted to know the rest of the stages, so I briefly outlined them. Here, expanded with more context, are the seven stages of becoming a Go programmer; see if you can see yourself on this pathway.
Programming and development
After your initial run on A Tour of Go, you start thinking "Now, how can I make this language behave more like an object-oriented language...?" After all, you are used to that stuff. You want to make robust code. You want polymorphism.
"There has to be away!" You say, and you find struct embedding. It allows you to cleverly delegate methods from the enclosing object to the embedded object without having to duplicate code. Great!
Of course, this is not true. Struct embedding only allows you to delegate method calls. Even if it looks like you are doing polymorphic method dispatch, the relationship is not IS-A. It's HAS-A, so the receiver of the method call is not the enclosing object: The receiver is always the embedded object to which the method call was delegated to.
You were lured to Go by the promise that it will allow you to easily run the concurrent code, which, it does via goroutines! All you need to do is use the go keyword, and you can make pretty much any function or method call run concurrently. It is only natural, then, that you want to maximize your code's efficiency by making as much code to run in parallel. And because you hid this fact by making your function calls to create goroutines automatically, the caller does not even need to be aware of this.
Yeah, so it might make your code a bit more complicated but look, now everything runs concurrently!
Go allows you to create millions of goroutines without sacrificing much efficiency, but you really should not use goroutines just because you can. Concurrent code is harder to maintain and debug than code that just flows in a single thread. I mean, have you given serious thought to whether your shared objects are really synchronized properly when accessed from multiple goroutines at once? Are you sure the order of execution is absolutely correct? Have you really checked if those goroutines actually exit when they're no longer needed?
Goroutines are best used only when they are necessary, and unless your requirements dictate that you do everything in memory or some such, you should never abandon the use of good old multi-process models.
And finally, try NOT to spawn goroutines behind your users' back, especially if you are writing a library. Explicit calls to use go calls usually give the user more flexibility and power.
Goroutines take you only so far. Use them only when it really makes sense.
After being disillusioned that you cannot make your objects behave in a polymorphic manner, you suddenly realize the capabilities offered by interfaces. Interfaces allow you to describe APIs; there has to be a way to use this to write more robust code.
Interfaces do give you a lot of power, but it's not an end-all solution. It still does not provide true polymorphism in the sense of object-oriented programming. You are also limited by the fact that interfaces can only define the API, and you cannot associate any data with it.
Also, although there are legitimate cases where exporting only interfaces instead of concrete structs make sense, it really should not be your default mode of operation. Interfaces are best when they are small-ish (as opposed to describing an entire list of methods defined for an object). Also, if you are not careful, you will have to either write a lot of extra code to fulfill the interface or to write code that requires a lot of type-assertions.
After you spent a lot of time pondering how to bend Go to work your way, you are now looking for that missing piece that will make everything work your way. "Wait, there are still channels!"
Channels implicitly handle concurrent access correctly. You believe you should be able to solve many of the obstacles so far by cleverly using channels to handle synchronization, returning values (a la future/promises), and flow control with select-statements with various channels.
"Why?! Why is it so painful to write Go code? It doesn't allow me to write code the way I have been doing."
You are frustrated. No polymorphism. Concurrency is hard. Channels don't solve your problems. You don't even understand why Go exists. You feel like you've been stripped of all the nice tools and constructs that other languages provide.
You believe that more powerful tools to express abstract ideas are absolutely necessary. Go just doesn't cut it.
Go is decidedly opinionated. I come from a Perl background, and for a while I could not believe how limiting Go was. So yes, I understand if you become frustrated.
At some point, you grudgingly decide to write Go code according to how most of the standard library is written. You also give up trying to be clever, and start writing straightforward code.
Then it comes to you: You just didn't want to accept the Go way.
Everything starts to make sense.
In all seriousness, learning Go does require some unlearning. I had to unlearn object-oriented programming a bit, as well as embrace the fact that no matter how many useful tools the language may give you, writing concurrent code is just too hard for mere mortals. I also had to unlearn to use exceptions.
Once I decided to accept the functionalities and constructs as they are, writing Go code became much easier, and definitely much more fun.
You have accepted the Go way. You now write everything, including what you would've normally used Perl/Ruby/Python for, in Go. You realize if err != nil no longer bothers you. You only use goroutines and channels when you must.
You are one with the Gopher. You feel its glorious chi, and cry when you realize its mercy for allowing your to write code in such a majestic language.
Congratulations. Now you are a Go programmer.