Hacker News new | past | comments | ask | show | jobs | submit login

If you’re the type of engineer who prides themselves on the raw amount of code you write, then Go is for you. If you’d rather focus on solving problems creatively and expressively, Go is not your tool.

I don't mean this as slight against those people that really enjoy writing lots of (Go) code. It’s just my observation after being in a few different contexts where Go was the language of choice. Personally Go is too verbose for me and this is especially painful/apparent when you get to writing the multitude of tests required to ensure your code works since Go’s type system/compiler doesn't lend you much in the way of helping ensure code correctness.

Go is essentially the new Java.




>If you’d rather focus on solving problems creatively and expressively, Go is not your tool.

>It’s just my observation

Well, my observation after dealing with js and ruby projects (ruby was my main language a few years ago) - those creating and expressive languages lead to fewer lines, sure, but in the end you are dealing with a pile of crap nobody want's to touch because some of the developers were so creative and wanted to express themselves more than they wanted to solve the problem.

Go is a tool if you actually want to solve the problem and make sure people after can quickly understand how and why you solved it. Rather than stand in awe of your creation.


> Go is a tool if you actually want to solve the problem and make sure people after can quickly understand how and why you solved it. Rather than stand in awe of your creation.

I disagree with that.

The problem in those cases is not expressivity. It's complex features that are very easy to misuse and have no alternatives within the language.

And Go is littered with those. It allows for lots of "cleverness" around Reflection and Empty Interfaces, which effectively turn it into a poorly-specified ad-hoc dynamic language. Same for templates. This is the same problem of Meta-programming in Ruby, and the same problem with complex OOP architecture that plagues some Java projects. It's all avoidable, of course.

Those features are not "expressive", quite the contrary, but they lead to the same problem of "pile of crap nobody want's to touch because some of the developers were so creative and wanted to express themselves".

It takes as much discipline within a team to avoid this in Golang (or any other "non-expressive language") than in more expressive languages.

On the other hand, lots of new ES6 features give it more expressivity without necessarily adding complexity and in fact making the code easier to read and more reliable.


>And Go is littered with those. It allows for lots of "cleverness" around Reflection and Empty Interfaces, which effectively turn it into a poorly-specified ad-hoc dynamic language.

I strongly disagree with that. You can do it, you can do clever code through reflection. But it is actively discouraged unless it's something required to solve the problem, e.g automatic JSON marshalling/unmarshalling.

No professional Go dev is going to immediately reach for empty interfaces or reflection without seeing what the solution looks like with verbose type safe code.


Maybe in your experience this doesn't happen, or maybe we have different thresholds for what we consider abuse, but it's definitely a thing for a lot of people.

Also, overuse of reflection doesn't happen overnight, or only because of inexperienced programmers.

In most complex projects it happens because someone wants to add more complex abstractions that exist in other languages to help with the work and to reduce programmer error and the only way to do it is via Reflection and Empty Interfaces, or maybe via templates.


