I appreciate it's error handling. It's burdensome, sure, but it presents almost no additional cognitive load when attempting to reason about control flow.
It essentially has no enums. However, it has a comfortable type system that can wrap primitive types, and you can define methods on those wrapped types. It's serviceable, but not elegant.
I have never been a fan of pattern matching outside of functional languages. The phenomenon I notice in languages that do have it, is the majority of uses cases seem to be a pattern match with two outcomes, one a Some() and the other a None(). It really seems like a more annoying way to write if {} else {}.
It has for, but it also has 'range.' It makes up for a lot.
It has nil pointers, which are just a "zero value." It's not uncommon to make it useful and safer in certain contexts. You can request a lookup in a map that is nil. You can ask for the length of a slice that is nil. You get sensible answers still. There are still footguns, but they're all single barrel.. which is _nicer_.
I don't need a perfect language. Good enough with low cognitive load and amazing cross-compile facilities I'll take any day of the week.
EDIT: In fairness, there is one thing and exactly one thing I miss about Go: compile times. Rust is simply no competition here, but in trade, it is doing a ton of extra work. Trade offs.
I you were to learn Rust from scratch, what project would you recommend for the "changing and breaking" approach?
But along the whole "break it" idea... I don't know if you're an IDE person or a text editor die-hard, but I've found that rust-analyzer helps a ton. I'm historically a "vim with no plugins" kind of guy, but I'm using VS: Code with the vim keybindings now, and even if I'm not changing some code and getting feedback from the compiler itself, using rust-analyzer to go "hey what's this type here? Where's it defined, let's go take a look" has helped a ton.
That said, types help and poking around helps, but it's not always a panacea. Today I'm working on fixing something that doesn't quite work, even though yesterday I figured out how to assemble everything I'm supposed to need from this library I'm using. "It compiles it works" is a thing people say, and while I feel that way often, it's not true all of the time, of course.
But sure, one is a low level language which can and thus must care about every little detail, while the other is a managed language. (And honestly, mixing the two as if they share the same niche is very off putting)
If you have never used C++ and your baseline is Go or JavaScript then it probably looks like a confusing hellscape. You have to learn a whole new kind of type system, a new nomenclature (“Vec”s instead of “Array”s), new concepts like allocators, plus the borrow checker and lifetime annotations, and module/crate system. Former C++ programmers only have to learn the latter half.
I'm not sure this is accurate - have you seen modern TypeScript? I actually find that more confusing than Rust to read.
(FWIW, I was predominantly Python/JS-centric before just going "all in" on Rust. It's not that bad, in my experience - if anything, I find Rust "just works" whereas I got tired of the churn in those environments)
Rust and C++ are like Hebrew and Arabic. Difficult languages for an outsider, but if you speak one you have a significant head start towards learning the other. TypeScript is Japanese: knowing Japanese doesn’t help you speak Arabic, despite both being difficult.
Obviously you didn't mean that as a universal rule, but for posterity, I'm a counter-example to that. I learned Rust before learning C++ or Go. If anything, I think this biased me _more_ towards Rust than the others; C++ just kind of felt like a more error-prone, less ergonomic Rust, and Go just felt like it took far too much boilerplate to get anything done.
On the other hand Golang can be fun in like a week of learning it.
Meanwhile Go doesn't even have algebraic data types. I can't imagine working with a language that doesn't have these kinds of functional features anymore after having gotten used to them.
I have had year+ long breaks between writing Rust applications. I have been able to get back into it without issue very quickly. At least one of those times I successfully completed a large, sweeping refactoring of a project of mine that had lay untouched for three years. I can confidently say it would have been extremely difficult to do that same task in other languages I consider myself very proficient in and use daily.
This is basically the "Anyone who doesn't love my favorite movie hasn't watched it enough times" argument. Programming language design is a complex space, there are no "correct" opinions.
This is subjective - I for example have no real issues reading Rust code, but find Go to look like utter spaghetti on the screen.
I think my comfort with Rust moreso comes from it feeling like any other C-ish language in appearance. It reads as expected to me.
No part of my comment was doing this, and this bit is entirely unnecessary and adds nothing to the comment you wrote except fan religious wars about languages.
That’s because Go does not have async code. It has sync code on an async runtime. Like e.g. Erlang/Elixir.
I have written both languages professionally in various contexts for years. In point of fact, I've been writing Rust since before Rust 1.0 came out, although I didn't get my first Rust job until about 3 years later. They're both excellent languages. I actually didn't learn Go until several years after learning Rust, and at the time I didn't appreciate it like I should have, since I was so excited about Rust. Only after getting professional experience with Rust did I truly begin to appreciate how nice Go is for the things it is good at.
Go is just so much better suited for network-connected services (including but not limited to web backends), and Rust is better suited for batch processing or embedded contexts where no compromise on performance can be made, in my experience. There are lots of reasons for this, but it's off topic from the question, and the answer to the question is that I absolutely do go back to Go. Neither language is a perfect tool for all problems, and I constantly dream about building my own language that learns from the best of both... there is certainly room for improvement on both ends.
You can compare its domain (to a certain degree) to Java or C#, in which case the latter two are superior due to the reasons mentioned previously (enums, pattern matching, etc.).
I don't want to take sides, I want to enjoy programming and learning, that's all. When zealots zeal, kinder people either leave or go underground.
Rust requires designing the memory management (ownership/lifetimes) of a program, which Golang doesn't, so even if/when the cognitive load is (hypothetically) equal, there's an additional, non-trivial demand. This is not something that everybody wants in a project (the given tradeoff may not be worth); for sure, if I had to introduce a language in my team only for tooling purposes (ie. small programs), Rust would not be the most productive choice IMO.
But it's not easy to quickly prototype stuff with. Just recently I had to write no less than 7 small prototypes and I gave up on the second one, relearned Golang -- took me an hour -- and finished 3 prototypes in a day.
I tend to go all the way in languages so I can use them freely afterwards. But... In Rust's case I just can't justify the effort. Golang really helps you start off a project faster.
I'll still 100% Rust. I'm working on it every day. But indeed, let's use languages where they are at their best.
The other is most UIs are wrappers around other functionality, often in libraries. Working on anything other than pure Go code bases adds additional friction, and dynamic libraries add to that complication considerably.
I believe Go could be a really great fit for UIs, but likely never will be as there is little economic incentive to put in the incredible heavy lifting required to build up the support libraries. There are certainly some small projects trying, but without massive resources backing it, it is a struggle to achieve the full fit and finish that we've come to expect.
There are two reasons to have match: exhaustiveness and destructuring/pattern matching. I don't think there's an if/else parallel to `match result { Ok(t) => { ... }, Err(e) => { ... } }`
The fact that a `nil` value can be made useful (which, AFAICT, means it won't blow up your program?) is not a good enough reason to include them in the language. Rust can do perfectly useful things with, say, `Option<Vec<T>>`, and it's possible to distinguish between `Some(vec![])` and `None`. If you care to ignore that distinction you're free to do `maybe_vec.map(|v| v.len()).unwrap_or_default()`. But IMO it's much better to tell the compiler to unify different cases (`None` and `Some(empty)`) than to have to work around the compiler's unification of those cases when you don't want it. It's always easier to increase entropy than to decrease it. (P.S.: should the sum of a nil list be 0, or NaN, or...?)
But if you look at e.g. Kotlin, or C# with "#nullable enable", it tracks whether a given reference can be null or not. So you write if-else code instead of match, but you have to do that in order to actually do something with a reference.
"Nil is useful", in Go, means that for expensive objects it's often appropriate to pass nil instead of an empty or defaulted object, and call methods on it that act as if it was an empty object or with some default/no-op behavior. This is most obvious with lists/maps but can be applied to all concrete types.
While I can imagine a language in which `Option<T>` can re-export some of `T`'s method set with a default behavior, in practice I don't know any which actually do this.
I wished Go formatter would compress
if err {
return err
}
to if err { return err }
Even better would be to extend the syntax to allow if err return err
But I am Ok with braces.In Rust you write this type of thing as:
if let Some(name) = order.get_name() {
println!("Order ready for {name}!");
} else {
println!("Order number {} ready!", order.num());
}
So, it's a destructuring pattern match just written as an if-else. Because we did the match here, we can't forget and end up using name when there wasn't one, the variable only exists when it's bound.I also personally like Go a lot. It's filling the gap between C++ and Python for me. If I need something compiled with proper threading support, but C++ would be an overkill, I reach for Go.
Go is designed with a human centric view, IMHO: "Make writing great programs easier rather than design a language with novel/cutting edge features, but with a high cognitive load", and I find it as a noble aim as Rust's guarantees and aspirations.
I understand why people love Rust, but I don't think it's the only one, or the proverbial silver bullet that we finally achieved. Yes, it's nice. Yes, it brings many things to the table, but it's not the final solution for once and for all.
You like Rust? Great, Go for it (unintended pun alert), but I think we need all languages from all paradigms and perspectives.
I find this talk [0] very informative and useful while interacting with other languages and communities. I'm not sharing this with any insinuations towards you, but just find it generally perspective broadening.
[0]: https://www.youtube.com/watch?v=YX3iRjKj7C0 - What Killed Smalltalk could Kill Ruby.
If it were Go's design philosophy, it would have allowed unused variables/imports. Those restrictions are there exactly because they help computers, reducing compilation time. The over-focus of compilation time also stems from monorepos being used by Google, whose purpose is also helping computers.
But, I'm not sure that is even the primary benefit. The primary benefit is that you won't be incentivized to leave unused imports and variables to litter your code, as I see happen all the time in languages that aren't so strict. The Go team has even stated that they decided there would be no warnings because they've learned that warnings get ignored.
If something can be abused, it’s going to be abused.
I don’t understand his horrid informal writing style trend. You are clearly not quoting anyone and just providing your own interpretation. So why in the hell are you using quotation marks? In this case you could just ditch the quotation marks altogether since the colon already acts as a separator.
So, why in the hell are you just berating me via a comment box?
I made a mistake, alright, and used quotation marks as a tone modifier, because I know no other way to do that.
However in this case you could just pointed me the right direction without berating, since pointing out the mistake already acts as a kind direction arrow.
Did I even hint that I was assuming anything about native vs. non-native English speaker? Or blaming non-native speakers? No. Play that wounded ESL violin somewhere else.
> I made a mistake, alright, ...
I said that I think that it is bad style. Alright. I didn’t say that it is wrong. (People are doing it a lot these days so apparently it isn’t wrong.)
> Play that wounded ESL violin somewhere else.
I'm not playing a wounded violin, or any violin for that matter. I'm a double bassist. Jokes aside, I'm not being apologetic, just explaining my position. On the other hand, if reading a comment squarely in your own tone bothers you, you might want to think on that, at least a little.
Your wording of your comment implies that I'm doing something wrong by using quotation marks, and I take a note for that. Why are you so upset because somebody admits that there's a room for improvement?
The only thing I'm not agreeing on is your way of sending the message to this shore, that's it.
The thing is, neither English punctuation, nor Hacker News has many facilities to convey tone while discussing. I like to do that in my writing style to convey feelings and tone of my comment. Quotation marks seemed like a usable way to do that, and I possibly got used to that from literary works I've read, so I copied the method.
However, if a native (or more knowledgeable) person tells me that I'm wrong, I tend to believe them and try to learn the proper way, that's it.
First it would need to add error handling before it could look to improve upon it.
I'm not entirely convinced it should. I spend my days in a variety of other languages that have added error handling in various ways and, in my experience, it always ends up making errors unnecessarily difficult to do deal with. I regularly wish the idioms of those languages recognized errors as being core to your application as any other value, not something to treat differently. Go really got things right for the type of software I write.
But not all software is solving the same problems. There are a lot of programs where you don't need to think about errors; where stopping the world is fine if you encounter one. Go is not at all a good fit for these situations. However, I think it is okay for Go to not try to be all things to all people. We already have plenty of other good languages that serve other niches. Right tool for the job and all that.
varFoo, err := GetFoo()
if err != nil {
return err
}Can be written as:
varFoo := GetFoo()?
Just like Rust, everyone would stop complaining about Go error handling. But they have this absolutist position on syntactic sugar, even for something like this that would make the language that much nicer to look at and work with.
I'm used to a question mark being around null handling, but you know, JVM languages lol, null is thought about a lot.
Go is an experiment in the exactly opposite direction: language puts pressure to really handle the error in a meaningful way every time. And if you bubble up, at least describe the situation a little bit better. Or explicitly give up and receive a penalty for that: the `return err` is nothing to be proud of, it's just a visual penalty for _lack_ of error handling.
I'm not saying it's better in every project, I'm saying it's valuable on some projects.
It is possible that with other error-related features added to the language you could avoid those traps, but Go doesn't feature those either, so simply adding that construct without thinking about the problem much more deeply doesn't buy you much.
If you are solving a stop the world when you encounter an error-type problem that might be okay, although I'd argue that you may as well panic instead. But, again, Go isn't designed for those problems and I'm not sure it needs to be. There are already plenty of good languages designed for that type of work.
The reasonable thing is perhaps to log it there at most, and bubble it up (possibly wrapped as you mentioned). It can be handled for example by a request handler, by returning a 50_ error.
But other layers of abstraction lose their utility value if they start to make assumptions about the caller. Maybe in your web service a 50x stop the world error is all you'll ever need if there is a database error, but the next guy using the code for another purpose could have very different requirements and when you have to actually deal with errors, exceptions become a royal pain very quickly. As such, the official line is simply that you shouldn't let errors casted to exceptions cross package boundaries.
But, again, Go isn't really designed for stop the world programming and it's okay to use another tool if your problem space is suited to stopping the world.
The nature and culture of Go makes it harder to change direction and be as amenable, but the merits of such conservatism is a "depends" type of thing and can mean incorporating things many years after it has become fashionable or accepted.
I'm not sure what the better way to do it is tbh.
And the worst part is when reading the code in such a situation, it can get very tricky to figure out exactly where the exception is thrown.
There's a reason that the JVM ecosystem has largely moved away from checked exceptions.
Pollute implies that it is unwanted, but this is something you do want. It is the most interesting and important part of your application logic. You want it up front and centre for all to see.
I think we all understand the human desire to want to believe that bad things won't happen, but when one becomes an engineer they have to set those emotions aside and realize that bad things will happen and that your job is to make sure that when bad things do happen that the failsafes achieve an acceptable outcome.
> I'm not sure what the better way to do it is tbh.
I'm not sure any better is fundamentally possible within an engineering context. The vast majority of the job is in understanding the failure modes and being able to communicate to the computer how to gracefully deal with the problems when they occur. As such, it stands to reason that the vast majority of the code is going to be related to errors.
If you are programming for hobby/learning purposes, where when bad things happen your program can simply crash with no consequence, there are different approaches that work well, but Go is decidedly not designed for this space. And, frankly, doesn't need to be as there are already plenty of languages designed for that. Go is, quite explicitly, meant to be an engineering language.
What kind of utopian programming job do you have that you don't have to think about errors? An error is not limited to technical issues like packet loss or unable to open socket. Its also "client A attempted to purchase item B which is limited to client C". How do you express this in Go?
I have no idea why people are so opposed to ADTs. Its like sliced bread with butter, or whatever the phrase is. Its not _that_ complicated...is it?
My job primarily requires thinking about errors. It is why I wish for Go-style errors in the languages I use.
But on rare occasions I write things like batch scripts, automations, etc. in which failure means addressing the issue in realtime and trying again. There you don't care much about errors other than ensuring that the world stops when an error occurs to allow you to fix the problem before continuing.
While contrived, if you run into a "client A attempted to purchase item B which is limited to client C" error in this space you're probably going to have to phone them up and tell them that you can't process the transaction, apologize for the mistake, remove record of their purchase, and then once complete run the script again. The program crashing with an error message is sufficient here.
Different tools for different jobs.
Honestly, I suspect the deficiency is on my end. Perhaps I’ve just spent too much time with Lisp and have experienced some professional deformation.
We got rid of the getter setter silliness to be in “constructor land.” ( immutable records from Java 17 )
Function are still not first class citizens, but … they have a working visa now. You can shove lambda anywhere.
It’s still awkward to write functional code
Meh. Recently I’ve been really productive with it and I like the sheer boredom and lack of surprises.
I think something way more radical than "C++ threads but with a built-in concurrent queue" is needed. If the default was to share nothing, and any shared memory had to be explicitly shared somehow, that would be a great step in the right direction. Maybe the compiler could even check that explicitly-shared variables are protected by a mutex; something like how Rust mutexes "own" the things they're protecting and you can't access the underlying object without having the lock.
I love writing lisp, but I hate reading & debugging lisp.
There's a Go code base that a friend and I first wrote 5 years ago and we incrementally update it as we find small bugs because of a system it interacts with that changes. We usually touch it once every quarter. I've had no trouble re-reading the code base and keeping it up-to-date. I can't say the same for a lot of other code bases I've worked in.
OCaml kind of gives you that, but the dev experience isn't as good as Rust or Go in my opinion. Still, I enjoy it a lot, more than Go or Rust.
It does have some unique ecosystem elements such as Coq, WhyML, Mirage, etc. But common components could be definitely improved.
https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
"More expressive" in my opinion tends to mean "less readable", but there is some personal bias here. The people I've met during my career who like "more stuff" are usually the younger, less experienced programmers who aren't as concerned about long term viability.
I'm in my fifth decade of programming and my approach is "do more with less". When people want more stuff, for me, it means that I'll be dealing with more cases of people not knowing how all the stuff works. Everyone likes to think they can deal with more cognitive overhead, but mostly people can't.
I'm not sure if pattern matching belongs in a language like Go at all. Admittedly I haven't given it much thought, but it doesn't feel right for what Go aims to be. If you want to do Erlang, do Erlang, but I might be wrong about this.
What's error prone about the for statement? I don't use the C-style for statement all that often as way more than 50% of my for loops are ranging or naked for loops with break/continue/return.
Not having nil tends to lead to having to invent sentinel values all over the place, doesn't it? Is that really better? And since you often end up doing things by value in Go anyway, it isn't like you don't have any choice.
To get into it, it might help to start by modifying an existing project, a lot less painful than trying to write something from scratch without being familiar with the idioms.
That's fine, we all have our preferences. I love Python, but I can't stand Ruby. I love Go, but hate C++. I make my living with C#, but won't touch Java.
It's okay. We're not wrong. We're just different.
In my opinion: Go needs a Kotlin. First and foremost to do away with implicit nulls (imho the biggest mistake), but here are other things that could be impoved you've already mentioned.
> has error prone C-style 'for'
Not really true. It has `for index, elt := range v` to iterate over arrays and maps. It's a pity you can't define your own range, that's true.
My issue for application and web server development are in the language design as it restricts me in my personal quest to write loosely coupled code that is marinated in unit tests - at least when compared to other languages.
That said I enjoy the language, have learned a lot from it, use Go often (previous day job) and have a gopher plushy - but I am just not a die hard loyalist. There are things about the language that blow others out of the water, but there are significant portions that leave a lot to be desired.
What I have noticed is the language design has inconsistencies and there are lots of exceptions built into the language to get around missing features; features which are sometimes filled in later leaving ambiguity around approaches.
For example, Go uses a nominal type system with the famous exception of interfaces with methods that are structurally evaluated. The issue is interfaces only feature structural type evaluation on the top level, anything nested beyond that is nominal.
So an interface that has a method which returns an interface requires an implementing struct to return the _exact_ interface from that same method.
You can accept a struct where an interface is a parameter but you cannot return a struct where an interface is the return type.
This is useful when building type safe dependency containers, something that makes unit testing much easier.
Instead people just overuse `Context`, prop drill it into everything and cast types at some point when trying to get things from it.
Other examples are the return types from functions. Go didn't have generics initially so a Result[T] wasn't possible - but also exceptions were not possible. Tuples were not allowed by the type system but the language makes an exception for the function return types - fair enough.
Without type parameters and a need for generic basic types like `map`, `slice` and `sync.Map`, the language made an exception for the the primitive types - giving them generics however this is now inconsistent with the current implementation of generics.
Usage of `make()` and the confusion brought on by managing "references" also adds a bit of friction to the language.
Generics are inconsistent as well - for instance you can have a struct with a type parameter but not methods - however you can have functions with type parameters that accept a struct as their first parameter.
The tooling is a little underpowered, the test coverage analysis tool doesn't consider branch coverage, only statement coverage so you can have 100% test coverage reported with only 50% truly covered.
Making mocks requires cumbersome type generation - this is something I am okay with but the types generated have terrible assertion capabilities.
But there are phenomenal things from the language. Packages are a great design choice, module management from git is simple and effective, `range` is chefs kiss, the compiler is beautiful.
I love the idea of goroutines but I'm not blown away by channels. They are fancy iterators with dedicated keywords which I find can clutter things up a little - but they were really cool when Go first came out and we were only just starting to think about how to manage asynchronous concurrency.
Possibly I'm misunderstanding the complaint, but that is because an interface is a fat pointer, so one returns a pointer to a struct implementing the interface.
It's because an interface is structurally evaluated only on variable assignment, but it's not always structurally evaluated which limits its usefulness
If I had to make a bet, I’d say Typescript will take over for everything not performance critical and Rust will fit in every spot that either requires high quality assurance or high performance.
> That said, I don't know what I'd want to drop from Rust so maybe I'm just fantasizing.
What is X? I don’t know.
Watch, learn, download, and enjoy this V program (https://youtu.be/6H7dprSwr74), among so many other examples (https://github.com/vlang/V/tree/master/examples).
And, its not necessarily an "either or" or "us versus them" situation, people can use both or whatever else "floats their boat". It's a bit disheartening to see programmers that fall into being so closed-minded or disparaging to other options or something new.