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.
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.
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.
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!
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 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.
}
s := DeserializeState([]byte(`{"data": {"key": "value"}}`)
fmt.Println(s) // prints 0 oopsThen why does it compile?
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.
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.
TP executables were fast. Compared favorably to C++. The compile-time was also very fast.
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...
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.
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.
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.
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.
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.
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.
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".
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.
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.
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.
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.
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.
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.
Also, 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.
"Featherweight Go"
https://arxiv.org/abs/2005.11710
-- Proc. ACM Program. Lang., Vol. 1, No. OOPSLA
It appears Go needs PhDs after all.
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.
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.
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.
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.
- 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?
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.
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...
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.
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.
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...
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++.
*sync.Pool, arena allocation strategies
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.
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.
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.
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? :-)
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.
> 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.
All were able to AOT compile to pure sugar free native code, with various forms of GC support.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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...
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.
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.