All of the Go projects where I work have a very strong "empty interfaces are bad" culture (noobs who don't know better will get called out in code review), and the only reflection I've ever run across is for marshaling/unmarshaling. I really don't see these things being overused at all. When I first started using Go, I did perpetrate some empty interface crimes, but it really only takes one experience getting bitten by thwarting the static typing before you learn to avoid that.


I'm glad you are having a good experience, but people work on different projects with different requirements and different constraints.

In Go, empty interfaces and reflection, or templates, are for some cases the only possible way to solve a large class of complex problems. If you don't encounter those problems then it's all good, but some people do.

You haven't also taken into account the possibility of people having to maintain pieces of Go code acquired or forked from somewhere else. Maybe my team was the one who inherited the code from someone who "perpetrated some empty interface crimes".

Not all Go development is greenfield development.

This is one of the things I dislike the most about the Go community. Every single criticism or suggestion to the language is dismissed as being user error, without even taking into consideration the use case or the different experiences one might have, or if the codebase was gotten from somewhere else, or even acknowledging that there are other programming styles. It is a giant echo chamber with people constantly saying "works on my computer".


I dunno, that seems a little unfair. I’m not saying empty interfaces aren’t a problem just because I’ve managed to avoid them, I’m just saying it is quite possible to write reasonable Go by knowing what language features are misfeatures. Obviously if you’re often running across and having to deal with code laden with empty interfaces, that’s a major problem and put in that situation, I’d probably have the impression that the language encourages bad behavior. I’m just saying that so far, I haven’t encountered it all that much so I don’t really perceive it as a problem. If you want to paraphrase this as “works for me,” so be it, but it seems like a particularly uncharitable interpretation.

I do think it’s a crime that the standard library contains some empty interfaces in critical packages that seem egregious. The fact that you can accidentally pass an rsa.PublicKey (instead of a pointer to an rsa.PublicKey) to a function that takes a crypto.PublicKey interface and not find out until runtime is hard to forgive.

Anyway, I’m not just saying “works for me” but I am saying that I’m not going to let what dumb things someone else might do with the language change my enjoyment of it. This view is likely influenced by the fact that my particular projects aren’t allowed to pull in 3rd party dependencies without a thorough review, so the problem of dependency fan-out that may pull in some unfortunate code is reduced significantly.


And TypeScript and Flow these days allow to properly type the wast majority of JavaScript “dynamic typing” patterns making reflection usage in Go and Java to look ridiculous for a supposedly statically typed languages.


>It takes as much discipline within a team to avoid this in Golang (or any other "non-expressive language") than in more expressive languages.

I think this will differ from team to team. I've been working within two different companies as a Go dev so far and haven't seen any Reflection misuse issues.

The difference I see here: while Go does indeed have those features-about-to-turn problems you will be called on not use them too much or use them at all from around every corner. They are there as a necessarily evil.

At the same time meta-programming and every thing that comes with ruby's dynamic expressiveness usually is the one of the selling points.

Go has its flaws and tradeoffs, and simply things that can and will be misused but you don't see articles that promote them as something that should win you over some other language.


> Go has its flaws and tradeoffs, and simply things that can and will be misused but you don't see articles that promote them as something that should win you over some other language.

I disagree there. I can think of two examples of things I consider very easy to misuse in Go, but are promoted by the community as being the superior solution to problems in articles and posts all the time: Go Error Handling and Go Code Generation.


>Go Code Generation.

Agree

>Go Error Handling

Go error handling has its downsides but how can you misuse it? If your code may generate an error = return an error to handle elsewhere. If the code you are calling returns an error = handle it.


I also shy away from sloppily "expressive" scripting languages like JS and Ruby. When I talk about expressive I'm referring to Lisp, Scheme, Rust, Haskell, Scala, Kotlin, etc. I do agree Go has a specific use as a technology that can be deployed in high-turnover environments to mitigate devs who have an attention span that lasts until they have to write tests and deploy their code. I mean this is exactly why Google likes Go. It's a least common multiple that can be picked up easily by anyone as engineers whirl around in their machine. You're not wrong. I, however, like to enjoy writing code and understand I have to test it and maintain it throughout its lifetime (or don't have the luxury of handing it off to a salivating crew of new grads looking for promotion-worthy work once I get bored) so I gravitate toward languages that are fun to write and easy to maintain. For me, those tend to be languages that offer formal macros/meta-programming, sound type systems, support higher-order programming, and preferably enforce memory safety. It doesn't mean my code is littered with that "look how clever I am let's admire my code greatness" stuff.. I prefer to keep most of it boring too focusing more on whether it's visually readable and logically easy to understand. But it's nice to have powerful features when they really matter and it's nice to know you can count on a compiler to help ensure memory safety and type correctness.


Rust has to be one of the least expressive languages ever made, so that’s a pretty weird one to group along with other actual expressive languages. That’s not a full-on dismissal of Rust, since expressivity isn’t its main goal.

Also, type system ‘soundness’ is a pretty empty desire. It’s easy to create a type system that is formally sound, yet not useful. This is the same argument as ‘type correctness.’ Java programs were ‘type correct’ before Java had generics, and the type system was extremely limited.

So types are an inherently meaningless goal, because types can mean so many things.


Rust is very expressive. It has a useful macro system, powerful generics, and generalized traits which means if you implement your types canonically you're using them via essentially the same patterns almost everywhere.

