Go is boring, and that’s fantastic
capitalone.com
capitalone.com
I don't like "modern" systems because I have a fetish for novelty. There's nothing novel about these concepts, they've been around since the 60s and 70s. I like these tools because they improve my ability to reason about the code, but more importantly they let the compiler and other static analysis tools reason about my code.
I am getting old and lazy. I want the compiler to do more for me, not less.
What I see is a situation where Go is gaining traction from two communities:
a) people who attempted to build large systems at scale in dynamic late bound languages like Python and Ruby and NodeJS etc, and hit the wall from a performance and maintainability POV. I could have warned you...
b) people who came from the Java world and got frustrated with the language and tooling there
People coming from a) especially but also b) to some degree will be perfectly comfortable with Go missing the nicer aspects of modern static typed languages because they never had them in the first place.
As for...
"Go doesn’t have a virtual machine or an LLVM-based compiler."
How is this any kind of pro or con? It's just an implementation detail.
When I was younger and less jaded, it actually was fun and exciting thinking up ways to hack around language limitations like that. Now it's just frustrating.
From my naive perspective, they seem like an easy win, and are 100% more important to me than generics.
Edit: true explanation here https://golang.org/doc/faq#variant_types
In practice, that means that there's just not much overlap between their semantics. Sum types let you say, "A Widget is either a Foo or a Bar, but nothing else." Interfaces give you no way to set boundaries like that. They say, "A Widget is anything that declares itself to be a Widget." And then you can declare Widgets Foo and Bar, sure, but anyone else can come along later and create Baz and Bof.
Interfaces, on the other hand, place restrictions on behavior. You say, "A Widget must support these operations," and, if Foo and Bar are Widgets, they must support those operations. Sum types don't let you do that. A sum type Widget with elements Foo and Bar places zero restrictions on what Foo and Bar look like. They could be literally anything.
The question, "What would happen if the elements of a variant type were themselves interfaces?" leaves me wondering if the authors' consideration of variant types consisted of taking a cursory look at Scala, which does (confusingly and hackily) co-opt Java's inheritance mechanisms in its implementation of sum types. Which does lead to some serious language warts. There are plenty of other languages which have both interfaces (or typeclasses) and sum types implemented more independently, though, and it does not typically lead to confusion.
That last paragraph is also somewhat bothersome, and makes me think once again that this response is more of an offhanded dismissal than a considered answer. The full question is essentially, "Why don't you implement this feature that would greatly increase the language's type safety?" and the response is, "Because you don't need it. See, if you just abandon type safety, what you can do is..."
I suspect that the real answer, even if the FAQ authors or the people who designed the language don't realize it, is that generics are practically (if not technically) a precondition for an algebraic type system. You could implement one without generics, but it wouldn't be very useful.
type foo either {
int
string
*bar
baz
}
... var f foo = ....
switch v := f.(type) {
case int:
// v has type int
case string:
..... type thing1 int
type thing2 int
type foo either {
thing1
thing2
}
var f foo = …
switch v := f.(type) {
case thing1:
data = int(v)
case thing2:
data = int(v)
…
and frankly that's a bit gross.I think `select` would be a better basis for dispatching than switch as it already supports getting data out of stuff, and it better represents the linear top-to-bottom dispatching.
type foo struct (
kind Kind
data int
}
Then you switch on kind and you use the dataThe type switch is useful when you have to consume the actual type, without runtime errors for type conversions
No, I'm pointing out that a proper sum type can (and often does) need that. It doesn't mean those are the only members.
> The type switch is useful when you have to consume the actual type, without runtime errors for type conversions
My point is that the type switch is garbage when multiple variants can have the same associated type.
With destructuring pattern match constructs you'd be binding variables to "inner" members of the sum type.
I do understand that. I'm not so sure it's so important, compared to Just being able to say that a box can contain either A or B or C.
Interfaces are great when you don't care what does a box contain as long as it quacks like a duck.
Sometimes though you need to carry around one out of N types of something and currently all you can do is to use an interface{} and deal with the possibility of a runtime error if somebody breaks the invariant.
It absolutely is, even more so because of Go's interface. The difficulty of properly discriminating between interfaces extending one another is a reason the FAQ gives for rejecting union types.
> compared to Just being able to say that a box can contain either A or B or C.
I'd argue that this is by far the lesser use case, and furthermore trivially and completely subsumed by the alternative.
> Sometimes though you need to carry around one out of N types of something and currently all you can do is to use an interface{} and deal with the possibility of a runtime error if somebody breaks the invariant.
And sometimes you need to carry around one of N values some of which have overlapping contents and currently all you can do is get bent.
Now with the new generics proposal, it seems they have hacked up some crude closed interface types, which are something like union types. :/
Secondly, they think interfaces cover many of the need of variant types and they don't want some non-orthogonal feature.
Since it's an interface{} it has no behaviour, the only thing you can do is a type assert / type switch.
By extension, you could add some methods: interface(A,B,C){Foo(bool) int} This fails statically unless A, B, and C implement said interface. If they do, then this interface has exactly the same semantics as the normal Go interfaces, except that only types A, B and C can be assigned to it (and not type D even if it implements Foo(bool) int
The most exciting part is finding that something is not nil but is actually nil. Great surprise, thanks typed nils!
I mean sure, I'd rather be working in Kotlin, but Java is at least acceptable now.
I've been working almost exclusively in C++ @ Google since then.
Java looks much improved now. But I still feel like there's a culture of complexity and bloat in Java code ... But I work in the Chromium code base and it has the same problem :-)
Complexity and bloat definitely come with that, I think some folk get used to building EnterpriseTurboWidgetFactoryImpl type horrors and never consider the language doesn't need to do that kind of thing any more... Java being so backward compatible I think also means it doesn't do enough to discourage that either though, which is also a reason why Kotlin is so nice -- the lowest effort solution is miles more understandable.
Did it ever though? I feel this junk comes from the Spring side more than pure Java. I'd love to know a good way to get rid of it as Spring is everywhere.
Maybe it was old school Spring that started it (it seems to have seen a big uptick in enterprise grossness in the 2000s so its an interesting hypothesis), but even Spring has moved on from it.
No, Java has always had ExcessivePatternFactoryImpl-itis.
And what's worse is that the language never had good supports for the patterns that the community seemed to prescribe. So you had an insistence on value and transfer objects and JavaBeans with pointless getters and setters leaking out their eyeballs, but no language support for properties or automatically managing these data objects. Or a desire to push the visitor pattern etc. but no pattern matching constructs. Apart from generics, which ended up being excessively complex and practically turing complete, the language was horribly anemic and repeatitive.
People like to bash Java without understanding how things turned out as they did.
Personally I don't know how we ended up back at "RPC good" land after going through "REST good, RPC bad" for 5-10 years.
a) Lack of static analysis tools means having to do more testing (manual or automated) for simple mechanical errors.
b) For super high throughput low latency systems, they are not up to snuff. When I worked in real time bidding in ad-tech this was actually a serious concern. I'd reach for Rust (or C/C++) in this scenario now.
And I don't think web development is where Go is getting traction. More in back end services and infrastructure.
> Java/Spring dependency injection is jumping through hoops just to provide a testable framework I find it hard to believe it's any easier.
As someone who develops with Spring everyday, I don't think it's the best solution for anything.
Having the code execution paths radically change by adding an annotation to a method or a class makes it very difficult to reason about what it will do when deployed.
If that annotation you found through Google does what you want and expect, that's great. But if it doesn't, or fails in unexpected ways, debugging it can be a nightmare.
The whole original point of dependency injection was to decouple dependency management from the code, to make it easier to test, and easier to reason about and analyze.
DI via annotations goes ahead and sticks them right back in there. And now we have, like you say, magic code that is difficult to reasona bout.
XML sucked, but annotations sucked more.
But I understand that that's what most people out there are doing. But they should also not delude theirselves that the toolsets appropriate for that environment are appropriate every where else. I see this bias a lot, on HN even, that everyone now is a "full stack developer" doing this kind of development.
Many of us work in entirely different stacks.
When you have an expectation from the exchange that you respond in under 80ms, and 25-50ms of that is eaten up by transport, you don't have a lot of time to mess around.
So you spend the first chunk of time optimizing I/O and how you're accessing it.
Then you start looking at computation -- improving caching, etc.
At a certain point you start noticing in your graphs that you're on average doing quite well. But there's those hiccups every Nth request...
And now you're in the game of fighting with your language's garbage collection algorithm...
*sync.Pool, arena allocation strategies
Try to improve allocations where possible. There's often plenty of small allocations happening in Java that can be avoided (string usage is one major driver). Less allocations means less frequent garbage collections.
Then one hack is to disable the garbage collection. Let the software run up to consume all the 100 GB of memory of the server and crash and restart. There is no pause of garbage collection when there is no garbage collection.
If it's not enough, the last resort is to write native code or switch to C++.
One of the things I really appreciate about golang (from a completely different field) is how quic teh builds are, and how fast binaries start up (it's like I wrote it in C).
The startup time is negligible in my experience (few seconds for JVM or python imports). I have to take over slow starting applications from time to time and it's always because of loading data and doing stupid shit on startup, irrelevant of the language. It's not a problem for production because server application only reboot once in forever.
Also, JIT languages have a very poor habit of doing a terrible first impression because of the warm-up time, especially in Java. If you are delivering applications to customers, that becomes a real burden - the very first time they use your shiny new application, everything is moving like molasses, until the JVM decides it's JIT time...
What did you do? You wrote servers in C? you wrote a Redis for instance?
My successor at the original startup rewrote everything in Python. And I watched from the exchange side as they struggled for two months to meet basic performance constraints. They eventually got it though. It certainly can be done.
It's worth pointing out this was 10 years ago. And in the meantime we've had the usual improvements in machine performance, and SSDs are a thing in data centres, etc.
- jit
- more functional tools
- better introspection
- sorbet
- ractor
- truffleruby, and jruby
Hopefully things will improve now ChrisSeaton is working there.
It can also be slower :)
Do these sites also have back end services written in other languages for the heavy listing? Anecdotally, seems to be a lot of stories to that effect.
And as noted in the article, Javascript/NodeJS is in a different performance category from Python and Java, more like Java or Go. But the simplicity of Go might make the performance and memory usage more predictable.
Lines of Code is a bad proxy for mission critical.
How do you define mission critical?
The main problem is just that these expressive tools scales poorly with regards to the number of people involved in the code base and its age.
The amount of time to write and/or duplicate code is just a fraction of the amount of time it takes to read/understand/debug code that have degenerated over time due to the number of people involved and various amounts of "expressiveness".
And no, a style guide isn't enough because these idioms change over time and there are a lot of social factors involved. Where for example the senior devs are actually the ones that a few years back decided that these overly-expressive code is the best thing since sliced bread.
Go code is very hard to read and review especially because it has so much repetition where little details can hide. "Is that a regular error check that logs and returns, or did I miss some actual logic there?", "Is that loop just trying to find a value in an array, or is it also doing something else?" etc - boilerplate upon boilerplate, which you learn to glaze over, until it's not really boilerplate and you miss something.
The examples you give doesn't seem to be specific to Go? In Go the error checks are, as most point quickly out, very explicit and often just err != nil checks. Not sure how they can be confusing, albeit repetitive.
I'm a bit surprised by the amount of down votes my reply has received given that this is a pretty common sentiment among very experienced devs within the game dev industry.
Errors are bubbled up manually. Loops often use array indices, instead of expressing the desired operations. You often need to use pointers instead of values just to avoid the cost of constantly copying structs.
And every time you encounter one of these things, you get to spend some time thinking about why this or that was chosen,or if you're missing some detail.
Java is much more readable, because it has far fewer of these concerns. Every time you see a catch block, you know it is there for a reason. If someone is making a copy of an object, you know they wanted to make sure the original isn't changed. With Streams, if I want to write an operation which filters a list, I can use filter(), not 'create a new array, go through the original, every time you find an element matching the condition, write it at some index in the new array, increment that index'.
Sure, most experienced people dislike exceptions, except for Bjarne Stroustrup, Herb Sutter, everyone else who designs the 3 popular C++ compilers, everyone who designs some of the the most popular C++ libraries (Boost, newer Qt), Java, .NET, Swift, JavaScript, OCaml, Python, and a few others.
And if you think that
int j;
for i := range someArray {
if hasSomeProperty(someArray[i]) {
filtered[j] = someArray[i]
j++
}
}
is as readable as filtered = someArray.Filter(x => hasSomeProperty(x))
then yes, we have to agree to disagree.You don't need to use indexes like that in Go. You would append to the new array.
The thing is that in practice such loops usually goes beyond just filtering on a boolean basis, so you combine whatever you want to do with that array in a single full iteration.
You're right about the indexes, I should have used append() there.
Related to loops though, the more you need to do in a single loop, the less clear the code becomes in the traditional loop style. Note that stream-style constructs don't iterate more than once either (not that doing M things per iteration vs doing M iterations is necessarily a clear performance win, depending on the size of the array etc).
For example, I would say that the readability difference is even more pronounced between these 2:
for i := range players {
isLocal := false
for _,localPlayer := range localPlayerIds {
if localPlayer.firstName == players[i].firstName
&& localPlayer.lastName == players[i].lastName {
isLocal = true
break
}
}
if !isLocal {
break
}
allLocalMoney += players[i].money
numLocalPlayers++
}
avg = allLocalMoney / numLocalPlayers
vs (C#-ish) avg = allPlayers.Where(p ->
localPlayerIds.Any(lp ->
lp.firstName == p.firstName
&& lp.lastName == p.lastName))
.Average(p -> p.money)
Add a little bit of grouping and things will get even worse for the first example. And sure, you could extract the checking into a separate function, but I wanted to come up with something that takes a few operations.On the other hand, I fully agree that sometimes you just need to do various actions (i.e. anything with side-effects) for each element in a list, and there there is nothing better for readability than plain loops. I just like the option of more explicit transformations when that's what I'm doing.
Debugging is not difficult at all, you can put breakpoints inside the lambda and use continue instead of sigle step. If required, you can also step through the library code, but that is not usually necessary.
And as the code grows, the high-level representation of what the code is meant to do stays clear, while the loop-based version grows in incidental details that you have to take a step back to understand.
Note that I've written commercial software in this style in a team with a about 1-200 other programmers. I am not coming at this from some hobby, 3-5 person project experience. It's just that different industries and different areas value different aspects of code. In my case, this is the middleware portion of a traffic generation solution that can simulate all L2-7 commonly used networking protocols, at the scale of a small city and beyond. Of course, the actual traffic generation code is extremely performance sensitive and is written in C and C++ (and quite a bit of Verilog) with a very different style, probably closer to what you are familiar with[0]. But there are many layers of configuration above that where we usually value clarity and correctness more than raw performance, and where we can afford to use these types of constructs, and our experience has always been that they vastly improve cooperation, not at all hinder it like you seem to imply.
[0] though we do have a real-time traffic stats analyzer library that is written in template-heavy, boost-heavy C++, and is on the critical performance path with soft real-time constraints, so this is also possible.
type
state = (on, off);
Go in 2020, about 50 years later, type state int
const (
on state = 0
off state = 1
)
Yeah we don't need those modern features from PhD level languages. type state int
const (
on state = iota
off
)Iterators were another one. The ability to express data transformation (mapps/filters/etc) in a very concise way blew me away. I had no idea what I had gotten used to in Go, though that's definitely not to say that I didn't feel the pain.
There's advanced features in Rust I could live without, but most of it just feels empowering. The beauty in it though, in my mind, is that you don't need to use all that advanced stuff. You can write Rust shockingly similar to Go.
The only thing Go truly nailed in my eyes is green threads. Those will always be better in Go than Rust (though futures are getting way better). Go nailed green threads.
But all the other "lack of features as a feature" left me frequently wanting for more tools to solve simple problems. And I was a Go nut. I have a Gopher plushie in my car for Petes sake.
Go was designed by very experienced programmers that understood the cost of abstraction and complexity well.
They didn't do an absolutely perfect job. It's probably true that Go would be a better language with a simplified generics implementation, enums, and maybe a bit more. That they erred on the side of simplicity shows how they were thinking. It's an excellent example of less is more.
Most programmers never gain the wisdom and/or confidence to keep things boringly simple. Everyone likes to use cool flashy things because it makes what can be a boring job more interesting.
But if your goal is productivity, and the fun comes from what you accomplish, then the code can be relatively mundane and still be very fun to write.
When designing a language there are intrinsic (what things the project wants to focus on, be they features of the language or the associated tooling that affect the language, like generics or compilation speed) and extrinsic (external impositions like being able to run on certain platforms, or interfacing with existing technologies like being able to run a statically linked binary in Linux or being able to debug using gdb or calling C libs without runtime translation) design constraints. All languages have (or should have) an objective of being easy to learn, pick up and use long term. It might just not be the top priority.
For the sake of argument you can take Python where expressiveness at runtime and clean syntax are prioritized over speed, Go where fast compilation and multithreaded microservices are prioritized over more complex language features, and Rust where fast binaries and expressiveness are prioritized over ergonomics (when push comes to shove this is the case, otherwise you wouldn't need to call `.clone()` or add `&` to arguments when calling a method ever), you can see how these objectives permeate every decision throughout the language.
When it comes to Rust in particular, I feel it is still a boring language despite the appearance of too many features, precisely because of how they interact between them and fit together naturally. It is not the best fit for every use case, but it is one of the projects out there that is embracing the fact that it can't be as easy to learn as it could be (without sacrificing some of the constraints that make it interesting as a systems language), but we can rely on the compiler being a necessary part of the developer toolchain to make the compiler understand the user's intent when they do things that make sense from extrapolated misunderstanding of the language and help them write the "correct" code instead. This has the added benefit that reading the code is easier because you have to "guess" much less what it is doing. Remember that if the code can confuse a parser it will also confuse humans. On the opposite end of the spectrum you have JavaScript, where it's grammar has a lot of optional or redundant ways of doing the same thing (think semicolon insertion), which makes the act of reading and debugging code harder. This is a reasonable approach in a case like the web, less so in a compiled language that can evolve independently from the end users' platform.
Precisely, and this is one area where go fails completely. The features don't interact well at all!
Tuple returns are everywhere, but there are no tools to operate on them without manually splitting the halves, checking conditionally if one of them exists, and returning something different based on each possibility. Cue the noise of subtly-different variants of `if res, err := nil; err != nil` in every function.
Imports were just paths to repositories. Everything was assumed to just pull from the tip of the branch, and this was considered to be just fine because nobody should ever break backwards compatibility. They've spent years trying to dig themselves out from under this one.
Everything should have a default zero value. Including pointers. So now we go back to having to do manual `nil` checking for anything that might receive a nil. But thanks to the magic of interfaces, if you call a function that returns a nil interface pointer, it will directly fail a nil comparison check! This is completely bonkers.
Go has implicit implementation of interfaces which makes exhaustive checking of case statements impossible. So you type-switch and hope nobody adds a new interface implementation. So you helpfully get strong typing everywhere except for the places you're most likely to actually mess something up.
Go genuinely feels like a language where multiple people each had their pet idea of some feature to add, but nobody ever came together to work on how to actually make those features work in concert with one-another. That anyone could feel the opposite is absolutely incomprehensible to me.
Yes, you could program C++ without even knowing what std::unique_ptr (and I talk to many college grads with C++ on their resume who don't know what unique_ptr is, or that C++ has more than one type of pointer). But Rust won't let you use raw pointers (as part of the language), whereas in C++ you will be told "make sure you have read Google's 10,000 word style guide before committing any code".
Like proper enums might be nice I guess, but really what I love are all the other things which are not there. Lack of enums has not hurt me deeply. What hurt me was languages where someone might conceivably express the COM apartment model and also think it was a good idea.
These aren't clever tools. They're dumb tools like chisels and hammers. Yeah, you can just use a screwdriver to chip things away or to whack something in, but there are better tools that are purpose-built and let you do the job faster, more precisely, and with less effort.
This applies in any case where one language is more complex than another. You can write almost any style of any language in C++, for example.
The problem is that every team ends up writing in their own subset of these languages, which means it's impossible to ever really achieve expertise. Each team's definition of the language is different, and no one has worked on every team. Ergo no one in the world is actually a C++ expert at any given company's "version" of C++, even if you know every C++ feature independently. You have to follow the style guide which tells you what subset of the language to use and how to use it. This isn't an insurmountable problem but it is a problem. Rust has the same issue.
With Go, everyone can feel free to use the entire language and every team's code ends up looking and feeling incredibly familiar, making it straightforward to contribute to most parts of any code base.
And it goes a step further. I read Go library code and it looks just like code I would have written. It's easy to understand and makes sense.
True, but my point was primarily that Rust and Go share many of the same patterns. Structs, methods and interfaces can look nearly identical.
This matters in my view when people think you need to go the most efficient and complex way to achieve similar goals as you might in Go. You were fine with performance loss in Go, so why complicate your life in Rust?
It's, at least to me, a useful lesson. Using every tool is a form of premature optimization. Go forced me not to do that, sure. Rust doesn't, sure. So I do hold more responsibility in Rust than I do in Go, but that doesn't mean I can't learn the positives of Go _(boring code/etc)_ without suffering some of the extremes of their decisions _(no enums, pattern matching, etc)_.
> With Go, everyone can feel free to use the entire language and every team's code ends up looking and feeling incredibly familiar, making it straightforward to contribute to most parts of any code base.
Yea, it's a trade-off I suppose. My problem with that though is when I realized I don't like Go's version of verbosity and spreading out logic. I've had pages full of helper functions just to do some minor iteration mapping, flattening, etc.
Having every team keep to the same standard of _(in my view)_ bad still feels bad. Consistent, sure, but consistently bad.
If you think Go's green threads are great, you should see erlang's. In erlang if you need to construct a mutual failure domain (have two green threads mutually destruct if one fails) you can use the link() function. Done. You can also trivially choose to have one of them be notified on the failure of the other instead of being destroyed.
Rust code is incredibly easy to understand. Far, far easier than C++ in my opinion.
Type safety does not guarantee exhaustive matching, unfortunately, because the underlying type is still an integer, but that's a separate issue.
https://play.golang.org/p/K0m4hfmw8C1
I wish Go had proper, exhaustive enums too (sum types preferably), but you're incorrect when you say that they don't have type safety.
Doesn't that make them type-unsafe? In C++ or Haskell, implicitly assigning integer literals like that isn't valid.
No, it doesn't. Go's enums are type safe. You can't accidentally mix two different kinds of enum values, or accidentally use some random value of type "int" where an enum value is expected. The type system protects you from values of the wrong type being used. This was demonstrated by that Go Playground link.
Literals in Go are untyped until they're used. How they're used determines the type, and then they have a very real type, with very real type safety being enforced. So, if you're using a literal "3" where an integer of type "state" is expected, the Go language specifies that the type of "3" is "state". This is an ergonomic issue when you're expecting exhaustive Sum Types, but not a type safety issue.
Should all literal integers just always be a specific type? Let's decide that all literals should be of type "int". Great. Now you can't type a large, 64-bit integer literal to pass as a value to a function, because that would overflow the "int" type, even though the argument is desired to be "int64".
There are trade-offs to every approach.
> In C++ or Haskell, implicitly assigning integer literals like that isn't valid.
I can't comment on how Haskell does things, but C++ is more complicated than you seem to think.
https://en.cppreference.com/w/cpp/language/integer_literal#T...
"The type of the integer literal is the first type in which the value can fit, from the list of types which depends on which numeric base and which integer-suffix was used."
Go's approach is equally type safe here. It assigns the type to the literal based on the expression the literal is used in.
As I said before: I really wish Go had proper Sum Types. But enums in Go are type safe, contrary to what you have claimed in several comments here.
"Enums" here are just another type of integer, exactly like in C# and C++. They're not a separate concept. I wish Go had Sum Types or even just exhaustive, non-integer (as far as the programmer knows) enums, but neither of those are requirements to have a type safe enum. The only difference vs C++ and C# is that Go has untyped literals, which are quickly handed a type based on where they're used.
Enums in Go have type safety, which is the point you disagreed with. You can't assign values of the wrong type to an enum-typed value without explicit conversion in Go. That's type safety.
There are linters that can be used if you want to enforce more restrictions: https://github.com/THE108/enumlinter
Linters that fit your team's expectations are a good thing to use in any language, and Go is no exception here.
Would this linter be unnecessary in other languages? Sure. I would argue it's still unnecessary, because I've never even once seen anyone accidentally type a literal value in Go where they meant to use one of the predefined enum values. It's certainly possible, but it hasn't caused me any lost sleep.
However, they are not enums in any way either, since there is no limit on their possible values to some restricted set - they really are just integers, even more than in C# or C++, even though there is no implicit conversion.
What do you mean by this?
C#: https://repl.it/repls/ExhaustedDisgustingRam
C++: https://repl.it/repls/BackGroundedRobot
C# and C++ offer exactly the same thing that Go offers... enums that are really just integers.
You can assign arbitrary values to those C# and C++ enums, even though you're not supposed to, just like in Go.
Rust offers something truly better, where the enum abstraction doesn't leak at all.
Note though that C++ allows using enums as bitfields, so some form of loading non-enumerated values has to be allowed.
Your statement implies that errors for using invalid values are guaranteed at runtime as a feature of the language. That entire statement is incorrect.
You really don't get errors... I literally pasted a link to a working, non-erroring example in the comment that you responded to. You clearly saw the code. Did you click "run"?
You only get an error at runtime if you add "-fsanitize=undefined" (or "-fsanitize=enum"), where the compiler will inject some code into your binary.
But the error doesn't actually stop code execution: it just prints a warning!
Here is a link with the sanitizer enabled, and no warning is even printed for using an invalid value: https://godbolt.org/z/NA7FNQ
So, not only is the sanitizer not even comprehensive, it's not actually a feature of C++. It's a best-effort feature from the compiler to add a non-standard runtime code sanitizer to your binary. Warnings for using invalid values are not guaranteed.
The point I’m making is that C# and C++ enums are not exhaustive, and not guaranteed to be any particular predefined value. They’re just integers. (unlike Rust: https://play.rust-lang.org/?gist=c9e499ba8d3abe08e5e54c74f54...)
C#, C++, and Go all share the same flawed enum representation strategy. Use Rust if you want to get better enums... those other languages aren't any better for enums. C#, C++, and Go enums are all type safe, and they're all perfectly capable of holding completely unexpected values. (Although, C++ in the older permissive mode was not type safe period when it came to enums, if I remember correctly. I admit it's been long enough that I could be misremembering.)
I already addressed your point in detail with you elsewhere.
Please, by all means review the final 3 lines I wrote in this comment for more info: https://news.ycombinator.com/item?id=23715897
I agree that being allowed to write any type of integer as a literal is theoretically an ergonomic issue when it comes to enums, but it’s one issue I’ve literally never seen happen even once in practice. I point out that there are linters available to address the issue if it’s one you feel so strongly about. Surely you use linters in every language?
I have been paid professionally for years to work in both Rust and Go codebases. Linters are essential for both languages, and CI is where you guarantee that no lint-failing code is allowed to pass.
I’m well aware of Go’s flaws, but people throughout this thread (yourself included) have made tons of baseless claims about Go. Just because it’s popular to hate on a given language doesn’t make it okay to use incorrect statements for that purpose, such as saying that Go’s enums aren’t type safe. They are a separate type. The language does allow you to write integer literals of any integer type. It also does this with untyped float and untyped string literals as well, as a fun fact.
Go has a number of legitimate flaws, including the absence of both generics and sum types. You could even legitimately complain about untyped literals —- they are really nice in some ways, but they do have some trade offs.
Now it is pretty much dead.
I bought Pascal, and then Modula-2, for my Atari ST. They were cheaper or the same price as a C compiler. Though C was a better choice for that system, since the OS was written in it and the calling conventions, etc. were all C.
On the Macintosh (and Lisa before it), by contrast, Pascal was the way to go.
So I think part of the reason C won out over Wirth languages back in the late 80s was because it just "fit" better with the systems that were emergent then. The swing towards Unix/Posix or Unix-like machines meant that the syscall interface for most things was defined with C calling conventions, and most example code was done that way.
And C++ also became quite popular, while the various object oriented Wirth / Wirth-like languages were not well standardized or available.
By the time this came around, universities taught C++ or Java in their intro to programming. Students used those. Then found the free version from discussion with peers.
One might ask how NPM succeeded and well, it was the only way to write code on the Web, so people had to use it, one way or another.
Nowadays, no-one blinks an eyelid when `npm install foo` drops 1000 packages in your node_modules directory.
TP executables were fast. Compared favorably to C++. The compile-time was also very fast.
https://www.embarcadero.com/products/delphi/product-editions
Otherwise Delphi is an excellent product.
However, since Delphi doesn't sell well we have the classic catch-22 situation - there aren't enough developers and the existing ones are retiring and hardly any new developers are using Delphi. So naturally companies are reluctant to commit to Delphi for new development.... further reducing the chance of bringing in more developers.
But then Borland decided selling Lifecycle tooling to Fortune 500 was more interesting than small developers.
Delphi and C++ Builder are still around, unmatched in several of their RAD capabilities, but a couple of generations were lost, even if now they try with the community editions.
It might have had to do with:
- Turbo Pascal difficulty in dealing with 32-bits x86. Turbo C++ had different memory models (but I think it was still on the 640k limit) and then DJGPP came with protected memory
Ok then we got Delphi. But to talk to Windows you needed the C/C++ interface. And don't get me wrong, Borland C++ libraries were much, much better than the MS VC 6.0 MFC. Really.
But for the times where you needed a "direct line" with Windows, C was the way to go, so maybe that was it.
But I don't know why Ada didn't take off either.
Ada was ahead of c++ by years, but it was unusably slow in compile debug cycles compared to c++.
And also expensive.
https://blogs.nvidia.com/blog/2019/02/05/adacore-secure-auto...
"Featherweight Go"
https://arxiv.org/abs/2005.11710
-- Proc. ACM Program. Lang., Vol. 1, No. OOPSLA
It appears Go needs PhDs after all.
type state int
const state (
On state = 1
Off state = 2
)
var s state
fmt.Println(s) // prints 0Your example case is something that I can't think of a reason to do. It isn't a case where you would need to be careful and knowledgeable to avoid it - there's just no reason to do it. You would use On and Off instead of defining a variable with this type.
Then why does it compile?
}
s := DeserializeState([]byte(`{"data": {"key": "value"}}`)
fmt.Println(s) // prints 0 oopsAlso, you could specify if a property has read and write capabilities or just read or write.. and even if what sort of accessor and setter methods are to be used.
http://docwiki.embarcadero.com/RADStudio/Rio/en/Properties_(...
I have yet to see this capability in ANY development environment with the sort of power that there is there in the Delphi component architecture.
Ironically, and for the same reasons, I want the opposite. I want the compiler to stop fighting me on every little detail. I'm exceptionally tired of writing boilerplate interfaces, thinking excessively about what size of integer I want to use, or digging into lambda calculus just to make a stone-stupid compiler (or worse, a borrow checker) happy.
For the niche that Go is applied in often.. .basically middleware type things, long running services, etc. I wouldn't necessarily jump to Rust.
I think Rust is well suited to lower level systems development, as a replacement for C/C++ there.
What's frustrating about Go to me is that we _need_ a nice clean compiled language with a static type system and garbage collection for services development. To replace Java, etc. But ... not this one. Go to me feels like a huge step backwards, and in my experience with code review with the Go folks at Google, it's a very strident and dogmatic language community, too.
I'd like something like OCaml, but with less functional religion, maybe.
So damn annoying and stupid that I finally broke down and modified the compiler to add a -warnunused flag so I don't have to suffer.
I like go for a lot of reasons. But these idiosyncrasies really drag the language down at times.
Variant types do have some similar use cases with interfaces, but I don't see any relationship with enums in terms of use cases, so I don't think that applies.
Also, let's not forget that even C has enums, so their lack of inclusion in Go is baffling (especially since they went and implemented the much more arcane yet limited `iota` feature).
Not in any useful sense of the word. A C enum is a typedef and a few named constant, it's absolute shit, and if the choice was restricted to "C enum or nothing" then "nothing" was absolutely the right call.
Go's typedecl + iota is actually a step up from C's enums, because (aside from "untyped constants") you need an explicit conversion from "any random integer" to your typedecl. It's not any more reliable or any safer at point of use, but it will prevent some misuses, or at least force the developer to consider what they're doing.
A great illustration lies in comparing enums in Objective-C (essentially just named integers), whereas the associated values in Swift enums make them "Sum" types.
An example at random from a domain Go is used a lot in (networking): When you define an enum of the resources recors in DNS you want the values to match the RFC definitions as they are used in DNS packets.
With that in mind Go's approach to enums makes sense: we usually want to label explicit values, potentially of an explicit type, we don't just want an abstract semantic label.
I haven't touched Java in a long time, but as far as I recall low-level operations on fields, bitmaps, etc. were not a strong point.
And yes, Java enums are very verbose if you want to use them for low-level operations, but that is almost entirely the fault of the rest of Java and not enums themselves specifcally.
I really favor the C/C++ binary interface for its simplicity but Go is really good for what it is built for.
But yes, compilers are always a good argument. I can understand however why C++ frustrated people. To get this beast to work for you, even after C++11 - it requires a lot from the developer. Today, things are a lot better though and they keep getting better.
It is pro in sense Rust developers spend inordinate time in blaming LLVM for slow compilation. Without Virtual machine is pro that steps to install, setup or upgrading virtual machines on target platform is no longer required
There are only a few code bases out there that are massive enough that compilation time on a modern machine is a major impediment to productivity. And in those cases I find that IDE and analysis tools on large codebases are an even bigger problem than compilation speeds...
Also the VM argument you give makes little sense -- you can have your language run on a VM without the VM being a seperately bundled package like Java or .NET. For example: Python runs on a VM. Its own VM, which is part of the Python runtime itself. Now, it's not a particularly good VM, and Python is a late bound dynamic typed language so it's also pretty slow, but there's nothing stopping one from having a fast JIT VM with a static language like Go. Not saying one should do that, but it's entirely possible and there's arguments to be made either way.
Depends upon what you mean by "major impediment to productivity". I think someone like Bret Victor would argue that if your feedback loop is greater than a couple of seconds, you're toast.
From that perspective, _the majority_ of _all_ codebases are an impediment to productivity.
Some of it really is LLVM, witness the latest LLVM release where Rust lost 10% in compile time and changed absolutely nothing other than the LLVM version.
However, the Rust folks are also painfully aware of just how much slow compilation is the fault of the Rust side. The whole idea of moving to MIR is to enable optimizations to be done with more context so LLVM doesn't have quite so much code that it has to generate and then spend all its time trying to optimize away.
However, Rust is always going to be slower to compile than a language like Go where compilation speed was an actual primary goal.
c) Minimalist-loving hipsters that read articles like this and bandwagon on Go just like any other trend (merits of the language aside). These are the same folks that use a hand crank to grind their coffee beans.
I know people who work at Capital One, I wonder how extensive Go usage is there...
I have started looking at job postings recently and I see a lot that have aspirational-Go in their postings. I have managed to avoid Go during my tenure here at Google (which isn't hard since it's not used much at all, despite what people outside of Google seem to think), but I'm starting to worry that if I move on I'm going to have to take a job writing it.
Who is hiring remote for Rust, Zig, OCaml, or Erlang? Those seem more palatable to me? :-)
On a more serious note, Go pisses me off every other day. Just off the top of my head, it's pedantic where it shouldn't and vice versa. I get that commented out usages of a variable are not OK for production, but why don't you give me an escape hatch, like --dev flag? And it won't complain about dead code at all if you stick an unconditional return in a middle of a function! Ugh.
You still need to be an expert on the virtual machine to not have things blow up. It's another factor of a complex equation.
My last job had a group of admins that thought any Java app from the Apache Foundation was the solution. Whenever things went wrong, their solution was, "just up the max heap size!" However, they're admins and had no idea about the app and the users had no Java experience to actually suggest tuning the JVM.
Yes, this was an org issue and a failure of tech leadership, but nevertheless, the JVM was a point of failure.
What you're speaking to is the issues with a _separate_ VM process and a cultural thing with Java generally, not a VM in particular.
Is this true of java? Is it possible to run a java program in a way where it is allowed to use up all of the system's heap if needed, but also plays nicely with other processes on the system (i.e. yields the memory back to the OS after it becomes unused)?
The original JVM philosophy seemed to be to cohost a bunch of stuff in one monolithic JVM process, rather than run a bunch of separate communicating processes. And in fact this "container" philosophy is what the original J2EE servers operated with.
> How is this any kind of pro or con? It's just an implementation detail.
This is actually one of my favorite things about Go. The fact that you get a single statically-linked binary at the end is so much nicer than having to mess around with JVMs and JAR files and CLASSPATHs. Sure, there's a lot of tooling to handle this in the Java world, but it adds significant complexity, and makes life difficult when you need to do something slightly different.
The fact that the go designers prioritize things like statically compiled binaries is the whole magic of Go in the first place. The language is whatever, it's fine, it's cool. But like the article says, the magic is in its holistic attitude towards process, tooling, and distribution.
All were able to AOT compile to pure sugar free native code, with various forms of GC support.
I can see how if you became fluent in a nicer language like Swift, it would be frustrating to move to Go and find your typical methods for expressing certain patterns are unavailable. They have been sacrificed for keeping the language overhead small, which in turn creates various warts and edge cases that give more ammunition for being frustrated with the language. I accept these tradeoffs when working in Go because I am typically thinking about concurrency and memory overhead in those projects, and Go makes measuring and reasoning about these properties of your program straightforward.
The first challenge is zero values: For a sum type like (int, float), there's no natural zero value. I think that sum types would have to follow the same design as Go's interfaces; a sum type value has to be nillable. This is unfortunate, but the Go team burned some bridges when it decided to support nils, and they now have to deal with that problem.
Secondly, there's the matter of what a sum type of interfaces means. I think this is solvable, too, and I don't agree that it presents a conflict. Sum types express the range of allowed values. What you do with a variable, once it holds a value, is no different than today:
type Source sum { io.Reader, io.ReaderAt }
var src Source
switch t := src.(type) {
case io.Reader:
case io.ReaderAt:
}
This is no different from: var src interface{}
switch t := src.(type) {
case io.Reader:
case io.ReaderAt:
}
[1] https://www.reddit.com/r/golang/comments/46bd5h/ama_we_are_t...I wish people would stop beating around the bush and own up to these being design mistakes of the language. If you can't have nice things because earlier poor decisions (e.g., supporting nils) has walled you into a corner, it's a problem. It's not an example of "refreshing simplicity", it's a shortcoming of the language. Period. Every language has some warts, just acknowledge them!
This is not a sum type, it's something similar but different, sometimes called a union type, because the point is to store a closed set of different types under a single umbrella.
A sum type has variants with associated data, but the type of that associated data is orthogonal to the variance, you can have multiple variants with no associated data, or the same associated data type (both are very common use cases), which would simply be impossible to express here.
With a proper sum type, this is not an issue because the Reader and ReaderAt associated data would be stored in explicitly and specifically different variants. In Rust parlance:
enum Source {
Reader(Box<io::Reader>),
At(Box<io::ReaderAt>)
}
match source {
Reader(r) => …
At(r) => …
}
there is no ambiguity or confusion possible.> The first challenge is zero values: For a sum type like (int, float), there's no natural zero value. I think that sum types would have to follow the same design as Go's interfaces; a sum type value has to be nillable. This is unfortunate, but the Go team burned some bridges when it decided to support nils, and they now have to deal with that problem.
The proper way to fix this would of course be the same one C# took: every C# type used to be default-able (although to their credit it was not generally implicit). With the introduction of non-nullable reference types, they had to change the language to handle cases where types would not be default-able.
Of course for Go that has wider-ranging implications e.g. currently I don't think there's any validity tracking because `var i <type>` tacks on an implicit zeroing of the value, it has no concept of "uninitialised values" whereas C# had that even before non-defaultable types. It also breaks their assumption and assertion that you should be able to add new fields to a structure and that'd get automatically filled with garbage without the caller being aware (also a terrible idea).
Go basically front-loaded their technical debt.
I may be wrong, but whether you have an indirection through a type constructor or not doesn't alter the meaning of the union. In the fictional Go syntax, you could also have the same indirection:
type Reader struct { R io.Reader }
type ReaderAt struct { R io.ReaderAt }
type Source sum { Reader, ReaderAt }
The difference is that Reader and ReaderAt are completely separate types, not type constructors, and that Rust has a shorthand for essentially valueless type constructors, allowing Rust's enums to act both like classic C enums and like sum types. But in Rust, as I understand it, enum variants aren't actually types. In your example, you can't have: let r: source::Reader;
Of course, you can do the same thing in Rust, at the cost of readability: struct Reader { ... }
struct ReaderAt { ... }
enum source { Reader(Reader), ReaderAt(ReaderAt) }
> The proper way to fix this would of course be the same one C# tookIt's just too late for Go to do anything here, I think. Zero values permeate the language. For example, consider structs. It's normal to do:
var f Foo
if err := json.Unmarshal(b, &f); err != nil {
...
}
It's unfortunate.There’s a difference in the ambiguity — or lack thereof — which is the purported reason why go could not have sum types.
> But in Rust, as I understand it, enum variants aren't actually types.
That’s correct. They’re just constructors for values if the enum type.
> It's just too late for Go to do anything here, I think. Zero values permeate the language. For example, consider structs. It's normal to do
It’s not too late for anything. Existing types do not have to change and don’t prevent adding new non-default-able types (which would be transitive).
Those types, with those new guarantees, would not allow for the magical zeroing and zero-extending of existing types but they would not break anything.
Not exactly. It's hardly a detail, and affects use.
The first (not having a VM) in Go's case translates into AOT static compilation, and you not needing to have a VM installed to run your programs.
The second (not using LLVM) translates in Go's case to faster compile times compared to LLVM based compiled languages.
While both have their drawbacks (e.g. LLVM compiled code would probably be faster), both are pluses for my preferences/use cases - not mere implementation details.
One example: People claiming the borrow checker gets in their way in Rust because it’s so strict.
Guess what? If your code doesn’t compile due to the rigidness of the borrow checker, it’s incorrect I’m sorry to say. It’s either got a data race, use after free bug or some other invariant. You don’t want that behavior in your code at all.
I’m happy to offload the responsibility to the compiler to check for this.
You won’t get it until you’ve been bitten.
There're plenty of bug-free code which the borrow checker doesn't permit. A very common example is nested calls with a &mut receiver: https://internals.rust-lang.org/t/accepting-nested-method-ca...
That's your opinion, and maybe even that of other programmers which have good intention. However advanced PL features are still sometimes overused because the users find them very interesting and can't resist applying them everywhere.
As an extreme example I read a statement from a Rust programmer about liking framework XYZ, because it creates those nice type-system puzzles and makes things super-safe. However for an outsider this super-safe code could also be undecipherable gibberish.
Overall I think that while type-help can be good to avoid errors there is also a line where the complexity in type system features outweighs the complexity of the initial problem. And that after you cross the line programmers spend their time trying to understand complex but less-error-prone code instead of simple but easy-to-understand-and-fix code.
Where that line exactly is depends on the problem space.I certainly think it is somewhere between Go's extremely simple approach and the extremely advanced feature sets that some other languages allow for.
It’s interesting how a language can be boring and so fun to use at the same time.
I guess I don’t want excitement at the language layer because there’s plenty of opportunity for it in other layers.
A boring language to me means there’s usually roughly one way of doing something, so you just need to write up that one thing and move on to the next part of what you’re building, knowing the past part is finished. Once something works well and has a good API, I almost never want to come back to it and rewrite it with a different set of language features that I happen to learn.
The process of writing Go is fun to me because starting with an empty Go program to do a new task is always a refreshing experience, it’s easy to reuse any past functionality that you’ve already created (or found) and you can focus on the new task. In contrast, when I was primarily using C++, starting a new codebase wasn’t pleasant because of all the housekeeping and boilerplate (like defining a uint16 type in a platform-agnostic way) I needed to copy/paste just to have a reasonable starting point.
Also, the feeling of gofmt/goimports running on save and formatting the code on save is a huge part of what makes it fun (fortunately, code formatters are much more commonplace now; I wouldn’t want to go back to not having them).
If this is your first programming language or attempt to learn to program I would say to just pick a small but simple app/program and just go ahead and build it. Don't spend too much time on reading articles or video tutorials just get your hands dirty as soon as possible.
The language spec at https://golang.org/ref/spec is a surprisingly approachable one-pager, it’s worth at least glancing over.
One of my all-time favorites talks is https://vimeo.com/53221560 from 2013. It shows off how Go scales by starting with a small task and continuously adding more requirements into the mix, while also demonstrating its concurrently support.
If you look closely, those aren't angle brackets, they're characters from the Canadian Aboriginal Syllabics block, which are allowed in Go identifiers.
I was following Go in the beginning (2009/2010) and the team behind Go had a great architectural understanding of how to make a platform library in a "cathedral" fashion. This is one of the big reasons Go did great as a new language.
People from Python and Ruby could use Go as a alternative on server/backend software because the core platform was there together with AOT compilation, GC and its was a much faster alternative to those languages. As people from Java could be released from the burden of too many abstractions, heavy runtime and the big memory pressure on servers.
Go was a good savior back than, and still is a good alternative to the infrastructure kind of sofware.
Giving the crowds using Go were distinct, each one of them missed different things in their former runtimes.
Golang is less expressive than other languages, like Rust or Haskell. You can't create fancy custom types and things like that. Of course there are drawbacks.
The benefit however, is that it's much harder to write "clever" code, which translates into being harder to write unmaintainable code. If you know Go, you can sit down at nearly any Go codebase and know what's going on pretty easily. That certainly isn't the case for C or C++, and probably others (other people have told me this is true of Rust, but I have not used it, so I will not claim that).
Golang is not the fastest, or the most expressive, or whatever else. But it's got enough expressiveness to not be painful for most use cases (slices, maps, and structs cover a lot of ground). It's fast enough for most use cases (faster than most scripting languages).
In my opinion, Golang has a very good ratio of (effort required to learn) / (utility of learning). I have no doubt that Haskell is extremely powerful... if you can learn how to use it.
I wouldn't be signing up to write a new OS kernel in Go, but for a userland program that doesn't have hard-RT requirements? I think Go strikes a better balance compared to C, C++, Java, Python, and others.
I don't think this necessarily follows. I don't think "clever"ness is what makes code unmaintainable. You can still create a tangled mess in Go just as easily as other, more expressive languages. In fact, I think it's more likely in Go simply because you _have_ to write more code in Go. More code is more maintenance. Go encourages you to repeat yourself and to not create / use abstractions. Nil pointers are still a thing. It's incredibly easy to accidentally shadow a variable and not handle an error. These are all solved problems in other languages, yet they persist in Go, and they are all maintenance headaches.
It's not the only factor, but it is certainly _a_ factor: "Clever code" is typically meant as derogatory. Fancy abstractions for simple use cases is a common cause of tech debt IME, and harder to unwind than under-abstracted code (tedious as that is to fix).
Yep, but here's the thing - if the problem is misuse of the tool, then the solution should be to not misuse the tool. Saying "we will remove features because some people misuse it" is like saying "we will remove headlights from cars because some people drive irresponsibly[1]".
Go remains my language of choice if I have to build efficient webservices, but I really feel sad when people make it sound like Go is awesome because it misses language constructs. No! It is awesome because it is FAST (quite near C) but it is very easy to do simple things like writing http servers, parsing JSON and so on (like Python et al).
[1] https://timesofindia.indiatimes.com/city/bengaluru/high-beam...
EDIT: fixed typo
For example, I'd look to languages like Pascal, Modula, Oberon (the last two an important influence on Go in the first place), Ada, and Nim for how to provide practical mechanisms that don't descend into Haskell/ML-style type-system magic. Several of those languages have also implemented generics successfully.
You can absolutely do whatever you want at runtime with reflection. The problem is that compile time type checking possibilities are poor in contrast. It's exactly like C in this regard. You can't type check a linked list in C at compile time. Go has typed arrays but you get my point, replace arrays with generic stacks, generic circular buffers or what not. this isn't "fancy types", it's basic CS.
Like someone internal at Google said people will use and adopt en masse anything they make, regardless of how good it is.
Someone else said no way, a bet was made, and Golang was released. Ignoring 20-30 years of programming languge theory advancements. Yet everyone laps it up.
And here we are today. I'm sure someone at Google is laughing.
I find Go completely tedious and inelegant to work with, its too verbose and i think verbosity hides intention and expressiveness.
Clojure C# F# Rust Scala Ruby Kotlin
Scala and Rust's build times get very noticeable very quick IME.
I haven't seen anything else ever be that bad.
I think the sense that this is a "Google language" is exagerated. What it is is a language created by a certain group of people at Google with Rob Pike at its origin, and they would have created this language wherever they were working. It's a continuation of the work they did on Limbo/Inferno, etc. not something adapted for Google's needs, as it is sometimes advertised as.
So I'm in no position to criticize Rob Pike, he's a far smarter person than me who has achieved a lot more than I ever will. But I also don't think Go solves any problems that I have, and my one experience with going through Go code review at Google turned me off the language probably forever.
Weird quirk of using Go at Google is that any backend service at Big G mostly copies protos, and in Go you end up with these many-page inline protos initializations, which are unreadable.
Go was different in that they had introduced an incremental readability process. Instead of one CL, you had to do many, each reviewed by different people, and over time after doing a whole bunch of CLs you could get readability. This is the process that Google eventually adopted for all languages, but they started with Go.
What I found was the Go reviewers were a) exceptionally pedantic [even by Google standards] but, worse, b) very contradictory. One would chastize me for using channels where they were unnecessary, so I'd remove their use and just use simple functions but then a later reviewer would scorn me for not using channels in the same code. It became a painful game of tennis. And the whole thing had a feeling of ideology over practicality.
And bend over backwards to defend it. "Being bad is actually good because it's more beginner-friendly" okay lol
Another beast that had Google's branding on it, had all sorts of technical awfulness, and was not popular within Google either. Used for a few projects, but nothing consumer facing other than some ads products.
I’d argue Go didn’t ignore those things, it just made a different set of trade-offs than most people are used to or expect from a modern programming language. The rationale for this is is well explained in https://youtube.com/watch?v=rFejpH_tAHM&t=306.
You are wrong.
Emphasis mine was added by me.
Sorry, source needed. You're a software developer, not a bridge builder. Yes, bridges in my country seem to work, fair enough. But I have the faintest clue if the design/building process really is going that smoothly. To write this down as a fact to build your argument is IMO a pretty big assumption.
And that's not even considering how many of those road or bridge building projects really are in time and budget.
There are a few factors that make bridges very reliable (but not perfect) in the US. A real engineer can probably describe this far better than I can.
1) Physics and material science are pretty well known quantities. There is innovation and new materials come to market, but by the time they are approved to be used in real bridges their qualities are fairly well known and tested. They have solid evidence of the capabilities of a material or part. (Tensile strength of an item under various environmental conditions, for example).
2) Standards & Very Firm Requirements (Where waterfall works!) They have a set of strict approved industry standards to rely upon. Not the "industry standard by convention" kind that you see around IT a lot, but the "tested, reviewed, approved, published by a standards body" kind. When a civil engineer is assigned to build a specific bridge they reference these standards that tell them "given this type of ground do this", "given this length of span do that". They know the lengths and widths, they check the traffic patterns, they know the desired weight limits. Challenges do come up, yet they have fewer unknown unknowns. Something changed from their expectations, maybe the ground is different than expected, but they still have standards that guide them in what to do in those cases.
3) Reviews, reviews, reviews. Other Professional Engineers have to sign off on the work of the initial designer(s). Deep reviews. Not what typically passes for software architecture and code reviews I've seen.
4) Legal responsibility. Professional Engineers have an amount of personal accountability at stake because real lives are at stake if something fails.
5) "User" common sense and intuition Nobody builds a 100,000 ton vehicle and then tries to drive it over a bridge. Most any human that thought about doing this can probably reason that doing so would break the bridge. In my experience software users have no idea that trying to push a 1TB file into a system that was designed to only handle 1MB files will cause it to fail. They often have no idea when their data volume increases. "I'm doing the same thing I did last month!"
All that and more, yet bridges still go over budget and over time and fail and people die.
I almost wish they'd make it more boring and take a few things out, but it's probably too late now to change the language much and people would complain vociferously. Things I never use and wish it didn't have: panic, goto, labels, struct tags, executable comments, even arrays.
The only things I'd like to see improved significantly are enums, errors, and user-space generic collections (coming soon!).
You and I must experience time differently. I read a couple days ago that the end of 2022 would be the earliest they might appear but probably further out than that.
I really wish in their mooted Go 2 they could just remove some features, as listed above, rather than add some. Perhaps everyone prefers a slightly different subset of the language though so that isn't really possible.
Here are some boring choices we made that paid dividends:
1. Postgres. It does what it does, it's stable, it's reliable, and continues to improve as time goes on. 2. Ember. It's by far the most stable front end framework going. It's made our job easy in uncountable numbers of ways. 3. PostCSS. Write plain CSS, use modern features, remove plugins as browser support grows.
A few years ago I'd have said Rails was a boring technology we chose too. These days I'd say it's a bit of a thorn. It's clear that some of the magical choices made make long term maintainability challenging, as are some of the performance characteristics. A few of the libraries used by the community have bitten us hard too insofar that they're either a maintenance nightmare, poorly maintained, or exceptionally difficult to migrate away from (e.g. CanCanCan, Active Model Serializers).
These days I'm playing with Elixir, which is also boring but allows you to do exciting things. I especially appreciate ExUnit having come from the RSpec world — assert, refute, end of story.
Aren't there usually full-time SREs focused on managing and maintaining SQL databases? I can get really really far, really quick with Cloud Firestore (for free at my scale), and stay focused on my frontend. But as complexity grows and requirements change I can sometimes find myself in sticky situations-- mostly because there are no tools for schema migration in Firestore.
But on the other hand with Firestore and similar noSQL products I can get scaleability, security, speed, stability and a lot more all out of the box.
Curious to hear your input.
I'm always terrified of ecomm using NoSQL for carts and transactions but I'd say it's overblown on my part I just prefer to rely on the durability of a SQL database using guardrails at the schema level to keep my data correct. Either tool is easily misused but Postgres really can handle a lot it just doesn't get the shiny attention some other host services do because it doesn't over promise things you probably won't need to use. That said, you can do really slick NoSQL style stuff with Postgres HStore and JSONB columns.
There are a lot more data-store-as-a-service offerings for NoSQL but I don't find any of them particularly beneficial for "plain" apps but agree you can get started a lot faster. Firestore is a lot of fun when you need things to sync between clients and there are tools to do the same / similar with Postgres (noted below).
https://www.postgresql.org/docs/12/functions-json.html
https://www.postgresql.org/docs/12/hstore.html
https://github.com/PostgREST/postgrest
This is my favorite learning material for ANY programming related skill ever: https://use-the-index-luke.com/, and it's all about SQL queries.
We created Supabase specifically to solve this pain point. It’s just Postgres, with a nice UX and features. You get direct access to your PG box so you can modify it in any way you want. We’re also open source, so there is no lock-in.
I hope try out PG (regardless of the hosting solution you choose). It’s an amazing tool
??????
> Rust's borrow checker is a fascinating way to get high performance and memory management, but it effectively turns the developer into the garbage collector, and that can be hard to use correctly
Isn’t the whole point of rust that you cannot get memory management incorrectly because compiler guides you?
> all Go code is formatted the way that go fmt says code should be formatted.
just like rustfmt, elm-format, prettier and I hope most modern or future languages
I agree with his main idea, I also like the idea of a boring language, but then his arguments misses the point completely, talking about benchmarks and such.
He should talk about why having no exceptions can be a good thing, or having no generics.
Having a very small API surface and stable language is one of the reasons I love Elm as well, nobody is able to do too smart abstractions with monads and such, it’s about code, readability, explicit better then implicit, etc, nothing to do with garbage collecting, performance, blablabla
Absolutely blew my mind. I love go fmt, prettier, black, etc and cannot understand why people still love to argue over styling at this point. I get that he was saying the formatter ships with Go but I don't subscribe to the idea that you can't use an opinionated formatter because the language authors didn't make one.
As a nearly related example, the LESS css pre-processor language was developed prior to `calc()` expressions. As a result, the LESS language parses arithmetic expressions. So `calc(10em + 10px)` compiles to `calc(20em)`. So in order to do `calc()` expressions, you have to use nasty hacks like `calc(10em ~"+" 10px)`. If LESS "shipped" with CSS, this wouldn't have happened.
I like the authoritarian approach of go fmt and the strong idioms in Golang generally. Maybe because I lost many an hour arguing about the correct way to configure PerlTidy, only to have my hard-won righteous perfectionism destroyed when everyone joined the Cult of Moose.
The compiler does “guide you” in memory management in Rust, but its “guidance” consists largely of refusing to compile buggy programs. Getting the program to compile e.g. doing the memory management correctly, is still a difficult matter, although this varies a lot by experience and program requirements.
To put it another way, if you are interested in shortening the time between writing a program and having a crash-free version, Rust will improve your situation. Although it will be a long time either way because bug-free programs are hard to write. If on Theo the hand you wanted to get something running your machine by this evening, Rust will spend a lot of time complaining about issues you are unlikely to immediately encounter.
>We agree that it's better to find problems earlier rather than later.
Agreed.
>We agree that people are awful at managing memory in programs.
Nope, manual memory management can be done properly and this is not rocket science.
>We agree that code reviews help find bugs.
Sure, but this is not the only way and not always the most efficient.
>We agree that on any project that requires more than one person, communication costs dominate.
Nope, this is a blatant over-generalization.
Outside of NASA (often, literally rocket science!), where has manual memory management ever been done properly? I don't think that it ever has. For that matter, I would be unsurprised if even NASA had manual memory management failures.
> 3: Do not use dynamic memory allocation after initialization.
https://sdtimes.com/nasas-10-rules-developing-safety-critica... https://news.ycombinator.com/item?id=4339999
(of course: failure is less catastophic than it would be for NASA!)
We use custom allocators and simple allocation policy, often with arenas and other region based system.
Certainly there's an argument to be made for simpler languages being better (maybe we should all be writing in Assembly?) but I'm personally not convinced.
https://www.angelo.edu/faculty/kboudrea/cheap/cheap3_murphy....
While Terraform is I/O bound to some degree, it has a large degree of concurrency - something quite miserable to do in Python.
Terraform does not take full advantage of even the type system available in Go, but a dynamic language would make things even worse. I do not think Go is the ideal language for programs like Terraform, but having spent many thousands of hours in that codebase now, the answer is stronger types (likely Rust), not fewer.
Go should have taken cues from ADA and how it implements generics. There is enough polymorphism to solve most cases where generics are useful, but also enough limitations so that a codebase isn't cluttered with `Type A<Type B<C,D,Type E<F,G,H>>>` horrors everywhere.
The "stylistic opportunity" for other languages is such that I can usually tell which developer on the team wrote what just at a glance. Not so with Go. It's all the same bland but beautiful gray.
Something to be said about languages that help external people understand them, as opposed to languages developed to improve developer satisfaction.
In Go you have to "manually" defer a file.Close() call -- vs. the destructor getting automatically called in C++ (it even gets automatically called when a containing object is destructed, if it's a member, and so on). Go's version doesn't seem very "automatic" to me.
The simplicity of Go actually hides a lot of runtime complexity that is happening: the GC, map lookups, scheduling of goroutines and so on. Which you have to understand and then ultimately learn how to tune from a distance (because you don't have direct control) for a lot of applications where it matters. For server applications (where the hardware is under your control) and also CLI tools (which can just run, only allocate memory, then just exit), this matters less. If your code is running on user's devices as an interactive application, it feels to me like how it performs on and uses their actual hardware is part of your responsibility as the developer, and some things about Go make that harder to do. Like you have to think about the GC and try to avoid it ("from a distance") if you run into that (which you do quickly in WASM, and which eg. Gio tries to explicitly design around, and so on).
I want to see languages that care about simplicity and also include such deterministic resource management as part of their design and offering.
To clarify this one point, Go allows you to attach a destructor (finalizer) to an object, but since the language is garbage collected, there’s no guarantee of when the destructor will be called, if ever, since disabling the garbage collector is entirely possible, even if not advisable.
The File object will Close the file when the Finalizer runs, since the standard library attaches a finalizer to it, but you should defer the Close to deterministically ensure that the file is closed when you expect it to be closed, and to free that OS resource of a file handle as soon as possible, since some OSes limit how many you can have at a given time. Finalizers also won’t be run when the program exits, which is fine for most things, but you’ll want to have ensured any buffers were flushed before then, and defer can guarantee that.
Languages like Rust and C++ have deterministic destruction at the end of a given scope thanks to RAII, so it makes sense for them to lean more heavily on destructors there.
Java File*Stream objects should have “close()” called on them, from what I’m seeing on a Google search, so this doesn’t seem to be a surprising pattern for a garbage collected language. I know in Python you’re encouraged to use a “with” block to ensure that file objects get closed.
For e.g. Monzo engineers have tweeted that they had to create an internal math library - https://twitter.com/_liclac/status/1264142576908722178
Once you bring Pandas, Scipy, LAPACK/BLAS, Numba/Cython into play...then i bet these benchmarks look a lot different.
In the article, it is said to match Java. In the Benchmark Game, it is certainly not among the fastest.
// Too bad editing is not available anymore
Edit: well not in FAQ but somewhere else. IIRC Rob Pike.
I think a few things contribute to that:
1) Relatively few language primitives -- it takes like a day to learn 90% of the keywords you'll be using day to day.
2) Transparency -- it's easy to look at any code base, and track down all the relevant code, even without an ide. Compared to a language like ruby, there's not a lot of 'magic' happening.
3) Conciseness. Unlike a lot of statically typed languages -- go code isn't verbose. It's statically typed, but the inference works well enough that you're very rarely having to use type hints all over the place -- Java has started to be more like this, but back when go started, it was a breath of fresh air.
4) Kubernetes. If you want to build on top of k8s, go is pretty much the only game in town. It just makes everything eaaier. Other languages have k8s support, but any language other than go will have more friction.
5) Go channels and concurrency -- I actually don't think this is that important, but knowing that it's available if you have that kind of performance demand is nice.
This hasn't been my experience. When I tried porting some Python code to Go, I found it wouldn't compile without lots of type casts (mostly between different integer sizes and signedness).
We trot out this particular analogy all the time in software engineering and I can't remember ever seeing any real data to back up the assertions.
I think the common software engineering myth about "real engineering" being somehow better and something one should aspire to is just really a myth.
If I were going to go learn some strange non-C-like-outer-worldly-syntax, I think your effort is best spent learning Rust, which is way faster, offers some strong failure guarantees, and fills a lot of use cases from embedded systems to WASM.
Not necesarrily disparaging for either lanfuage, as COBOL, boring as it is, has brought tremendous value in large scale software engineering.
There are of course big differences too, like i18n, variable width strings, ... But if you give COBOL a c-style syntax,some dynamic size data structures, and you squint a bit, you end up quite close to Go
> And Go hasn’t added any major features since Go 1 was released in 2012.
Isn't Go in the process of adding generics as we speak? https://blog.golang.org/why-generics
It is extremely natural to think in terms of state, so much so that almost all algorithms you'll find in computer science are commonly expressed in imperative terms.
The bad part about Go is that it's impossible to write type-safe immutable collections. Apart from that, I don't think it encourages mutable global state anymore than any other language with first-class mutation.
There's nothing stopping you from using Go's maps without mutation. You can go ahead and send copies of maps through channels, just like you would if they were really immutable. Sure, the interface is clunky, but so is every other interface in Go, so I don't see too much reason to complain here...
To be fair, Go authors are pretty smart having created something more usable than C while not even seriously looking at how things are done in other languages.
(FWIW, chief on my list of criteria is the "debug story": what kind of hell am I in when things inevitably go wrong?)
oh...
Go DOES have a virtual machine, thats what gives you all the lightweight threads and garbage collection. You may call it "runtime", but there is no difference between a heavyweight runtime or a lightweight VM.
And go DOES have exceptions, thats what panic does. The are just used less frequently than in other languages (e.g. Java, where even the happy path may contain exceptions, e.g. in Swing).
Go programs do not run on a VM. They run on a specific CPU architecture, and specific OS. You would need an actual VM to run the same program on something different. That is unlike Java which on its own is a VM to run programs on.
I would argue the other half of your point is nonsensical in the same matter. You just conflated two different definitions of exceptions, which are similar, and have intersections, but not identical.
There's a big difference. A runtime is just functions that get called, but a VM means that you have bytecode that it's interpreting instead of native code.
The VM bit doesn't make sense to me at all. By that logic any language which implements a GC or has a native support for threads constitutes a VM?
There's no difference between a runtime and a VM? As an embedded developer I would argue that there's quite a difference between the two.
edit: Clarification on error type being "always" returned from a function.
But Go's functions "recover()" and "panic(...)" are very much like "catch" and "throw" and are all about the callstack. There's just no "try", because this special snowflake implementation of exceptions works with slightly coarser granularity than is typical (function granularity instead of block granularity), since it's living under the same roof as the error type.
That analogy should go the way of most old bridges.
Declared functions in Go are immutables.