Adding an element to a nil slice works, but adding an element to a nil map panics. time.Time uses its own epoch incompatible with anything else. defers are function-scoped instead of block-scoped. The list goes on, these are just the first ones that came to my mind. And they are not some magical tradeoffs, they are just poor choices. I like go, but no, it is not well-designed
> defers are function-scoped instead of block-scoped.
I think this is a basic tradeoff with defer and not a problem per se. Are users more likely to need to clean up resources every time through a loop, or more likely to acquire resources that outlive the loop? And which is more confusing if you expected the other? I think it's a tough call. (I understand why a systems programming language would insist on block scoping to avoid allocating.)
The appeal of Go is that it has just the bare minimum feature set to be useful for modern backend engineering. It's a language that you can completely fit within your head within a few months of using it, as opposed to certain languages like C++ (or say C# to a lesser but increasing extent) that most will never know completely even after decades of working with it. It's basically the RISC of programming languages.
And if I had to guess, it doesn't have enums so that it can remain flexible when serializing/deserializing enum-like types over the wire. Imagine you can't parse an incoming payload containing an enum field because your service is one version older than the one that extended the enum type (or the enum type is defined in a package dep... you get the idea). Enums are actually a terrible idea now that I think about it.
> The appeal of Go is that it has just the bare minimum feature set to be useful for modern backend engineering. It's a language that you can completely fit within your head within a few months of
To me, golang symbolizes the shift of philosophy of Google as a company. It changed from "it's a smart nerd company for smart nerd people" to "golang will allow us to hire cheaper devs because golang will prevent them from making mistakes". I mean, this makes sense from business perspective, I won't deny this fact, but it's the programming equivalent of Ferrari making a SUV: tremendously profitable, but sad to see.
BTW
> golang doesn't support overloading because overloading is bad. Having said that, it's 'go', not 'golang', like the verb 'go', which already has a thousand meanings depending on the context
The law of large numbers says that, the larger the organization, the closer to average the employees will be. Google had no chance of maintaining "a smart nerd company for smart nerd people" at the scale it grew to be.
You can point out flaws so it's not well designed? By this logic nothing is.
And pointing out missing features in a programming language is just about the weakest criticism possible.
Piling on lots of features is easy and fun. That's why almost all language designers do it. Most popular languages destroy themselves with features.
Rust is in the process of gaining every single feature anyone can imagine. Following C++ right off the complexity cliff.
Go is one of the very few languages to show incredible restraint in adding features because its designers understood the combinatorial complexity problem, among other things.
Not having unnecessary features is one of the best features of Go.
The issue with Enums is that the "unnecessary" feature means that people have to encode the behaviour with things that aren't suitable. E.g. creating a new type that's a thin wrapper over string or integer means it can be set as some non-valid value, even without some sort of explicit cast that says "I know I'm doing something that might fail". I don't see that that is an improvement over any other language. It's not simpler because, as people have shown by the examples trying to prove that you don't need it, they don't have all the (necessary) behaviour that enums do to prevent this sort of problem.
I see this suggested all the time as though it’s supposed to be a replacement. It isn’t sufficient, it’s a poor emulation of what enums can be used for and doesn’t offer the same protections, since Status can be set to any integer value without casting or any knowledge that it’s broken.
In your suggested method it doesn’t even catch that case and provide an error if the value isn’t in the range.
func main() { var s Status = Cancelled fmt.Println(s) // Output: ??
If the compiler - not some linter - protests that the list of names is different from the number of items in the enum, then I think this is at least a half-decent design of an enum type. Not a great one (because the author still had to repeat the names), but at least something that isn't a fundamentally broken design of an enum type.
But if the compiler is silent, and the output of the Println(Cancelled) is "Failed" then I'm not angry, I'm disappointed.
I got caught with maps initially, and had to question why everything else was just "var x string", which also gives it a value, but maps needed a "var x = make(make[string]string)", otherwise on first assignment it panics.
I don't think that's weird. You can only append to a slice using the `append` built-in function. `append` may initialize and return a new slice at any call. It would be weird for it to not do so when passed a nil slice.
Adding elements to a map allocates memory and moves stuff all the time under the hood, it's no different from append. If panicking on nil is weird for append, it's just as weird for maps
append returns a new pointer in case the slice moved, map assignment does not return a new pointer. There’s no way for the operator to change the reference you hold from a nil to a pointer to the newly allocated value. When memory is allocated under the hood on assignment to a non-nil map, your reference to the object remains the same, but some internal pointers change.
You can write a func Set[k, v](map map[k]v, k k, v v) map[k]v yourself that returns a map reference if you want this style of “maybe return a new pointer”.
It means that zero value time.Time when converted to unix time gives you a negative number corresponding to a date two thousand years ago. With the way go insists on using zero values for expressing optionality this is a rather common mistake. Interacting with anything outside the go ecosystem is done in terms of unix time and this makes it very error-prone. And I've seen this trip people off in real life. And yes, you can argue that this is programmer's fault, but this is exactly what makes it a bad design: it makes people's lives more difficult for no real purpose, just because somebody at Google wanted to feel smart that day.
i think he means that internally, time objects simply use unix nano and the location dictates the offset. since it is internal thing, not sure why that is relevant.
Seems many people mistake “well-designed” to mean “perfect”. Read the article: he doesn’t claim that. No language is perfect, they are all trade offs.
Furthermore, it’s fine to not like the design. It might not be to your taste or your requirements. But that doesn’t mean it wasn’t well designed. I can point to lots of things that I don’t like the design of that work great for others.
Go is super productive, has a great ecosystem, has great concurrency, produces good machine code, compiles as fast as anything out there, has great tools, and is growing in popularity. Flaws? Of course. Perfect? No.
I feel like Go falls much into the latter category.
I needed to write some code recently that did DSP. There is not any equivalent for this like SciPy in Go that bundles together lots of the common DSP operations you need. There are lots of partial implementations in different libraries of for e.g. filters, all of which use different interfaces.
A lot of this I think is because of the lack of SIMD, you can't get brilliant performance out of the box with a Go implementation because the standard compiler doesn't support it.
> No language is perfect, they are all trade offs.
Uh, no they're actually not. A tradeoff means you accept some downside and get some upside in return.
Initially go was designed without generics. The tradeoff here was you got a single pass compiler which was faster. Well, actually, you could have a single-pass compiler with backpatching, but I get it, that's hard. It's a crap tradeoff, but okay, I can accept it's a tradeoff. And everybody at the time said, "Go doesn't need generics, they complicate the language." So we casted our nested types in and out of object and lost all type safety in that code, but hey, tradeoffs.
Except everyone kinda realized it was a crap tradeoff and so `go generate` was proposed as a solution. In a 1984-esque revision of the past, suddenly, having a second pass is okay! Never mind how important it was that go is a single-pass compiler. Nevermind that this is basically macros with all their pitfalls. But now we can generate 3 different versions of the parameterized type, one for each nested type, so we get type safety. 2 passes, macros, downsides, type safety, upside, and a pretty big upside at that.
But, here's the problem: now you have two different systems that are used for doing a lot of the same things that don't interoperate with each other very well. And that's not a tradeoff, because there's no upside. It's not "I don't like this but it works great for others" because it doesn't work great for anyone. It's not a tradeoff, it's a side effect of having chosen wrong the first time.
And... now Go has generics. And yeah, generics aren't really a tradeoff, they're just obviously pretty good. But, now we've got 3 different systems for doing a lot of the same things, which again, don't interoperate all that well with each other.
And here's the thing: from a language design perspective, this was extremely obvious that Go was going to need generics. Generics are pretty much the clearest win from the ML-family languages which had already made it into C# and Java by the time Go was conceived. It's absurd that this feature wasn't included in Go from the beginning.
> Uh, no they're actually not. A tradeoff means you accept some downside and get some upside in return.
Yes, there is always a trade off. For everything, everywhere, all the time. You might not like the decision but trading flexibility for simplicity was a win for people learning the language. A lot of good software was built without generics.
All of them have significant design flaws and quirks, many of which people here write pages of complaints about. But in most cases, it's easy to learn how to avoid them (granted, that's more of a stretch for C, but plenty of people manage, and that's enough).
I'm not against pointing out issues, especially for actively developed languages like Go. However, it's important not to turn criticism into bashing the language or the people using it. That just comes off as snobbish and isn't constructive.
Each of these languages is well-designed for specific needs, and not so much for others. And that's okay.
I think people mostly try to scare others away from using these technologies to push something they believe is better. But instead of that, you could perhaps write a competing article praising and comparing your favorite stack and that would be much more useful, in my opinion.
I wish more people thought this way. Why does the language you choose have to be your religion and lifestyle, when it’s just a tool? They all have flaws and areas they are useful in.
For many folks, because it means which kind of job they are able to land, with which people they hang around at least 8h a day, what they talk about, to which conferences they go if any, and so on.
People do this with all sorts of things, but ultimately want validation that they made the “right” choice. At least that’s the most charitable assumption I can make.
It's one thing to say these are decent tools, but to say something is well designed is a much more specific claim.
I have some fondness for C, but I would not say it is well designed compared to modern alternatives. The reason I use it is largely portability which is more a function of its age and ubiquity than its design. You can of course make the argument that it was well-designed compared to the languages of the time when it was designed, and I don't really know enough to disagree with you there.
TypeScript, I will say, I think is actually very well-designed. It has it's problems but it's a massive step forward from anything that existed before it.
> I think people mostly try to scare others away from using these technologies to push something they believe is better. But instead of that, you could perhaps write a competing article praising and comparing your favorite stack and that would be much more useful, in my opinion.
Strong disagree. Polyanna programming where we pretend all the tools in the world are so great results in horrible codebases, and then the programmer who thinks every tool is awesome becomes frustrated, doesn't know why, and moves on blissfully to a new project/company where they begin to plant the seeds of chaos anew. But someone has to deal with that crap codebase they left behind, and at this point in my career that person is often me. Work on codebases for more than a few years and the only way to stay sane is to recognize problems in code so that they can be fixed before they become bigger problems, and doing that requires you to be critical.
Working on a team, the biggest uphill battle is often preventing people from inserting every random tool they get excited about. Usually it's libraries, not languages, that are the problem, but I've seen both.
I used to love Go and its simplicity. It's still my least disliked backend language. But after more than a decade of using it nearly full time (but not exclusively), I've fallen out of love with it.
I want sum types. I want non-nullable types. I want enums (preferably, based on sum types).
I'm okay with the explicit error handling, but would be fine with Rust style error handling too.
Having these features would change the language so much that you'd probably want to re-implement the stdlib, so it probably won't happen.
Surprisingly though, while I absolutely love Go's concurrency model, being able to kick off new goroutines with a dedicated "go" keyword and special channel operators have not been a big deal at all. It's usually abstracted away by some library, or you set it up once in your application and never touch it again.
> But after more than a decade of using it nearly full time (but not exclusively), I've fallen out of love with it.
I believe this is true for anyone and any language. Spend a few years in a language on stuff that you have to do (which is the main differentiator between work and hobby projects) and two things happen: the benefits are taken for granted while the warts are magnified fivefold.
The corollary to this is the quote "I won't use something until you tell me why it sucks" (can't find source). If you use something long enough you know the ways it sucks. This makes your criticism more valuable than enthusiastic praise from a hobbyist.
In my opinion your comments on Go concurrency misses a point, the fact that you do it once "and never touch it again" is a very very strong selling point, it probably means it was very well designed; not a weakness but a strength.
I didn't mean it's a weakness. I mean it's not as amazing of a selling point as it was made out to be when the language first came out.
If you're using something only rarely and not touching it again, there isn't much of a difference between go.New(func(){}) from some "go" package, and go func(){}.
To reiterate, I'm not against the "go" keyword. It's whatever ¯\_(ツ)_/¯
I’m not a coder day-to-day (I was in the past, mainly C#) but I still write toy stuff to make my life easier, and the stuff I write in Go I’m more likely to be able to return to a year later and be able to quickly understand what I was doing, why I was doing it and fix/enhance what I need to.
The readability of Go is why I defend it all day every day.
The fact that a programmer with no Go experience can look at a codebase and quickly get a sense of what's going on is a massive advantage for large teams, code handoffs or long term maintainability.
Go is nobody's favorite language but that's why it's my favorite language.
100% this one of the big reasons I fell in love with go. Single executable, no runtime stuff to install and crazy portable. I can look at anyone's go code and very quickly get up to speed. I don't worry about how they format it. Patterns are pretty universal which makes it easier. Does it have its warts? Oh, you better believe it but I know that any code base I go through will pretty much have the same warts the same way. Does go fit every need? Nope on that front too but man its pretty good and what it was designed to do.
As it should be! Shame they didn't make the error type a sum type that forced you to check it isn't an error. I prefer that to exceptions. But the product type that Go uses is ok.
Go is an OK language, and that's the point. An average programmer can write web services in Go without too much trouble.
Goroutines mean the programmer doesn't have to struggle to avoid blocking, or decide what should be "sync" and what should be "async".
Garbage collection means the programmer doesn't have to think much about ownership.
The error handling is a bit verbose. If Go had generics from the beginning, it could have benefited from something like Rust's Result type and "?" operator.
That's effectively the same thing as Go's usual error handling, but more concise.
The criticism that Go is for "average programmers" almost universally comes from people overestimating how good they are.
I've worked directly with hundreds of highly paid programmers over decades. I can count on my fingers the number that would be at all constrained by using Go where it's a suitable choice (an important caveat).
The rest would have their work highly improved by using Go as much as possible, precisely because it's harder to write bad code in Go compared with most languages.
To be fair, Go almost go the "?" operator like Rust. The proposal was well received. It wasn't the lack of "Result" that held it back, it was that nobody could figure out how to deal with the handling problem. It's not entirely clear what the Rust-equivalent is for all the other moving pieces associated with "?" (e.g. its defined traits). The latest "?" proposal forces a handling body on each use to address that issue. But at that point all you've done is added another way to write "if". Is that a win?
> If you want to add two days you can't just call Add(0 /years/, 0 /months/, 2 /days/), you need to use AddDate.
Sorry for nit-picking, but 2 days is a duration of time, and you can add one using Add, like this:
t.Add(time.Hour * 24 * 2)
I agree about FFI being a weak point in go when you need to interact with other languages, but it can hopefully be mitigated by using tinygo, which is still being developed.
What “sorta just happened” is Swift 6. I knew Swift, I loved Swift. Swift designed itself right away from what I was willing to put up with anymore.
Go in comparison seems unnecessarily pedantic, occasionally too verbose, and occasionally unnecessarily terse. But it never makes me lolsob minus the lol
This feels like SAT/ACT reading comprehension question! I think that "But it never makes me lolsob minus the lol" means he meant he doesn't like the changes Swift 6 made.
I hate go. Specifically I hate go packages. It was poorly designed. Any time you create a package in go you can’t have circular dependencies. Which makes sense right? When you make a package in rust or node or python those packages can’t have circular dependencies.
If you have two files in go or any other language, circular dependencies are usually permitted which also makes sense. It’s only packages that don’t permit this.
But guess what? You create a package in go simply by putting everything in a folder. So people when writing a go project they often make dozens of packages just by the is ocd need of organizing things into folders.
But when you organize things into folders the files in those folders aren’t project files anymore. They are different packages and you can’t have circular dependencies.
And that’s a big problem as this design conflicts with programmer instincts on how to organize things.
We organize things by meaning and name and subject matter. But simply using folders in go we are forced to organize things via dependencies and this causes a clash and makes organizing things 10 times harder than it should be in go.
Imagine I have a chicken and an egg. Chicken goes in the animal package and egg goes in the food package. It is now fundamentally impossible to make a chicken lay an egg and an egg hatch a chicken at the same time. Our tendencies to organize things via meaning has clashed with this requirement of organizing things via dependencies.
Does it make any sense at all? We probably don’t want packages to be interdependent in this way. But the stupid thing is just be creating a freaking folder we create this problem in go and it doesn’t make sense because if the files are outside of a folder you don’t hit this problem.
I would say go is actually poorly designed. I can’t think of any popular language that has this strange inconsistent problem.
It actually causes huge headaches in organizing things in go but idiot developers when they hit this circular dependency problem they literally believe the documentation when it tells them that they aren’t organizing things correctly and they need to think harder about design. So in a lot of go shops they spend an inordinate amount of time naming folders and organizing shit just to get their ocd need of organizing things by name to jive with gos requirement to organize things by dependency.
In the real world things can be in different categories and also interdependent at the same time. Go’s universe doesn’t allow this.
I’m here to tell you that It does make sense to put eggs in the food folder and chickens in the animal folder. Go is just stupid in this area, not you. Don’t believe the idiot who wrote that in the docs.
> Imagine I have a chicken and an egg. Chicken goes in the animal package and egg goes in the food package. It is now fundamentally impossible to make a chicken lay an egg and an egg hatch a chicken at the same time.
I am familiar with people eating unfertilized eggs. I am even familiar with balut. But I am not familiar with the delicacy of eating an egg as it hatches. If you eat an egg as it hatches, wouldn't that actually be eating a chick (the chicken) rather than an egg?
I suspect the real problem here is that your domain model is ill-conceived. In fact, while Go has no lack of faults, I suspect the vast majority of the complaints directed towards it come down to the limited feature set not enabling papering over bad design decisions.
Your joking. I have a hundred food items and a hundred animal items. Cows produce milk. Chickens produce eggs. Eggs produce chickens.
It makes 100 percent sense to make two folders. Food and animals.
‘I suspect your “domain model” is ill conceived’??? You buy into that? Don’t follow what those idiots tell you.
You realize that we use these actual separations between food and animals in the real world? And now because of some design philosophy the domain model is ill conceived? The result is a more convoluted domain model to make things flow.
Actually I think this is an easier way to explain it to you. The “domain model” or aka categorization of food and animals works in reality and every other language because it’s not a bad design decision. The reason why it doesn’t work in go is because go is the bad design decision.
> You realize that we use these actual separations between food and animals in the real world?
The distinction between "animal" and "food" isn't necessarily faulty, but an egg considered to be for food and an egg considered to be for hatching are deemed distinct types in the real world. An egg from the grocery store is not going to hatch no matter how hard you coddle it. This is not the same type of egg that will hatch a chicken. Reusing the same model for both types of eggs does not work in the real world, and thus it stands to reason that it doesn't work in software either.
Revisit the model in a way that actually reflects the real world and your circular dependencies are bound to disappear.
Who said it has to be a grocery store. I’m in a farm. Some eggs I eat, some eggs I hatch. I have a food bin and an animal pen. Two different boxes for two different things.
Makes sense in the real world. Why doesn’t it make sense in go? You literally think there’s some elegant underlying principle here having to do with categorization do you? It’s mind boggling to me how arbitrary decisions made by the designers of go are thought of as fundamental design rules.
When people use the word “domain model” I know the complex nomenclature has actually led them astray. These are just folders and categories. The term domain model doesn’t change a thing other than making it sound smarter.
Exactly. Like you describe, this domain model has at least two different types of eggs. This matches what was described in the original comment. But, in the original comment, the author was trying to shoehorn all functionality into a single egg type. And, unsurprisingly, it didn't work for him. It wouldn't work in the real world either.
Revisit the model in a way that actually reflects the real world and your circular dependencies are bound to disappear.
Essentially. More likely, without straying too far from the original, you would have something like `animal.Egg` and `food.Egg` (although it is likely that you would want to stray from the original in an actual setting).
You are going to do that type of thing regardless of the language (and especially in languages with more advanced type systems!), even where circular imports and other code structure methodologies are supported. You wouldn't want your `scrambleEgg` function to accept an egg intended to be hatched. The bones are unpleasant.
>You're going to do that type of thing regardless of the language
No I'm not. I'm going to make one Type and it's going to be called EGG. You're going to make an Egg interface and divide it into two subtypes EatableEgg and HatchableEgg.
I don't think you picked up on it but I have one thing, you created 3. And you created 3 just to avoid an orthogonal issue of dependencies. You aren't getting it. In my statement above I was implying that YOU are shoehorning things, NOT me.
Also you have to create a new package now called Types and that's where you put your Egg interface. So go forces you to create a third meta category while other languages I can choose whether I want that or not.
Or I just don't use folders at all in go. Then this issue doesn't even exist. It only exists because I happened to want to use folders, but I can get rid of circular dependencies simply by organizing everything by files.
Enjoy your scrambled bones, then, I guess. But even if you want to model the world as having only one egg type, that type decidedly does not cleanly fit into "animal" or "food", so your taxonomy doesn't work.
> You're going to make an Egg interface and divide it into two subtypes EatableEgg and HatchableEgg.
This seems like a poor assumption. In the real world, if an "EatableEgg" is fertilized into a "HatchableEgg" there will be a type conversion. What was an "EatableEgg" is now a "HatchableEgg" and the "EatableEgg" no longer exists. What do you need the interface for?
> I don't think you picked up on it but I have one thing
Right, but we're talking about types, not things. Things can come in many different types. You even told us a story about how you consider eggs, which are the same thing, to be of different types, so this didn't go unnoticed by you earlier.
> Also you have to create a new package now called Types and that's where you put your Egg interface.
If you were to have a such a thing, wouldn't it go in something like "reproductive structure"? "types" is a strange fit alongside "animal" and "food".
Revisit the model in a way that actually reflects the real world and your circular dependencies are bound to disappear.
>Enjoy your scrambled bones, then, I guess. But even if you want to model the world as having only one egg type, that type decidedly does not cleanly fit into "animal" or "food", so your taxonomy doesn't work.
scrambled bones my ass. There's no instance where that happens. I have a scramble function that takes an egg type as a parameter I don't know where you're pulling the scrambled bones from.
I don't model the world that way, that's not my objective. I live on a farm, I'm just modelling things the way I modelled it on the farm. I organize my eggs into the food box and my hens into the chicken pen. I do this in the real world. I want to do the same thing in my programming and I can't. Understand?
>Right, but we're talking about types, not things. Things can come in many different types. You even told us a story about how you consider eggs, which are the same thing, to be of different types, so this didn't go unnoticed by you earlier.
If you make two different types, and you instantiate those two types you have two different things. I don't think you're getting it. Those two different things cannot represent the real world thing because the real world thing can hatch OR be eaten. The things you created can only do one or the other.
>If you were to have a such a thing, wouldn't it go in something like "reproductive structure"? "types" is a strange fit alongside "animal" and "food".
You have to do this because your Egg interface can't go into animal or food becuase it will cause a circular dependency.
On my farm I don't have a bin or a box (aka type) called reproductive structure or types. I don't need to do this in the real world but I need to do this in go because Go is poorly designed.
Look, I can make a folder called animals and food and I can put anything I want in those folders in other languages. In go, I can't. It's that simple. Go makes this arbitrary restriction out of nowhere and it makes things worse. You're shoehorning new nomenclature and reproductive concepts into your organizational scheme while I can run with what's done in REALITY with significantly less primitives.
>Revisit the model in a way that actually reflects the real world and your circular dependencies are bound to disappear.
I think you're out of touch with the real world. When you type things in the real world (aka putting things into a box or a pen) you don't care about dependencies. I don't put my eggs in the chicken pen because they came from the chickens. I put it in the food pantry. Understand? Folders are designed to do the activity I just mentioned, they are not designed to form some complicated taxonomy of unnecessary concepts.
> I have a scramble function that takes an egg type as a parameter I don't know where you're pulling the scrambled bones from.
Then you're in for an educational treat. Believe it or not, inside a "hatching egg" is a little chicken! And chickens have bones. As your scramble function accepts any type of egg, it may accept an egg that contains said bones.
But it need not be that way. With consideration about your types, you can have the compiler ensure that you only allow "eating eggs" into your scramble function. Just like we normally take care in the real world to ensure the same kind of separation because most people would not be amused if their scrambled eggs contained bones. Apparently you roll the dice, but know that is unusual.
> and you instantiate those two types you have two different things.
I don't disagree, but I don't know of any programming language that has a concept for things. Certainly none that you would actually use for production software. Types are the pinnacle of popular computer science thus far. At some point you are going to have to accept that software and the real world aren't exactly the same.
But revisit the model in a way that actually reflects the real world and your circular dependencies are bound to disappear.
> I organize my eggs into the food box and my hens into the chicken pen.
And the roosters? Those eggs aren't hatching otherwise...
> You have to do this because your Egg interface can't go into animal or food becuase it will cause a circular dependency.
But, most importantly, because it would otherwise be confusing for anyone else who has to work on the code. "Why is a chick about to leave its shell considered food? I don't know anyone who considers that to be food." is the first thing they are going to say upon encountering this codebase.
Code is written for humans. Remember that.
> I put it in the food pantry.
You may put "eating eggs" in the pantry, but I can't imagine you put "hatching eggs" in the pantry. So why are you putting "hatching eggs" in "the food pantry" when taken to the computer? Again, this model you have doesn't even work on a human level, never mind any technical constraints when applied to software.
> Then you're in for an educational treat. Believe it or not, inside a "hatching egg" is a little chicken! And chickens have bones. As your scramble function accepts any type of egg, it may accept an egg that contains said bones.
Except the egg I modeled doesn’t do this and I generally don’t encounter this in the real world so I don’t have to account for it. So your example is meaningless pedantic pointlessness.
> I don't disagree, but I don't know of any programming language that has a concept for things.
The concept of type is a categorization. Instantiating a type is creating a thing that goes in that same category. You’re not getting it because this statement is categorically incorrect.
> Types are the pinnacle of popular computer science thus far. At some point you are going to have to accept that software and the real world aren't exactly the same.
Of course not. The food category and the animal category are not part of the real world. The universe doesn’t categorize things. Categories are made up bullshit by human beings. They aren’t real in any sense. You’re just not seeing this.
What’s going on with golang is that it’s saying your arbitrary categorization of things IS REAL. When you don’t categorize it the go way you actually did something fundamentally wrong is what go tells you. And your issue is, you believe in this go philosophy. You think that what go tells you is real.
Go says a chicken cannot go into the animal category and exist with an egg that goes into a food category. Go says if you try to model things this way something is fundamentally wrong with your understanding of categories and reality and it throws a compiler error to tell you so.
The problem here is that you believe this bullshit. You think golang actually said something profound when really the designer was just an idiot.
> But, most importantly, because it would otherwise be confusing for anyone else who has to work on the code. "Why is a chick about to leave its shell considered food? I don't know anyone who considers that to be food." is the first thing they are going to say upon encountering this codebase.
Code is written for humans. Remember that.
You’re out of touch with humans and you don’t see it. As a human I don’t think about accidentally finding chicken bones in my eggs so I categorize eggs as food and chickens as animals. What’s inhuman and overly pedantic is to consider every possible case and permutation of chickens and eggs and try to model everything in your program. How detailed do you want to go? Everything is made out of atoms so everything belongs in the same category? Be real. You are arbitrarily increasing the resolution of what is encompassed in your model to make some arbitrary and false point about domain modeling.
What you’re not seeing is The model is made up by me to simplify things. In go I can’t make the model simple because I have to model things according to golang.
> You may put "eating eggs" in the pantry, but I can't imagine you put "hatching eggs" in the pantry. So why are you putting "hatching eggs" in "the food pantry" when taken to the computer? Again, this model you have doesn't even work on a human level, never mind any technical constraints when applied to software.
I put eggs in the pantry. I can still hatch those eggs if I want. I can eat them too. You put technical constraints on reality for no other arbitrary reason other then to satisfy gos dependency rules. In your model you restricted your abilities. I changed my mind and I want to use half of my eggs in the pantry for hatching. Your “domain modeling” doesn’t allow for this.
Look this is all arbitrary bullshit. You’re just going to keep making up cases where your model is right but for every case there is also a case where mine is right and yours is wrong. This is because there’s an infinite amount of perspectives to look at this subject. How we choose to look at that subject is an arbitrary choice. You want to make two types of eggs and categorize it that way?be my fucking guest. Most people who farm don’t do it this way but go ahead.
Golang takes this arbitrary choice away. It tells everyone that eggs don’t exist. It’s either a hatching egg or an eatable egg. You live in a universe of golang that tells you that this is the only fundamental way to categorize things: by a dependency tree.
Because we don’t know which came first, the chicken or the egg. In your universe of golang that you live in your saying it’s a fundamental truth that we can therefore never categorize chickens as animals and eggs as food. We must destroy the concept of an egg and realize there are two types of eggs. One that is eatable food and Another that is a hatchable animal.
That’s how your brain sees reality. If this is the case so be it. Can’t be fixed. I can’t change you. More likely you’re just stubborn.
> So your example is meaningless pedantic pointlessness.
Okay, but it is not my example. It was the example given in the very first comment. It what was asserted to have created the circular dependency.
> The concept of type is a categorization.
Okay, sure, but it remains that things are decidedly not categorizations. And, as before, there is no known programming language that has a concept for things. It's an interesting idea! I can see that programming languages could benefit from having such a concept. If you happen to be looking for a CS research project, this just might be it. But if you just want to write production software using existing tools you're not going to find it.
> I can still hatch those eggs if I want.
Your pantry has suitable environmental conditions for incubation, does it? The environment necessary for incubation is not ideal for other foods normally kept in a pantry, so this doesn't really add up. But, hell, let's pretend you do. Do you still not want some way to identify those eggs being incubated and those eggs ready to eat for others, if not yourself, looking into your pantry?
> Golang takes this arbitrary choice away.
Okay, but we're not even talking about Go. We are barely even talking about programming. This model of yours doesn't even fit the real world. Come back with a better model and you won't leave other humans scratching their heads and you might even find the technology won't fight you so hard at the same time.
I've been writing Go for about 5 years now and can confirm cicular dependency issues were typically handled buy a small refactor or considering where the borders of the domain should _really_ be
I think the issue the OC was hinting at is that the borders of the domain are forced to be the files. Having a folder for "food" and a folder for "animal" should not imply that they are in different packages by the OC's reasoning.
Perhaps both of them should be the same package, but to achieve this, you can't organise it into folders.
_This_ is what OC was complaining about. Considering where the borders of the domain should be is a second handed issue that is only due to the bad choice of having packages be determined by folder structure, which is the complaint of OC.
Right. And people conflate the issue. Especially go developers. They can’t see the difference.
They naturally create folders based off of semantic meaning instead of packaging. Then they hit the dependency issue and they think creating the folder was a domain modeling problem when really they just created the folder based off of semantic meaning and not dependencies.
The whole thing has confused go developers. They think putting things into the right folders actually has some deeper design meaning when really it’s just to make it easier for humans to find shit.
This bothers me from time to time. When I first started using Go, I think I complained to the Go team about it. They told me to think more carefully about where package boundaries are. Having the smallest package possible doesn't necessarily mean the design is good.
I inherited an app from a coworker, and he just put every file in the main package. This was actually fine and caused no problems.
These days, I still have an addiction to making tiny packages, which the Go team would likely not approve. But I'm a big girl and I do what I want! The workarounds I use are:
1) A `types` package. type Chicken struct { Children []Egg }; type Egg struct { Mom, Dad Chicken }.
2) A `utils` package. Let me explain. I would never name a package `utils`. That's illegal and I'm told that the compiler will explode into a thousand pieces if you try. All the king's horses and all the king's men wouldn't be able to put it back together again. So don't test it. Just trust me. But, you can definitely have something like `package hatchery` with a functions related to collecting eggs from chickens. Or you can have `package eggutil` for packing eggs, frying eggs, and shipping eggs.
I would say that I typically have a `types` package (called `types` if I'm hand-writing the code, or myapppb if it's protobufs like at Google), and then structure the rest of my app as model/view/controller. The model puts these types into the database. (Call it `eggdb`.) The controller does whatever business logic you care about (getting chickens together to fertilize eggs, call it `husbandry`). The view calls into the controller to implement your API / gRPC endpoint / beautiful Gtk+ GUI / whatever.
And then you don't really have problems with circular dependencies.
Having said all that if someone puts it all in main.go I'd probably approve the PR. The Go team let my coworker do it in 2014 and nobody died.
You’re not seeing the problem. You spend an inordinate amount of time trying to organize it and you come up with an elegant way so the catharsis of that solution makes you think you found the path to nirvana. Really you’ve been duped.
This restriction makes sense for packages. But not for folders. The problem arises in go because folders and packages are the same thing and that’s stupid because people don’t typically use folders as if they were packages. Folders are typically used just to categorize things of similar meaning or naming or whatever you want.
No other popular language has this issue. I can choose to organize things how I want in any other language. I have the freedom. I can make a folder A and B and C and D and just organize things in ABC order. But can’t with go. If I dont want to do this in any other language I have the option to create a package.
Creating a package and creating a folder are completely orthogonal tools and concepts and the genius who invented go just made a horrible design decision here.
> that’s stupid because people don’t typically use folders as if they were packages. Folders are typically used just to categorize things of similar meaning or naming or whatever you want
You might do that. That does not mean everyone organizes things like this.
What your chicken/egg example fails to realize is that things rarely just belong in one category. Chickens can be animals or food.
To me it just sounds like you are trying to write another language while using Go. Of course you will see problems. Just as you would see problems trying to write Go code when using Java.
In the end it comes down to this: if the language does not fit your mental model on how to do things, don't use it. But don't go around shitting on the language just because you are used to using folders differently than other people. Using a language also means learning and using the conventions of that language.
>You might do that. That does not mean everyone organizes things like this.
Most people do organize things arbitrarily based off of their own categorizations and opinions. In fact such an overwhelming majority of people who use operating systems do this that it's pretty much universal.
>What your chicken/egg example fails to realize is that things rarely just belong in one category. Chickens can be animals or food.
So what. I choose not to eat my chickens. So I Choose to categorize it in a way that makes sense to me. Why should I conform to your point of view? Why should I conform to golangs point of view? Why can't you and I choose how to do it?
>To me it just sounds like you are trying to write another language while using Go. Of course you will see problems. Just as you would see problems trying to write Go code when using Java.
No. I'm complaining about go. I think it's dumb. I'm not doing OOP here, I hate OOP. Go is waaay better. This is orthogonal to that problem.
>In the end it comes down to this: if the language does not fit your mental model on how to do things, don't use it. But don't go around shitting on the language just because you are used to using folders differently than other people. Using a language also means learning and using the conventions of that language.
Yeah that's how all complaints and criticisms are sidelined. "You don't like it, don't use it. Use something you like." Obviously.
I'm doing exactly that, while saying that go packages are a poor and horrible design decision.
I think a lot of people hated java, and found go and they think because go is so much better then java then that means go can do no wrong.
I don't think you realize there's stuff way better then go out there. But that's besides my point. What I use is separate from my point: Golang packages are poorly designed.
> I don't think you realize there's stuff way better then go out there.
Every language makes trade-offs. For you the trade-off Go makes is bad. I disagree. I like some languages better in certain parts, while I prefer Go's solution in other parts. It's all preference, there is almost never an objective "better" or "worse" like you seem to think.
Then you disagree with the majority of people. That's my point. This agree/disagree side lines the fact that people can be right or wrong. My claim is not only do you disagree, but you're wrong.
Majority of what people? Get off your high horse, your opinion is not a fact. Give me objective metrics to measure how "good" a language or language feature is, otherwise you are just wrong.
I'm looking forward to trying out your own perfect language that is objectively the best for any use case ever and no one can find any faults with it. Because surely such a language exists.
>Majority of what people? Get off your high horse, your opinion is not a fact.
Just look at reality and how people organize things everywhere. It's arbitrary. Adults are placed in offices and kids are placed in schools. Then on family trees kids and Adults are organized by dependency. How people organize things in the REAL world is AN arbitrary choice. But in GO you have NO CHOICE. It has to be by family tree. That is NOT an opinion. Everything I said is FACT.
The metric is basically the entire world and everything in it and how we fundamentally type (aka organize) things within it. But if you want to get more specific just look at every other programming language except go and look at how the entire world uses folders in operating systems. These are tools to allow people to arbitrarily organize things.
>I'm looking forward to trying out your own perfect language that is objectively the best for any use case ever and no one can find any faults with it. Because surely such a language exists.
Don't have one. I'm just commenting on what I hate about golang.
So a hand-wavy "look at the world bro" instead of actual metrics. Got it.
> I'm just commenting on what I hate about golang.
No you're not. You say Go (or a part of Go) is bad, which is vastly different. If you stuck to "I don't like it", you would not have gotten so much push back, but you insist in being right and everyone else is stupid and wrong.
Because metrics don’t exist. If the only way you can move forward and know something is because someone conducted some sort of statistical experiment with data analysis on it then I don’t even know how you get up in the morning. Do you need science to prove that when you jump off the bed there’s a floor that your feet will land on?
No you don’t. You’re not an idiot. It’s common sense that allows you to do this and it’s common sense that will allow you to see that the entire world works the way I say it does. You don’t need science for this. At least most people don’t. Maybe you’re different and you need science to do an analysis for you before you open any door to make sure reality still exists behind it.
> No you're not. You say Go (or a part of Go) is bad, which is vastly different. If you stuck to "I don't like it", you would not have gotten so much push back, but you insist in being right and everyone else is stupid and wrong.
You’re right. Let me rephrase what I meant. I’m commenting about what is fundamentally terrible about golang and if you disagree you’re stubborn and biased.
For me it starts with allowing only source code packages, followed by direct URLs to source code repos directly on the source files, instead of an indirect mechanism.
To be fair, Go modules provide such indirect mechanism, by rewriting source files, with redirection information, really? Yet another hack.
This is solved simply by having a chicken.go and an egg.go in the same package.
Go appears to encourage large source files, since it allows you to define multiple "classes" (types with methods) within the same file. This is probably intentional to combat the complexity of having otherwise cohesive logic littered across several small files in OOP-first languages.
But I like your example and get your point. What if chicken.go and egg.go diverged a few levels earlier in the dir path. All the Go repos that I've seen in production have very flat folder structures, so I guess this is just how Go is written.
I joined a company who wanted really clean code in their code base. That meant really good categorical organization of things into folders based off of semantic meaning about what those primitives did. You can imagine the nightmare that caused.
People simply just didn't understand what I was saying that there was no inherent meaning in trying to make our naming conventions with folders work with go's dependency rules. They thought the work of untangling all the dependencies, folders and the naming was some quest towards a perfect design.
You can also see in this thread, that a lot of people debating the issue with me believe the same thing. They think this arbitrary rule is speaking to a fundamental design axiom and they use big words to call it "domain modelling"
Agree that the language has to be analyzed under the original design goals to be fair. Disagree that Go is definitely well-designed in that circumstance, however. A major weakness of this essay is that all perceived issues with Go have to be irrelevant to the original design goals, quite absurd if you think about that.
I would instead say that Go had big things right, while smaller things might be not as right---but not exactly "incorrect". For example explicit error handling is the big idea that Go got right, but both Rust and Zig did a lot to refine error handling experiences; comparing them with Go is just a non-starter.
> Agree that the language has to be analyzed under the original design goals to be fair.
I mean if you're analyzing languages in a void, I guess, but we're not. We're looking at languages as tools to solve programming programs. And almost nobody has the problems that Go was designed around, so the original design goals are kind of irrelevant.
I mean, how many of you have worked on a million line codebase? If you haven't, why would you care that compiling millions of lines quickly was a design goal for Go? Yes, smaller codebases are also faster to compile, but partial compilation works just fine for that in other languages.
I should note that I do see merits of "unfair" analyses, in fact they are probably as important as fair analyses. It's just that such analyses would inevitably cover what designers originally didn't even think of, so such analyses should ideally be considered sort of bonuses instead of main criticisms.
I feel this article misses the point in many cases. Take, for example, error handling. It's labourious in Go, but that's not the only issue. I'm not going to go into all the details in this comment (others have covered this at length) but:
1. Go's error handling has four possible cases (result and no error, no result and error, result and error, no result and no error) when only two make sense.
2. A consequence of the above is that it requires null values (nil in Go)
I don't see how this can be considered good design.
> 1. Go's error handling has four possible cases (result and no error, no result and error, result and error, no result and no error) when only two make sense.
A partial success, that returns both a result and an error definitely makes sense. For example, you’re streaming data over a network, receive some bytes, but get disconnected before it’s complete. It would make sense to have both the data received so far as well as the error, in order to resume.
I’m sure there’s an appropriate situation for the 4th case as well.
This is why it's table stakes for a decent type-system to have both product-types and sum-types. They comprise both sides of the coin, and you use the one that actually makes sense for the piece of code being written.
A justification for Go's deficient type-system I've seen repeatedly over the years, along the lines of "Well actually the occasional function can legitimately return both a result AND an error" is nonsensical. It's basically excusing giving 98% of error-returning functions a semantically wrong return type (a tuple) because it happens to be correct in 2% of cases.
Every function should have a semantically correct return type! If a product-type is occasionally correct for an error-returning function, use one just for those functions! And use a sum-type for the other 98% where that is correct for them!
Exactly. One can think of examples where these cases make sense, but there are many more examples where there are more cases (e.g. there many be multiple kinds of partial successes) or fewer (most functions, which either succeed or fail.)
Also, the design requires null values. A point which the replies have ignored.
If you consider (T, error) to be logically coupled, there is actually effectively an infinite number of cases. (T, U, error), (T, U, V, error), (T, U, V, W, error), etc. Although I suspect this take is suffering from trying to project idioms from another language onto Go.
It looks like the arguments on Apple ecosystem when arguing for design faults as virtues, us are unable to understand without enlightenment, like we are holding it wrong, tablets don't need pens, don't understand keyboard design, ...
From what I've seen, the people who complain about go's design are people who are more concerned about the ways in which it is not correct (by some definition) and less concerned about just getting stuff done.
I don't "like" go (or any other programming language), but I'm able to get more done in it in a shorter amount of time than any other language, the created thing consumes tiny amounts of resources, and most importantly when I _or anyone else_ goes back to change something weeks, months, years later it's easy to re-establish or gain contextual understanding.
These were the first lines of Go I ever wrote 15 minutes ago, as a response to the claim that Go had working enums. This is the kind of thing that makes me itch.
https://go.dev/play/p/MMPMh7_U81-
I think "being simple" doesn't necessarily mean "must have subtle sharp edges and papercuts everywhere". Just like javascript I think it falls into a pit of being superficially "low complexity" or "simple", but all the subtle gotchas and unhelpful tooling (The fact a compiler can't help me realize I had added an entry to the enum, but forgotten to update a name list just means the whole feature is no better than just plain ints that we had in C since before I was born).
I like go, and I use it to get stuff done. I'd be able to get stuff even more done with sum types and pattern matching and better generics and iterator tools and it's a shame Go doesn't have these things.
Go is a bunch of preferences and aesthetics. It clearly appeals to a bunch of folks, others have a hard time putting up with it.
Everyone is free to both love and/or hate go. The language's spirit is not going to change. I'd much rather see more realism, honesty, and acceptance. Instead, this class of discussions always seems to become incessant "no u" ping-ponging.
Certainly not what I'd consider curiosity and critical thinking.
The error handling is explicit only in a limited way. As the author points out, you have to explicitly propagate an error condition, but you don't have to explicitly not-propagate it. Exceptions are reversed in this way: propagation is implicit, and not-propagating is explicit. I would prefer that handling is fully explicit in all cases, and not depend on supplemental linting or sheer discipline. It's a bit incongruous that golang compiler is so fussy about unused imports, but has nothing to say about silently ignored errors.
I also find that stack traces are generally more useful than plain wrapped errors. I want to know the code path in most cases where I am looking at an error.
More generally, it's disappointing that golang has so many idiosyncrasies given its philosophy of straightforwardness. I struggle to think of any useful, general purpose language that has fared better, though.
I think Go is a pretty good choice for getting something out the door quickly, without getting in the way too much.
The other week, I was working on two APIs: one for a team that uses Java primarily and another where I was free to pick the technology as needed and was looking in the direction of making it easy to deploy, even without containers, where I picked Go.
With Go's standard library (and a few packages here or there), I had something up and running within a few days and it mostly just worked. Things that I wanted to do were just an additional method away, which, while not as sophisticated as the various convention over configuration options out there, kept everything very discoverable and observable.
With Java, I had to setup a Spring Boot project and its security configuration mechanism (SecurityFilterChain, WebSecurityFilterConfig and lots of Filter implementations) and ran into issues where calculating file checksums for uploaded files broke because Files.exists() returned that files exist for ones with UTF-8 symbols in the name, whereas internally FileUtils.checksumCRC32() used file.isFile() which returned false. Then, I also needed -Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8 and for another mechanism had to look into JCE briefly, as well as configuring Maven to push packages to Nexus, as well as how to disable Spring Boot trying to connect to PostgreSQL and RabbitMQ while bringing up the app context for tests, or to connect to a special instance for tests (and hopefully eventually figure out how to automate a separate schema per CI run). Oh also deciding between OkHttp and Apache HttpComponents. Oh and also adding Apache Tika to the project, yet realizing that I can't add tika-parsers-standard due to some related dependencies.
Now, this isn't a completely fair comparison because the APIs didn't need to do 1:1 the same things and I could have chosen not to go with Spring Boot, but it's so entrenched that it's most likely the majority of projects that you'll see out there (I also like Dropwizard, but I haven't seen many people use it at all). That might sour some people's perception of using that language professionally: https://earthly.dev/blog/brown-green-language/
There are things about Go that are annoying, but it not having all of those levels of abstraction that you need to think about was comparably nicer. Plus, not needing to think about JDK versions and instead just getting a single file that you can ship. Oh, and on a related note, projects like Wails are also really nice (I tried Tauri, the builds were too long and the Electron bundles are a bit too big for my needs in some other projects): https://wails.io/
I like many of the languages out there (even Java and ones like .NET) but the projects and ecosystems around them can make them harder to use sometimes.
You were certainly bitten by the Java ecosystem, but I would agree that you hold some responsibility. Adopting Spring Boot so casually is really quite questionable. It's essentially opting to build your application within a giant, bonkers codebase. In the golang community there is a held value of using the standard library and tools, which is helped those being very good. I would suggest that Java is a better experience if you follow that same approach. If you want to serve HTTP, use the jdk.httpserver module. If you want to make HTTP requests, use java.net.http. If you want to interact with the filesystem, use java.nio.file. To build, use the included tools in the JDK. When it comes to cases where a third party dependency is warranted, such as use cases for Tika, you will have to deal with some questionable design decisions, but it's much easier when you have just a few libraries that you use by hand, and no insane framework trying to stitch everything together for you. For getting those dependencies, you are going to be acquiring them from public maven repositories, but I would suggest using Coursier or Maven Resolver instead of actually using Maven.
To me it looks like more lack of familiarity with Java.
Having used Java and .NET ecosystem since their early days to this day, I fail to see why the team wasn't able to deliver in the same timeframe, also Spring Boot isn't the answer for everything.
No matter how many blog posts try to justify Go, it is a lost ship. Yes it is used by many companies (I still don't understand why), but that does not make it a well designed language.
> But secondly designing errors as explicit values has been a trend-(re)setter. Go, Rust and Zig have all chosen to use this approach.
Just because Go treats errors as values, you can't compare Go's error handling with Rust's. Rust provides clean abstractions with Algebraic Data Types, ? operator over Result type to reduce polluting code with if-else nonsense in every single function. I bet atleast 30% of Go code in a project is error handling for the sake of handling.
Further Go's syntax inconsistency irritates me more. I still don't understand if I should put a comma or semicolon or nothing between members in a struct definition. Also
My company has 2 teams that use go for different reasons:
* Easy to crossbuild and you distribute a single executable (although of course someone managed to pull some dependency that was dynamically loaded and made our CLI depending on a running Gnome session)
* C developers who can't write C code that doesn't segfault every 3 minutes (because they never understood how threads work) can still have their ego but also produce somewhat useful code.
> A lot of that is from lack of async, or rather that async is hidden behind go routines.
That’s an interesting take, but one that I’ve seen in multiple places in this thread. I find that introducing goroutines into a program immediately increases the cognitive load as now you need to think about joining, data races, and need to think about error handling differently (golang.org/x/sync/errgroup is an excellent library by the way).
I encounter a lot of different software stacks when helping clients as a consultant. Consultancy is something I do next to my main job which is being a CTO of a small, bootstrapped startup. That sounds like a posh job but what it really means is that I do most of the technical work together with one other person.
I've always been opinionated about tools and languages. And we probably all have our biases and preferences. What I've realized doing consultancy is that it's rarely advisable for me to tell my clients to change whatever the hell they are doing. It's actively harmful to do so. And I encounter pretty much everything you can name. Including Go.
At the same time, it's taught me to steer clear of certain tech stacks for my own things. I know I don't like them by virtue of having seen others do things with them. Go, is one of those things that's arguably alright and widely used. But I wouldn't prefer it based on what I've seen.
The reasons are mostly non technical:
1) it's a transition language for people. They might start with something like Javascript or python and they might crave something a bit more rigorous. Go is an easy choice. It's there, it's widely used. It has great libraries, and so on. But I've seen people progress to other languages as well. It's a phase people go through as they develop themselves. And there's a definite "if you have hammer, every problem looks like a nail" thing going on with it. For me Go would be a step backwards, not forwards. I have some languages I'm interested in learning. Go is not one of them.
2) it's a Google language and Google has this big "not invented here" syndrome that often translates into them reinventing wheels. In this case C++. They embarked on their journey around the same time several other languages kicked off. Rust, Swift, Kotlin, etc. all emerged roughly around the same time (15-20 years ago). Swift is Apple's language and it suffers from the same issue. So, even though Google's take on this is interesting. It's not the only one or necessarily the best one (subjective of course). Rust and Kotlin are a bit less tied to one company. And both are widely used across most/all big tech companies and less entrenched in just one of them. Full disclosure, I prefer Kotlin out of these languages and actually use it for a lot of things people might think Go is good at.
3) It's a conservative language community. Despite being relatively new, the community seems to resist change a lot. Endless debates about exception handling, package management, generics, etc. and other aspects of the language seem to progress at a snails pace with people bickering endlessly about whether that's good or bad design. That stuff just bores me. Just fix it already. I don't want to read/hear endless reasons why something cannot/must not/should not be a thing because reasons that boil down "we don't like change".
So, no Go for me. But some of my clients use it and it's of course fine for them. I consult them on what they should build, not how to build it. A lot of that stuff works just about the same regardless of the language. Use what works for you, what you are familiar with, etc. Not what others tell you to use.
> Cleverness makes you quit your job when the codebases pass 10k lines
I've very much felt that in codebases that are hundreds of thousands of lines of code.
Essentially it becomes: "Is it easier to track down this bug and fix it over a week or two (hopefully the solution isn't needed by tomorrow night), or is it easier to quit my job?"
Sometimes the answer unironically is the second option.
> I can think of two examples of major aesthetic shifts in Go code that seemed ugly at the time but were adopted because they simplified software engineering and now look completely natural.
Adding an element to a nil slice works, but adding an element to a nil map panics. time.Time uses its own epoch incompatible with anything else. defers are function-scoped instead of block-scoped. The list goes on, these are just the first ones that came to my mind. And they are not some magical tradeoffs, they are just poor choices. I like go, but no, it is not well-designed
> defers are function-scoped instead of block-scoped.
I think this is a basic tradeoff with defer and not a problem per se. Are users more likely to need to clean up resources every time through a loop, or more likely to acquire resources that outlive the loop? And which is more confusing if you expected the other? I think it's a tough call. (I understand why a systems programming language would insist on block scoping to avoid allocating.)
Also missing null safety in modern language is weird.
And enums.
The appeal of Go is that it has just the bare minimum feature set to be useful for modern backend engineering. It's a language that you can completely fit within your head within a few months of using it, as opposed to certain languages like C++ (or say C# to a lesser but increasing extent) that most will never know completely even after decades of working with it. It's basically the RISC of programming languages.
And if I had to guess, it doesn't have enums so that it can remain flexible when serializing/deserializing enum-like types over the wire. Imagine you can't parse an incoming payload containing an enum field because your service is one version older than the one that extended the enum type (or the enum type is defined in a package dep... you get the idea). Enums are actually a terrible idea now that I think about it.
Modern backend with yesterday's technology.
The appeal of Go is goroutine.
> The appeal of Go is that it has just the bare minimum feature set to be useful for modern backend engineering. It's a language that you can completely fit within your head within a few months of
To me, golang symbolizes the shift of philosophy of Google as a company. It changed from "it's a smart nerd company for smart nerd people" to "golang will allow us to hire cheaper devs because golang will prevent them from making mistakes". I mean, this makes sense from business perspective, I won't deny this fact, but it's the programming equivalent of Ferrari making a SUV: tremendously profitable, but sad to see.
BTW
> golang doesn't support overloading because overloading is bad. Having said that, it's 'go', not 'golang', like the verb 'go', which already has a thousand meanings depending on the context
I find that hilarious
The law of large numbers says that, the larger the organization, the closer to average the employees will be. Google had no chance of maintaining "a smart nerd company for smart nerd people" at the scale it grew to be.
You can point out flaws so it's not well designed? By this logic nothing is.
And pointing out missing features in a programming language is just about the weakest criticism possible.
Piling on lots of features is easy and fun. That's why almost all language designers do it. Most popular languages destroy themselves with features.
Rust is in the process of gaining every single feature anyone can imagine. Following C++ right off the complexity cliff.
Go is one of the very few languages to show incredible restraint in adding features because its designers understood the combinatorial complexity problem, among other things.
Not having unnecessary features is one of the best features of Go.
The issue with Enums is that the "unnecessary" feature means that people have to encode the behaviour with things that aren't suitable. E.g. creating a new type that's a thin wrapper over string or integer means it can be set as some non-valid value, even without some sort of explicit cast that says "I know I'm doing something that might fail". I don't see that that is an improvement over any other language. It's not simpler because, as people have shown by the examples trying to prove that you don't need it, they don't have all the (necessary) behaviour that enums do to prevent this sort of problem.
I very much agree. The fewer ways a programmer can become creative, the easier it is to fully understand their code.
In a business environment, this is a obvious win.
Especially when programmers are seen as cogs in the business machine.
package main
import "fmt"
// Define an enumeration for status type Status int
const ( Pending Status = iota InProgress Completed Failed )
func (s Status) String() string { return [...]string{"Pending", "InProgress", "Completed", "Failed"}[s] }
func main() { var s Status = InProgress fmt.Println(s) // Output: InProgress }
I see this suggested all the time as though it’s supposed to be a replacement. It isn’t sufficient, it’s a poor emulation of what enums can be used for and doesn’t offer the same protections, since Status can be set to any integer value without casting or any knowledge that it’s broken.
In your suggested method it doesn’t even catch that case and provide an error if the value isn’t in the range.
It seems like none of the other comments here use or are even aware of `go generate`. Go has excellent code generation features [1].
I think parent should be using `stringer` [2] instead.
1: https://go.dev/blog/generate
2: https://pkg.go.dev/golang.org/x/tools/cmd/stringer
I'm well aware of Stringer but I don't again think it's a good replacement of having enums built in!
> I don't again think it's a good replacement of having enums built in!
I didn't say anything along those lines, in fact I was commenting generally about this issue that you guys and the other commenters mentioned.
I was just suggesting a solution to the the problem stated.
Next day I have this (added one entry). I forgot to update the string list elsewhere however.
const ( Pending Status = iota InProgress Completed Cancelled Failed )
func (s Status) String() string { return [...]string{"Pending", "InProgress", "Completed", "Failed"}[s] }
func main() { var s Status = Cancelled fmt.Println(s) // Output: ??
If the compiler - not some linter - protests that the list of names is different from the number of items in the enum, then I think this is at least a half-decent design of an enum type. Not a great one (because the author still had to repeat the names), but at least something that isn't a fundamentally broken design of an enum type.
But if the compiler is silent, and the output of the Println(Cancelled) is "Failed" then I'm not angry, I'm disappointed.
Edit: https://go.dev/play/p/MMPMh7_U81-
OK, but I can set s to 1000, and it still compiles.
Also known as design hack, workaround for what other languages support natively since 1970's.
Only one step better than Assembly or Fortran.
If language specific quirks lead to a language being "not well-designed", how many languages do you think are actually well-designed (if any)?
I got caught with maps initially, and had to question why everything else was just "var x string", which also gives it a value, but maps needed a "var x = make(make[string]string)", otherwise on first assignment it panics.
There's a good explanation somewhere, ah here it is: https://go.dev/blog/maps
But you get used to it, and move on, especially with the sort of TDD workflow that go test encourages
I know why it works the way it works, it doesn't make it a good design, which is the point of this discussion
I don't think that's weird. You can only append to a slice using the `append` built-in function. `append` may initialize and return a new slice at any call. It would be weird for it to not do so when passed a nil slice.
Adding elements to a map allocates memory and moves stuff all the time under the hood, it's no different from append. If panicking on nil is weird for append, it's just as weird for maps
append returns a new pointer in case the slice moved, map assignment does not return a new pointer. There’s no way for the operator to change the reference you hold from a nil to a pointer to the newly allocated value. When memory is allocated under the hood on assignment to a non-nil map, your reference to the object remains the same, but some internal pointers change.
You can write a func Set[k, v](map map[k]v, k k, v v) map[k]v yourself that returns a map reference if you want this style of “maybe return a new pointer”.
> time.Time uses its own epoch incompatible with anything else.
Could you please elaborate?
time.Time uses its own epoch internally but has methods like .Unix() which convert to Unix epoch time. Never seen this be a problem.
It means that zero value time.Time when converted to unix time gives you a negative number corresponding to a date two thousand years ago. With the way go insists on using zero values for expressing optionality this is a rather common mistake. Interacting with anything outside the go ecosystem is done in terms of unix time and this makes it very error-prone. And I've seen this trip people off in real life. And yes, you can argue that this is programmer's fault, but this is exactly what makes it a bad design: it makes people's lives more difficult for no real purpose, just because somebody at Google wanted to feel smart that day.
i think he means that internally, time objects simply use unix nano and the location dictates the offset. since it is internal thing, not sure why that is relevant.
Seems many people mistake “well-designed” to mean “perfect”. Read the article: he doesn’t claim that. No language is perfect, they are all trade offs.
Furthermore, it’s fine to not like the design. It might not be to your taste or your requirements. But that doesn’t mean it wasn’t well designed. I can point to lots of things that I don’t like the design of that work great for others.
Go is super productive, has a great ecosystem, has great concurrency, produces good machine code, compiles as fast as anything out there, has great tools, and is growing in popularity. Flaws? Of course. Perfect? No.
But, it’s well designed.
> has a great ecosystem
A great ecosystem is one where there is good 1 library to do a thing and you just pick that one.
A bad ecosystem is one where there are 25 libraries to do some things and 0 to do some other things, and none of the 25 libraries are very good.
I feel like Go falls much into the latter category.
I needed to write some code recently that did DSP. There is not any equivalent for this like SciPy in Go that bundles together lots of the common DSP operations you need. There are lots of partial implementations in different libraries of for e.g. filters, all of which use different interfaces.
A lot of this I think is because of the lack of SIMD, you can't get brilliant performance out of the box with a Go implementation because the standard compiler doesn't support it.
> No language is perfect, they are all trade offs.
Uh, no they're actually not. A tradeoff means you accept some downside and get some upside in return.
Initially go was designed without generics. The tradeoff here was you got a single pass compiler which was faster. Well, actually, you could have a single-pass compiler with backpatching, but I get it, that's hard. It's a crap tradeoff, but okay, I can accept it's a tradeoff. And everybody at the time said, "Go doesn't need generics, they complicate the language." So we casted our nested types in and out of object and lost all type safety in that code, but hey, tradeoffs.
Except everyone kinda realized it was a crap tradeoff and so `go generate` was proposed as a solution. In a 1984-esque revision of the past, suddenly, having a second pass is okay! Never mind how important it was that go is a single-pass compiler. Nevermind that this is basically macros with all their pitfalls. But now we can generate 3 different versions of the parameterized type, one for each nested type, so we get type safety. 2 passes, macros, downsides, type safety, upside, and a pretty big upside at that.
But, here's the problem: now you have two different systems that are used for doing a lot of the same things that don't interoperate with each other very well. And that's not a tradeoff, because there's no upside. It's not "I don't like this but it works great for others" because it doesn't work great for anyone. It's not a tradeoff, it's a side effect of having chosen wrong the first time.
And... now Go has generics. And yeah, generics aren't really a tradeoff, they're just obviously pretty good. But, now we've got 3 different systems for doing a lot of the same things, which again, don't interoperate all that well with each other.
And here's the thing: from a language design perspective, this was extremely obvious that Go was going to need generics. Generics are pretty much the clearest win from the ML-family languages which had already made it into C# and Java by the time Go was conceived. It's absurd that this feature wasn't included in Go from the beginning.
> Uh, no they're actually not. A tradeoff means you accept some downside and get some upside in return.
Yes, there is always a trade off. For everything, everywhere, all the time. You might not like the decision but trading flexibility for simplicity was a win for people learning the language. A lot of good software was built without generics.
It was a trade off, you just don’t like it.
Replying to that extremely well argumented comment with what is basically "NO I AM RIGHT!" is disrespectful.
I like Go, I like TypeScript, and I like C.
All of them have significant design flaws and quirks, many of which people here write pages of complaints about. But in most cases, it's easy to learn how to avoid them (granted, that's more of a stretch for C, but plenty of people manage, and that's enough).
I'm not against pointing out issues, especially for actively developed languages like Go. However, it's important not to turn criticism into bashing the language or the people using it. That just comes off as snobbish and isn't constructive.
Each of these languages is well-designed for specific needs, and not so much for others. And that's okay.
I think people mostly try to scare others away from using these technologies to push something they believe is better. But instead of that, you could perhaps write a competing article praising and comparing your favorite stack and that would be much more useful, in my opinion.
I wish more people thought this way. Why does the language you choose have to be your religion and lifestyle, when it’s just a tool? They all have flaws and areas they are useful in.
For many folks, because it means which kind of job they are able to land, with which people they hang around at least 8h a day, what they talk about, to which conferences they go if any, and so on.
People do this with all sorts of things, but ultimately want validation that they made the “right” choice. At least that’s the most charitable assumption I can make.
It's one thing to say these are decent tools, but to say something is well designed is a much more specific claim.
I have some fondness for C, but I would not say it is well designed compared to modern alternatives. The reason I use it is largely portability which is more a function of its age and ubiquity than its design. You can of course make the argument that it was well-designed compared to the languages of the time when it was designed, and I don't really know enough to disagree with you there.
TypeScript, I will say, I think is actually very well-designed. It has it's problems but it's a massive step forward from anything that existed before it.
> I think people mostly try to scare others away from using these technologies to push something they believe is better. But instead of that, you could perhaps write a competing article praising and comparing your favorite stack and that would be much more useful, in my opinion.
Strong disagree. Polyanna programming where we pretend all the tools in the world are so great results in horrible codebases, and then the programmer who thinks every tool is awesome becomes frustrated, doesn't know why, and moves on blissfully to a new project/company where they begin to plant the seeds of chaos anew. But someone has to deal with that crap codebase they left behind, and at this point in my career that person is often me. Work on codebases for more than a few years and the only way to stay sane is to recognize problems in code so that they can be fixed before they become bigger problems, and doing that requires you to be critical.
Working on a team, the biggest uphill battle is often preventing people from inserting every random tool they get excited about. Usually it's libraries, not languages, that are the problem, but I've seen both.
I used to love Go and its simplicity. It's still my least disliked backend language. But after more than a decade of using it nearly full time (but not exclusively), I've fallen out of love with it.
I want sum types. I want non-nullable types. I want enums (preferably, based on sum types).
I'm okay with the explicit error handling, but would be fine with Rust style error handling too.
Having these features would change the language so much that you'd probably want to re-implement the stdlib, so it probably won't happen.
Surprisingly though, while I absolutely love Go's concurrency model, being able to kick off new goroutines with a dedicated "go" keyword and special channel operators have not been a big deal at all. It's usually abstracted away by some library, or you set it up once in your application and never touch it again.
> But after more than a decade of using it nearly full time (but not exclusively), I've fallen out of love with it.
I believe this is true for anyone and any language. Spend a few years in a language on stuff that you have to do (which is the main differentiator between work and hobby projects) and two things happen: the benefits are taken for granted while the warts are magnified fivefold.
The corollary to this is the quote "I won't use something until you tell me why it sucks" (can't find source). If you use something long enough you know the ways it sucks. This makes your criticism more valuable than enthusiastic praise from a hobbyist.
In my opinion your comments on Go concurrency misses a point, the fact that you do it once "and never touch it again" is a very very strong selling point, it probably means it was very well designed; not a weakness but a strength.
I didn't mean it's a weakness. I mean it's not as amazing of a selling point as it was made out to be when the language first came out.
If you're using something only rarely and not touching it again, there isn't much of a difference between go.New(func(){}) from some "go" package, and go func(){}.
To reiterate, I'm not against the "go" keyword. It's whatever ¯\_(ツ)_/¯
I’m not a coder day-to-day (I was in the past, mainly C#) but I still write toy stuff to make my life easier, and the stuff I write in Go I’m more likely to be able to return to a year later and be able to quickly understand what I was doing, why I was doing it and fix/enhance what I need to.
The readability of Go is why I defend it all day every day.
The fact that a programmer with no Go experience can look at a codebase and quickly get a sense of what's going on is a massive advantage for large teams, code handoffs or long term maintainability.
Go is nobody's favorite language but that's why it's my favorite language.
100% this one of the big reasons I fell in love with go. Single executable, no runtime stuff to install and crazy portable. I can look at anyone's go code and very quickly get up to speed. I don't worry about how they format it. Patterns are pretty universal which makes it easier. Does it have its warts? Oh, you better believe it but I know that any code base I go through will pretty much have the same warts the same way. Does go fit every need? Nope on that front too but man its pretty good and what it was designed to do.
> can look at a codebase and quickly get a sense of what's going on
I don't even need to look. Error checking is going on :D
As it should be! Shame they didn't make the error type a sum type that forced you to check it isn't an error. I prefer that to exceptions. But the product type that Go uses is ok.
70% of any code is handling errors. It's awful (and by handling I mean just passing them to the calling function).
And no exceptions make it really easy to ignore errors instead. Which is much worse.
Go is an OK language, and that's the point. An average programmer can write web services in Go without too much trouble.
Goroutines mean the programmer doesn't have to struggle to avoid blocking, or decide what should be "sync" and what should be "async".
Garbage collection means the programmer doesn't have to think much about ownership.
The error handling is a bit verbose. If Go had generics from the beginning, it could have benefited from something like Rust's Result type and "?" operator. That's effectively the same thing as Go's usual error handling, but more concise.
The criticism that Go is for "average programmers" almost universally comes from people overestimating how good they are.
I've worked directly with hundreds of highly paid programmers over decades. I can count on my fingers the number that would be at all constrained by using Go where it's a suitable choice (an important caveat).
The rest would have their work highly improved by using Go as much as possible, precisely because it's harder to write bad code in Go compared with most languages.
> and "?" operator.
To be fair, Go almost go the "?" operator like Rust. The proposal was well received. It wasn't the lack of "Result" that held it back, it was that nobody could figure out how to deal with the handling problem. It's not entirely clear what the Rust-equivalent is for all the other moving pieces associated with "?" (e.g. its defined traits). The latest "?" proposal forces a handling body on each use to address that issue. But at that point all you've done is added another way to write "if". Is that a win?
> If you want to add two days you can't just call Add(0 /years/, 0 /months/, 2 /days/), you need to use AddDate.
Sorry for nit-picking, but 2 days is a duration of time, and you can add one using Add, like this:
I agree about FFI being a weak point in go when you need to interact with other languages, but it can hopefully be mitigated by using tinygo, which is still being developed.Does this do what you want across/during DST switchover?
What do you want to happen during a DST switchover in this case?
What “sorta just happened” is Swift 6. I knew Swift, I loved Swift. Swift designed itself right away from what I was willing to put up with anymore.
Go in comparison seems unnecessarily pedantic, occasionally too verbose, and occasionally unnecessarily terse. But it never makes me lolsob minus the lol
> Swift designed itself right away from what I was willing to put up with anymore.
I swear I can't tell if this phrase is supposed to be positive or negative? Then again, I'm not a native speaker...
This feels like SAT/ACT reading comprehension question! I think that "But it never makes me lolsob minus the lol" means he meant he doesn't like the changes Swift 6 made.
I think SwiftUI is to blame. In order to make a nice terse DSL for WWDC talks they really contorted the language design.
I hate go. Specifically I hate go packages. It was poorly designed. Any time you create a package in go you can’t have circular dependencies. Which makes sense right? When you make a package in rust or node or python those packages can’t have circular dependencies.
If you have two files in go or any other language, circular dependencies are usually permitted which also makes sense. It’s only packages that don’t permit this.
But guess what? You create a package in go simply by putting everything in a folder. So people when writing a go project they often make dozens of packages just by the is ocd need of organizing things into folders.
But when you organize things into folders the files in those folders aren’t project files anymore. They are different packages and you can’t have circular dependencies.
And that’s a big problem as this design conflicts with programmer instincts on how to organize things.
We organize things by meaning and name and subject matter. But simply using folders in go we are forced to organize things via dependencies and this causes a clash and makes organizing things 10 times harder than it should be in go.
Imagine I have a chicken and an egg. Chicken goes in the animal package and egg goes in the food package. It is now fundamentally impossible to make a chicken lay an egg and an egg hatch a chicken at the same time. Our tendencies to organize things via meaning has clashed with this requirement of organizing things via dependencies.
Does it make any sense at all? We probably don’t want packages to be interdependent in this way. But the stupid thing is just be creating a freaking folder we create this problem in go and it doesn’t make sense because if the files are outside of a folder you don’t hit this problem.
I would say go is actually poorly designed. I can’t think of any popular language that has this strange inconsistent problem.
It actually causes huge headaches in organizing things in go but idiot developers when they hit this circular dependency problem they literally believe the documentation when it tells them that they aren’t organizing things correctly and they need to think harder about design. So in a lot of go shops they spend an inordinate amount of time naming folders and organizing shit just to get their ocd need of organizing things by name to jive with gos requirement to organize things by dependency.
In the real world things can be in different categories and also interdependent at the same time. Go’s universe doesn’t allow this.
I’m here to tell you that It does make sense to put eggs in the food folder and chickens in the animal folder. Go is just stupid in this area, not you. Don’t believe the idiot who wrote that in the docs.
> Imagine I have a chicken and an egg. Chicken goes in the animal package and egg goes in the food package. It is now fundamentally impossible to make a chicken lay an egg and an egg hatch a chicken at the same time.
I am familiar with people eating unfertilized eggs. I am even familiar with balut. But I am not familiar with the delicacy of eating an egg as it hatches. If you eat an egg as it hatches, wouldn't that actually be eating a chick (the chicken) rather than an egg?
I suspect the real problem here is that your domain model is ill-conceived. In fact, while Go has no lack of faults, I suspect the vast majority of the complaints directed towards it come down to the limited feature set not enabling papering over bad design decisions.
Your joking. I have a hundred food items and a hundred animal items. Cows produce milk. Chickens produce eggs. Eggs produce chickens.
It makes 100 percent sense to make two folders. Food and animals.
‘I suspect your “domain model” is ill conceived’??? You buy into that? Don’t follow what those idiots tell you.
You realize that we use these actual separations between food and animals in the real world? And now because of some design philosophy the domain model is ill conceived? The result is a more convoluted domain model to make things flow.
Actually I think this is an easier way to explain it to you. The “domain model” or aka categorization of food and animals works in reality and every other language because it’s not a bad design decision. The reason why it doesn’t work in go is because go is the bad design decision.
> You realize that we use these actual separations between food and animals in the real world?
The distinction between "animal" and "food" isn't necessarily faulty, but an egg considered to be for food and an egg considered to be for hatching are deemed distinct types in the real world. An egg from the grocery store is not going to hatch no matter how hard you coddle it. This is not the same type of egg that will hatch a chicken. Reusing the same model for both types of eggs does not work in the real world, and thus it stands to reason that it doesn't work in software either.
Revisit the model in a way that actually reflects the real world and your circular dependencies are bound to disappear.
Who said it has to be a grocery store. I’m in a farm. Some eggs I eat, some eggs I hatch. I have a food bin and an animal pen. Two different boxes for two different things.
Makes sense in the real world. Why doesn’t it make sense in go? You literally think there’s some elegant underlying principle here having to do with categorization do you? It’s mind boggling to me how arbitrary decisions made by the designers of go are thought of as fundamental design rules.
When people use the word “domain model” I know the complex nomenclature has actually led them astray. These are just folders and categories. The term domain model doesn’t change a thing other than making it sound smarter.
> Some eggs I eat, some eggs I hatch.
Exactly. Like you describe, this domain model has at least two different types of eggs. This matches what was described in the original comment. But, in the original comment, the author was trying to shoehorn all functionality into a single egg type. And, unsurprisingly, it didn't work for him. It wouldn't work in the real world either.
Revisit the model in a way that actually reflects the real world and your circular dependencies are bound to disappear.
Come on. Think about it. The egg hatches or is eaten.
Now you have HatchingEgg, and EatableEgg types. Let's be real about where the shoe horning is happening.
Essentially. More likely, without straying too far from the original, you would have something like `animal.Egg` and `food.Egg` (although it is likely that you would want to stray from the original in an actual setting).
You are going to do that type of thing regardless of the language (and especially in languages with more advanced type systems!), even where circular imports and other code structure methodologies are supported. You wouldn't want your `scrambleEgg` function to accept an egg intended to be hatched. The bones are unpleasant.
>You're going to do that type of thing regardless of the language
No I'm not. I'm going to make one Type and it's going to be called EGG. You're going to make an Egg interface and divide it into two subtypes EatableEgg and HatchableEgg.
I don't think you picked up on it but I have one thing, you created 3. And you created 3 just to avoid an orthogonal issue of dependencies. You aren't getting it. In my statement above I was implying that YOU are shoehorning things, NOT me.
Also you have to create a new package now called Types and that's where you put your Egg interface. So go forces you to create a third meta category while other languages I can choose whether I want that or not.
Or I just don't use folders at all in go. Then this issue doesn't even exist. It only exists because I happened to want to use folders, but I can get rid of circular dependencies simply by organizing everything by files.
> No I'm not.
Enjoy your scrambled bones, then, I guess. But even if you want to model the world as having only one egg type, that type decidedly does not cleanly fit into "animal" or "food", so your taxonomy doesn't work.
> You're going to make an Egg interface and divide it into two subtypes EatableEgg and HatchableEgg.
This seems like a poor assumption. In the real world, if an "EatableEgg" is fertilized into a "HatchableEgg" there will be a type conversion. What was an "EatableEgg" is now a "HatchableEgg" and the "EatableEgg" no longer exists. What do you need the interface for?
> I don't think you picked up on it but I have one thing
Right, but we're talking about types, not things. Things can come in many different types. You even told us a story about how you consider eggs, which are the same thing, to be of different types, so this didn't go unnoticed by you earlier.
> Also you have to create a new package now called Types and that's where you put your Egg interface.
If you were to have a such a thing, wouldn't it go in something like "reproductive structure"? "types" is a strange fit alongside "animal" and "food".
Revisit the model in a way that actually reflects the real world and your circular dependencies are bound to disappear.
>Enjoy your scrambled bones, then, I guess. But even if you want to model the world as having only one egg type, that type decidedly does not cleanly fit into "animal" or "food", so your taxonomy doesn't work.
scrambled bones my ass. There's no instance where that happens. I have a scramble function that takes an egg type as a parameter I don't know where you're pulling the scrambled bones from.
I don't model the world that way, that's not my objective. I live on a farm, I'm just modelling things the way I modelled it on the farm. I organize my eggs into the food box and my hens into the chicken pen. I do this in the real world. I want to do the same thing in my programming and I can't. Understand?
>Right, but we're talking about types, not things. Things can come in many different types. You even told us a story about how you consider eggs, which are the same thing, to be of different types, so this didn't go unnoticed by you earlier.
If you make two different types, and you instantiate those two types you have two different things. I don't think you're getting it. Those two different things cannot represent the real world thing because the real world thing can hatch OR be eaten. The things you created can only do one or the other.
>If you were to have a such a thing, wouldn't it go in something like "reproductive structure"? "types" is a strange fit alongside "animal" and "food".
You have to do this because your Egg interface can't go into animal or food becuase it will cause a circular dependency.
On my farm I don't have a bin or a box (aka type) called reproductive structure or types. I don't need to do this in the real world but I need to do this in go because Go is poorly designed.
Look, I can make a folder called animals and food and I can put anything I want in those folders in other languages. In go, I can't. It's that simple. Go makes this arbitrary restriction out of nowhere and it makes things worse. You're shoehorning new nomenclature and reproductive concepts into your organizational scheme while I can run with what's done in REALITY with significantly less primitives.
>Revisit the model in a way that actually reflects the real world and your circular dependencies are bound to disappear.
I think you're out of touch with the real world. When you type things in the real world (aka putting things into a box or a pen) you don't care about dependencies. I don't put my eggs in the chicken pen because they came from the chickens. I put it in the food pantry. Understand? Folders are designed to do the activity I just mentioned, they are not designed to form some complicated taxonomy of unnecessary concepts.
> I have a scramble function that takes an egg type as a parameter I don't know where you're pulling the scrambled bones from.
Then you're in for an educational treat. Believe it or not, inside a "hatching egg" is a little chicken! And chickens have bones. As your scramble function accepts any type of egg, it may accept an egg that contains said bones.
But it need not be that way. With consideration about your types, you can have the compiler ensure that you only allow "eating eggs" into your scramble function. Just like we normally take care in the real world to ensure the same kind of separation because most people would not be amused if their scrambled eggs contained bones. Apparently you roll the dice, but know that is unusual.
> and you instantiate those two types you have two different things.
I don't disagree, but I don't know of any programming language that has a concept for things. Certainly none that you would actually use for production software. Types are the pinnacle of popular computer science thus far. At some point you are going to have to accept that software and the real world aren't exactly the same.
But revisit the model in a way that actually reflects the real world and your circular dependencies are bound to disappear.
> I organize my eggs into the food box and my hens into the chicken pen.
And the roosters? Those eggs aren't hatching otherwise...
> You have to do this because your Egg interface can't go into animal or food becuase it will cause a circular dependency.
But, most importantly, because it would otherwise be confusing for anyone else who has to work on the code. "Why is a chick about to leave its shell considered food? I don't know anyone who considers that to be food." is the first thing they are going to say upon encountering this codebase.
Code is written for humans. Remember that.
> I put it in the food pantry.
You may put "eating eggs" in the pantry, but I can't imagine you put "hatching eggs" in the pantry. So why are you putting "hatching eggs" in "the food pantry" when taken to the computer? Again, this model you have doesn't even work on a human level, never mind any technical constraints when applied to software.
> Then you're in for an educational treat. Believe it or not, inside a "hatching egg" is a little chicken! And chickens have bones. As your scramble function accepts any type of egg, it may accept an egg that contains said bones.
Except the egg I modeled doesn’t do this and I generally don’t encounter this in the real world so I don’t have to account for it. So your example is meaningless pedantic pointlessness.
> I don't disagree, but I don't know of any programming language that has a concept for things.
The concept of type is a categorization. Instantiating a type is creating a thing that goes in that same category. You’re not getting it because this statement is categorically incorrect.
> Types are the pinnacle of popular computer science thus far. At some point you are going to have to accept that software and the real world aren't exactly the same.
Of course not. The food category and the animal category are not part of the real world. The universe doesn’t categorize things. Categories are made up bullshit by human beings. They aren’t real in any sense. You’re just not seeing this.
What’s going on with golang is that it’s saying your arbitrary categorization of things IS REAL. When you don’t categorize it the go way you actually did something fundamentally wrong is what go tells you. And your issue is, you believe in this go philosophy. You think that what go tells you is real.
Go says a chicken cannot go into the animal category and exist with an egg that goes into a food category. Go says if you try to model things this way something is fundamentally wrong with your understanding of categories and reality and it throws a compiler error to tell you so.
The problem here is that you believe this bullshit. You think golang actually said something profound when really the designer was just an idiot.
> But, most importantly, because it would otherwise be confusing for anyone else who has to work on the code. "Why is a chick about to leave its shell considered food? I don't know anyone who considers that to be food." is the first thing they are going to say upon encountering this codebase. Code is written for humans. Remember that.
You’re out of touch with humans and you don’t see it. As a human I don’t think about accidentally finding chicken bones in my eggs so I categorize eggs as food and chickens as animals. What’s inhuman and overly pedantic is to consider every possible case and permutation of chickens and eggs and try to model everything in your program. How detailed do you want to go? Everything is made out of atoms so everything belongs in the same category? Be real. You are arbitrarily increasing the resolution of what is encompassed in your model to make some arbitrary and false point about domain modeling.
What you’re not seeing is The model is made up by me to simplify things. In go I can’t make the model simple because I have to model things according to golang.
> You may put "eating eggs" in the pantry, but I can't imagine you put "hatching eggs" in the pantry. So why are you putting "hatching eggs" in "the food pantry" when taken to the computer? Again, this model you have doesn't even work on a human level, never mind any technical constraints when applied to software.
I put eggs in the pantry. I can still hatch those eggs if I want. I can eat them too. You put technical constraints on reality for no other arbitrary reason other then to satisfy gos dependency rules. In your model you restricted your abilities. I changed my mind and I want to use half of my eggs in the pantry for hatching. Your “domain modeling” doesn’t allow for this.
Look this is all arbitrary bullshit. You’re just going to keep making up cases where your model is right but for every case there is also a case where mine is right and yours is wrong. This is because there’s an infinite amount of perspectives to look at this subject. How we choose to look at that subject is an arbitrary choice. You want to make two types of eggs and categorize it that way?be my fucking guest. Most people who farm don’t do it this way but go ahead.
Golang takes this arbitrary choice away. It tells everyone that eggs don’t exist. It’s either a hatching egg or an eatable egg. You live in a universe of golang that tells you that this is the only fundamental way to categorize things: by a dependency tree.
Because we don’t know which came first, the chicken or the egg. In your universe of golang that you live in your saying it’s a fundamental truth that we can therefore never categorize chickens as animals and eggs as food. We must destroy the concept of an egg and realize there are two types of eggs. One that is eatable food and Another that is a hatchable animal.
That’s how your brain sees reality. If this is the case so be it. Can’t be fixed. I can’t change you. More likely you’re just stubborn.
> So your example is meaningless pedantic pointlessness.
Okay, but it is not my example. It was the example given in the very first comment. It what was asserted to have created the circular dependency.
> The concept of type is a categorization.
Okay, sure, but it remains that things are decidedly not categorizations. And, as before, there is no known programming language that has a concept for things. It's an interesting idea! I can see that programming languages could benefit from having such a concept. If you happen to be looking for a CS research project, this just might be it. But if you just want to write production software using existing tools you're not going to find it.
> I can still hatch those eggs if I want.
Your pantry has suitable environmental conditions for incubation, does it? The environment necessary for incubation is not ideal for other foods normally kept in a pantry, so this doesn't really add up. But, hell, let's pretend you do. Do you still not want some way to identify those eggs being incubated and those eggs ready to eat for others, if not yourself, looking into your pantry?
> Golang takes this arbitrary choice away.
Okay, but we're not even talking about Go. We are barely even talking about programming. This model of yours doesn't even fit the real world. Come back with a better model and you won't leave other humans scratching their heads and you might even find the technology won't fight you so hard at the same time.
Actually, eggs bought in a grocery store absolutely can hatch.
https://www.youtube.com/watch?v=bMQ99Y64t90 and many more such examples
That is an unusual looking chicken egg.
I've been writing Go for about 5 years now and can confirm cicular dependency issues were typically handled buy a small refactor or considering where the borders of the domain should _really_ be
I think the issue the OC was hinting at is that the borders of the domain are forced to be the files. Having a folder for "food" and a folder for "animal" should not imply that they are in different packages by the OC's reasoning. Perhaps both of them should be the same package, but to achieve this, you can't organise it into folders.
_This_ is what OC was complaining about. Considering where the borders of the domain should be is a second handed issue that is only due to the bad choice of having packages be determined by folder structure, which is the complaint of OC.
Right. And people conflate the issue. Especially go developers. They can’t see the difference.
They naturally create folders based off of semantic meaning instead of packaging. Then they hit the dependency issue and they think creating the folder was a domain modeling problem when really they just created the folder based off of semantic meaning and not dependencies.
The whole thing has confused go developers. They think putting things into the right folders actually has some deeper design meaning when really it’s just to make it easier for humans to find shit.
This bothers me from time to time. When I first started using Go, I think I complained to the Go team about it. They told me to think more carefully about where package boundaries are. Having the smallest package possible doesn't necessarily mean the design is good.
I inherited an app from a coworker, and he just put every file in the main package. This was actually fine and caused no problems.
These days, I still have an addiction to making tiny packages, which the Go team would likely not approve. But I'm a big girl and I do what I want! The workarounds I use are:
1) A `types` package. type Chicken struct { Children []Egg }; type Egg struct { Mom, Dad Chicken }.
2) A `utils` package. Let me explain. I would never name a package `utils`. That's illegal and I'm told that the compiler will explode into a thousand pieces if you try. All the king's horses and all the king's men wouldn't be able to put it back together again. So don't test it. Just trust me. But, you can definitely have something like `package hatchery` with a functions related to collecting eggs from chickens. Or you can have `package eggutil` for packing eggs, frying eggs, and shipping eggs.
I would say that I typically have a `types` package (called `types` if I'm hand-writing the code, or myapppb if it's protobufs like at Google), and then structure the rest of my app as model/view/controller. The model puts these types into the database. (Call it `eggdb`.) The controller does whatever business logic you care about (getting chickens together to fertilize eggs, call it `husbandry`). The view calls into the controller to implement your API / gRPC endpoint / beautiful Gtk+ GUI / whatever.
And then you don't really have problems with circular dependencies.
Having said all that if someone puts it all in main.go I'd probably approve the PR. The Go team let my coworker do it in 2014 and nobody died.
You’re not seeing the problem. You spend an inordinate amount of time trying to organize it and you come up with an elegant way so the catharsis of that solution makes you think you found the path to nirvana. Really you’ve been duped.
This restriction makes sense for packages. But not for folders. The problem arises in go because folders and packages are the same thing and that’s stupid because people don’t typically use folders as if they were packages. Folders are typically used just to categorize things of similar meaning or naming or whatever you want.
No other popular language has this issue. I can choose to organize things how I want in any other language. I have the freedom. I can make a folder A and B and C and D and just organize things in ABC order. But can’t with go. If I dont want to do this in any other language I have the option to create a package.
Creating a package and creating a folder are completely orthogonal tools and concepts and the genius who invented go just made a horrible design decision here.
> that’s stupid because people don’t typically use folders as if they were packages. Folders are typically used just to categorize things of similar meaning or naming or whatever you want
You might do that. That does not mean everyone organizes things like this.
What your chicken/egg example fails to realize is that things rarely just belong in one category. Chickens can be animals or food.
To me it just sounds like you are trying to write another language while using Go. Of course you will see problems. Just as you would see problems trying to write Go code when using Java.
In the end it comes down to this: if the language does not fit your mental model on how to do things, don't use it. But don't go around shitting on the language just because you are used to using folders differently than other people. Using a language also means learning and using the conventions of that language.
>You might do that. That does not mean everyone organizes things like this.
Most people do organize things arbitrarily based off of their own categorizations and opinions. In fact such an overwhelming majority of people who use operating systems do this that it's pretty much universal.
>What your chicken/egg example fails to realize is that things rarely just belong in one category. Chickens can be animals or food.
So what. I choose not to eat my chickens. So I Choose to categorize it in a way that makes sense to me. Why should I conform to your point of view? Why should I conform to golangs point of view? Why can't you and I choose how to do it?
>To me it just sounds like you are trying to write another language while using Go. Of course you will see problems. Just as you would see problems trying to write Go code when using Java.
No. I'm complaining about go. I think it's dumb. I'm not doing OOP here, I hate OOP. Go is waaay better. This is orthogonal to that problem.
>In the end it comes down to this: if the language does not fit your mental model on how to do things, don't use it. But don't go around shitting on the language just because you are used to using folders differently than other people. Using a language also means learning and using the conventions of that language.
Yeah that's how all complaints and criticisms are sidelined. "You don't like it, don't use it. Use something you like." Obviously.
I'm doing exactly that, while saying that go packages are a poor and horrible design decision.
I think a lot of people hated java, and found go and they think because go is so much better then java then that means go can do no wrong.
I don't think you realize there's stuff way better then go out there. But that's besides my point. What I use is separate from my point: Golang packages are poorly designed.
> I don't think you realize there's stuff way better then go out there.
Every language makes trade-offs. For you the trade-off Go makes is bad. I disagree. I like some languages better in certain parts, while I prefer Go's solution in other parts. It's all preference, there is almost never an objective "better" or "worse" like you seem to think.
> Golang packages are poorly designed
Agree to disagree.
Then you disagree with the majority of people. That's my point. This agree/disagree side lines the fact that people can be right or wrong. My claim is not only do you disagree, but you're wrong.
Majority of what people? Get off your high horse, your opinion is not a fact. Give me objective metrics to measure how "good" a language or language feature is, otherwise you are just wrong.
I'm looking forward to trying out your own perfect language that is objectively the best for any use case ever and no one can find any faults with it. Because surely such a language exists.
>Majority of what people? Get off your high horse, your opinion is not a fact.
Just look at reality and how people organize things everywhere. It's arbitrary. Adults are placed in offices and kids are placed in schools. Then on family trees kids and Adults are organized by dependency. How people organize things in the REAL world is AN arbitrary choice. But in GO you have NO CHOICE. It has to be by family tree. That is NOT an opinion. Everything I said is FACT.
The metric is basically the entire world and everything in it and how we fundamentally type (aka organize) things within it. But if you want to get more specific just look at every other programming language except go and look at how the entire world uses folders in operating systems. These are tools to allow people to arbitrarily organize things.
>I'm looking forward to trying out your own perfect language that is objectively the best for any use case ever and no one can find any faults with it. Because surely such a language exists.
Don't have one. I'm just commenting on what I hate about golang.
So a hand-wavy "look at the world bro" instead of actual metrics. Got it.
> I'm just commenting on what I hate about golang.
No you're not. You say Go (or a part of Go) is bad, which is vastly different. If you stuck to "I don't like it", you would not have gotten so much push back, but you insist in being right and everyone else is stupid and wrong.
Because metrics don’t exist. If the only way you can move forward and know something is because someone conducted some sort of statistical experiment with data analysis on it then I don’t even know how you get up in the morning. Do you need science to prove that when you jump off the bed there’s a floor that your feet will land on?
No you don’t. You’re not an idiot. It’s common sense that allows you to do this and it’s common sense that will allow you to see that the entire world works the way I say it does. You don’t need science for this. At least most people don’t. Maybe you’re different and you need science to do an analysis for you before you open any door to make sure reality still exists behind it.
> No you're not. You say Go (or a part of Go) is bad, which is vastly different. If you stuck to "I don't like it", you would not have gotten so much push back, but you insist in being right and everyone else is stupid and wrong.
You’re right. Let me rephrase what I meant. I’m commenting about what is fundamentally terrible about golang and if you disagree you’re stubborn and biased.
For me it starts with allowing only source code packages, followed by direct URLs to source code repos directly on the source files, instead of an indirect mechanism.
To be fair, Go modules provide such indirect mechanism, by rewriting source files, with redirection information, really? Yet another hack.
This is solved simply by having a chicken.go and an egg.go in the same package.
Go appears to encourage large source files, since it allows you to define multiple "classes" (types with methods) within the same file. This is probably intentional to combat the complexity of having otherwise cohesive logic littered across several small files in OOP-first languages.
But I like your example and get your point. What if chicken.go and egg.go diverged a few levels earlier in the dir path. All the Go repos that I've seen in production have very flat folder structures, so I guess this is just how Go is written.
I joined a company who wanted really clean code in their code base. That meant really good categorical organization of things into folders based off of semantic meaning about what those primitives did. You can imagine the nightmare that caused.
People simply just didn't understand what I was saying that there was no inherent meaning in trying to make our naming conventions with folders work with go's dependency rules. They thought the work of untangling all the dependencies, folders and the naming was some quest towards a perfect design.
You can also see in this thread, that a lot of people debating the issue with me believe the same thing. They think this arbitrary rule is speaking to a fundamental design axiom and they use big words to call it "domain modelling"
Agree that the language has to be analyzed under the original design goals to be fair. Disagree that Go is definitely well-designed in that circumstance, however. A major weakness of this essay is that all perceived issues with Go have to be irrelevant to the original design goals, quite absurd if you think about that.
I would instead say that Go had big things right, while smaller things might be not as right---but not exactly "incorrect". For example explicit error handling is the big idea that Go got right, but both Rust and Zig did a lot to refine error handling experiences; comparing them with Go is just a non-starter.
> Agree that the language has to be analyzed under the original design goals to be fair.
I mean if you're analyzing languages in a void, I guess, but we're not. We're looking at languages as tools to solve programming programs. And almost nobody has the problems that Go was designed around, so the original design goals are kind of irrelevant.
I mean, how many of you have worked on a million line codebase? If you haven't, why would you care that compiling millions of lines quickly was a design goal for Go? Yes, smaller codebases are also faster to compile, but partial compilation works just fine for that in other languages.
I should note that I do see merits of "unfair" analyses, in fact they are probably as important as fair analyses. It's just that such analyses would inevitably cover what designers originally didn't even think of, so such analyses should ideally be considered sort of bonuses instead of main criticisms.
> But don't say it wasn't (well) designed.
I’ll say that it isn’t well designed. I draw a line at treating an unused import as an error.
The creators of Go had opinions, and so do I. That’s how it works.
Go is not well-designed because it is simple. It is poorly designed because it fails to be so. All the weird interactions with nil for example.
I feel this article misses the point in many cases. Take, for example, error handling. It's labourious in Go, but that's not the only issue. I'm not going to go into all the details in this comment (others have covered this at length) but:
1. Go's error handling has four possible cases (result and no error, no result and error, result and error, no result and no error) when only two make sense.
2. A consequence of the above is that it requires null values (nil in Go)
I don't see how this can be considered good design.
> 1. Go's error handling has four possible cases (result and no error, no result and error, result and error, no result and no error) when only two make sense.
A partial success, that returns both a result and an error definitely makes sense. For example, you’re streaming data over a network, receive some bytes, but get disconnected before it’s complete. It would make sense to have both the data received so far as well as the error, in order to resume.
I’m sure there’s an appropriate situation for the 4th case as well.
Does it make sense for every function to have those four cases, though?
This is why it's table stakes for a decent type-system to have both product-types and sum-types. They comprise both sides of the coin, and you use the one that actually makes sense for the piece of code being written.
A justification for Go's deficient type-system I've seen repeatedly over the years, along the lines of "Well actually the occasional function can legitimately return both a result AND an error" is nonsensical. It's basically excusing giving 98% of error-returning functions a semantically wrong return type (a tuple) because it happens to be correct in 2% of cases.
Every function should have a semantically correct return type! If a product-type is occasionally correct for an error-returning function, use one just for those functions! And use a sum-type for the other 98% where that is correct for them!
Exactly. One can think of examples where these cases make sense, but there are many more examples where there are more cases (e.g. there many be multiple kinds of partial successes) or fewer (most functions, which either succeed or fail.)
Also, the design requires null values. A point which the replies have ignored.
Your example doesn't make sense because that's not how the syscall works.
Go's Write makes a promise to not do short writes except on errors, which means it has to be a loop over write(2).
Streaming data, there was no error but also no new data available.
That is an error :)
I can think of situations where all four make sense. Not sure what you mean?
result+no error: data returned, no error happened. - Typical function call where you want some data.
no Result+error: nothing to return, error happened. - That typical call failed.
result+error: partial result, up to error. - Wrote x bytes to disk, then ran out of space.
no result+no error: nothing to return, no error. - Data for a passed query. Nothing came back, but that's not an error.
But not every situation. Make illegal state unreprestable is a good ideal.
> Go's error handling has four possible cases
If you consider (T, error) to be logically coupled, there is actually effectively an infinite number of cases. (T, U, error), (T, U, V, error), (T, U, V, W, error), etc. Although I suspect this take is suffering from trying to project idioms from another language onto Go.
(T, error) is just a special case of (T, E).
At least we can agree it is (T, E) and not (T or E).
It looks like the arguments on Apple ecosystem when arguing for design faults as virtues, us are unable to understand without enlightenment, like we are holding it wrong, tablets don't need pens, don't understand keyboard design, ...
From what I've seen, the people who complain about go's design are people who are more concerned about the ways in which it is not correct (by some definition) and less concerned about just getting stuff done.
I don't "like" go (or any other programming language), but I'm able to get more done in it in a shorter amount of time than any other language, the created thing consumes tiny amounts of resources, and most importantly when I _or anyone else_ goes back to change something weeks, months, years later it's easy to re-establish or gain contextual understanding.
These were the first lines of Go I ever wrote 15 minutes ago, as a response to the claim that Go had working enums. This is the kind of thing that makes me itch. https://go.dev/play/p/MMPMh7_U81-
I think "being simple" doesn't necessarily mean "must have subtle sharp edges and papercuts everywhere". Just like javascript I think it falls into a pit of being superficially "low complexity" or "simple", but all the subtle gotchas and unhelpful tooling (The fact a compiler can't help me realize I had added an entry to the enum, but forgotten to update a name list just means the whole feature is no better than just plain ints that we had in C since before I was born).
> I'm able to get more done in it in a shorter amount of time
That's because you know it better than other languages. It's certainly not a universal experience.
I like go, and I use it to get stuff done. I'd be able to get stuff even more done with sum types and pattern matching and better generics and iterator tools and it's a shame Go doesn't have these things.
ITT: apologists and haters.
Go is a bunch of preferences and aesthetics. It clearly appeals to a bunch of folks, others have a hard time putting up with it.
Everyone is free to both love and/or hate go. The language's spirit is not going to change. I'd much rather see more realism, honesty, and acceptance. Instead, this class of discussions always seems to become incessant "no u" ping-ponging.
Certainly not what I'd consider curiosity and critical thinking.
The error handling is explicit only in a limited way. As the author points out, you have to explicitly propagate an error condition, but you don't have to explicitly not-propagate it. Exceptions are reversed in this way: propagation is implicit, and not-propagating is explicit. I would prefer that handling is fully explicit in all cases, and not depend on supplemental linting or sheer discipline. It's a bit incongruous that golang compiler is so fussy about unused imports, but has nothing to say about silently ignored errors.
I also find that stack traces are generally more useful than plain wrapped errors. I want to know the code path in most cases where I am looking at an error.
More generally, it's disappointing that golang has so many idiosyncrasies given its philosophy of straightforwardness. I struggle to think of any useful, general purpose language that has fared better, though.
I think Go is a pretty good choice for getting something out the door quickly, without getting in the way too much.
The other week, I was working on two APIs: one for a team that uses Java primarily and another where I was free to pick the technology as needed and was looking in the direction of making it easy to deploy, even without containers, where I picked Go.
With Go's standard library (and a few packages here or there), I had something up and running within a few days and it mostly just worked. Things that I wanted to do were just an additional method away, which, while not as sophisticated as the various convention over configuration options out there, kept everything very discoverable and observable.
With Java, I had to setup a Spring Boot project and its security configuration mechanism (SecurityFilterChain, WebSecurityFilterConfig and lots of Filter implementations) and ran into issues where calculating file checksums for uploaded files broke because Files.exists() returned that files exist for ones with UTF-8 symbols in the name, whereas internally FileUtils.checksumCRC32() used file.isFile() which returned false. Then, I also needed -Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8 and for another mechanism had to look into JCE briefly, as well as configuring Maven to push packages to Nexus, as well as how to disable Spring Boot trying to connect to PostgreSQL and RabbitMQ while bringing up the app context for tests, or to connect to a special instance for tests (and hopefully eventually figure out how to automate a separate schema per CI run). Oh also deciding between OkHttp and Apache HttpComponents. Oh and also adding Apache Tika to the project, yet realizing that I can't add tika-parsers-standard due to some related dependencies.
Now, this isn't a completely fair comparison because the APIs didn't need to do 1:1 the same things and I could have chosen not to go with Spring Boot, but it's so entrenched that it's most likely the majority of projects that you'll see out there (I also like Dropwizard, but I haven't seen many people use it at all). That might sour some people's perception of using that language professionally: https://earthly.dev/blog/brown-green-language/
There are things about Go that are annoying, but it not having all of those levels of abstraction that you need to think about was comparably nicer. Plus, not needing to think about JDK versions and instead just getting a single file that you can ship. Oh, and on a related note, projects like Wails are also really nice (I tried Tauri, the builds were too long and the Electron bundles are a bit too big for my needs in some other projects): https://wails.io/
I like many of the languages out there (even Java and ones like .NET) but the projects and ecosystems around them can make them harder to use sometimes.
You were certainly bitten by the Java ecosystem, but I would agree that you hold some responsibility. Adopting Spring Boot so casually is really quite questionable. It's essentially opting to build your application within a giant, bonkers codebase. In the golang community there is a held value of using the standard library and tools, which is helped those being very good. I would suggest that Java is a better experience if you follow that same approach. If you want to serve HTTP, use the jdk.httpserver module. If you want to make HTTP requests, use java.net.http. If you want to interact with the filesystem, use java.nio.file. To build, use the included tools in the JDK. When it comes to cases where a third party dependency is warranted, such as use cases for Tika, you will have to deal with some questionable design decisions, but it's much easier when you have just a few libraries that you use by hand, and no insane framework trying to stitch everything together for you. For getting those dependencies, you are going to be acquiring them from public maven repositories, but I would suggest using Coursier or Maven Resolver instead of actually using Maven.
To me it looks like more lack of familiarity with Java.
Having used Java and .NET ecosystem since their early days to this day, I fail to see why the team wasn't able to deliver in the same timeframe, also Spring Boot isn't the answer for everything.
> If you see a function call in Go you know there is one definition you need to check.
...unless it's a method on an interface type. I get the point being made about overloading, but the above statement of it is a bit too strong.
I enjoy writing Java and C# so much more than Go.
No matter how many blog posts try to justify Go, it is a lost ship. Yes it is used by many companies (I still don't understand why), but that does not make it a well designed language.
> But secondly designing errors as explicit values has been a trend-(re)setter. Go, Rust and Zig have all chosen to use this approach.
Just because Go treats errors as values, you can't compare Go's error handling with Rust's. Rust provides clean abstractions with Algebraic Data Types, ? operator over Result type to reduce polluting code with if-else nonsense in every single function. I bet atleast 30% of Go code in a project is error handling for the sake of handling.
Further Go's syntax inconsistency irritates me more. I still don't understand if I should put a comma or semicolon or nothing between members in a struct definition. Also
a := 10
...
a := 20 // error. a is defined.
a, b := 20, 30 // accepted. why ??
My company has 2 teams that use go for different reasons:
* Easy to crossbuild and you distribute a single executable (although of course someone managed to pull some dependency that was dynamically loaded and made our CLI depending on a running Gnome session)
* C developers who can't write C code that doesn't segfault every 3 minutes (because they never understood how threads work) can still have their ego but also produce somewhat useful code.
> a, b := 20, 30 // accepted. why ??
I guess it is a dept of error handling. Otherwise you have to write "foo,err1 := Foo();bar,err2 := Bar(),.."
So are you saying you would take on the initial cognitive load of Rust vs. Go for _every_ project?
Go’s unreasonable success is due to Google but also its main feature: low up front cognitive load.
A lot of that is from lack of async, or rather that async is hidden behind go routines.
> A lot of that is from lack of async, or rather that async is hidden behind go routines.
That’s an interesting take, but one that I’ve seen in multiple places in this thread. I find that introducing goroutines into a program immediately increases the cognitive load as now you need to think about joining, data races, and need to think about error handling differently (golang.org/x/sync/errgroup is an excellent library by the way).
Oh and did I mention deadlocks?
I encounter a lot of different software stacks when helping clients as a consultant. Consultancy is something I do next to my main job which is being a CTO of a small, bootstrapped startup. That sounds like a posh job but what it really means is that I do most of the technical work together with one other person.
I've always been opinionated about tools and languages. And we probably all have our biases and preferences. What I've realized doing consultancy is that it's rarely advisable for me to tell my clients to change whatever the hell they are doing. It's actively harmful to do so. And I encounter pretty much everything you can name. Including Go.
At the same time, it's taught me to steer clear of certain tech stacks for my own things. I know I don't like them by virtue of having seen others do things with them. Go, is one of those things that's arguably alright and widely used. But I wouldn't prefer it based on what I've seen.
The reasons are mostly non technical:
1) it's a transition language for people. They might start with something like Javascript or python and they might crave something a bit more rigorous. Go is an easy choice. It's there, it's widely used. It has great libraries, and so on. But I've seen people progress to other languages as well. It's a phase people go through as they develop themselves. And there's a definite "if you have hammer, every problem looks like a nail" thing going on with it. For me Go would be a step backwards, not forwards. I have some languages I'm interested in learning. Go is not one of them.
2) it's a Google language and Google has this big "not invented here" syndrome that often translates into them reinventing wheels. In this case C++. They embarked on their journey around the same time several other languages kicked off. Rust, Swift, Kotlin, etc. all emerged roughly around the same time (15-20 years ago). Swift is Apple's language and it suffers from the same issue. So, even though Google's take on this is interesting. It's not the only one or necessarily the best one (subjective of course). Rust and Kotlin are a bit less tied to one company. And both are widely used across most/all big tech companies and less entrenched in just one of them. Full disclosure, I prefer Kotlin out of these languages and actually use it for a lot of things people might think Go is good at.
3) It's a conservative language community. Despite being relatively new, the community seems to resist change a lot. Endless debates about exception handling, package management, generics, etc. and other aspects of the language seem to progress at a snails pace with people bickering endlessly about whether that's good or bad design. That stuff just bores me. Just fix it already. I don't want to read/hear endless reasons why something cannot/must not/should not be a thing because reasons that boil down "we don't like change".
So, no Go for me. But some of my clients use it and it's of course fine for them. I consult them on what they should build, not how to build it. A lot of that stuff works just about the same regardless of the language. Use what works for you, what you are familiar with, etc. Not what others tell you to use.
Go and Python are both firmly rooted in anti-cleverness
Cleverness makes for macho code golfing and aha moments in small codebases
Cleverness makes you quit your job when the codebases pass 10k lines
Not sure why this was flagged.
> Cleverness makes you quit your job when the codebases pass 10k lines
I've very much felt that in codebases that are hundreds of thousands of lines of code.
Essentially it becomes: "Is it easier to track down this bug and fix it over a week or two (hopefully the solution isn't needed by tomorrow night), or is it easier to quit my job?"
Sometimes the answer unironically is the second option.
That is funny, Python gets as clever as C++ and Common Lisp, those unaware of it aren't really reading Python docs.
Go is rooted in: "let's make a language with managed memory that still manages to crash with memory errors".
They should have hired someone that knew what he was doing.
I always say that you can write clever code on your free time.
When on the clock, make stuff that's readable and easy to manage.
> make stuff that's readable and easy to manage.
So, not go.
> Where you see a hack I see an elegant design.
https://groups.google.com/g/golang-dev/c/r4rdPdsH1Fg/m/JG3Wl...
> I can think of two examples of major aesthetic shifts in Go code that seemed ugly at the time but were adopted because they simplified software engineering and now look completely natural.
https://research.swtch.com/vgo-principles
> type Duration int64
https://github.com/golang/go/blob/519c0a2323700934cbec97b75d...
> default values
https://gotipplay.golang.org/p/jS6wwznSR7F