Don't confuse not-expressive with articulate. Rust requires your to be articulate about what you're doing so you don't gloss over ownership details with fancy one liners like is common in other languages. You can be both expressive and articulate. Not worrying about memory ownership, while possible in other languages, is not the hallmark of expressiveness. For example, imagine a C analog to a Rust program that implements all the same checks and memory discipline that Rust does.. Rust is way more expressive, relatively.


What? Rust is on the more expressive ends of the language spectrum. To go further you have go to functional languages.


To be fair, judging solely from your comment here, I would never want to be working in a team with you.


I think it's ok.

Like everything human, programmers can be polar opposite without anyone being "obviously wrong."

Whatever rolls the ball.


To be fair, judging solely from your comment here, I would never want to be working in a team with you.


Why? I'm not being facetious, I'm curious what part(s) of the comment make you think that?


Isn't JS a Lisp with 'C' syntax?

Ruby also is a very expressive and clean language. I just don't get why you put Scala or Kotlin on unsloppy side.


I see what you mean with ruby, it encourages to be clever, and I had some exposure to work on somebody's "clever" code. I'm not as familiar with js, but feels similar to me as well.

What makes things worse they are both dynamic languages, but there's a middle ground.

It feels like Rust for example (I'm still planning to learn it) looks like have the right amount of functionality. Java or even C. I think what you experienced is the extreme case of it.


This is just the old conflict between ops and dev. A software dev wants a little code to express a lot, while maintaining some central guarantees, and doesn't really care about edge-cases beyond not having to code them explicitly. Needless to say, a few edge cases will turn out not to be what the dev intended, no matter how smart they are. Tests focus on the central guarantees and nothing else.

Ops is the next guy who comes in, who is only ever supposed to keep everything running. Changing things is second priority, if it's anywhere at all on the priority list. (S)he will HATE any edge case they don't know exactly what happens because it can be bad, and it's the source of all their work. They want to manually code every edge case, and have that covered by a test.

C/C++ ("smart" C++ specifically), Haskell, Lisp, ... are for the software devs.

Go, Java, C#, ... are for the software ops folks.

A large bank should probably be using Java. Someone trying to start up a new bank may prefer Haskell.

I don't see this conflict resolve any time soon. Personally I find the large bank situation somewhere between soul-crushing and suicide-inducing. But I've met plenty of people very happy in such situations, and great for them. I may even consult for them, because for 1-2 months or so software ops can be quite interesting. Figuring out how to introduce changes in a software ops org is ... usually a challenge in addition to figuring out how the software system should change.

You always come out well, because there's always large improvements that can be made because nobody's doing that. They're busy triple-checking that the next parent-teacher conference of employee 271 doesn't interfere with the oncall schedule.

And frankly, I could (and probably can still) use a few lessons in "this edge-case fuckup is not acceptable and you missed it !". I've been humbled several times by steering a large problem past all security measures of a software ops organization in the past.


>Ops is the next guy who comes in, who is only ever supposed to keep everything running.

>Go, Java, C#, ... are for the software ops folks.

Can't say anything about Java or C# but one of the reasons we use Go is because the requirements change often and you can adapt your code quickly.

At the same time our code (my main field is systems integration) is required to keep running obviously. New requirements should not change this fact.

So on a spectrum I'd say Go is 7/10 OPS. Subjectively.

Obviously Go probably is not the type of language you should chose for your MVP or prototyping. Unless you are sure you have a full picture in your head already.


I think we shouldn't point fingers at Java when it comes to Go's verbosity.

I can simply do dict.contains("foo") in Java vs having to go through a verbose hoop:

    if val, ok := dict["foo"]; ok {
        //do something here
    }
Common operations like filtering/transforming a collections are very concise in Java - I'm certain doing so in Go will take atleast 5x as many lines:

    collection.stream().filter(...).map(...).collect(...)
Don't get me started on having to go through iota hoops to declare enums.


If I was trying to justify Go's verbosity in this case I think I would say "a property check on the dict incurs a cost, by making it somewhat more verbose you encourage people not to check more times than needed".

At least that's the kind of thing I've heard about other verbose things you need to do in Go.


There’s a lot of things wrong with this. First, this has nothing to do with the actual reason for the verbosity. The actual reason is that a value may or may not be present in a map, that’s a universal problem. Most languages just return nil or an Option value, but Go returns an error and forces you to handle it. No comment from me on whether that’s good or bad, but that’s the reason the code is like that.

The real problem is you’ve just made up a reason that the code is that way, and the reason is also crazy. No language purposefully makes something verbose simply to discourage doing it because of a performance concern. Even if they did, key lookups are constant-time complexity and are among the most efficient things you can do in all of programming. So that wouldn’t even make sense as something you’d want to discourage.


Err... Go does exactly what "most languages" do too! You can write:

    value, _ := map[key]
and if map[key] doesn't exist, value will be set to the "zero value" of the map's type. So Go doesn't force you to handle the error. The actual reason for the "verbosity" is just that you can't simply write

    if(map[key]) { ...
because map[key] returns two values (and "if" wants to have a boolean expression). But if you really have to do this so often that it's bothering you, you should question yourself why...


You can actually write:

  value := map[key]
and if map[key] doesn't exist, value will (still) be set to the "zero value" of the map's type.

The actual reason you cannot simply write:

  if(map[key]) { ...
aside from the extraneous parens, is because Go syntax does not permit implicit default values/truthiness in its 'if' statements like that (as you say "if wants to have a boolean expression"). That's the real reason for the verbosity here.

But you certainly can write:

  if map[key] != "" { ...
if the map contained strings, or:

  if map[key] { ...
if it contained booleans, etc. (For what that may be worth: checking for default value isn't the same as checking membership in all instances, of course).

Because map[key] can in fact return one — or two — values, depending on its usage context.


Go forces you to handle the error, which you agree with because you handled it by explicitly ignoring it via _.


I was just speculating, this is the kind of thing I heard as justification for other things in Go.

I guess in this case the real reason is trying to force you to handle the error, and that the error and value are returned separately.


Last time I wrote Java was before it had nice things so I may be slightly out of date in my assessment of its verbosity. I really wish it had decorator syntax like Kotlin does:

    class Foo(val delegate: Collection) : Collection by delegate
    {
        override fun length() = delegate.length() + 42
    }

    val foo = Foo(delegate: LinkedList())
    foo.add(1)
    foo.add(2)
    for e in foo {
        print(e)
    }
    foo.removeAll()
    foo.length() // = 42
I think a lot of the verbosity I remember was due to the sheer size of the type of interfaces you'd encounter in practice and an inability to easily compose them without implementing your own set of forwarding decorators (io stream style). That and getters/setters or rather the lack of any language-level support for dynamic properties.


Do you really want to inherit all the functions of Collection? Isn't this the anti-pattern everyone complains about on inheritance vs composition? This just looks like it's confused about whether it is a collection or has a collection.

Personally, I'd just rather take a collection by composition and only expose the small part of the API I actually want.

I guess though this is the standard decorator pattern you'd normally see from GoF.


It also breaks LSP when you “extend but reduce”. Not saying there aren't uses for “have-a”. More that writing a decorator in Java is awful.


I'm a C++ developer who has been writing more and more go code over the last year, and go's iota enum (alongside generics - having to write a search method for every different slice type gets grating after a while) is my biggest gripe with go. They really aren't any better than just global constants


This feels largely like a mischaracterization, and does not align with my experience. I'd change it to say, if you are focused on solving the problems creatively rather than using the language creatively, Go is a viable option. But if you're interested in creative usage of the language itself, Go is not right for you.


I get what you're poking at and I think we're mostly saying the same thing. However I want to point out that at the level of the stack we're discussing, the application of the chosen language is essentially: plumbing. The user-facing "creative" bits I think you're referring to (those in the system at large, which may or may not use Golang middleware) are most likely a react or mobile app. I'm not trying to devalue the self worth of individuals who write Go code by suggesting that the problems their products are solving aren't creative and fun. Not at all. Rather, my point is that the application of their Go code to build yet another solution for processing requests and serving responses ends up as pretty mundane and verbose imperative program logic that requires non-trivial amounts of effort to properly test and verify. Creativity in the "plumbing" slice of the stack to me looks like concise, readable, expressive code that leverages a type system to model a problem ___domain and then incorporates expressive logic governing how the types interact, the correctness of which is mostly enforced by the compiler itself and where testing is only required to ensure properties the type system can't capture or to make sure traits of a given type are implemented correctly. Go just simply isn't a language that allows for that level of "creativity" in building what would otherwise just be pretty mundane service plumbing.


I agree with this. Go works like my brain, and I don't mind that. Sure you can do X in some fancy way in other languages, but that just leads to feature creep. I've never read Go code I didn't understand at first glance. I cannot say that about a single other language.


And this is totally fine if you're okay reading and writing noticeably _more_ code to solve the same problems. There's nothing wrong with favoring verbosity over expressiveness, it's just not my style.

I disagree that expressiveness lends to feature creep, I've personally never seen that happen, but that's another topic.

For me, the verbosity of Go would be a lot more justified if it aslo had some sort of memory safety system in place (similar to Rust) that could help prevent concurrency related errors. I understand that you're supposed to used channels for everything in Go, but last time I wrote a channel heavy concurrent server the compiler did nothing to prevent me from mutating shared state across different go-routines and I was unable to land on a pure channel based implementation (I needed a run_once to initialize some shared state and a WaitGroup to track active listeners, and this was the case in all the other impls I audited/referenced as well). I just don't feel like I'm getting a lot of value in exchange for the verbosity when writing Go.


That's a completely fair assessment. I don't fault 'you guys' at all because I don't expect your brain works like mine. Read: I don't think you're wrong and I'm right...never.

What I fear myself is feature creep. A design by committee that ruins the language for me, and is never good enough for you.


If sharing state, you're much better off using locks and test with go's race detector. There'll be subtle bugs or contention though, unless you don't share state.

If not, you might try 0mq, immutable lists or any other facility. Channels are just the in-process idiomatic way to communicate state, but not required at all.


In my somewhat brief experience with Go I realized that one better avoid channels even for supposedly idiomatic cases. For example, try to implement a priority queue with channels. Or why it is not possible to use the select statement against a file descriptor? Another problem is proliferation of unnecessary threads that are entirely avoidable when using mutexes.


I agree. One should have good use cases for channels, and they're overused just to get new people started with goroutines.

Mutexes go a long way, and underpins channels anyways.


Channels are also hardly foolproof. You need to internalize the (non-obvious) channel axioms[1], and there are many other pitfalls and annoyances like:

- Needing to determine ownership of closable objects passed across channels.

- Defining what special token (or OOB signaling mechanism) is used for marking end of data.

- Synchronizing who is closing the channel

Go channels are quite painful to use and often a simple lock would do better. This write-up[2] summarizes the issues with them quite nicely:

[1] https://dave.cheney.net/2014/03/19/channel-axioms [2] https://www.jtolio.com/2016/03/go-channels-are-bad-and-you-s...


> Go works like my brain

can't really fault you on that either. features/looks etc. it really comes down to wether it matches how you think. I can make big claims about functional languages being superior for x y and z but if I'm being really honest with myself. its more about that fact that it fits how my brain already models problems.


What makes me sad about Go is that the verboseness comes not from the language but from its formater.

If the formater allowed to write in one line if err != nil { return err }, it would drop the number of code lines by the third.


Please omit the last line. Go is actually far more verbose than modern java when you are comparing anything bigger than a single snippet of code. Modern functional Java is fairly concise. Go is not - though maybe this will change with generics and functional packages after Go 2.0.


Go is like Java 1.2 from 1998.


> Go’s type system/compiler doesn't lend you much in the way of helping ensure code correctness

Do you have any examples? Not a golang programmer myself.


Go's weak type system encourages use of dynamic type ops (such as casting interface{}, etc) and obviously the compiler is out of the loop at compile time so it can't help.


I've written my fair share of Go over the years and I've cast to interface{} exactly zero times.


Explicitly, I believe you, but I'd bet you've often implicitly cast to interface{} (even some very legitimate cases like json.Marshal() are still technically casts to interface{}).




Consider applying for YC's Summer 2025 batch! Applications are open till May 13

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: