评论删除后,数据将无法恢复
This is an additional post in the “Go is not good” series. Go does have some nice features, hence the “The Good” part in this post, but overall I find it cumbersome and painful to use when we go beyond API or network servers (which is what it was designed for) and use it for business domain logic. But even for network programming, it has a lot of gotchas both in its design and implementation that make it dangerous under an apparent simplicity.
What motivated this post is that I recently came back to using Go for a side project. I used Go extensively in my previous job to write a network proxy (both http and raw tcp) for a SaaS service. The network part was rather pleasant (I was also discovering the language), but the accounting and billing part that came with it was painful. As my side project was a simple API I thought using Go would be the right tool to get the job done quickly, but as we know many projects grow beyond their initial scope, so I had to write some data processing to compute statistics and the pains of Go came back. So here's my take on Go woes.
Some background: I love statically typed languages. My first significant programs were written in Pascal. I then used Ada and C/C++ when I started working in the early 90's. I later moved to Java and finally Scala (with some Go in between) and recently started learning Rust. I've also written a substantial amount of JavaScript, because up to recently it was the only language available in web browsers. I feel insecure with dynamically typed languages and try to limit their use to simple scripting. I'm comfortable with imperative, functional and object oriented approaches.
This is a long post, so here's the menu to whet your appetite:
That's a fact: if you know any kind of programming language, you can learn most of Go's syntax in a couple of hours with the "Tour of Go", and write your first real program in a couple of days. Read and digest Effective Go, wander around in the standard library, play with a web toolkit like Gorillaor Go kit and you'll be a pretty decent Go developer.
This is because Go's overarching goal is simplicity. When I started learning Go it reminded me when I first discovered Java: a simple language and a rich but not bloated standard library. Learning Go was a refreshing experience coming from today's Java heavy environment. Because of Go's simplicity, Go programs are very readable, even if error handling adds quite some noise (more on this below).
But this may be false simplicity though. Quoting Rob Pike, simplicity is complicated, and we will see below that behind it there are a lot of gotchas waiting to bite us, and that simplicity and minimalism prevent writing DRY code.
Goroutines are probably the best feature of Go. They're lightweight computation threads, distinct from operating system threads.
When a Go program executes what looks like a blocking I/O operation, the Go runtime actually suspends the goroutine and resumes it when an event indicates that some result is available. In the meantime other goroutines have been scheduled for execution. We therefore have the scalability benefits of asynchronous programming with a synchronous programming model.
Goroutines are also lightweight: their stack grows and shrinks on demand, which means having 100s or even 1000s of goroutines is not a problem.
I once had a goroutine leak in an application: these goroutines were waiting for a channel to be closed before ending, and that channel was never closed (a common deadlock issue). The process was eating 90% of the CPU for no reason, and inspecting expvars showed 600k idle goroutines! I guess the CPU was used by the goroutine scheduler.
Sure, an actor system like Akka can handle millions of actors without a sweat, in part because actors don't have a stack, but they're far from being as easy to use as goroutines to write heavily concurrent request/response applications (i.e. http APIs).
Channels are how goroutines should communicate: they provide a convenient programming model to send and receive data between goroutines without having to rely on fragile low level synchronization primitives. Channels come with their own set of usage patterns.
Channels have to be thought out carefully though, as incorrectly sized channels (they're unbuffered by default) can lead to deadlocks. We will also see below that using channels doesn't prevent race conditions because Go lacks immutability.
The Go standard library is really great, particularly for everything related to network protocols or API development: http client and server, crypto, archive formats, compressions, sending email, etc. There's even an html parser and a rather powerful templating engine to produce text & html with automatic escaping to avoid XSS (used for example by Hugo).
The various APIs are generally simple and easy to understand. They can sometimes look simplistic though: this is in part because the goroutine programming model means we just have to care about "seemingly synchronous" operations. This is also because a few versatile functions can also replace a lot of specialized ones as I found out recently for time calculations.
Go compiles to a native executable. Many users of Go come from Python, Ruby or Node.js. For them, this is a mind-blowing experience as they see a huge increase in the number concurrent requests a server can handle. This is actually pretty normal when you come from interpreted languages with either no concurrency (Node.js) or a global interpreter lock. Combined to the language simplicity, this explains part of the excitement for Go.
Compared to Java however, things are not so clear in raw performance benchmarks. Where Go beats Java though, is on memory usage and garbage collection.
Go's garbage collector is designed to prioritize latency and avoid stop-the-world pauses, which is particularly important in servers. This may come with a higher CPU cost, but in a horizontally scalable architecture this is easily solved by adding more machines. Remember that Go was designed at Google, who are all but short on resources!
Compared to Java, the Go GC also has less work to do: a slice of structs is a contiguous array of structures, and not an array of pointers like in Java. Similarly Go maps use small arrays as bucketsfor the same purpose. This means less work for the GC, and also better CPU cache locality.
Go also beats Java for command-line utilities: being a native executable, a Go program has no startup cost contrarily to Java that first has to load and compile bytecode.
Some of the most heated debates in my career happened around the definition of a code format for the team. Go solves this by defining a canonical format for Go code. The gofmt
tool reformats your code and has no options.
Like it or not, gofmt
defines how Go code should be formatted and that problem is therefore solved once for all!
Go comes with a great test framework in its standard library. It has support for parallel testing, benchmarks, and contains a lot of utilities to easily test network clients and servers.
评论删除后,数据将无法恢复
评论(10)
不好的地方是,感觉不太适合写业务代码,动态更新方面不是很成熟.