Both languages don't do much about Tony Hoare's Billion Dollar Mistake, and to me that's an immediate black mark on type safety, though they fail in this regard in distinct ways.
In Java all of your user defined types are, alas, represented only referentially and a null reference is always possible, so even if there is no such thing as an invalid Goose, your variable of type "Goose" may just be null anyway.
In Go, it's possible to actually make a Goose and have it as just a local variable living on your stack, but, the language insists that anybody can conjure one into existence despite having no basis for doing so, the resulting Goose is at best now in some invalid "null" state.
To me, avoiding the Billion Dollar Mistake in new software is table stakes. Even at work, in the relatively boring C# language, we can avert the Billion Dollar Mistake in new codebases and gradually wean old ones off this idea. The CLR is hostile to properly fixing the mistake eco-system wide, but this is a marked improvement.
> Both languages don't do much about Tony Hoare's Billion Dollar Mistake, and to me that's an immediate black mark on type safety, though they fail in this regard in distinct ways.
Java is well on its way to alleviating the problem. [1]
I'm not completely sure what this example is trying to illustrate. Most languages with proper sum types would have `Map<K, V>.get` return an `Optional<V>`. If V happens to be `Optional<Integer>`, then the result will be an `Optional<Optional<Integer>>`, which is a perfectly good type that represents the two ways a value may be missing from the map.
> the language insists that anybody can conjure one into existence despite having no basis for doing so
You can have a goose (unepxorted) that is created via NewGoose() (exported - really a constructor). Of course within a package there is nothing to stop you doing the wrong thing. I really like Go. But I do find myself occasionally writing overly verbose types just to avoid this issue. Which, if I’m honest with myself, is a bit
silly.
There’s a good counter argument to using to unepxorted types like this below.
> In Go, it's possible to actually make a Goose and have it as just a local variable living on your stack, but, the language insists that anybody can conjure one into existence despite having no basis for doing so, the resulting Goose is at best now in some invalid "null" state.
If you really don't want people instantiating instances of a type on their own, can't you just make the type private but make a factory for the type public?
"If you really don't want people instantiating instances of a type on their own, can't you just make the type private but make a factory for the type public?"
"Yes", but really, no. It is syntactically valid to write a function that returns an unexported type, and you can technically get an instance of that unexported type in your other package by using := to assign it to a variable. However, you can not name the type in the other packages at all. So, you can not say "var x mypkg.unexportedType", even if you can "x := mypkg.ReturnUnexportedType()". You can not put it in a struct/channel/slice/map/whatever, since you can't name it and there's no equivalent of := for types. You can't refactor the function that contains the "x" in any way that involves passing it to a new function because the type signature for the new function would have to mention the unexported type, which it can't do, and there is no inference for function types that would let you elide it. Reflect restrictions on unexported types are still in effect so even trying to put it in an "any" isn't really all that useful.
Basically, it's useless, even though it's just barely sort of valid, because it's much more than just creation that is blocked. In practice you have to export types you want users to be able to assign to, put in structs, etc., and if they are exported, their zero values can be created by any package that imports them. One of the standard linters in golangci-lint will warn you if you accidentally write an unexported type into an exported type or value, because you didn't mean that, even if you thought you did.
You can write an interface that you export, and then return an unexported data type that conforms to that interface. However, you then can't prevent someone from having a nil instance of that interface, so in terms of preventing invalid data, this doesn't really do anything useful. This is more useful for manipulating package documentation by not exporting types unnecessarily and having to write docs for them (or, if you don't write docs, having them appear in the godoc) then as access control.
I wouldn't put that much drama into nulls, these are mostly academic discussions. In real world its much less drama in well maintained codebases, once you learn working with them you don't even actively think about it. In the meantime, basically all companies run on it, and will do so for next 50 years minimum, plus you have what... 17 million devs working with it?
Defensive programming is generally a good approach regardless of domain or language used, and if used well you don't have to worry about nulls in Java, at least not in your own code.
Every thread about null has someone come in and make this claim that it doesn't come up in production in well-maintained codebases, but it feels very No True Scotsman: every counterexample that someone can point to immediately doesn't count, because the definition of a "well-maintained codebase" seems to be one in which null pointers aren't an issue.
I guess that's a fine definition in theory, but every codebase that I've ever worked in that was written in a language with null references has had null pointer exceptions as one of the top recurring bugs. Maybe that means all my codebases have been poorly maintained, but if so that's a very good reason to design languages that require less maintenance.
I worked at a Java shop for a while, with a small and new-ish codebase built by experienced devs, and most of the errors we encountered were null pointer exceptions. It was always just, "alright where do we need to add another if-statement this time"
Of course you could be "defensive" by adding an if-statement every single time you use any object. But that would mean an order of magnitude more lines of code, and even then it would be easy to miss cases
This is the “as long as programmers are perfect and remember to always do the right thing everywhere, everything will be great” argument.
Developers are not perfect and the footguns in languages have a cumulative effect. Any one thing isn’t necessarily a big deal but when you have multiple interacting footguns (always check for nil, nil interfaces don’t actually equal nil, default zero values need to be sane even when they make no sense, etc.) both the surface area for errors and the level of care needed to avoid them increases dramatically.
Of course it's physically possible for a software project to exist where all contributors flawlessly handle nullability.
The point is rather that, because this can be automated, it ought to be automated.
The same could be said of automated tests ("in well-maintained codebases contributors manually run through a large suite of tests to ensure there are no regressions.")
Do you have null dereference bugs that you occasionally solve, even if only during development? The "billion dollar mistake" is a reference to the economic cost of all bugs it created as a concept. That doesn't mean you don't build techniques to work around it, but it does mean that that's the economic efficiency left on the floor.
In other words, the cognitive overhead you spend on worrying about nulls (however you do that), programmers in other languages spend on other things.
The reason Hoare used the word billion was to emphasize how costly it was compared to other things. That was certainly true in C code 40 years ago and more. Those bugs were hard to find and hard to fix. In java you get a full stack trace, it's quick to fix and it doesn't come up often. Like the OP said - In practice it's not really an issue. "Worrying about nulls" is not something that most programmers do.
As a Java/Kotlin dev now writing Go for a living I agree with quite a bit in here and actually think both languages have things they could learn from the other.
Having said that I cannot understand where the idea that "the [Go] libraries and ecosystem are so much better" comes from. The average quality of both the JDK libs and the popular 3p libs (Jackson, Jooq, even ...Spring) are one of the things I miss most about writing for the JVM and I'm yet to come across another ecosystem that comes close.
I'm increasingly bullish on the JVM ecosystem. The runtime is amazing, performance-wise, and the third-party libraries are really good, if hard to get into. With Kotlin you can get null safety, and IntelliJ is the best code authoring experience for me.
Java has some incredible libraries with impressive performance characteristics. The ecosystem is also full of strange/unnecessary build time magic, obnoxiously verbose patterns, and horrible error handling.
I mean I think there is a philosophical divide. Go is very minimalist in terms of external dependencies. Those that are their are very good in my experience. The builtins are just the right amount of opinionated imo. I concede that that statement was grounded in my preference for Go's take on external dependencies.
I also generally am writing REST APIs and doing simple "enterprisey" stuff.
Well, that was a conscious choice of the JDK devs when they went down the path of removing things.
Raw Java is Pretty Close to being "out of the box" OK for "REST, enterprisey" stuff.
The built in HTTP server is usable and functional. Of course TLS is built in. JDBC is built in (need a driver). While XML is built in, JSON is not. Logging is built in. JMX for monitoring is built in (lots of things can talk to JMX).
Are these all "top tier" feature rich implementations? No. But they're completely usable. With some thin veneers you can make them richer and more friendly. I've been using my own trivial Logger wrapper for years (mostly to support varargs).
If you're willing to take a bit of a step, JAX-RS on Java SE works. That "single" dependency just knocks it out of the park in terms of "enterprisey" REST services.
No container, no "micro profile", just JAX-RS (but you still need a JSON library). JAX-RS really elevates the game. It'll even run on the stock HTTP server.
Add "just one more" with JPA, and you get not just the whole ORM, you also get a "free" database connection pool (otherwise, an OTS connection pool would be a nice addition).
All bottled up into a simple deployable jar, no dependencies outside of a compatible JDK. No dockers, no containers, just a jar and a JDK, a JDK that can be installed anywhere (just set JAVA_HOME and put the .../bin on your path). Drag and drop. systemd fixed the need for crafty service scripts -- it can just run the jar.
JAX-RS Jersey, JPA EclipseLink, Jackson (for JSON), and the JDK is just crazy capable tool set. You can also do MVC web stuff with Jersey. Jersey comes with Validation as well. The HK2 runtime will let you do your own injection if that's your thing (not as nice as CDI, but it's "free" with Jersey).
But, alone, (plus Jackson), the JDK will let you do anything you want, just need to write some routing logic.
Go is too minimalist in my opinion. It’s like time warping back to 2005. You want enums, nope. You want functional constructs (map, filter), nope. You want string array contains, nope. It can feel very limiting coming from newer languages designed in the last 10 years.
Strong, strong agree on this. That plus the verbosity really adds to the activation energy of getting things done. I reckon I end up writing (and having to read) 5x the lines of code for the same result that then ends up being less type safe and lacking null safety. I guess that's the price you pay for simplicity.
Functional programming is not "newer", it is as old as Lisp, which dates back to the 60s. The people who made Go simply decided that the language did not need those features.
So instead we have 10 different libraries and 100 different custom implementations to do those things in Go. Compared to almost every other major language where you can do functional stuff (and everything else the Go developers decided we didn't need) natively:
Java: Arrays.stream(nums).map(n -> n * n).toArray();
Kotlin: nums.map { it * it }
Python: list(map(lambda x: x*2, nums))
C#: nums.Select(n => n * n).ToArray();
Ruby: nums.map { |n| n*2 }
Rust: nums.iter().map(|&n| n * n).collect();
Perl: map { $_ * $_ } @nums;
Rob Pike has talked before[1] on this exact subject, about how all languages are merging into one giant PL-theory sludge where every camp is adding and stealing features from one another, and that one of purposes of Go was to avoid following in this same direction.
You could just use a for-statement, you know. I feel like there's a reactionary avoidance to iteration to the point that it doesn't really make sense.
I feel like we've jumped out of the frying pan of the 90s' OOP craze, directly into the fire of doing the same cargo cult behavior with functional programming. Always avoid loops, always use higher-order functions. It just makes sense. If you are not provided with the shotgun spread of higher-order functions out of the box, be frustrated.
I vaguely recall reactions to Haskell in the late 90s where vitriol was spewed over the lack of inheritance and classes.
Kotlin feels like the Go 2.0 that the author is looking for in many ways. It is interesting that Kotlin hasn't fully taken off in the way you might expect.
Reflecting on my own reasons : it comes down to that I'm not particularly fond of IntelliJ and even less fond in principle of being critically dependent on a language that is only so controlled by a single vendor, and has poor IDE support outside that vendor's IDE. If I loved IntelliJ I might not care.
However I also find that there is still impedance mismatch with Java and it's just enough that I always just prefer to accept the warts of Java rather than use a language that is less known and supported.
> and has poor IDE support outside that vendor's IDE
Ironically, the IntelliJ support for Kotlin is also kind of weak.
---
For the main point, Kotlin feels like something that would be anathema to a large group of Go devs who favor a small language. It's precisely the limits that viewpoint imposes that has meant Go is not my favorite language, but its clear that group is a big driver of Go's userbase.
Yes exactly - somehow internally it grates against my instinct that simpler is always better to have a bolt-on language that layers all its own classes and APIs on top of a host language. I feel like that just can't be better in the long run and it if it is better now it can only be a temporary state of affairs.
But then, I also like Groovy which is kind of the ultimate version of a bolt on language, so maybe I am just ex post facto rationalising my internal preferences.
Java matches my "taste" much more than Go does. I appreciate the more powerful type system. I also like the verbosity because it's always obvious what everything is - as it's written out in full. Go's compiler can infer types in lots of places, which is cool, but I appreciate being able to see the types when reading code & refactoring Java. People complain about Java's exceptions, but you can very easily create a Result<Error, Value> class for your own code, whereas it's much harder to emulate exceptions in Go. As for comparing the different flavors of the billion dollar mistake: I also found `null` far easier to understand and not footgun myself than Go's zero values & nil.
Kotlin (from the perspective of this blog post) stands above both Java and Go here because it's type system is even more expressive, it has null safety and it's less verbose than both.
I love Kotlin because it's unapologetically pragmatic, it largely forgoes ideology and instead focuses on getting things done. Go and Java have ideologies and while I am more partial to the Java ideology over the Go one I would prefer to not have to subscribe to either.
> Java is very “simple” precisely due to its verbosity.
The author and I clearly have different definitions of simple. Indeed, I believe that the syntax bloat of Java, which is not a source of simplicity, is a significant source of complexity and causes overengineered frameworks to proliferate in the ecosystem to make up for the language defects. Moreover, Java is full of features and keeps adding new ones every six months. Every new feature is more complexity, possibly added for a good reason, but nevertheless increasing the complexity of the language.
Go feels like a giant hack and it's opinionated approach to modules and libraries and where source files can live and embedding git urls in imports..... :-( Little bits of it are joyous - like goroutines and channels..and binaries that run on almost any distro without one needing to install something else first. I don't need to use them often enough though. At certain points you have to do things that feel ugly.
Java is just the perfect home for type enthusiasts. You sit down to prototype some idea and you need to debate what kind of class structure it's going to need for the first 25 minutes - something really abstract or something more short term...are you really going to develop this idea into something big with all sorts of generic high level classes or are you going to be cheap and short about it? Are cats and dogs the same from the point of view of this system or not?
In both of them people love writing the kind of code that jumps all over the place and injects all sorts of dependencies and just looking at it, it's almost impossible to understand what anything is doing. There's no such thing as "TOO SOLID"
I admit I have a very jaded view. C/C++ have the freedom to put your source code wherever you like, compile it in parallel on a cluster or not and so on but then they are cursed to hell with the C preprocessor. Perhaps Turbo Pascal is nice?
"Write once, run anywhere" specifically meant that the execution semantics and the interface to the environment (OS, storage, GUI, etc.) of a Java program are the same everywhere. Relating that to "simple, secure, stable" seems like a stretch -- of course there's a little overlap in practice, but those are really four separate dimensions. Java has had its own long-running battles about simple, secure, and stable.
Of course, in reality the isolation isn't perfect, which is how we got things like deterministic (slow) floating point and "write once, test everywhere".
Go does have a similar attitude, practically speaking, if you stick to the standard library. It's more like "cross-compile and run wherever".
I really like Dart, and it's clearly far superior to its competitors in the "old imperative" space, like Python, Javascript (though Typescript obviously has fancier types), Java, and even Go IMO.
The frustrating thing is that it's so close to the functional features that Rust embraced (mainly that everything is an expression), but it just brushes against it. Like, with Python and Javascript, you know they're shit. Nobody expects to be able to write a switch that evaluates to a lambda or whatever in Python. But Dart is just so close to greatness but just misses!
They even added switch expressions recently, but with a different syntax to switch statements! So close!
Still, overall it's a great language and really deserves more market share. In particular the dev tooling is the best of any language I've used. The LSP server is instantaneous. Nothing else comes close.
If the future is a mix between the two, I would prefer starting from Go and adding ideas from Java, than the other way around. Go feels lighter, simpler, more pleasant to work with. But Java has had a lot of good ideas added over time that would be nice to see in Go.
I sometimes like to compare programming languages to vehicles. Java is perhaps the long haul truck and Go the smaller and more agile delivery van. The smaller van is more approriate for the things I work with for my own pleasure, but the truck has its places for the bigger enterprise jobs.
In Java all of your user defined types are, alas, represented only referentially and a null reference is always possible, so even if there is no such thing as an invalid Goose, your variable of type "Goose" may just be null anyway.
In Go, it's possible to actually make a Goose and have it as just a local variable living on your stack, but, the language insists that anybody can conjure one into existence despite having no basis for doing so, the resulting Goose is at best now in some invalid "null" state.
To me, avoiding the Billion Dollar Mistake in new software is table stakes. Even at work, in the relatively boring C# language, we can avert the Billion Dollar Mistake in new codebases and gradually wean old ones off this idea. The CLR is hostile to properly fixing the mistake eco-system wide, but this is a marked improvement.
reply