Three Months of Go from a Haskeller’s perspective (2016)
memo.barrucadu.co.uk
memo.barrucadu.co.uk
https://memo.barrucadu.co.uk/blub-crisis.html
> I have realised in recent conversations about programming languages, and in reflection of my very negative and kind of arrogant blog post about Go, that I have become trapped by the Blub Paradox. I have become dismissive of non-Haskell languages. I think in Haskell. Languages less powerful than Haskell are obviously too limited to get any real work done in, and languages with more advanced features than Haskell (like dependent types) are really just weird and not actually solving a problem I would ever face.
> The arrogance! Such a know-it-all!
> I have decided to call such a realisation a “Blub Crisis”. Haskell is my Blub. The only solution is to become proficient—actually proficient—in a different language, and once again learn a new way of thinking.
He slams go for not doing stuff the Haskell-way (e.g. pure code vs effectful code).
This is not how you approach new things
His only criticism that resonated with me was go get defaulting to head, which befuddled me when I first saw it, but that's a compromise too; one oriented to the large codebases that golang is designed for. It's actually a pretty good compromise compared to the propeller head Haskell dork "Things break backwards compatibility in Haskell, and the users just update their code because they know the library author did it for a reason." -aka this appears to be a statement from an academic wanker who obviously never had to ship on a timeline in his entire fucking life.
Golang has extremely basic tools for anything beyond formatting/compiling compared to most modern languages. This is just a limitation of the current ecosystem, not something you can say has tradeoffs (except of course for the Go team's time/prioritization of features).
Delve is getting better, but it has a LOT of ground to cover before it is half as useful as gdb or a java/C# debugger. Same goes for the profiling tools, especially on the CPU side.
I agree with everything the author said after having spend some time with Go.
Heck, I come from a C background, and that's a complaint I have about Go. Sometimes you want to have a function accept a pointer to a large structure to avoid copying, but have the compiler prevent you from making any changes. In C you'd write "const"; in Go there's no way to do that.
But Haskells view on this topic is far far more opinionated than just supporting 'const'.
To elaborate this point I'd say that the most important practical use of all that "monad mumbo-jumbo" in Haskell is that you can tag your functions with what they can and can't do and then the type system tracks this for you:
-- pure function
f1 :: Text -> Int
-- can fail
f2 :: Text -> Maybe Int
-- can read from some MyEnv record
f3 :: Text -> Reader MyEnv Int
-- can keep a set of bool as state around
f4 :: Text -> State (Set Bool) Int
etc... and of course to go nuclear: -- can do anything
f5 :: Text -> IO Int
The tracking part is that you can't call f5 from within f1, the type checker says no. It enforces separation between all these various effect boundaries.Also we don't have to stop here. A natural next step is defining exact effects one is after. For example say I want my function to be able to get some entity from a DB:
-- any type that is an instance of Entity can identify itself by uuid
class Entity e where
identify :: e -> UUID
-- The effect we want, ie. get an entity via its uuid
class (Monad m, Entity e) => GetEntity e m where
getEntityById :: UUID -> m (Maybe e)
now we can say things like: data User = MkUser {uId :: UUID, uName :: Text}
-- a User can identify itself
instance Entity User where
identify = uId
-- as User is an Entity so we can get it via its uuid (if it exists)
getUserById :: (GetEntity User m) => UUID -> m (Maybe User)
getUserById = getEntityById
Also notice how getUserById does not say anything about IO or a DB. All it states is that whatever context it will run in that context must know how to get a user via its uuid. You can then plug in whatever actual context you want, say: newtype Prod a = MkProd {unProd :: ReaderT ProdDB IO a}
deriving newtype (Functor, Applicative, Monad, MonadReader ProdDB, MonadIO)
-- get a user from a DB for real
instance GetEntity User Prod where
getEntityById :: UUID -> Prod (Maybe User)
getEntityById eid = do
ProdDB {..} <- ask
-- query the DB, etc...
or newtype Mock a = MkMock {unMock :: State (Map UUID User) a}
deriving newtype (Functor, Applicative, Monad, MonadState (Map UUID User))
-- get a user from a mock DB
instance GetEntity User Mock where
getEntityById :: UUID -> Mock (Maybe User)
getEntityById eid = gets (Map.lookup eid)
All in all the programmer have fine-grained control over what various parts of their code can or can't do. type FooContext m = (GetTime m, GenUUID m, Log m, Auth m, ReadDB m, WriteS3 m, BarApi m, etc...)
foo :: (FooContext m) => UUID -> m Foo
The main downside is ergonomics, ie. the amount of boilerplate needed, but personally I don't think that's a high price to pay.In Rust, for example, mutable data is allowed, but, when the owner of mutable data shares a reference to it, it gets to decide whether the borrower is also allowed to mutate the data. This doesn't eliminate the more challenging things you can do with shared mutable variables, but it does mean that enabling them requires mutual consent.
Nim does an interesting thing, too. It has a two-color function mechanism where "procedures" are allowed to have side effects, and "functions" are not. But even functions are allowed to use mutable variables behind closed doors. That can arguably be an ergonomic win. Many people find that, within a sufficiently bounded context, an implementation that uses mutable variables might be more maintainable than a purely functional implementation.
The main reason Haskell goes even further, and bans mutable variables from the insides of functions as well, was never really about maintainability, per se. It was done that way because Haskell, as a lazy language, couldn't allow anything that might require statements to be evaluated in a particular order. That design decision turned out to lead to an impressive bounty of interesting and useful discoveries. But there also seems to be something of a tendency to swaddle the bathwater with the baby.
First, there's only so much I can hold in my brain at once. If the computer can do some of the tedious busy work, let it.
Second, the first perspective talks about catching errors and oversights. A sufficiently non-bad programmer could make do without. But static typing also allows for some programming techniques that wouldn't work without.
As a silly example, Haskell makes heavy use of overloading-by-return-type. You just don't the information about what return-type is expected available dynamically, it needs to be static.
There's also lots of optimizations that are safe only when the compiler knows more about your code. (Of course, a sufficiently non-bad programmer could write directly in the machine language of every processor she's writing code for..)
In practice, typing helps me a lot with eg exhaustiveness checking for my pattern matches.
The type system that dialyzer exposes is pretty weak. So not a good standard to measure all of typing against.
The reason I can say that: you don't have to provide proofs that a value has to be positive to get things to compile.
There's some interesting work on making such contracts work well in a lazy language with higher-order functions.
The world record 100m men's sprint for double-leg-below-the-knee amputees is now frozen at 10.57s (record set in 2013); omitting Usain Bolt who is clearly freakishly good even among world-class sprinters, the 100m world record is 9.74s.
I am very happy to use strong static type systems.
There are CVEs for use-after-free bugs in Firefox, IE, the Linux kernel and more. Rust's type system helps prevent those. There are CVEs for double-free bugs in OpenSSH, OpenSSL, and Kerberos. Again, Rust's type system helps prevent those. There are CVEs for null dereferencing in the Linux kernel, CUPS, and OpenSSL. Many languages have type systems that help preventing those.
There's many more classes of bugs that can be prevented by judicious use of robust type systems. Dismissing strong, static typing as "a crutch for bad programmers" is just plain wrong.
All of this, of course, assumes the only use for static types is preventing issues. Algebraic data types actually allow you to write simpler, more concise code in languages that support them. Typeclasses make for a whole model of polymorphism that's just incredibly painful to implement in languages that don't support them.
No, it's correct. The problem is that innocentoldguy doesn't realize that he is a bad programmer. (That's not an attack on him - we all are bad programmers.)
So here we are, bad programmers. Here's this crutch - or, less negatively put, here's a tool that can help. Maybe we can be somewhat better bad programmers if we use it.
In fact, if you want to be a better (less bad) programmer, learn to use the available crutches more effectively.
Or that planes are a crutch for bad travelers who can't fly under their own power.
Growing complexity kind of mandates a static strong typing at one point. Sure, complexity can be managed and reduced -- and distributed among smaller projects (i.e. splitting big projects) -- but even that has a limit.
There are other more modern libraries that defer the errors to runtime but also make them much more clear (like `norm`). Libraries like `boundaries` can also help you enforce, a-hem, boundaries in your code and making sure that you write less spaghetti code.
I personally stick to Dialyzer in personal projects but it's an uphill battle to constantly keep it happy while OCaml / Rust / Haskell will just immediately yell at you with a well-described error (well, Rust anyway, the other two can be vague sometimes).
Learning OCaml and especially Haskell (I've done some of both) requires understanding a complex type system and learning how to encode what you want in it. On top of that you have to learn to code in the 'functional' way - representing your data, pure code, recursion (or reduces), higher order functions etc.
When you write Elixir you eliminate most of the type system stuff, and you just have to learn to code in a functional way. There is surprisingly little 'language' to learn - functions, tuples, lists, maps, modules and that's about it. There is then the OTP (processes and supervisors) side of it, but that's separate (and if you're writing a web backend in Phoenix, mostly ignorable).
We're just (hopefully) wrapping up a 9 month project with a team of about 8, none of which had written much Elixir before the start (a background of mostly C). It has been a very smooth process, and definitely the right choice of language.
And I'll look for ways to use it at my $dayjob. Rust is an excellent language but I feel that for some pieces of our stack Rust is kind of a bazooka in a scenario where a 9mm pistol would do.
These days my ideal app is written in Elixir for almost everything due to the insane parallelisation and fault-tolerance that it offers (and the copy semantics that make shared memory bugs impossible) with Rust sprinkled at the critically important-to-be-fast places -- like working with sqlite3 for example.
All of that could be achieved with Rust or any other language really. But it requires tons of tooling, CI/CD jobs, Git hooks, static analyzers, linters, formatters etc. And a good chunk of all that doesn't even exist for many languages.
So I prefer to make my own blend that has the important advantages at the right places.
As soon as you have algebraic data types and parametric polymorphism, static typing is less onerous, so dynamic typing is less appealing.
What do you mean exactly with "liking" untyped languages?
I personally program mainly in both a ("cognitively demanding") typed and an untyped languages, and they have different use cases.
For relatively small programs, untyped languages are much faster to develop. A program I wrote yesterday in a few hours would have taken 2 to 4 times as much in a typed language.
On large libraries (I consider gaming engines as so), common wisdom suggests that typed languages make the project easier to manage (modify and so on).
There's obviously a wide range of use cases in between.
Besides the unstructured trees, another example, although more high-level, is interfaces. They're a concept required in statically typed languages (some languages they have a simpler design than others), but not in dynamically typed languages (which have duck typing).
If you have a couple of types that you need to abstract, in a S.T. you'll need to define an interface, the method signatures, which include the return types, and maybe (in lower level languages) the generic types and the super interfaces. Even something as (relatively) simple as generalizing a tuple and a matrix types will require some design.
In D.T. languages, zero effort is required. As long as an object responds to a method (name), it's game.
I think it's not realistic to assume that the concepts that need to be taken care of in a S.T. language are comparable to the ones in a D.T. language. And more concepts imply more cognitive load. And cognitive resources are limited :-)
I don't use go so I don't have an opinion if they made the right choice but I sympathize. I suppose you have to be Ken Thompson to not give a crap and say I'm going to create a language without support for generic programming in 2009. The closest thing would be a tenured professor but they wouldn't dare unless under a pseudonym.
any :: (Functor f, Foldable f) => (a -> Bool) -> f a -> Bool
tells me that `any` has to work across the whole collection `f a` (list/tree/whatever), because that's
how folding works, and that it will get the answer by calling the function on the collection's elements (the collection is a Functor, and the only thing you can do with one is `map` a function over it).[of course this is assuming that `any` isn't implemented as `any _ _ = True`]
[of course this is assuming that `any` isn't implemented as `any _ _ = True`]
but actually there are many more possibilities. For example, this function might return True if the provided data structure has exactly 5 elements, never using the provided (a -> Bool) function or mapping over anything.
Almost all of your “whys” have sensible, practical, answers. The practical bit is the sticky bit that gets set when you get “older”.
I don't think you can tell that right now. Statically-typed languages have gotten much, much better over the past 30 years (progressively), and over the last 20 years we've all aged, you know, 30 years, so right now I think there's a lot of correlation there rather than necessarily causation. I have also switched to static typing as I've "gotten older", but I would still totally rate things in the order "1990 static < 2010 dynamic < 2020 static", even if I had to choose right now. Static was a real mess in the past.
It also isn't even necessarily big jumps that make that true as much as a steady development of innovations, since obviously it's not like all modern static languages are Hindley-Milner or anything. But a slow dribble of both little features and improvements in understanding of how to use everything over the years, like type deduction (getting rid of the usually-redundant specification of type on both sides of "="), getting away from the idea that OO === matching physical models, libraries that have learned to take more interfaces instead of concrete types, languages like Go that privilege composition over inheritance instead of the other way around and the general trend towards composition, getting iterators embedded more deeply into languages like C#, control flow improvements like usable threading methodologies ("share memory by communicating, don't communicate by sharing memory", and even as much as I dislike it vs. the alternatives, having async/await is better than not having any options even though I prefer Go-style threading by a mile), and so on and so on... a steady dribble of little improvements that one step at a time changed the cost/benefit tradeoffs of a static vs. dynamic language from clearly dynamic circa 2000 to fairly clearly (IMHO) static for any non-trivial code base in 2020. And that's before we talk about Rust or anything like that.
But it means your web scraping code, crud app, {some other boring everyday app} or whatever is now adorned with all this deep mathematical complexity that if you just used nodejs or something it would be easier to understand.
Premature optimization is probably a good term for it, because the good think about haskell is it is easy to make refactoring. Make things specific now and more general later, and it should be backwards compatible for the most part.
But making things into "arrows" is part of the sport I think.
If you are gluing components together, then surely the components have a common interface that uses compatible types. And surely you need to tell the runtime what those types are in some way, even if that is simply by writing them to return appropriate values.
In theory, a statically typed language with ideal ergonomics would make that as easy to specify as a dynamic language, but would have the benefit of warning you of mistakes as early as possible.
Of course, many statically-typed languages trade-off some amount of ergonomics for other features.
This is not guaranteed; for example, the representation of a JSON object is trivial in dynamically typed languages, because there are no type bounds.
Ease of metaprogramming is another. Possibly also debugging - I don't get powerful REPLs in the statically typed languages I've used, as much as those in the the dynamically typed language I use.
Concepts like these don't represent best practices for sure at large scale, but they do make things easier at least at small scale, and they don't need to be necessarily supported.
Taken to the extreme, Perl variable autoinitialization and context sensitivity are very convenient for 3-statement programs, but they'd be horror above that. It doesn't make sense for any language to try to support that.
Mind you I've never gotten along with pure functional languages. I've mostly done CRUD applications and I'm not sure functional languages are appropriate for that use case.
Reason being: they are immutable in general but allow you escape hatches that you can relatively easily identify in your code if you need to troubleshoot.
Firstly there's the obvious observation that Y Combinator companies very rarely use lisp, but there's also really a lack of any solid evidence of any kind anywhere that language choice has such a big impact.
I am a fan of exploring languages and making a good choice for the problem at hand and always remaining open-minded but I can't stand the religious flame war side of it and I really do not think that article helps, at all.
To me languages are always a trade-off between abstraction and control, to which each problem has an appropriate range of languages to apply.
I also think the concept doesn't apply here as go is certainly less powerful and has fewer abstractions than Haskell (by design) - so if anything it is the 'blub' language* according to the article here (my point being not to criticise go but rather to point out how silly the beating the averages concept is).
I don't think his original piece was arrogant, I think this is something of an overreaction and a problem with assessing any language - adherents meeting criticism with 'you just have to use it more' can use that excuse indefinitely. Better to address individual points.
* I love go and contributed to the core compiler back in 2011/12 so this isn't an attack on it.
> But when our hypothetical Blub programmer looks in the other direction, up the power continuum, he doesn’t realize he’s looking up. What he sees are merely weird languages. He probably considers them about equivalent in power to Blub, but with all this other hairy stuff thrown in as well. Blub is good enough for him, because he thinks in Blub.
It's very, very hard to argue Go is "up the power continuum" from Haskell, or that Haskell is a Blub language compared to Go. Everything else can be debatable, but surely not this.
He could have simply said "I was arrogant about Go, and need to look at it from a fresh perspective instead of comparing it to a language I'm more familiar with", no appeals to Blub needed.
Also interesting: it's his only article tagged Go, from 2017. He never mentions Go again. Since he has several more articles about Haskell, I wonder if he eventually quit his job, or whether he simply didn't have anything else to say about Go.
When a Lisp user looks at Haskell, they are sure they're looking down, because Haskell doesn't have macros. But when a Haskell type (pun intended) looks at Lisp, they are also sure that they're looking down, because Lisp doesn't have a decent (Hindley-Milner) type system.
This situation - two languages, each sure that they're above the other - shows the problem. You cannot rank languages on a one-dimensional axis called power.
Another way of getting there is to ask: Power? Power for what? For writing programs. What kind of programs? General programs? I've never written a general program in my life. I've written a bunch of specific ones, though. What I care about is power to write this program - the one I'm currently trying to write. (Why do I care about a language's power to write a program that I'm not trying to write?)
Instead, think in terms of yak shaving. What are the yak shaving aspects of the program I'm trying to write? Think of that as a vector in a multi-dimensional space. Think of languages as branches on a tree. Which branch extends farthest in the direction of the yak-shaving vector? Use that language.
But you have to have kind of an expanded idea of what "yak shaving" means. In particular, if you pick a "non standard" language, training your team becomes part of the yak shaving.
I agree with all points with the author. But working in teams, or even mutiple teams on the same software, then go solves a lot of problems for you..
Yeah its a dumb language, missing a lot of features. But there is only 1 way to program, dependency management is sane (no circular dependencies), and the language is build for readability, not for writing code fast and elegantly...
Often short dense code with complex types is just really hard to read for your overage joe programmer.
We're not all computer scientists here.
Go is designed to write maintainable server side programs that can utilize concurrency in computer these days. Therefore they left out generics... I hope its coming. I do wish error handling and generics where part of the language... And a tuple type indeed.
I think people make the mistake of measuring how long it takes you to read x lines, when really you need to measure how long it takes to read x functionality.
There've been times when I replaced a 200 line class with 4 lines of Scala. Those lines were very much the kind of complexly-typed code that people attack Scala for. It took time and effort to understand what they did. But they were still a lot more maintainable than a 200 line class.
Go is not an old Fortran (see [1] about "the only real structure is an array") so there is always choice between array-of-structures and structure-of-arrays.
[1] https://www.ee.ryerson.ca/~elf/hack/realmen.html
Haskell (and C++ for that matter) can hide the difference with associated types (and templates).
PS Speaking about arrays - I once read Go's specification for some obscure fun and realized that I read a third of it and still am reading about arrays and stuff and not about programming.
(And Go ain't very good at concurrency. At least they should have let you mark values as immutable, or communicated entirely via copying like Erlang.)
Yes, tuples are a really weird thing with Go. If they had completely left them, that would be bad but sort of defendable. But instead they give you a half-baked implementation of some of what tuples do with their 'multiple return types'.
(And multiple return types aren't even a good fit for signaling errors. For error-handling you want to return _either_ the result _or_ the error, in a way that the compiler can check that you handled the error.
Instead as far as the types are concerned they are always returning both the result and an error, and human reviewers have to make sure that they are checked properly.)
And Go let's you communicate by copying. That's what a Channel is. Pass a struct and that is copied. Pass a pointer and the pointer is copied. The thing it points to isn't copied for glaringly obvious reasons.
And what would you suggest is a good alternative for returning multiple parameters. Currently this basically forces you to handle any possible errors and results in software you can very easily reason about.
Not sure what you mean about human reviewers needing to check errors.
I'm the opposite, I don't find it readable at all.
I find Go too verbose to be readable.
Maybe it is because what I am used to but I find I am often "pattern matching" loops into `map`s `filter`s and similar in my head. Of course doing this, and making sure that I didn't miss a side effect or condition that makes it not-quite-a-map takes a lot more brainpower than just seeing `slice.map(|path| path.basename())`.
When people disagree on readability its often bc they mean two different things. We would benefit from different terms for readable-in-the-small and readable-in-the-large.
I'm not sure what those glaringly obvious reasons are. In Erlang, you just send a copy of the whole struct. Not some kind of references or pointers.
See eg https://play.golang.org/p/P3qUtFenp2q
But as I am saying, if you want to send pointers (or references) over a channel, they should support marking things as immutable.
> And what would you suggest is a good alternative for returning multiple parameters. Currently this basically forces you to handle any possible errors and results in software you can very easily reason about.
Go doesn't force you to handle errors at all. The compiler will happily mix up the branches of your `if` that checks for errors, or let you get away without checking anything at all.
My suggestion would be eg algebraic data types. Especially sum-types. Or in more C inspired terms: tagged unions plus pattern matching. Or 'an enum with parameters'.
> Not sure what you mean about human reviewers needing to check errors.
In a language without any checking at all like JavaScript, human authors and reviewers have to make sure that you don't accidentally eg add a string to an int. Or otherwise, have to make sure that at least you have enough test coverage.
In Go, the compiler can yell at you when you are trying to add an int to a string.
In OCaml or Haskel or Rust (or any language with algebraic data types), the compiler can make sure that you check your errors. So in eg Haskell syntax that looks like this:
case someFunctionThatMightGoWrong(someParameter) of
Error errorDetails -> handleErrors(errorDetails)
Success someValue -> doSomethingSensible(someValue)
Crucially, `someValue` is only in scope in the branch where we match the right pattern. So you can't accidentally go on computing with that variable in the wrong branch.Does this make sense? If not, I can try to explain in some other way.
Here's a playground for reference:
The broader point is that there is no way in Go to ensure that data passed over a channel will not be modified concurrently without modifying the type of the data specifically for the channel use case.
A deep_copy() primitive would fix this.
With respect to your second point, I see what you mean but I still don't think that's a negative of Go. You can pass in copied values without having to have a deep_copy() primitive (loop map values and copy to new map, dereferencing a pointer, etc.). Like other parts of Go, if you want to make your data immutable there is no syntactic sugar. You have to explicitly write it out.
It is a negative of Go, because Go pushes you into the pit of failure by its design: it is much, much harder to do the right thing (send deep copies of objects over channels) than it is to do the wrong things (send pointers or shallow copies over channels).
Go has no facilities to would allow you to abstract away the repetitive parts of error handling.
Copying large structs is not ideal, but passing a pointer to the channel means the other side of the channel can mutate that data. If Go had immutable types, you could pass a constant pointer, allowing the other end of the channel to read from the pointer but not modify it.
> And what would you suggest is a good alternative for returning multiple parameters. Currently this basically forces you to handle any possible errors and results in software you can very easily reason about.
What he's talking about is returning a union type. You have a single return value, it's type is either a valid result (i.e. a string) or an error. The compiler expects you to do runtime type checks (basically) to assert whether your return value is actually a value or not.
The explicit return values get clunky in some situations. If I have a function that processes an array of items, where each item can fail individually, in Go the cleanest way to handle that is to make a struct that holds a pointer to a value (so you can check if it's nil) and an error. And you return an array of those structs. In languages with Options, you can simply return an Option object, and let the upstream caller figure out what to do if it's an error.
Javascript/Typescript Promises are a similar, though less featureful, implementation of the same kind of idea.
Not sure what you mean about human reviewers needing to check errors.
Not quite. I am suggesting a tagged union. Not a runtime type check.
You'd check the tag.
Some language only support the runtime type-checked version of union types. That's a bit silly. For example, it makes it much harder to write a function that may either error out itself, or produce an error message as its bona fide value.
The lack of any type of abstraction is not great for teams. The need to write boilerplate code harms readability more than writeability.
Understanding what a single statement does is not important. Understanding intent is more important. And Go provides very few tools on it.
The reason for Go's popularity is mostly tooling. I like structural typing too. But other than that it is a very tedious language.
And that, is precisely why it is a good language for teams.
That line of thinking promotes workarounds over long-term solutions. If your team has people who are abusing tools (like types, abstractions etc), the solution is to teach them how to not do that. The solution is NOT to remove the tool itself! That is like saying that a drilling machine is easier to misuse so "only the hammer is a tool for teams". Ugh.
Go is awesome because of things it _can_ do (it has the speed of C and enough libraries to write full-blown webservices with it). Please don't encourage the whole marketing agenda of touting its shortcomings as advantages. It doesn't work. All it does is it turns people away from the language.
I was a C# developer writing all these abstractions like every other C# developer does. It's idiomatic C#. OOP exists, you're encouraged to use it to abstract and decouple code.
Even if you try and strip all that away, you'll never escape it because every library you depend on is written this way. For example, try understanding https://github.com/protobuf-net/protobuf-net like I once did. The cognitive load is huge.
This is C#, this is what having a huge toolset results in.
A simple language with simple code isn't a shortcoming. It's an advantage. Overly complex code is a shortcoming.
I liked writing heavily abstracted code in C#, that's what I used to do every day. But I never liked reading other people's code because without the intimate knowledge of the code it is a huge mental load. Onboarding new developers into a code base they are unfamiliar with is also very time consuming.
In Go I realised that the only abstraction you need is an interface. There's no "abstraction first" mentality like there is in C#. You can define interfaces at any point in time, whereas in C# you must define an interface first, and if you don't then you end up going the OOP route with more abstraction.
And because of this small toolset, reading other people's code is easy. It all looks the same.
Not by all, only by a subset of Go developers (until very recently I was one as well, maintained a moderate-size webservice). I specifically said "some Go developers" in my comment because that is what happens (at GopherCons, meetups and so on).
I don't really have a lot of C# experience, but I have read the same story about C++ from lot of people. I think you will agree that abstractions are not a problem, abuse is. Now, I have worked on a C# project for a very short time where I have seen the problems you described. But I do not follow the thinking that a tool was abused and hence its the tool's fault. That is silly.
I was a Go developer for ~4 years where I struggled because I had to constantly spend mental effort trying to ignore the people in the Go community who, instead of focusing on what Go does well, spent all their time (in GopherCon, meetups and so on) rationalizing missing features from Go. Here I was - with this nice language that let me write webservices which are efficient and do not carry a heavy runtime (CLR, JVM etc). But it is severely missing features that can help you be more productive when programming. After all that, listening to people say "its feature, not a defect" just makes you lose all hope that things will get any better.
A better approach is what (thankfully) some folks from the Go team take. You will find blogs from the core team where it is explained that Generics aren't missing because they are outright bad. They are missing because the Go team had not yet[1] figured out how to do it the right way (TM). _That_ is fine, it is the truth.
Finally, you say that it just isn't possible to avoid writing complex code with a powerful language. Well, I have heard that as well, from the same people. But it isn't true, I have worked on a C++ project where I was careful to keep things simple[2] and had quite a few developers on the team (with only college-level C++ experience, most of them Go devs) contribute to the project without much trouble.
But hey, maybe it is personal preference then. I would rather use powerful tooling and put cognitive effort into keeping things simple - rather than using something else and feeling limited by it all the time.
[1] now they have, for the most parts [2] for example, it would use templates but avoid nesting more than one level, and so on. You get the idea.
In Go it's quite difficult to make idiomatic overly complex code. If someone is making something really complex in Go I can easily see what they're trying to do and suggest an easier more idiomatic way of doing it.
In C# trying to understand some overly complex abstraction is a task in itself. Then trying to simplify it in a way that pleases everyone else who is of the mindset that these abstractions are good is another challenge. Most Go developers are of the opinion that there is only 1 or 2 idiomatic ways to solve most problems. But in C# there's so many more because of the more advanced language.
I'm not trying to justify the shortcomings of Go either. From day 1 of learning Go I was craving for generics and I read people saying "you don't need generics, just use interface{} and type switch.". It's ugly and it's one of the worst parts of Go, I absolutely hate seeing interface{} in a function.
But in the context of working in a team I've worked on sizeable projects in Go and bringing new devs of various skill levels onto the project has been a breeze. The code they write is the same as the code I write because it almost has to be, there's not many ways they can stray from the path laid out ahead.
But that's exactly where Go projects end up at. Reading any 1-4 Go lines is easy. Trying to mentally decode a function of 70 lines almost never is. It is verbose as hell. And then you are like "a-ha! this goes through those two collections, filters one of them, maps the other and then combines the results through that algorithm". But you lost 20 minutes until you got to that conclusion.
Compare this with OCaml or Elixir for example where such a function would literally be 10-15 lines.
(It's not an unique disadvantage to Go, mind you, but we're discussing it currently. Other languages have the same problem, e.g. C# and Java.)
Just because Go lacks many forms of abstraction, doesn't mean complex code will not be created with it. Quite the opposite. It's lack of expressiveness will encourage "frameworks", elaborate encodings, code generation and various productivity aids. Look at what the lack of generics has done to the Kubernetes code base:
"The core team replaced a compile-time language feature that was missing (Generics) with their home-built runtime system"
https://medium.com/@arschles/go-experience-report-generics-i...
No. What you see in K8S is the exception rather than the norm in Go - but is the norm in other languages.
I suggest that is because not many large applications have yet been built in Go and it is still relatively young. If Go was used more sparingly as a "domain-specific language for concurrent network services", then I might be inclined to agree with you, but it seems to be marketed and promoted as a general purpose language.
On top of that I wish `null` wasn't and sum types + pattern matching were (and were used for error handling).
I have never gotten it. I find Go code somewhat easy to write, just just give up on trying to be concise or elegant, but not at all easy to read. The excessive boilerplate just makes me feel like my brain does not have the working memory to piece it all together.
The constructs are simple, true, but if you are trying to write anything non-trivial, then simple syntax will not make your problem simpler. It is just expressed in tens of times more lines than in Elixir, Haskell or even PHP. This takes a toll on the reader.
As an example - you can accomplish interesting projects with Lego. And it is easy to get going for sure. But at some point, when you are trying to do something non-trivial, it becomes a hindrance, despite it's simplicity. Hence "I built a life-size XYZ in Lego" becomes impressive in it's own right.
Oh, and Go modules is a story of a train wreck.
With the relevant HN discussion: https://news.ycombinator.com/item?id=24429045
Drop in replacement for `go fmt` and probably does most of what you want.
This looks perfect actually.
The versioning and slice sorting have since been solved, by go.mod and sort.Slice respectively.
The problem with "Make it an error to not initialise a struct field" is you lose source compatibility when adding new struct fields.
Was he being ironic, or serious? Love to hear more.
If you allow the struct definition to specify a default value then the struct author can set default in the cases where there is a sensible default, and leave it compile-time incompatible in the cases where there really is no good default and the person doing the upgrade needs to make a decision.
I thought Go didn't allow functions with default argument values either?
Go modules has solved this problem.
I find that in practice Go modules work very well. Certainly better than the alternatives and I've been through most of them.
Incrementing the major version of your module is very clunky though.
Mind you, a big plus, I think, is that Go doesn't have the ridiculous library ecosystem that others I've worked with (JS/Node, Java) have; I only use a handful of libraries at the moment (for REST api, .env file handling, database interaction and logging, and some additional ones for development like mocking, neat diffs for tests, and code generation (xsd & swagger to code).
I think this is a great selling point. Probably due to the stdlib being so extensive.
Still, I agree with some of the bad points. Like him, I would have liked more types with a stricter compiler that helps refactoring (sum types, zero values).
The official documentation of the language is also very disappointing, as noted by Eric S. Raymond in his conversion notes[^1] from Python to Go. The author of this blog post criticizes the tooling for the lacks of features that in fact do exist[^2], but are hard to find due to the poor documentation (split over a a shallow "tour of Go", blogs, other official docs, with no links between them).
[^1]: https://gitlab.com/esr/reposurgeon/blob/master/GoNotes.adoc
I really don't like Go, but I won't deny that it basically does what it's supposed to do.
One day hopefully
f := os.Open("file") or {
return err
}
After a couple of years with Scala, I'm also really missing proper FP, pattern matching, optional types, immutability, etc. Still, I'm pretty happy with Go and it has made me more excited about coding again.That's it. A large majority of the people don't think on those terms.
Go tries to be pragmatic and hit a decent balance between productivity and language features.
In my case, my team is composed of most of DBA experts, they have no problem following Golang code.
So your programming languages of choice should base on your team.
While that is a good thing to keep in mind when making tech decisions, make sure not tunnel vision only on it.
More about that here https://news.ycombinator.com/item?id=24880273
I would like to use Go, Haskell, Rust or whatever that I like (or my team as well) but this is just one piece of the whole cake IMO.
(I have no idea, but I found this https://www.well-typed.com/blog/2019/10/nonmoving-gc-merge/ "Low-latency garbage collector merged for GHC 8.10", which is more recent than the OP blog post)
Is the “backwards compatibility at all costs” point about Go still valid? https://blog.golang.org/using-go-modules ...it seems like you can specify versioned deps now
Because with gofmt there's nothing to argue about, it is what it is. With Python’s significant whitespace there was plenty of rope to play tug-o-war with: tabs or spaces? How many? That was enough to fuel the flame wars.
Prime examples:
- http requests. For an API it's almost always a generic request parametrized by some type.
For example, you API always returns `{result: ...some data..., nextPageToken: ...}`. Well, that's a `PagedResponse<...the type of data...>`
- cache
Caches store objects. In go any `.get` from a cache will return an `interface{}` that you have to type-assert because you can't do a `Cache<T>`.
and so on.
Re: caches, what kinda caches do you mean? For the most simple use case you have maps, which in Go are already generic. Of course, anything more advanced would be greatly helped with generics, since at the moment they (I presume) store `interface{}` types and the consumer has to cast them back to the real types. Or they use code generators, which is also pointed out in the article.
To add some potential goodness coming from generics: Option types to avoid nil, Either types to replace the value/error tuples (which the article points out is a weird outlier, because you don't have tuples elsewhere in the language).
Those make me wonder if parts of the language and codebases written therein would actually improve compared to the weirdness of multiple return values and the like.
I'm not talking about the HTTP package itself. In Java and C# you do something along the lines of
// Java
CompletableFuture<PagedResult<Contract>> getContracts(...);
CompletableFuture<PagedResult<Client>> getClients...(...);
CompletableFuture<PagedResult<Book>> getBooks...(...);
// C#
JsonSerializer.Deserialize<PagedResult<Contract>>(responseBody);
JsonSerializer.Deserialize<PagedResult<Client>>(responseBody);
JsonSerializer.Deserialize<PagedResult<Book>>(responseBody);
And that's basically it. With Go you end up having fifteen identical PagedResult types for every single type that can be returned from the API because you can't parametrize anything: // Go
type ContractsResult struct {
Result []*entities.Contract
NextPageToken string
}
type ClientsResult struct {
Result []*entities.Client
NextPageToken string
}
type BooksResult struct {
Result []*entities.Book
NextPageToken string
}
> Or they use code generators, which is also pointed out in the article.Code generators are just bandaid for glaring holes in the language. Worse still, I don't know if you can even specify code generating tools in your go.mod. For example, wire's installation instructions say "you need to install wire globally and have it on your $GOPATH" [1] So your go build will just fail mysteriously until you have all the necessary tools installed.
> Of course, anything more advanced would be greatly helped with generics
That... that is exactly what I'm talking about.
> Option types to avoid nil, Either types to replace the value/error tuples
Indeed. I forgot about those :) Yup, that would be a great use case for generics.
I do enjoy this experience. Very text oriented. It’s subjective. And it has served me well.
"Use it more" is not an answer. If you have found a good way to produce software and it has served you well -- even better if you had other ways in the past and your latest one is objectively better! -- then you are not under any obligation to "give it a chance".
Many people longed for certain ways of doing things and one day they found the tools that enable them. That's quite fine. No need to insert platitudes in there which can't ever be satisfied (like "use it more", especially if you are not inclined to).
? You can't forget to check the error because not checking err is a compiler error unless you forcibly mute it by using _
val, err := something()
val2, err := somethingElse()
if err != nil // etc
You can - and all the examples usually do - reuse and shadow an 'err' variable, and tooling won't complain as long as something is done with it at least once.If Go wants to enforce not ignoring errors, it still has a few holes like this to fix.