It's definitely more streamlined, but my concern would be that newcomers might not understand that it's just an alias. Learning Go really reframed my concept of what an interface is, and I thought that using an empty interface to represent "any type" was kind of ingenious, and helps reinforce its ethos.
An empty interface can represent any type because every type inherently implements an interface with no methods. And that's what Go is all about -- implicitly implementing interfaces.
If a newbie hops into Go and just starts using "any" I think they might assume it's a magic type that's at the base of everything, missing out on the fact that they're still taking advantage of interfaces.
Counterpoint: I'm an experienced Go dev but still think of "interface{}" as a semi-special "any" type, just because it fits my brain better conceptually. "Any type" is simpler than "an interface with an empty method set, which is of course matched by any type". I think this is a win.
Go is the most readable language I’ve used in terms of understanding other peoples complex code bases, and I’ve been doing this a long time and have used a lot of languages.
Edit: It’s also one of the most approachable languages.
Why don't you list the languages that you have used? Otherwise there isn't really any new information.
For example for, having done Java, Scala, Python, Groovy, Haskell, Typescript and a couple others, Go reads extremely horrible. It feels as bad as enterprisey Java to me.
It's far easier for you. It definitely isn't for me or in general. Thank you for answering the list of languages though. Makes me wonder what the concrete points are for, since I really have a different opinion. Maybe our minds just work very different; :)
From the list of languages you mentioned I understand that you probably enjoy ways to express algorithms in an elegant and (maybe) terse way. Maybe that's easier for you to understand. However all that abstraction is very far removed from what the compiler or the interpreter actually execute on the CPU.
For me an easy to understand program is one where the way memory is structured and the way execution modifies said memory is easy to follow. That's why Go, despite being very verbose in some cases, it's easier to reason about, than codebases doing similar things in higher level programming languages.
Just ran into a great example today. Was using typescript/javascript to hit an api that has a limit of 5 requests per second. One of these fails saying that it's making more than 5 requests per second, the other doesn't, is it obvious why? It took me several hours to figure it out, wouldn't have had this problem in go.
choices.forEach(async (ce) => {
let ce = choices[index]
Deno.sleepSync(220);
let cf = await getChoice(ce); // this makes the API call
Deno.sleepSync(220);
});
for (index = 0; index < choices.length; index++) {
Deno.sleepSync(220);
let cf = await getChoice(ce); // this makes the API call
Deno.sleepSync(220);
}
This is such a common use-case, it should really be in the streaming-library of choice. It's also a good example of how more abstract code is often better and has less edge-cases. In this example, of you have 4 choices, then these can send all at once without delay. This will be much faster compared to the code you posted, which will wait after each request, even though the rate-limit is not applied.
Apart from that, I don't think the second example is complete, where does ce come from here? And also, I don't know Deno, but calling "sleepSync" already looks like a bad idea to me, no matter where it's used - especially since calling a sync operation within an async doesn't make much sense.
I messed up my copy/paste, the 'let ce' line in the first example should be in the second example.
Regarding your example, where is this throttle method implemented in javascript? And, your code is not easier to reason about for me than the synchronous golang code. It's not clear how throttle is affecting the function call that happens before it. Compare to this.
for _, v := range choices {
getChoice(v)
time.Sleep(200)
}
I'm neither a javascript pro, nor a fan of this language. There are probably better solutions out there.
> And, your code is not easier to reason about for me than the synchronous golang code
I never said it is. If you would be familiar with only assembler, then assembly code would most easy to read. And you need to spend some time upfront to learn other languages/techniques that can make certain problems easier.
So here's the thing. Let's change the idea of this code a little bit and make it a challenge.
Let's say we want to improve the code:
1. Improve performance by sending requests as quickly as possible while respecting the rate limit. I.e. if we have 3 requests, send them all at once. If we have 7 requests, then they should all be sent after 1 second has finished.
2. If a request fails, we retry it up to 3 times and don't count it towards the api rate limit
3. If all retries for a request fail, we just skip it and continue with the rest.
I think this is a very practical real world example. I'm curious how elegant this can be solved in Go. I will also solve it and post my online-runnable solution afterwards. :-)
And then we reevaluate which solution is easier to read.
Here you go. Took me about 15 minutes to write, using < 50 lines of code, and not using any external libraries outside of the Go Project. The 15 minutes includes the time to create the server I used to test it.
Immediate question that I have (since I can't execute it easily):
What happens if we have 11 requests, each request takes 1 second to be responded to, but one of the first 5 requests fails 3 times and each time the reponse until failure takes 0.1 seconds.
Will all 10 successful requests be completed within 2 seconds then? Or will the failing request block the 11th requests for 0.3 seconds, meaning that all 10 successful requests will only be completed within 2.3 seconds?
I’m limiting it to 5 concurrent requests which is independent of the rate limit itself. The failing request will hold one of those 5 slots until all 3 failures are done, but won’t otherwise block anything.
13 lines and should behave like yours (i.e. is not optimal in the sense of fully utilizing the rate limit).
Curious to see your solution that fully utilizes the rate limit - it's not so trivial I think. :)
Anyways. I have to say that the Go-code is better than I expected (even for the suboptimal solution). But given my experience with higher abstractions (here streaming) I personally find the streaming solution much more readible.
Thanks, this was a fun experiment. I was curious to see what language you used. I've done a bit of scala many years ago, and I have to say that I HATED other peoples weird and often over complex scala code. I think this is the main difference in our point of view. I 100% agree that you can write better abstractions with a language like Scala, but you can also shoot yourself in the foot far far easier. If you have a small team of very talented devs, something like scala can be amazing. At a large company, with thousands of developers of varying levels, I find you get worse results with something like scala relative to Go.
I also think that a dev who just graduated college would be able to jump into my code and work on it fairly easily. I think they'd need a lot of extra training/learning to understand yours.
One question about yours, is it using a separate OS thread for each HTTP call, or some kind of async io?
> I also think that a dev who just graduated college would be able to jump into my code and work on it fairly easily. I think they'd need a lot of extra training/learning to understand yours.
this nails it. It's also what Go was designed for. People just shouldn't make the mistake to assume that this means would also be easier to understand a whole code base. It's not, it takes longer, since there will be so much more code. We pay the long upfront costs to learn our languages and tooling for the same reason we sent people to school for years: in the end it pays off.
> One question about yours, is it using a separate OS thread for each HTTP call, or some kind of async io?
Just like Go it uses "green threads" and will work and run requests in parallel even when just having one thread and/or cpu core.
Which means the solution isn't really optimal, since if every second request fails, the code won't take full advantage of the rate limit and only do 2.5 successful requests per second.
I think the requirement is a bit vague. I'm assuming an HTTP call, and that the rate limit exists on the server. Given this, there are two different error conditions: 1) the request makes it to the server, is counted against the server-side rate limit, but fails for some reason. 2) maybe the network is down, the server never sees the request, so the failure is not counted against the rate limit.
I opted to assume all failures are of the first type, which means my code should also behave as expected, but will be slower when encountering failures of the second time.
I'm actually curious, I think it would be easier to change my code to handle both types of errors, but I could be wrong since I don't know the scala stuff you are using that well.
Yeah, I guess I could have been a bit more clear. In the end it's fine, I just wanted to make it a bit more interesting and any additional constraint would have worked :)
The reason that the Scala code is so much shorter is because streams compose really well and a lot of common building blocks (such as the throttling) can be provided. This effect gets more important when the code base is growing.
Also, when things become more complicated, loops like the one you wrote tend to become very complicated. At least that has been my experience. It's fun to write it, but not so much to read or maintain it.
My experience has been the opposite. If I jump into a random Go code base, it’s been easier to figure out what’s going on than with languages that provide a lot more abstraction power. The reason, IMO, is that a bad abstraction is worse than less abstraction, and people are more likely to create bad abstractions than they are good ones.
I think it’s better with functional languages like scala and clojure, because bad class hierarchies with inheritance are the worst abstractions, but Go hits the right balance for me and I think for large organizations that have a lot of devs who move on and off of code bases on a regular basis.
I agree that you can get a better result with powerful languages, but that it’s unlikely in large orgs. I think software engineering at scale is more about sociology than people admit or realize.
I think I might agree when it comes to Java developers creating crazy class-hierarchies with no meaning. To me that is pretty much as bad as a desert of for-loops and low-level code, maybe even worse.
I'm praying for you that you at some point end up in a project with a bunch of good developers that work on a code base with a lot of good and high abstraction. The pleasure to improve your own skills in this area and feeling your productiviy rise is worth the pain of learning things to get there. It will be difficult to find this in bigger companies though.
> I think software engineering at scale is more about sociology than people admit or realize.
Yes, but it's only a matter of time. Software engineering is a growing and young field. There will be a time when we laugh about the bad code that was written, without standards and training. This time is not yet, but I'm looking forward to it. :)
Thanks for the great convo, really enjoyed it. I have been on a project with good devs, wrote a large scale web crawler in clojure for a famous research lab. These days I’m responsible for hiring/training/retaining hundreds of devs across many code bases, some dating back decades. It gives you a whole different perspective. Also, I don’t get to code all day anymore, but I still love to code, so squeeze it in on fun hobby projects. This also gives one a whole different perspective about what is important, and what is really worth keeping in one’s head.
I would probably require a book length post to truly answer this, but here's the best short version I can do. I think it comes down to a few things.
1. Some languages on your list are very flexible, and that allows them to be written in a very readable form with great discipline. People tend to not have great discipline thought, for various reasons: their bosses are pushing them to go faster and cut corners, they aren't experienced enough yet, etc. I'd say that python and typescript/javascript fall into this category.
2. I think inheritance is the devil, and a more functional style is better (by functional I mean first class functions, passing parameters, etc not purely typed). Java/Groovy fall into this category, they overuse inheritance and things like dependency injection with xml or annotations rather than far easier to read function calls with parameters.
3. Then there is haskell :) Haskell requires a phd in rocket science to be proficient, and most people have other things they want to do with their life. You also can't hire a team of 1000 rocket scientists so most companies can't go this route.
Edit: One other thing I'll add about typescript/javascipt because it's a bit of an oddball. There really isn't a typescript/javascript style because most devs end up writing javascript. The java people try to write java in it, the python people python, etc. This alone makes it hard to jump into a random JS/TS codebase and figure out what's going on.
Regarding Go, it takes away a lot of flexibility, which makes the code longer generally, but it makes it much more consistent across code bases. It's lack of inheritance prevents the devil from showing up, and it's largely a simple function call with parameter passing style all the way through the code base. Structural typing is also a godsend.
Edit 2: Also, package management in python is a complete train wreck.
Go is ok-ish, if you use small programs and solve exactly the same problems the language designers had in mind in exactly the same ways the language designers had in mind.
The constant repetition and near-but-not-quite copy-paste boilerplate everywhere makes it hard to spot the crux of what's actually going on.
I've used all of those and more and spent a decade deep in the purely functional Scala/Haskell camp. Diving into a moderate or large Go code base is always far easier for me even though I've used Go much less than the others. Before I even knew Go I would often look at various algorithms in Go just because it was so easy to understand exactly what was happening.
For algorithms, this might indeed be different. Not only are algorithms usually small and very focussed snippets, the emphasis is on performance and hence mutability, language primitives and shortcuts are common.
I can definitely see that languages like python or go beat pure functional languages in that regard.
For business logic and glue code (which in my field is the vast amount of code) I think it is the opposite though.
I agree with the parent though, Go code is super-readable and accessible. I've coded actual useful stuff in Java, C, C++, Pascal, Python, PHP, Pascal, Basic, (Jenkins) Groovy, Typescript, Javascript - and am probably forgetting some.
I also dabbled around in Rust and a bunch of other languages, but I wouldn't call that experience, although when it comes down to the initial accessibility and impressions, in this context, that is relevant I think. Go was hands down the most readable and accessible of the bunch, which probably has some roots in having a C/C++ background, and it being a pretty simple language.
Dev-env wise, it used to be a mess with the GOPATH etc, but that has mostly been resolved. And once past that, it was easily the language I picked up the quickest. It took me 2 or 3 days to actually write something useful. I've jumped head-first into codebases of large projects without even thinking about it, which I would have been very hesitant about if they had been written in another language. I've had to do that plenty for C++ and Java projects, but that always took some convincing of myself, and was never a pleasant experience.
The readability of Go is intrinsically linked to the error handling in Go. If the spec started accepting "clever" rules to do with auto-magic return / assignment of error values instead of explicit handling, then the cognitive load of code review increases. If every error path is explicit, then the review becomes simple and self-explanatory (at least when it comes to error handling).
I'm not sure error handling in Go is really explicit. Go checks that you assign an error and usually handle it, but it doesn't check that you handle all values of the error. If a function suddenly returns a new error value, the compiler won't help you here. This is like in language with unchecked exceptions, you have to read everything carefully.
By that logic, assembly would be the most simple and self-explanatory language. I'm not sure if these two attributes are sufficient to determine how readable/productive a language is in the large.
That combination of trade-offs makes it a great language to solve interview style problems in on a white board.
You can alleviate those larger scale problems in Python a bit with good IDE support, embracing type annotations, and going with a style that prefers immutability over action-at-a-distance.
I agree, and I think this mistake is at the root of most of what are (at least to my biased point of view) the big mistakes made by go.
Syntactical niceties and simplicity / obviousness are sometimes a tradeoff, but go 1.0 felt like the designers just folded their arms and refused to consider any niceness.
I still don't understand why anyone would use go when there are so many better choices, but at least they're fixing some stuff now :)
> I thought that using an empty interface to represent "any type" was kind of ingenious
It makes me a bit sad to read that. We all are on a journey to be become better developers every day. But things like that are like a distraction. They make you think you found a really cool and smart concept, but you actually didn't and it's essentially just a hack.
Not sure what the solution can be. But PL designers should be more clear about features that are just "hacks" and considered "bad" but necessary in practice due to certain constraints. Go's {} and nil-errorhandling are examples. Nulls (in any language) are another common example.
It's not a hack, it's the natural way of expressing it in the chosen type system. The use cases for interface{} are often hacks, because of a lack of parametric polymorphism, but that's different.
I kind of understand what you mean, as it's similar to what happened with prototypes and the late-comer class syntax in JavaScript, and in that case, it was a net positive change as most would probably say.
I'm an experienced programmer whos just learning go now and I never made the connection that interface{} meant 'an interface with no methods'. I've always thought 'interface{}' was the clearest wart on the language. Languages shouldn't be about teaching me things, they are just a tool. Keyboard input goes in, computer instructions come out. any is easier to read, more intuitive in its meaning and takes less keyboard input.
> Learning Go really reframed my concept of what an interface is, and I thought that using an empty interface to represent "any type" was kind of ingenious
There's a few other languages out there that work that way. The split is probably is probably 20/80. Though "popular" languages heavily learn towards Java-esque interfaces - with TypeScript and Go being the odd ones in that bunch.
An empty interface can represent any type because every type inherently implements an interface with no methods. And that's what Go is all about -- implicitly implementing interfaces.
If a newbie hops into Go and just starts using "any" I think they might assume it's a magic type that's at the base of everything, missing out on the fact that they're still taking advantage of interfaces.