Unfortunately, Go is not even close by the number of tools written for it, at least for web services, where I work. It's hard to imagine something useful that doesn't have either JVM port, or originally written in Java (testing/mocking, builders, freaking ORMs, frameworks).
It's not always good though, in Java world you have to choose between multiple competing tools, while other ecosystems have mostly only one, often adopted from Java version (except Javascript, Rust, they have their own zoos). But IMHO Java is still the nursery for all kinds of tools for development.
Java might in theory be faster but I have never seen a real Java team put out code that was capable of doing 10k requests per CPU per second. Always hitting per thread limits or logging lock contention or GC issues etc. except one thing written by a genius with NIO. Ordinary people can hit those performance numbers with Go and without special smarts.
Of course one shouldn't mistake those. But one has to understand, a tool is only as valuable as it actually produces result, not as what it could produce given a theoretical perfect programmer. C is an extremely capable programming language judging on "what you could do with it", but in reality, most programmers fail to create robust C code. And most Java code suffers from programmers using it in a less than optimal manner.
Also, Go does not suffer from some fundamental Java problems, though they slowly get corrected. Go has value types, unboxed arrays. Generally, no struct in Go is boxed automatically. You have first class functions in Go and of course, it is statically compiled. It is true, that Hotspot often produces very good code, but for a lot of szenarios, a statically compiled Go executable beats it, as run-time compilation is not required.
As I wrote, Java is in the process of adding features that mitigate its problems. My post was pointing out a few things where Go is clearly ahead of Java.
Structural types are one of those things that annoy me in both theory and practice, when I have to touch Go code.
In theory, it's annoying because it prevents you from expressing constraints (think: monad laws, or Rust's Sync/Send traits). In practice, it makes stuff like go-to-definition nearly useless, which makes it a pain to read code written by other people. Especially when combined with the almost sadistic module system.
It is a great alternative for things that otherwise you would build with C++,C or Java. Not so much for things you would otherwise build with python, ruby, etc.
If you take a bit of time to think about the data flow, you can get it about as succinct as Python. And then you get to distribute a static binary that will be light on the CPU is needed and use all the CPUs if needed.
Agree over the java part. But Python is still orders of magnitude more productive, being it because of the ecosystem, available libraries, frameworks, or almost any other metric. The only advantage over python is performance, and maybe static typing depending on your taste.
Go more than makes up for this in the deployment aspect, since deploying Python in 2020 (Python 2, Python 3, conda, pip, venv, system interpreter, poetry, etc.) is still a mess.
pip is broken in no many aspects, between pip and pip3, things installed in your home folder vs /var, "binary" entries that don't exist after installing a tool, cryptics stack traces because of missing libs / not compatible for your platform ( native build ) ect ...
Why do we need virtual env in 2020?
It's one of the worse "modern" language to build, even Java / NodeJS are better.
- installs an entire package manager, which may or may not conflict with system tools or a different package manager,
- assumes pyenv will build Python against the system libraries without additional flags (it won't if you're running Big Sur, you'll need zlib from Homebrew),
- assumes you are on macOS, and does not attempt to work on Windows or Linux, or account for the idea that someone might not want to deploy a program using Docker.
Which makes rolling back a bad push really interesting. Give me a append only repo with my static binaries that my compute nodes deploy from. I even went to the trouble to put Python into RPMs for this.
Go makes arbitrarily large codebases feasible. I find Python to be exponentially more painful as codebase size increases. For personal projects this is fine, but for industrial use cases Go has a significant competitive advantage.
For me, this is the consequence of a reasonable static type system. Without generics and the current error handling story, it almost feels like Go makes large codebases necessary in the projects I've used it, compared to other statically typed languages such as Kotlin, Java, Rust, and C#.
Go 2 on the other hand looks like it will solve this problem, and I eagerly await its release.
In my codebase at work there are a few cases where code is (poorly duplicated) that would benefit from a proper generic solution. But I don’t expect go2 to significantly reduce our codebase size, even if we replaced the aforementioned components.
I agree that some form of static typing is critical to scale a codebase, or an ecosystem. Let’s also not forget the importance of a packaging system with robust version management. Go modules and proxy.golang.org are also critical to Go’s success at (code) scale.
> Agree over the java part. But Python is still orders of magnitude more productive, being it because of the ecosystem, available libraries, frameworks, or almost any other metric.
It may be more productive if you are the sole consumer of your code. If you need to ship code to be run by third parties Python tends to result in overly complex and tedious setup process. One shouldn't ignore that users of Python applications often have to pay the price.
Not the OP, but i would have guessed that the (subjective) axes of "being better" are uncontroversial - by which I mean prepend "there are many who value that" in front of each:
- a GC that can has relatively smart defaults without having to tweak a ton of parameters
- don't have to ship a vm
- concurrency baked in from the start
- simpler language
- doesn't try to square everything into the OO paradigm
Speaking strictly about the language, and not the platform, I'm skeptical about the lack of more modern core library and language features like immutability, pattern matching, and records. They spent almost 10 years saying "we don't need generics", but now are implementing it
> They spent almost 10 years saying "we don't need generics", but now are implementing it
You're making things up. Not a single member of the Go team ever said "we don't need generics". In fact, the FAQ stated from day one that they "continue to think about it", and that "Generics may well be added at some point".
Yes. IIRC, Rob Pike or someone else on the Go team even said something like "it's hard, so we are taking our time about it, may implement it later". Edit: see [1] below.
They've always also said "show us some compelling use cases", as if those still need to be shown ~15 years after after every other language except C got them.
The real value is that Google is pumping a lot of money into writing high quality libraries.
Any language with similar backing would do equally as well.
The golang standard library and high-quality third-party libraries are really great
Its a good compromise between python and java. We gradually moved our codebases from python to golang. This has reduced our infra cost quite a bit. Some of our mission critical stuff is still in C++ and I'm fairly certain golang would not replace any of those.
I hope the golang community takes a thing or two from rust in terms of package management.
I've been using Go as my main programming language for the past 5 years or so, and it's still my favorite language (picking from PHP, Java, C++, Python, and a few more I have experience with).
Its simplicity makes it relatively easy to write high quality software and reduces maintenance, as there are no breaking changes in the language itself. Tooling is on point, you get everything you need, standard formatting (which helps to keep all code bases in the wild consistent), and cross compile.
One of the things that might make it even better are generics, but I haven't really missed them so far. In conclusion, I don't think I will switch to anything else soon.
It’s a unique and different language that clearly hit some kind of niche. It’s great when a language has any users and this one has quite a bunch. That’s super cool!
Personally I don’t like it. Not low level enough for when I need to go low level, not high level enough when I want to go high level. Also I would have wanted generics right from the start; that should be the norm for typed languages unless the types are really super simple (Go’s are complex enough to warrant generics in my opinion).
But, maybe the reason why Go has the niche it has is because other programmers prefer it the way it so. So it goes with languages - it’s an aesthetics thing just as much as it is a technical thing.
Go has wide enough adoption that I don’t think you can call it niche anymore. It’s become somewhat of a de-facto systems/infrastructure programming language and is replacing Python in a lot of places where it was traditionally used for automation.
The language is simple, performant and writing concurrent code is intuitive (although I feel Go developers tend to get carried away using channels/goroutines).
Because every time I need to use a lock I know I am not smart enough to do so. Channels and go routines, I can reason about. (Tho they are slower - I have a runtime stats package that just stuff the numbers into a channel and then returns to the calling goroutine. I did an implementation using Atomic’s and it could sustain like 20x more calls per second before using all the callers CPU. But I still use the channel implementation.
Rust is ideal for this scenario: it lets you use the faster locks / atomics, and the compiler will check everything for you (assuming you can use a standard implementation rather than needing to implement your own - but there are well tested libraries for most common use cases).
It is on my list of things to learn but learning go made me so happy the urgency has decreased. Did they ever finish that rewrite of the core unix cmd line utils from C to Rust? Like find and if config etc. a rust busybox I guess.
In general there has been more effort put into trying to improve on the default tools through projects like ripgrep (grep), bat (cat), lsd and exa (ls), and fd (find) rather than creating drop-in replacements that don't provide all that much over the battle tested originals.
Oh, I didn’t mean lock vs channels/goroutines. I meant in general prematurely optimizing by using Go’s concurrency primitives. It’s so easy to goroutine-ize a simple for loop with a waitgroup, but it can quickly get complicated with stop channels, cancellations, error handling/propagating, graceful shutdown which is then a source of bugs.
To me it is a modern C with the lessons from Python learned. It is simple in the ways C is and the gotchas are all things i do naturally based on C. I like having structs and like having to check errors in each call. It forces one to concretize the idea that any reference to anything off of the CPU or the RAM might do anything. You have to have an error strategy for robust code. I don’t think people complain that Linux code does a lot of error checking. To be sure I mostly use to for network servers or making a lot of network calls quickly. (E.g. I have tools to do stuff in AWS that can reliably get S3 to start throwing 503 errors in Go. But they can re-encrypt a ton of data using one host and a small amount of time.
I agree as well. Going from C to Go as still a newcomer to C, I liked that Go seemed to be more "defined". For example that there is one style and doing something like placing a bracket differently results in a syntax error. This is good for newcomers because different styles and philosophies about a language can quickly make one feel lost.
For me, C only seems tedious because to every question I have about it, I will inevitably find 10 different answers, maybe all from different periods of time. And I understand that I can look at the C standard but there are things that it doesn't address. Also, venturing into embedded programming, you can quickly come across implementations of the standard libraries that aren't nearly as well-defined.
So considering these things, I think it is a good "modern C" as long as you don't go too low level.
Go was the first language that put a real effort in building a community, best practices and a healthy, sustainable ecosystem. This starts to pay off as there are more and more actively maintained, high quality libraries available.
Decent for writing web backends, APIs, RPC services. The high number of libraries also help. The language is very simple, which is good or bad, depending on who you ask.
Tooling could use a lot of work though. After switching between vim-go, gopls, and a handful of other language server implementations, I've given up on auto-completion, definitions etc altogether. Maybe it's my setup...
Overall, I'm pretty happy with running a fairly big Go app in production. Would I choose Go for my next serious project? Most likely.
No, but I've heard really good things about it, and even tried out the demo when it initially came out a few years ago. I was hesitant to change my Vim workflow though.
Thanks for mentioning it, I think I'll take another look at it now. Having a real IDE probably has more benefits once the code grows beyond tens of thousands of LOC.
Non-exported struct fields are a way of preventing your library from being composable. Checking if a value implements an interface is a way of writing surprising bugs. The standard library uses both of these features extensively, to give users bugs and prevent itself from being composable.
Checking that the user uses all values bound to variables but letting them discard errors by not binding them to a variable is a weird choice. Channels panicking instead of returning an error is a weird choice.
It's ok and I would like to to use it instead of Java.
The particular combination of things is, some library includes some method signatures that accept and return a specific struct (not a specific interface). Then, also, there is no exported way to make one of that struct, or to set its fields. Now you cannot write a library that wraps the original library and exposes the same API (unless you use unsafe and reflection).
In the OO languages I have encountered there seems to be a stronger convention toward using interfaces instead of struct or class types in this sort of situation, but people are certainly free in Java to use a final class type as the type of their parameters and return values if they want to make their library less composable.
No, but it's a more prevalent problem in the actual standard library of Go. The Java standard library does not regularly use final classes with private constructors and setters as the parameter and return types in its APIs (which would also prevent people from implementing those APIs). It uses interfaces.
Related complaint, the standard library taking structs as input is kind of annoying. I'm specifically thinking of net.Dialer having a net.Resolver, where that's a struct and not an interface, so it's difficult to sub in custom logic there (but still possible, at least, with some contortions).
Single-binary deployment and (these days) good dependency tracking (downloading multiple versions of a library) is great; I don't worry about trying to get somebody else's source code built I do the way when I get python packages. The python side is largely because setting up a venv is error-prone (at least, for me, somehow) and leaky. I still think having the version baked into the import path is kind of dumb, but I suspect that's really just because the documentation failed to explain the reasoning and good workflows (I understand just changing the path in go.mod may be enough).
I wish they had things like generics, but I fear that an incompatible version break would cause too much strife, much in the same way of python 3 / perl 6. Given how the vendoring tools story went, I'm not yet confident the core team can manage the community very well.
On the positive side, the error handling has been decent for me so far; an error interface is easy to understand, compared to rust (where I think you can use Box<dyn Error> but they don't actually say that in the basic documentation). I also like the decently sized standard library, again in comparison to rust where things are too minimal. I think ruby has a decent compromise, where the standard library is (mostly) also available as downloadable packages if you need a newer version. Somehow I've had worse experience with ruby gems compared to go modules, though; I think that's because the bundler documentation is rather bad — it feels more like an essay rather than a reference.
Not having a central repository of packages is a definite plus in my book for go; it leads to less of a problem with people taking the good package names (since most names are bad anyway). Less worries about cutely named libraries like devise (ruby, for auth) or hyper (rust, HTTP). IMHO, libraries should be plainly named, apps can have weird names if they want.
1. Built in support for tooling, testing, formatting etc
2. Trying to make concurrency mainstream
3. Large and developer friendly standard library, there is hardly any programming language where web backed can be written without using a framework or a library. Go is one of those.
My fingers are getting real tired from typing if err != nil all the damn time.
I do like it for small things - compiling to one (albeit huge) binary and its relative speediness is nice. I wouldn't use it for large systems though. The amount of code really balloons over time, relative to, say, python and that SLOC correlates to bugs and maintenance cost.
I have debated this. I was actually going to formulate mine into a pre-processor so that if I can change a macro it will retro-actively update the other ones. I thought it would be useful for this, and for structs that I want to replicate for various types.
At what point are we just creating a new language that compiles to Go, though?
I'm not the person you're replying to, but I've had similar experiences with larger programs in golang. It's quite terrible to deal with, and overly verbose. I'd say anything more than 2000-3000 lines and golang becomes tedious to manage (and that isn't much given how verbose golang is).
Interesting. I currently maintain 2 repositories, 20k and 60k LOC, and I consider both applications fairly small as it's super easy to cruise its modules/files. In my books, a large system is probably around 500k LOC and I'm curious how that'd work with Go. It's all about project structure and organization of packages. A single file with 3k LOC is most likely unmanageable. 20 files with 150 LOC each is also not much helpful. However, 5 files with 600 LOC each is a much more balanced split. You are not getting lost in files and each file is of reasonable size. Obviously, it's not always possible and highly depends on application specifics, but I'm just trying to make a point. One should always strive for a project structure that doesn't require too much of a brain juice to comprehend the structure. Otherwise it's possible to get lost in 2-3k LOC regardless of a programming language.
I work on a program that is 100+ KLOC. It's quite atrocious honestly. I can't help but think that if it were written in Java, it would have been much shorter, probably at least 50% if not even more.
This is not really what I had in mind. I know java has these features, but how would they make your program shorter? Could you be more specific? Maybe share an example of a real problem that is made easier by these features?
Often people will give made up examples, but the trouble with made up examples is you can't tell if they actually matter in practice or not. Sure, if we invented a way to turn popcorn into gum, itd be easier to turn popcorn into gum, but no one wants to do that in reality. I'm after real examples.
Exceptions create drastically shorter applications versus Go's explicit returns. I'll have to see if I can run an analysis later; I would wager that 20-30% of the lines in our monorepo are related to error handling. This is in large part due to the style of having the if err return err take 3 lines, but that's the prevailing style. Additionally, it requires an explicit handling block everywhere that might return an error, even if you don't want to handle that error there (i.e. in a web app, not handling an exception defaults to a 503 error. It takes 0 code to return a 503 on an error, which is usually the default error path).
For generics, I needed to write a map merge for two map[string]interface{} maps. Fairly basic, for scalar values prefer the value from the first map on conflict, for slices append the two arrays, for maps do a recursive merge. I used a type switch to get the type of each value, when I realized that []string and []interface{} were different, and that I wouldn't be able to append to a []interface{} unless I convert the []string. Fine, so I need a function that creates an []interface{} and converts each index in the []string to an interface{} and puts it in the array. Except because of the type signatures, I now also need one for every scalar type in Go. Is it an overwhelming amount of effort? No, but it did turn 6 lines of code with 12 lines of unit tests into something like 50 lines of code with 100 lines of unit tests (more type switches). Generics should have allowed me to say that I don't care what type the input is (and interface{} doesn't work here). Worse, now if I adjust a unit test, I have to do it in several places, not just one.
Those are the things that frustrate me in Go. It ends up feeling like I'm brute forcing software development; it's not the fastest way to do it, but it'll work if we're just willing to rewrite the same function 10 times with different type signatures, or write if err statements 10 times so that we can get the error up to the 11th caller in the stacktrace, who's actually going to do something useful about the error.
That isn't the point where I'd consider it non maintainable or anything, it's just the point where I'd probably consider the trade offs less worthwhile.
When writing quick-and-dirty Go programs, I define a function called "bailif" (pun intended) that turns non-nil errors into panics. i.e. `func bailif() { if err != nil { panic(err) } }`. Each error can be "handled" at first by just `bailif(err)`.
There are other alternatives as well. For example, Rust uses a Result tagged union. You can also shorthand it using the ? operator, which does the same as the if err != nil { return nil, err; } but without all the boilerplate. There are some inconveniences, like needing to specify the error type by hand, but overall the amount of code is far smaller, it is a lot harder to mess up return types, yet it remains equally explicit in forcing you to handle every error, which is something that Go doesn't do.
I find myself reaching for it over other languages when I want to build small servers with a bit of in-memory state or a bit of heavy processing. For little search-engines, Go is perfect. While writing servers in Flask + Python is much more convenient, I still prefer Go because I don't run into the limits that Python has.
The development process is fluid enough that I wish the language was suited to more usecases. When I need to handle complicated data (e.g. abstract syntax trees), I use Rust or Haskell because of their rich data types. But it tends to be much harder to get things running in those languages (borrow-checker and monads, respectively). I want to reach for Go to make those problems go away, but then realize that it would be really painful to try to express the program in Go. I understand that other people are unhappy with Go's development process, particularly if you deal with package versioning.
The language design itself may have been backed into a corner where adding generics will create an ugly mess (what will the standard library look like if it tries to maintain backwards compatibility?). Time will tell.
The tooling is mostly pretty good, but many things feel half-baked (compared to more mature ecosystems). On the spectrum of 'written in a weekend' to 'dozens of developer-years of work', the package "net/http/pprof" feels closer to the weekend side. There are bright sides, like having the production parser available as a library.
It's a language that is frustrating in different ways from other languages. Instead of fighting with Cabal/Stack, you have to write a million `if err != nil {}` statements. Part of what makes it frustrating is seeing how good it could have been.
What limitations do you run into with the flask/python setup? (I’m not asking what ARE the limitations, I’m asking which ones specifically you run into)
For large data sets, it sometimes spends all day in GC while just loading the data into memory (this is actually more of a problem in Ruby than in Python, but it still exists here).
It's also not very efficient about using memory, so it can be easier to get a dataset to fit in RAM in Go vs. Python.
Python also makes it hard enough to make use of multiple cores that it becomes easier to just use Go.
The same goes for making it fast. It can be done in Python, but I tend to be happy with the performance of my Go code out of the box.
I’m interested in learning more about Ruby’s garbage collection issues. Could you recommend any resources on this? Or any tools in the Ruby language that would allow me to test this myself?
Have you ever tried Pypy? I know when I had issues with GC and some memory usage (specifically dealing with large datasets as well, I was parsing large MySQL query results -- many millions of rows) Pypy didn't really drop memory usage much, but it did give me an almost 10x speedup. Enough that I went "oh, guess I don't need to use a different language."
I had a python server written in Python with greenlets to hat I switched to PyPy and the load it could handle went up maybe 4x. (5k per second to 20k per second. I took two weeks to switch to go and it went up another 10x to 100k per second and had less bugs (in fact a parsing bug in the python caused messages greater than 64k to be dropped; the Go handled partial reads properly and starting sending 2M msgs (to Kafka). The subscribers started getting the long messages and barfing making the system go wonky. Oddly all the giant messages were corrupt data anyways.
Code size was about the same due to stuff being implemented in Python manually being handled by Go runtime.
Flask is better in the little mechanics of interacting with HTTP - parsing a parameter out of a URL (in Flask, it's a function argument; in Go, it's at least one extra line per arg), returning JSON (just return a dictionary in Flask), handling errors. Flask also is able to auto-reload the code when it changes (turn on development mode) which is really convenient.
Plus all the ways that Python is more convenient than Go.
But...you are comparing a web framework with a Go's standard library. I mean, it's not a fair comparison. I'm pretty sure there are Go modules that support those handy things.
It does. It's rock solid and most people use it in production. But the parent is more concerned about quick and handy functions. IMHO, such functionality is usually part of third-party libraries and is certainly an option in Go. It's just idiomatic Go favors KISS principles, less external dependancies and a more self-contained software. The benefit is that pretty much all the Go source code is more-or-less identical, regardless of an author. It makes it super simple to understand and deep dive into a random complex piece of software.
Go is not about abstractions, Go is about solving problems here and now.
> But...you are comparing a web framework with a Go's standard library. I mean, it's not a fair comparison.
Why do you feel that the comparison isn't fair? Aren't we talking about modules that provide the features necessary to implement a type of application? Would it make any difference if tomorrow there was a PEP making Flask a standard component?
Unless you are aware of any Go module that addresses the downsides of Go with regards to Python, your observation is mute and unhelpful.
There are, but Go's type system doesn't really accommodate them very well. `interface{}` tends to abound, and it tends to feel less magical because you're still generally wiring things up yourself (i.e. you still have to explicitly unmarshal the request JSON).
In some ways, that explicitness is better because you know what's happening. In other ways, I'm incredibly tired of typing out 'if err := req.Bind(myStruct); err != nil { return err }'.
> While writing servers in Flask + Python is much more convenient
The only time I had to use Flask, it was a nightmare to deploy. To this day I still fail to understand why I had to mess with Nginx, gunicorn, WSGI or whatever just for a basic backend. The dev experience was fine, but I don't remember it being more convenient than using Go
But I still need gunicorn, don't I? My point is that with Go, I just have to execute my single binary and I'm good to go. It doesn't prevent me from adding a reverse proxy and a loaf balancer if I want to, but I don't need gunicorn or other runners.
Also if a single line is enough, maybe adding it clearly to the doc would be nice.
> But I still need gunicorn, don't I? My point is that with Go, I just have to execute my single binary and I'm good to go.
I don't see how your point is relevant. I mean, with interpreted languages you need to run the interpreter, but if having to write an additional word or line of code bothers you that much then you should not worry about it if you dockerize the app or, god forbid, use a launcher script. I mean, in non land no one ever complained about how hard it is to add scripts to mom's start and prestarte targets to launch a server by doing mpm start. So why is this suddenly relevant with Go?
it's a history thing: back then, everything was serverless^WCGI and you didn't need to do any http servery things at all: just read stdin and write to stdout. web servers were an afterthought and turns out if you needed performance, you had to do it in C anyway, so nothing came prepackaged. then the web 2.0 happened and suddenly hello world is being done over http instead of printing to stdout, so new entrants obviously designed their stdlib around this.
OCaml is good for handling abstract syntax trees (strong typing, pattern matching) and its mix of imperative and functional programming don't require using borrow-checker nor monads. You can also consider Reason, if you prefer a more C-like syntax.
Also consider F#! In some ways it is more limited (no first class modules) but in others it is arguably better (operator overload, C# ecosystem, a slick take on do-notation).
I really ought to learn OCaml (besides just the toy programs I've written). I think if I got past the messy ecosystem (is tuareg the right thing to use, or is that the old thing?) I'd really like it.
I always wonder if I just showed up late when I hear about the ecosystem! I just get opam for packages, dune for building, and merlin for editor integration (I did pay the complexity task using a language server for neovim, but merlin integrates with vim pretty trivially). It was easier than setting up Java + mvn|sbt|whatever + Intellij, easier than Python... there's maybe more going on than Go because the tools aren't all in one distribution, but man are the returns over Go massive (for the things I value).
I just migrated fully from OCaml to Rust. Couldn't be happier. I cannot even start to explain how big a difference a stable and mature ecosystem (including tooling) makes.
Language-wise, Rust leaves some (small) things to be desired with regards of ergonomics, but this isn't even close to offsetting the general feeling of developer friendliness and fitness to the problems it aims to solve. The borrow mechanics/lifetimes are godsend and one starts to wonder why all languages are not like this.
Rust completely exhausted the problem domains of these languages (for me): Java, Go, OCaml, even Python for something that would require more than 50 lines of code. Once you "get it", it's not harder to write than these.
OCaml still has some very advanced features (types) like polymorphic variants, GADTs and others, which I have used in the past to elegantly solve (in a type safe way) problems. But at least for my use-cases (and the software that I migrated) these were mostly problems created by the functional, garbage collected nature of OCaml. With Rust's imperative (but still very type-safe) nature I just don't need to apply such complicated idioms - it seems to fit perfectly to the "real world problems". And on top of that I get 3-4x speedup "for free" (for the program that I migrated). I added some very "lightweight" (as in "easy to do") parallelism and I got another x3 speedup (for the most frequent use case). This kind of speedup makes a dramatic difference. It enables me to do things that were previously not possible in the OCaml version.
And yes, on top of that I get to use Cargo, which feels decades ahead of every other package manager I have used and the IDE support (at least in IntelliJ/CLion) is pretty good (OCaml is nowhere near).
I use go (long term) in places where I need as part of my DR plan the ability to fix stuff without expert access. The toolchain is simple enough that I can maintain it in this scenario alone, and meanwhile the stdlib is complete enough as to be useful for solving a multitude of problems, while offering the ability to produce programs that run on many systems and integrate with them with sufficient efficiency. It's a really usable systems glue language, and it powers chunks of my telco infrastructure. This pro is mostly about the implementation/engineering design.
My biggest gripe with the language is that it doesn't scale that well for teams. It requires a certain culture & approach to produce maintainable programs that goes against the grain for many developers, and while I enjoy the simplicity it offers, that simplicity leads to cost of ownership problems in many team scenarios. This con is mostly about the language.
As a specific tactical item, the runtime multiplexer could do with a refactor. The merging of the pollers essentially could continue until it is much cleaner, there are more layers there than necessary to solve the problem. You can follow the FD close path to understand the strains of the current factoring. Such a cleanup could probably remove some overhead for io heavy workloads too, maybe even drop some lock contention. Oh, and the spins should all be removed from the runtime before we really start using it on very high core count systems. This opportunity is mostly a side effect of a long history of incremental improvement.
Imho because it is highly opinionated and the designers of the language also suggests best practices.
They don’t do anything wrong in suggesting best practices although some of those best practices only work when you write infrastructure code and not micro services (with business logic) and some programmers cannot really compute that and they stick with the religion.
I also had the same experience of not scaling for teams for this reason, because you had people who would not allow for discussion.
It is honestly never happened to me before.
Given it could only be a matter of having found just a bad apple in the team, my first reaction was that this language doesn’t scale with teams.
> Imho because it is highly opinionated and the designers of the language also suggests best practices.
In my experience this is actually a plus. I've lead a department that made the transition from Java to Go, and one of the really noticable things were entire classes of fruitless (and frankly, unnecessary) discussions disappeared. Having an opinionated language that comes with a canonical formatter and lots of established idioms made it easier to scale up to more people because more of the code looked homogenous more of the time.
However, with any new language someone takes up there is the risk that people will try to bring along their old habits from other languages and be unhappy if they can't just continue writing C/C++/Java/Python/C#/JavaScript/Lisp in Go. If this takes hold, you will waste a lot of time having fruitless discussions.
It is easier to adapt to a coding style that is shared than it is for every project and every team to invent their own. For instance, I've worked for only one company where using C++ was even somewhat pleasant: and it was in large part because a) the company had a very strictly enforced code standard for C++ (and other approved languages), and b) you had to demonstrate you could stick strictly to the code standard before you were able to approve changelists. At the time that company had some 7000 developers, of which probably 3-4000 had "C++ readability" rating.
The upshot being that after getting used to it, it was pretty easy to understand other people's code. And you would spend a lot of time reading other people's code.
I suspect Go being opinionated is a direct result of this experience.
They set out to make a pretty generic ALGOL/C-ish/Java "OO"ish language with sharp corners filed off and some decent concurrency primitives put in. They executed successfully (they got the language out there, built an ecosystem, didn't undermine their own goal in the language design or make it unusable.)
If I had to choose between Go and C++, I'd probably pick Go because of C++'s complexity, so they really nailed it.
In the grand scheme of things, it's a really forgettable language. Frankly, I haven't programmed in it. I don't need to. I know ALGOL+(N+1) when I see it.
> In the grand scheme of things, it's a really forgettable language.
Strangely enough, for me that is one of the major plus points. A forgettable language (which is not the same as a language which you forget how to use) is one which gives you the tools for the job and then gets out of the way.
Some other languages feel like prima donnas with their shiny brilliance and complex possibilities. I'll take a small, dirty, simple, and performant back-seat one like Go every time.
After 9 years of ruby, I finally switched to Go as my professional language 3 years ago, after a couple years of side projects with it.
My feelings after building for that long : safety and productivity.
I initially thought it was because of moving to compiler and type checking, but then I learned C for my personal use (an other thing passing through Go allowed for me), and was surprised to realize my C compiler was not allowing me close to the same peace of mind than my Go one. Go is just a well-designed language that makes everything straightforward.
The main reason I started learning and building with C is that I'm not confident the code I write in Go will age well. The discussions around Go-2 make me extremely nervous (it has something that reminds me of angular-2, python-3, symfony-2, bootstrap-3, and others major redesigns who left developers in a lot of troubles, I hope time will prove me incorrect).
So basically, I'm enjoying it, but I worry if it will last. I guess that means I really love it.
Re. Go 2. The Go Team people have repeatedly said that they
want to avoid the Python 3 situation, so even if there will
be a real, compatibility breaking Go 2, it won't be anywhere
near that level of incompatibility. They'll probably just
fix a few nits like string(int).
Re. C. You really should use at least one static analyser
when programming in C. Clang-tidy has been my go-to tool
for that, and it's been working pretty good.
Can't say for the rest, but Symfony 2 was a resounding success. Current Symfony 5 is very similar to Symfony 2, with just a ton of refinements and facelifts.
Oh yes, I'm not saying those are bad projects. What they have in common is that migrating from previous version to them was especially difficult and painful.
The few symfony developers I know indeed loved symfony-2 very much. But migrating their projects from symfony-1 to symfony-2 was felt basically as a rewrite.
You're right, Symfony 1 -> Symfony 2 was definitely a rewrite, but from then on, the general design was kept almost intact (again, not counting the evolution and streamlining of the framework).
I guess the first attempt is always tricky, let's see if Go 2 can find a smooth upgrade path.
I really like it. It makes it really easy to have easy to develop monoliths--Go will scale (vertically and horizontally) and can easily handle doing everything. There are some annoyingly verbose and not DRY parts, like adding instrumentation or context, but it leads to less weird side effects.
The tooling is also very good, though I wish more of golangci lint would move into stdlib so I could just run golint.
309 comments
[ 6.0 ms ] story [ 275 ms ] threadIt's not always good though, in Java world you have to choose between multiple competing tools, while other ecosystems have mostly only one, often adopted from Java version (except Javascript, Rust, they have their own zoos). But IMHO Java is still the nursery for all kinds of tools for development.
https://greenlab.di.uminho.pt/wp-content/uploads/2017/09/pap...
Also, Go does not suffer from some fundamental Java problems, though they slowly get corrected. Go has value types, unboxed arrays. Generally, no struct in Go is boxed automatically. You have first class functions in Go and of course, it is statically compiled. It is true, that Hotspot often produces very good code, but for a lot of szenarios, a statically compiled Go executable beats it, as run-time compilation is not required.
Value types are around the corner and I bet they will be done first than generics in Go.
eg: https://www.youtube.com/watch?v=0c-1KJwSMCw
In theory, it's annoying because it prevents you from expressing constraints (think: monad laws, or Rust's Sync/Send traits). In practice, it makes stuff like go-to-definition nearly useless, which makes it a pain to read code written by other people. Especially when combined with the almost sadistic module system.
- great community
- ample libraries
- ecosystem support
- no breaking changes (yet)
- no magic, limited and simple
- inbuilt testing/profiling/dependency manager
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/in...)" curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/maste... | python - brew install pyenv // update shell with pyenv pointer pyenv install 3.9.0 pyenv global 3.9.0 poetry init poetry config virtualenvs.in-project true --local poetry install
For deployment, Docker python3:alpine, install poetry, copy over pyproject.toml and install
Why do we need virtual env in 2020?
It's one of the worse "modern" language to build, even Java / NodeJS are better.
Well, for starters, that particular multi-liner:
- installs an entire package manager, which may or may not conflict with system tools or a different package manager,
- assumes pyenv will build Python against the system libraries without additional flags (it won't if you're running Big Sur, you'll need zlib from Homebrew),
- assumes you are on macOS, and does not attempt to work on Windows or Linux, or account for the idea that someone might not want to deploy a program using Docker.
For me, this is the consequence of a reasonable static type system. Without generics and the current error handling story, it almost feels like Go makes large codebases necessary in the projects I've used it, compared to other statically typed languages such as Kotlin, Java, Rust, and C#.
Go 2 on the other hand looks like it will solve this problem, and I eagerly await its release.
I agree that some form of static typing is critical to scale a codebase, or an ecosystem. Let’s also not forget the importance of a packaging system with robust version management. Go modules and proxy.golang.org are also critical to Go’s success at (code) scale.
How many years of Java experience do have?
- a GC that can has relatively smart defaults without having to tweak a ton of parameters
- don't have to ship a vm
- concurrency baked in from the start
- simpler language
- doesn't try to square everything into the OO paradigm
Depends on the JVM being used
> don't have to ship a vm
You get to ship a runtime instead.
Which Java also allows since around 2000, given how long commercial AOT compilers do exist for Java.
> simpler language
Java 1.0 was also Go simple, and just like Go does nowadays, full of code generation tools to work around that simplicity.
> doesn't try to square everything into the OO paradigm
Interfaces are part of OOP, introduced by Objective-C protocols.
Is picking a jvm not tweaking a parameter?
i like: being able to cross-compile things into a single binary. raw speed in case of simple services
i dislike: the way generics and package management were (are handled/ignored?) and the super-verbose error handling.
go would not be my first choice but i can understand why people find it appealing
Also, the dependency management.
You're making things up. Not a single member of the Go team ever said "we don't need generics". In fact, the FAQ stated from day one that they "continue to think about it", and that "Generics may well be added at some point".
Also googled and found this:
http://lambda-the-ultimate.org/node/4554
which includes this section:
Go might have generics in future
with this text:
The Go FAQ recognizes the issue and says that generics may be added at some point... http://golang.org/doc/go_faq.html#generics
Considering how Java generics turned out I am fine with the Go team taking the time to think about a proper design for generics in Go :-)
By ted stockwell at Tue, 2012-06-26 15:03
[1] The golang.org link above about generics also says this:
We haven't yet found a design that gives value proportionate to the complexity, although we continue to think about it.
https://blog.golang.org/using-go-modules
The golang standard library and high-quality third-party libraries are really great
Go's dependency management was designed after analyzing Cargo and its problems: https://research.swtch.com/vgo-mvs
One of the things that might make it even better are generics, but I haven't really missed them so far. In conclusion, I don't think I will switch to anything else soon.
Personally I don’t like it. Not low level enough for when I need to go low level, not high level enough when I want to go high level. Also I would have wanted generics right from the start; that should be the norm for typed languages unless the types are really super simple (Go’s are complex enough to warrant generics in my opinion).
But, maybe the reason why Go has the niche it has is because other programmers prefer it the way it so. So it goes with languages - it’s an aesthetics thing just as much as it is a technical thing.
The language is simple, performant and writing concurrent code is intuitive (although I feel Go developers tend to get carried away using channels/goroutines).
In general there has been more effort put into trying to improve on the default tools through projects like ripgrep (grep), bat (cat), lsd and exa (ls), and fd (find) rather than creating drop-in replacements that don't provide all that much over the battle tested originals.
But, personally, C can get really tedious.
For me, C only seems tedious because to every question I have about it, I will inevitably find 10 different answers, maybe all from different periods of time. And I understand that I can look at the C standard but there are things that it doesn't address. Also, venturing into embedded programming, you can quickly come across implementations of the standard libraries that aren't nearly as well-defined.
So considering these things, I think it is a good "modern C" as long as you don't go too low level.
Tooling could use a lot of work though. After switching between vim-go, gopls, and a handful of other language server implementations, I've given up on auto-completion, definitions etc altogether. Maybe it's my setup...
Overall, I'm pretty happy with running a fairly big Go app in production. Would I choose Go for my next serious project? Most likely.
Thanks for mentioning it, I think I'll take another look at it now. Having a real IDE probably has more benefits once the code grows beyond tens of thousands of LOC.
At this point gopls works way better than godef+gocode ever did for me.
Checking that the user uses all values bound to variables but letting them discard errors by not binding them to a variable is a weird choice. Channels panicking instead of returning an error is a weird choice.
It's ok and I would like to to use it instead of Java.
In the OO languages I have encountered there seems to be a stronger convention toward using interfaces instead of struct or class types in this sort of situation, but people are certainly free in Java to use a final class type as the type of their parameters and return values if they want to make their library less composable.
Single-binary deployment and (these days) good dependency tracking (downloading multiple versions of a library) is great; I don't worry about trying to get somebody else's source code built I do the way when I get python packages. The python side is largely because setting up a venv is error-prone (at least, for me, somehow) and leaky. I still think having the version baked into the import path is kind of dumb, but I suspect that's really just because the documentation failed to explain the reasoning and good workflows (I understand just changing the path in go.mod may be enough).
I wish they had things like generics, but I fear that an incompatible version break would cause too much strife, much in the same way of python 3 / perl 6. Given how the vendoring tools story went, I'm not yet confident the core team can manage the community very well.
On the positive side, the error handling has been decent for me so far; an error interface is easy to understand, compared to rust (where I think you can use Box<dyn Error> but they don't actually say that in the basic documentation). I also like the decently sized standard library, again in comparison to rust where things are too minimal. I think ruby has a decent compromise, where the standard library is (mostly) also available as downloadable packages if you need a newer version. Somehow I've had worse experience with ruby gems compared to go modules, though; I think that's because the bundler documentation is rather bad — it feels more like an essay rather than a reference.
Not having a central repository of packages is a definite plus in my book for go; it leads to less of a problem with people taking the good package names (since most names are bad anyway). Less worries about cutely named libraries like devise (ruby, for auth) or hyper (rust, HTTP). IMHO, libraries should be plainly named, apps can have weird names if they want.
The Perl world has solved this by renaming Perl 6 to Raku (https://raku.org using the #rakulang tag on social media).
1. Built in support for tooling, testing, formatting etc
2. Trying to make concurrency mainstream
3. Large and developer friendly standard library, there is hardly any programming language where web backed can be written without using a framework or a library. Go is one of those.
I do like it for small things - compiling to one (albeit huge) binary and its relative speediness is nice. I wouldn't use it for large systems though. The amount of code really balloons over time, relative to, say, python and that SLOC correlates to bugs and maintenance cost.
(looking at you, java)
At what point are we just creating a new language that compiles to Go, though?
* Exceptions for error handling
* Streams (map/filter/etc.)
* Generics
* Superior collections library (Set, ConcurrentHashMap, LinkedHashMap, etc.)
* Records
* Enums
* Upcoming feature: pattern matching
Often people will give made up examples, but the trouble with made up examples is you can't tell if they actually matter in practice or not. Sure, if we invented a way to turn popcorn into gum, itd be easier to turn popcorn into gum, but no one wants to do that in reality. I'm after real examples.
Exceptions create drastically shorter applications versus Go's explicit returns. I'll have to see if I can run an analysis later; I would wager that 20-30% of the lines in our monorepo are related to error handling. This is in large part due to the style of having the if err return err take 3 lines, but that's the prevailing style. Additionally, it requires an explicit handling block everywhere that might return an error, even if you don't want to handle that error there (i.e. in a web app, not handling an exception defaults to a 503 error. It takes 0 code to return a 503 on an error, which is usually the default error path).
For generics, I needed to write a map merge for two map[string]interface{} maps. Fairly basic, for scalar values prefer the value from the first map on conflict, for slices append the two arrays, for maps do a recursive merge. I used a type switch to get the type of each value, when I realized that []string and []interface{} were different, and that I wouldn't be able to append to a []interface{} unless I convert the []string. Fine, so I need a function that creates an []interface{} and converts each index in the []string to an interface{} and puts it in the array. Except because of the type signatures, I now also need one for every scalar type in Go. Is it an overwhelming amount of effort? No, but it did turn 6 lines of code with 12 lines of unit tests into something like 50 lines of code with 100 lines of unit tests (more type switches). Generics should have allowed me to say that I don't care what type the input is (and interface{} doesn't work here). Worse, now if I adjust a unit test, I have to do it in several places, not just one.
Those are the things that frustrate me in Go. It ends up feeling like I'm brute forcing software development; it's not the fastest way to do it, but it'll work if we're just willing to rewrite the same function 10 times with different type signatures, or write if err statements 10 times so that we can get the error up to the 11th caller in the stacktrace, who's actually going to do something useful about the error.
That isn't the point where I'd consider it non maintainable or anything, it's just the point where I'd probably consider the trade offs less worthwhile.
Wrapping everything statement which can produce error is more tedious.
The development process is fluid enough that I wish the language was suited to more usecases. When I need to handle complicated data (e.g. abstract syntax trees), I use Rust or Haskell because of their rich data types. But it tends to be much harder to get things running in those languages (borrow-checker and monads, respectively). I want to reach for Go to make those problems go away, but then realize that it would be really painful to try to express the program in Go. I understand that other people are unhappy with Go's development process, particularly if you deal with package versioning.
The language design itself may have been backed into a corner where adding generics will create an ugly mess (what will the standard library look like if it tries to maintain backwards compatibility?). Time will tell.
The tooling is mostly pretty good, but many things feel half-baked (compared to more mature ecosystems). On the spectrum of 'written in a weekend' to 'dozens of developer-years of work', the package "net/http/pprof" feels closer to the weekend side. There are bright sides, like having the production parser available as a library.
It's a language that is frustrating in different ways from other languages. Instead of fighting with Cabal/Stack, you have to write a million `if err != nil {}` statements. Part of what makes it frustrating is seeing how good it could have been.
It's also not very efficient about using memory, so it can be easier to get a dataset to fit in RAM in Go vs. Python.
Python also makes it hard enough to make use of multiple cores that it becomes easier to just use Go.
The same goes for making it fast. It can be done in Python, but I tend to be happy with the performance of my Go code out of the box.
Code size was about the same due to stuff being implemented in Python manually being handled by Go runtime.
Plus all the ways that Python is more convenient than Go.
Go is not about abstractions, Go is about solving problems here and now.
Why do you feel that the comparison isn't fair? Aren't we talking about modules that provide the features necessary to implement a type of application? Would it make any difference if tomorrow there was a PEP making Flask a standard component?
Unless you are aware of any Go module that addresses the downsides of Go with regards to Python, your observation is mute and unhelpful.
In some ways, that explicitness is better because you know what's happening. In other ways, I'm incredibly tired of typing out 'if err := req.Bind(myStruct); err != nil { return err }'.
The only time I had to use Flask, it was a nightmare to deploy. To this day I still fail to understand why I had to mess with Nginx, gunicorn, WSGI or whatever just for a basic backend. The dev experience was fine, but I don't remember it being more convenient than using Go
If you want something basic, gunicorn can run flask with one line.
Also if a single line is enough, maybe adding it clearly to the doc would be nice.
I don't see how your point is relevant. I mean, with interpreted languages you need to run the interpreter, but if having to write an additional word or line of code bothers you that much then you should not worry about it if you dockerize the app or, god forbid, use a launcher script. I mean, in non land no one ever complained about how hard it is to add scripts to mom's start and prestarte targets to launch a server by doing mpm start. So why is this suddenly relevant with Go?
Language-wise, Rust leaves some (small) things to be desired with regards of ergonomics, but this isn't even close to offsetting the general feeling of developer friendliness and fitness to the problems it aims to solve. The borrow mechanics/lifetimes are godsend and one starts to wonder why all languages are not like this.
Rust completely exhausted the problem domains of these languages (for me): Java, Go, OCaml, even Python for something that would require more than 50 lines of code. Once you "get it", it's not harder to write than these.
OCaml still has some very advanced features (types) like polymorphic variants, GADTs and others, which I have used in the past to elegantly solve (in a type safe way) problems. But at least for my use-cases (and the software that I migrated) these were mostly problems created by the functional, garbage collected nature of OCaml. With Rust's imperative (but still very type-safe) nature I just don't need to apply such complicated idioms - it seems to fit perfectly to the "real world problems". And on top of that I get 3-4x speedup "for free" (for the program that I migrated). I added some very "lightweight" (as in "easy to do") parallelism and I got another x3 speedup (for the most frequent use case). This kind of speedup makes a dramatic difference. It enables me to do things that were previously not possible in the OCaml version.
And yes, on top of that I get to use Cargo, which feels decades ahead of every other package manager I have used and the IDE support (at least in IntelliJ/CLion) is pretty good (OCaml is nowhere near).
PS. And Traits are extremely cool
My biggest gripe with the language is that it doesn't scale that well for teams. It requires a certain culture & approach to produce maintainable programs that goes against the grain for many developers, and while I enjoy the simplicity it offers, that simplicity leads to cost of ownership problems in many team scenarios. This con is mostly about the language.
As a specific tactical item, the runtime multiplexer could do with a refactor. The merging of the pollers essentially could continue until it is much cleaner, there are more layers there than necessary to solve the problem. You can follow the FD close path to understand the strains of the current factoring. Such a cleanup could probably remove some overhead for io heavy workloads too, maybe even drop some lock contention. Oh, and the spins should all be removed from the runtime before we really start using it on very high core count systems. This opportunity is mostly a side effect of a long history of incremental improvement.
In my experience this is actually a plus. I've lead a department that made the transition from Java to Go, and one of the really noticable things were entire classes of fruitless (and frankly, unnecessary) discussions disappeared. Having an opinionated language that comes with a canonical formatter and lots of established idioms made it easier to scale up to more people because more of the code looked homogenous more of the time.
However, with any new language someone takes up there is the risk that people will try to bring along their old habits from other languages and be unhappy if they can't just continue writing C/C++/Java/Python/C#/JavaScript/Lisp in Go. If this takes hold, you will waste a lot of time having fruitless discussions.
It is easier to adapt to a coding style that is shared than it is for every project and every team to invent their own. For instance, I've worked for only one company where using C++ was even somewhat pleasant: and it was in large part because a) the company had a very strictly enforced code standard for C++ (and other approved languages), and b) you had to demonstrate you could stick strictly to the code standard before you were able to approve changelists. At the time that company had some 7000 developers, of which probably 3-4000 had "C++ readability" rating.
The upshot being that after getting used to it, it was pretty easy to understand other people's code. And you would spend a lot of time reading other people's code.
I suspect Go being opinionated is a direct result of this experience.
If I had to choose between Go and C++, I'd probably pick Go because of C++'s complexity, so they really nailed it.
In the grand scheme of things, it's a really forgettable language. Frankly, I haven't programmed in it. I don't need to. I know ALGOL+(N+1) when I see it.
Strangely enough, for me that is one of the major plus points. A forgettable language (which is not the same as a language which you forget how to use) is one which gives you the tools for the job and then gets out of the way.
Some other languages feel like prima donnas with their shiny brilliance and complex possibilities. I'll take a small, dirty, simple, and performant back-seat one like Go every time.
My feelings after building for that long : safety and productivity.
I initially thought it was because of moving to compiler and type checking, but then I learned C for my personal use (an other thing passing through Go allowed for me), and was surprised to realize my C compiler was not allowing me close to the same peace of mind than my Go one. Go is just a well-designed language that makes everything straightforward.
The main reason I started learning and building with C is that I'm not confident the code I write in Go will age well. The discussions around Go-2 make me extremely nervous (it has something that reminds me of angular-2, python-3, symfony-2, bootstrap-3, and others major redesigns who left developers in a lot of troubles, I hope time will prove me incorrect).
So basically, I'm enjoying it, but I worry if it will last. I guess that means I really love it.
Re. C. You really should use at least one static analyser when programming in C. Clang-tidy has been my go-to tool for that, and it's been working pretty good.
The few symfony developers I know indeed loved symfony-2 very much. But migrating their projects from symfony-1 to symfony-2 was felt basically as a rewrite.
I guess the first attempt is always tricky, let's see if Go 2 can find a smooth upgrade path.
Try learning Kotlin. I think you'll find it a significant upgrade in safety and productivity compared to Go, but especially compared to C.
"Well, let me tell you about this other language called Rust..."
Classic Rustacean: sees zero daylight to inject oneself, does so anyway.
The tooling is also very good, though I wish more of golangci lint would move into stdlib so I could just run golint.