https://news.ycombinator.com/item?id=25622389
Realize a Go []int ("slice of int") is just a C struct like this, passed by value:
struct intSlice {
int* addr;
int len;
int cap;
};
The memory at addr is not owned by the slice. All the slice operations are simply notation for manipulating the struct. Go's garbage collection makes the whole thing work wellThis can be confusing if you're used to C++'s std::vector (which owns the memory) or Python's slices. Go's slices are a shallow pointer/length system exactly like is used in C all the time. For example:
void sort(int* addr, int len);
becomes func sort(a []int)
A Go slice is just a formalization of C's pointer/length idiom, with terse notation for manipulationThis reminds me of Numpy's ndarrays, which act more like "owning containers" (which may sometimes share ownership and alias) than "non-owning references".
If I want to write flat/simple code I'll just C, Go doesn't really offer anything to me because I want to be relatively low-level and able to make the compiler save my time.
Go feels like it's aimed at creating applications that run at a level above where C traditionally runs while keeping the pared down feature set of C, and it just doesn't work well imo.
I loved playing with Go, but eventually it feels like you're conditioning yourself to be ok with a lot of "bad" code. Tons of repetition, tons of boilerplate by other names.
It's like the simplicity gets in the way of itself.
-
My take is, use Go for a bit, take the "less is more" mentality with you to a language more suited for real work.
ps: I'm not saying you can't use Go for real work before someone flies at me. I'm just saying that Go's strength is the mentality people have when using it, as a language it's not particularly enabling, and that's by design if anything...
I get that you can use it for real work, I'm not saying you can't. I'm saying, even by design to some degree, it's not particularly enabling compared to other languages.
That's meant to be a strength if anything, but I did not find the tradeoff of simplicity was able to overcome the additional effort it took to deal with what was missing
There's an implicit ymmv here of course, because I'm not saying no one can find Go useful, I can see how in certain worlds with certain teams with certain priorities it'd all work out.
It's just not for me.
In Node??? Have you actually written any Go code?
Imagine evaluating the time to market for an experimental back-end written in Rails, Laravel, Phoenix, etc vs Go! The difference is staggering.
Comparing frameworks and a language, really?
Different languages make writing the DSLs used in Rails-like frameworks easy, difficult or impossible. Much of my professional career was spent working on Node back-ends with a variety of frameworks and none of them were even close in terms of succinctness or productivity.
Not well or hastily written Go is easily like that. I can only guess why you point out the repetition but when you look at well written Go code like in the Go std library, there isn't much repetition happening and it looks rather elegant. At the same time it can be quite some effort to create such code and might take several iterations. Documentation-wise I like the official documentation, it gives a lot of pointers why Go is how it is.
Also obviously it's not for every use-case. Where it works really well IMHO is where error handling is a vital part of the application. (And of course anything concurrent is quite a breeze)
I recall a point where I had written a function that needed to return a channel, but of course had to forgo types due to the lack of generics.
The end result was having to wrap that channel in another channel that added typing at each call site.
I asked around in go circles if that made sense, and called out how bad the felt but the answer was "no that's great! channels are cheap! the repetition is good because it's simple!"
-
The error handling has some sore points to that end too, and the implicit shadowing with shorthand assignments on one hand makes it easier to deal with multiple errors, but on the other hand introduced subtle logic bugs on one than more occasion where "err" was silently shadowed, which I greatly disliked.
I'm actually suprised Go didn't forgo shadowing for the shorthand operator and force people to label their errors when dealing with multiple, it seems very "in brand", but I guess even Go draws the line somewhere lol
-
Then there's the whole stack trace situation. Which after plenty of reading still just wasn't making sense. I mean the idea of logging a stack of wrapped messages sounds very nice, but man proper stack traces not being the first class citizen of error handling just did not make sense to me in a language I hear referenced for systems work so much (and I realize they can be had)
I saw proposals to rework Go's error handling, I don't know if any progress has been made to that end, but it's another example where simplicity can get in the way of itself
-
And I'll balance this all out by saying it was still fun to write, I don't want to seem like I'm just shitting on Go for existing.
It's just these little warts that I kept getting over with just a little bit of "idiomatic Go" which I often found was just writing a little bit more code than you're used to, started to add up.
And eventually I realized I was creating something that, while very easy to reason about, had a lot more to reason about than it needed to. That's where I kind of petered out in my personal usage.
About generics, it depends on the problem at hand. When writing a generic Matrix multiplication type that for instance should work both on real and complex numbers (or more obscure types) generics are a thing. On the other hand, when working for instance with golang.org/x/net/html, I really appreciate the interface definitions and the possibility to do type switches on those.
The error handling problem can also be dealt with. I also prefer to not shadow variables, IMHO the ideal function has all its variables defined already in the signature.
I mean there are stack traces when a panic is called, or something that wraps it like a log.Fatalf. I guess the idea is the author is giving much more thought to how errors are dealt with and how they are printed in a both useful (=greppable) and beautiful way. As an end user that doesn't want to dive into the code I probably always prefer to see a nicely formatted error message (or even a gracefully handled error) instead of a 100 line stack trace. Probably Go is not just the language spec but also how to use it. Therefore saying "Learning Go in 5 minutes" is probably a bit far-fetched ;-) (Probably for any language that would be true...)
But I also think the stack trace message example is the perfect example of what I mean by "take the Go mentality with you"
In a language like Kotlin for example, Go got my in the habit of attaching more information to exceptions as they bubble up.
I was already generally in the habit of wrapping library exceptions, but I started religiously using a Result nomad implementation and wrapping errors with additional information so that my stack traces looked very "Go-like" in terms of human readability, but kept traditional stack trace information.
It's small things that I think using Go makes you "re-appreciate" in other languages
github.com/pkg/errors is a simple way to dump expensive stacktraces into each new/wrapped error. Or you can just panic and recover in your own app, it's possible though discouraged.
It's proponents are so quick to go defensive that no one is capable of reading any complaint in an even mildly charitable light.
I mean I shouldn't literally have to say "I'm not saying you can't use Go for real work before someone flies at me." When talking about a language as widely used as Go right? Like that's common sense!
And yet of course I'm getting replies that do exactly that because of how darn defensive people need to be about it.
Saying other languages are more suited to real work doesn't mean Go isn't suited at all, it literally means other languages are more angled towards the "end product" or the "real meat and potatoes" of just making a thing work than Go.
It's literally a strength of Go. Go doesn't want to include the kitchen sink or even confine you to having a kitchen at all and I respect that.
But I'm saying that other languages that include more in the way of affordances can help one be more productive, and I believe combining a language that includes more, with the mindset of not abusing the buffet is a winning combination.
Ymmv, I'm not an oracle, I just expect people to read things in a reasonably charitable way which usually works fine as long as they're not being defensive.
You can write any language any which way, but some languages just have a culture of boilerplate vs abstractions. The most popular Python and Java libraries seem to love endless layers of indirection, and when it doesn't fit exactly what you need, you bang your head against the wall monkeypatching it to make it work since your whole app is already written in X mega library.
Go culture seems to be quite a bit more boilerplate-y and uses smaller more composable libraries. It's just refreshing.
Really I just need to buckle down on Rust, but I think it's a fair bet that teams and shops that bring in a lot of young programmers and want to ramp them up fast may cringe a bit at the learning curve of Rust.
That's why for now in terms of a bet on employability, I am prioritizing experience with Go.
As I’ve mentioned recently though, I don’t think I’d ever want to use Go as a forever solution to a complex problem. I’m sure some people could do it and do a great job, but I’m not one of those people. I need the ability to abstract and ensure more safety, or I’ll never feel secure with what I’m building.
I’ve only read through the rust book without using it for anything (yet) and have done no work on golang, so I’m legitimately curious to hear your input, not challenging you on semantics :p
Edit: Rust does not use # for comments. Sorry I just had that mixed up in my head, but thanks for the answers!
The difference is that attributes are always meaningful to the compiler (and can only be used in specific places); other than doc comments (which turn into attributes), comments never are meaningful to the compiler.
(edit: HN's comment markup eats the * character, I replaced them with <star> above)
Oof what a dumb mistake, sorry about that!
For some examples: A comment that appears right before a type or function definition is exported as documentation by the "go doc" tool.
Another one is build constraints, which appear as a comment at the top of the file, like: `// +build linux` to say "only include this file if the build target is linux."
Other tools use their own comment format to recognize comments meant for them.
They’ve done this to avoid breaking the language stability guarantee, not sure if there were other reasons. I think it’s a mistake personally to create this sort of metalanguage in comments (now you have two problems!) but it’s easy to avoid using this most of the time as the uses are pretty esoteric.
There are good reasons to keep them separate anyway (allows config without recompile for example) and for the apps I work on distribution isn’t a problem. So I haven’t really had to use comments as code in 7-8 years of go. Perhaps it’ll become more common with the embed stuff. But then I don’t use struct tags either so I guess I’d prefer Go to be even simpler.
Hiding certain meta-information, like exporting a function with a C-compatible signature when building a shared library is not a great way of doing things, but on the other side, it is a special usage which isn't part of the language itself, so not part of its syntax.
Things like generics and garbage collection involve some deep tradeoffs, so I get Go's take on them, even if I don't necessarily agree with it. On the other hand IMHO Go's crappy error handling boilerplate is not something I find acceptable in a modern programming language.
For me, the gold standard in error handling is Rust, where the type system simply does not allow you to ignore errors. This means that the existence of error handling is checked at compile time. Of course, this doesn't mean your error handling is _good_, but I think that's beyond any language design to enforce.
But the Rust system is entirely built around a generic Result type, where both the non-error result (an int, string, struct, etc.) _and_ the error type itself are both generic.
Which is excellent.
the gold standard in error handling is Rust
Really? What library is in fashion this week for handling errors? I have so far used vanilla Rust, error-chain and Failure.
Seems like there is so much choice to improve the gold standard:
https://blog.yoshuawuyts.com/error-handling-survey/#librarie...
Personally I'm glad Go has stayed out of exceptions (panics don't count), at scale it's so much easier to reason about flow.
• https://github.com/rebeccaskinner/gofpher
• https://speakerdeck.com/rebeccaskinner/monadic-error-handlin...
I've been talking to the local Go user group. No one even knows about that concept. This continues a pattern of general ignorance/lack of looking beyond one's horizon I've noticed.
It is probably reasonable to say that Zig is closer to C's current philosophy in practice, which encompasses many additional developments over the decades to deal with various aspects of C and programming real machines that have arisen as both have changes, and Go closer to C's original philosophy of simplicity and relative ease of use. (In modern times, C is not one of the easier languages to use any more, due to significant advancements in the field of making easy-to-use languages. But it was originally intended to be an advancement there itself.)
By similar logic, I can say with a straight face that all of Go, C++, Zig, Rust, and Swift (and more) are "evolutions in general-purpose programming languages like C", despite their wild differences, because the world has gotten larger, and there is room for a lot of variations like that, each addressing different aspects of the newer, larger world.
This seems to be a common belief, but since Go kept pointer syntax as prefix, you're still stuck needing parens in some unfortunate cases:
https://blog.golang.org/declaration-syntax (see the part about pointers near the bottom)
As Rob Pike writes in that link, they could've fixed it and still had declaration resembling use if they had chosen a suffix operator for pointers. He notes that Pascal uses ^, but it seems like @ might be a better choice:
int foo // an integer
int foo@ // pointer to int
int foo[10]@ // 10 pointers
int foo@[10] // pointer to array
int foo()[10]@ // function that returns an array of 10 pointers
In my opinion, they almost fixed it and then punted. As such, it's just different, but not really better.The ABI in Rust is doable: https://people.gnome.org/~federico/blog/rust-stable-abi.html
And it doesn't feel like C++ when writing it. The syntax and language features are nice and feel modern.
It does require more attention because of what it compiles down to, but the compiler is there to help with it.
I haven't been excited about any programming languages in years, but Rust has certainly taken my attention lately.
C is simple, to what today is nearly an absurd degree, but it at least integrates well with an enormous ecosystem of existing tooling and libraries. Go, by contrast, feels the need to re-invent every possible wheel, often with a seemingly intentional effort to be different just because.
C ABIs are rock solid on every platform. Any language or tooling in the world can load functions from a C library. By contrast, try calling Go from another language. You will quickly give up and use network protocols or subprocesses instead.
The converse is true too: on major platforms, the C library defines the operating system more than the kernel does. (I mean, in a Unix sytem, the entire libc is documented in the man pages. Documentation for writing C is literally built into the operating system.) By contrast, Go decides to avoid the standard platform ways of calling every operating system function, and instead implements syscalls directly into the kernel, basically just because Go hates interoperating with anything that's not Go.
C is a good language not because it is a good language, but because it is a good ecosystem. Unix and C are closely intertwined. (Even Win32 and C are pretty closely intertwined!) I could imagine a Go-based Plan9-like system -- if that existed, and had decades of engineering behind it, I could see the argument being made that Go's ecosystem compares to C's. But while Go may aspire to that, it isn't anywhere near there yet.
And when you start comparing Go with other languages on the basis of language features, I think Go fails laughably more often than not. (I'll give you "C++ is a clumsy mess", though.) But compare Go with Rust, with C#, with Haskell -- for that matter, compare Go with Java and I think Go is a less powerful, less useful language to write real code in.
If I want a lightweight, quick-and-dirty program, I'll use Python. If I want to write a reusable library that gets plugged into other software, I'd go with C (or at least write the interop layer in C). If I want a high-performance server, these days I would probably go with Rust. If I want a pragmatic, well-rounded language to write a large project in, I think C# would probably be my first choice.
What space does that leave for Go? Outside of microservices (where I contend that Rust is a much better choice, and there are lots of other options), Go's niche seems to be command-line tools. I sort of understand that, because Go's static linking makes redistributing binaries relatively easy (although Go by no means has a monopoly on that). But in the space of command-line tools, Go's argument handling is frankly bizarre: it looks at decades of precedent about what users expect from argument syntax and behavior and decides to throw it all away and reinvent the wheel.
I guess that means this comment (rant?) has ended up where it started: Go defies convention, basically "just because". By contrast, C defines convention. "If you like C, you will love Go"? Nope, not at all.
Go straddles an interesting space. Currently I work on a back-end data crunching software, distributed (running on hundreds or sometimes thousands of nodes), each node running 64 or 128 cores with 128 or 256GB RAM. The problem we solve is both IO, CPU, and memory bound. Anyway to look at it, it's a hard problem.
The old version of this program was written in C. Then it was rewritten in Go.
The people who decided to switch to Go was looking for the following features: - Be able to manually lay out memory for performance. So not Java. - Mmap binding to file system, for performance. - More abstractions than C, for adding software features. - Cross platform. The program sometimes runs on Android and IOs. - Easy to learn for beginners. It's not possible to only hire PhDs or C++ gurus. - Easy to dig down into assembly, for performance. - GC doesn't get in the way, for performance as well as adding features.
Maybe these days, C# might work? May Rust? But 6-8 years ago, Go fit the bill and it worked out very well. And the language doesn't feel "limited" in any way. Yeah, I miss generics sometimes. Maybe error handling could be better? Sometimes I need to work around the GC. But day-to-day, I think I focus more on solving actual problems (performance bottlenecks, adding features), instead of pondering if I need to create another AbstractFactory, or tuning GC knobs, or figuring out which pointer type to use.
To me, Go feels very balanced. I can write python or Java like code and get decent performance. If I need more performance, I can dig down pretty deep make CPU work faster. The abstractions are light enough that a new person can figure out what's going on and add features, fix bugs, or improve performance.
I like Go.
Go fixes C's well-understood flaws (declaration resembling use, unintuitive operator precedence, unrestricted address math, silent casting, zero-terminated strings, etc.)
Go puts essential C idioms directly into the language (pointer/length is formalized as slices, packages are part of the language instead of just being naming convention, etc.)
Go is mature, stable, widely-used, well-supported, and well-understood. C++ is a clumsy mess, but C is quite elegant and Go is a continuation/modernization/enhancement of that
If you like C, you will love Go