Hacker News new | ask | show | jobs
by realrocker 3899 days ago
From the book:

"But it has comparatively few features and is unlikely to add more. For instance, it has no implicit numeric conversions, no constructors or destructors, no operator overloading, no default parameter values, no inheritance, no generics, no exceptions, no macros, no function annotations, and no thread-local storage."

5 comments

> no default parameter values

Hearing this leaves a bad taste in my mouth, because one of the (mis)features I've found in Go is its implicit default value in struct initialization. In Go, you don't get a compile error when you miss some fields while initializing a struct. e.g.

  type T struct {
    a int
    b string
    c float64
  }
  
  t := T{c: 1.5}
will happily set `t.a` to 0 and `t.b` to an empty string. While this is useful for maintaining backward compatibility (adding more fields to a struct doesn't break upstream code), it also hinders discovering genuine mistakes at compile time. So typical Go programs end up using 0 or an empty string as a sentinel value. This is probably what made me feel Go's dynamic nature the most. It really is pretty close to a dynamic language.
I should explicitly state I seem to be in the minority in the Go community here, but: You don't get a compiler error if you initialize by name. You do get a compiler error when you initialize by position. In my minority opinion, you can and should use that to your advantage whenever possible. Some structs are clearly "configuration-like", for instance, and you don't want an error if a new option shows up, which will probably default to whatever you had before anyhow. Some structs are clearly data structures, and you'd really like to know if your two-dimensional point suddenly grew a third parameter. Of course it's not a bright shining line, but it's often pretty easy to tell which you have, or which thing you want, and use the correct initialization.

In this case, if you used:

    T{4, "hello", 3.5}}
most future type changes to the T struct will become compiler errors. (It won't be if the types are compatible, for instance, changing the first to a float would still result in a legal struct. If you have richer types in play that is less of an issue.)

golint will then complain at you, but you can pass a command-line switch to turn that off.

(This, amusingly, puts me in the rare position of siding against the Go community, on the side of the language designers. Bet you may not have known there is such a position to take. :) )

One non-obvious downside is that the Go 1 compatibility guarantee doesn't apply to struct literals that don't use field names. (I suspect you're aware of this, but other readers might not be.)

So it's possible that a future version of Go could add a field to some struct you're using and your code will stop compiling when you upgrade. It's an easy fix, of course, so it's not that big of a deal, but it's worth realizing.

The point is that if I'm using stuct literals, I want the compiler to stop me for those structs.

I'm explicitly rejecting the idea that all struct changes should be possible without producing compiler errors. Compilers errors when the guarantees your code is based on changes is a feature, not a bug.

That's a pretty risky thing to do.
I just don't get this attitude. I'm asking for the compiler to break my code if something I depended on changes. The alternative is the risky one! This is the safe alternative.

Compiler errors aren't evil. They're a tool. They work best when there is a one-to-one correspondence between problems and errors. That's not possible in the general case, but the closer we get, the better. And the worst case is not when I get a spurious error. That's easy to deal with. The worst case is when I don't get an error I should have. If you're going to worry about "riskiness", that's the risk that should keep you up at night. Not compiler errors for things that turn out to be no big deal, and can quite likely be fixed with one quick go fmt -s.

In all other cases I'd want the compiler to break my code. The problem here is that this technique is very fallible, the chance for false negative, undetected errors is high. It's risky because there's a ton of cases where you won't get a compiler error. It's unduly making you feel safe, which is not a good thing in my opinion. This is mostly why I think it's risky, because you feel safe when you shouldn't.

With keyed-fields, the worst case is that you have uninitialized fields, which typically doesn't cause much problems and get caught quickly where it matters. With unkeyed-fields, you might have code that compiles but sets unexpected fields. Things that would otherwise panic, now just keep working without you noticing, until strange things happen and you have to review all initializations and remember the struct layout every time you see the struct being created.

Personally, I don't like both techniques anyways. It's too error prone. I'll prefer writing a small constructor where I handle initialization deliberately. It's not super Go-ish but at least I centralize all the issues surrounding struct initialization in 1 place: the constructor. Then when I change what fields go in the struct, I change the function signature and the compiler breaks and doesn't let things fall through silently.

I agree. But the industry is hurtling down a tunnel of weak typing and runtime checking. So compiler features are diminishing in relevance at a geometric rate.
I see the exact opposite trend happening. Weak typing is plateauing. It's the last moment of apparent strength before long, slow, but inevitable collapse. Most interesting work is being done on the static side right now, partially because there's no more work to be done on the dynamic side. (A great deal of being dynamic is precisely throwing away all the structure you might build further features on.)

You can also see this in how all the dynamic languages are working on adding "optional" or "incremental" dynamic typing. Static languages, by contrast, generally create one dynamic type, stick it in a library somewhere, and let the small handful of people who really need it use it. Few, if any, of them are adding any dynamic features. The motion trends are clear.

So I get to go back and look at the struct to see the order every time I initialize an instance? Or watch everything break when the noob on the team alphabetizes the struct fields? Yeah, that's a great solution.
If you're doing this, it is, by definition, on structs you choose to do it on. If you lack the judgment ability to decide when you want that, fine, never do it.

And the noob that is so noobish that they change code and don't even compile it to check to see whether it works is a menace well beyond this issue. That's an overpowerful argument; the real problem is the noob that isn't even running the compiler. The noob doesn't "break struct initializations" specially, they break everything.

> If you lack the judgment ability to decide when you want that, fine, never do it.

The choice in question is whether I want code that breaks silently when I add a field to a struct (named fields in initializers), or code that breaks silently when I swap fields of the same type in a struct (positional fields in initializers). Please tell me more about how "judgment ability" makes this anything other than a choice between brittle code and brittle code.

> And the noob that is so noobish that they change code and don't even compile it to check to see whether it works is a menace well beyond this issue. That's an overpowerful argument; the real problem is the noob that isn't even running the compiler. The noob doesn't "break struct initializations" specially, they break everything.

Compilation will not catch all situations where struct fields are reordered. Consider the rather common case where two fields on a struct are of the same type. If a noob swaps the order of these fields, it will compile just fine using your method of struct initialization. It's even quite possible that if unit tests initialize the structs in the same way, this could get past unit tests as well.

This is a pretty obvious case, and the fact that I have to explain it to you is yet another example of having to dumb things down for Go users who don't know the first thing about programming language design.

"Consider the rather common case where two fields on a struct are of the same type."

Or perhaps even the even more complicated case I already mentioned upthread, that an int will still happily initialize a float?

"Consider the rather common case where two fields on a struct are of the same type. If a noob swaps the order of these fields, it will compile just fine using your method of struct initialization. It's even quite possible that if unit tests initialize the structs in the same way, this could get past unit tests as well.... dumb things down for Go users"

What does any of this have to do with Go? All languages with structs have these "problems"! Even Haskell will have the exact same problems (even before you turn on OverloadedStrings). You're reaching so hard to be dismissive of some sort of stereotypical programmer that only exists in your head that you've completely surrendered reason. You should reconsider whether that's really who you want to be.

That looks a lot like C to me, which I wouldn't call a dynamic language.

  typedef struct Foo {
    int a;
    int b;
  } Foo;

  Foo f = (Foo) { .a = 1 }; // This will initialize b to zero.
Isn't it funny how C has so many ways to accomplish the same thing. Why did you use a typedef with a tag?

  typedef struct {
    int a;
    int b;
  } Foo;

  Foo f = { .a = 1 };
Tagless structs can't be forward-declared.

(Obviously, this looks like a local struct, so there's possibly no need for forward declaration. But you might have a snippet to generate this sort of thing for you. Or maybe it's just force of habit. And so on.)

> Or maybe it's just force of habit.

Or cargo-culting.

edit: wow somebody felt threatened. I have no shame stating that I cargo-culted exactly that for a while before I actually wondered what I was doing.

I'm guilty of this when writing C. I really have no desire to learn the language properly (it's hard enough to fit C++ in my brain), so I'll just follow the patterns others have set.

Microsoft does stuff like:

    typedef struct _FOO {
        ...
    } FOO, *PFOO;
Yeah ok fine, I'll do that.
It's one of those features that seems mad until and unless one runs into the situation that justifies it.

Go provides default values to avoid the C error-factory of random undefined behavior resulting from re-use of whatever is in a memory address; that much is clear. But the reason Go lets you partially instantiate an object (and separates out construction from state) is to make it easier to write unit tests, where the common case is that you want to circumvent the "main line" object construction pathways.

I have long felt that floats should default to NaN, so that any attempt to perform operations with them before they're initialized results in an error.
From the preface: "achieving maximum effect with minimum means."

Sort of the anti-Perl? I say this as someone who likes both Perl and Go.

Go is very contrarian, and I applaud this.

> From the preface: "achieving maximum effect with minimum means."

Wouldn't be out of place at a marketing agency, with about the same level of truth too.

> Sort of the anti-Perl?

In what sense? The one thing you can say about Perl is that it's a huge language, so the anti-perl would be a very small language. Go isn't a very small language (like Forth), it isn't even a small one (like Smalltalk or Self) it's about the same size/complexity as an early Java. Somewhat bigger in some ways (more magical builtins and constructs) somewhat smaller in others (simpler visibility rules, no synchronised methods/blocks), but in the best case it's a wash.

> Go is very contrarian, and I applaud this.

Perl is also very contrarian.

I think it's fair to call Go an anti-Perl.

Perl is very liberal. There's always more than one way to do things.

Go is very conservative, in comparison.

I'd agree that both are contrarian, but for very different reasons.

> I think it's fair to call Go an anti-Perl.

Then again, pretty much anything can fairly be called an anti-Perl, possibly even Perl.

Go is very contrarian, and I applaud this.

It takes more than reversing the order of parameters and using known-braindead ideas like codified tabs-are-good syntax to make contrarian ideas valuable.

Just because you change green lights to mean stop and red lights to mean continue doesn't make contrarian suddenly better than the way things were.

While I think your comment is unconstructive, I do have to say that I don't quite understand why Go decided to force hard tabs.

Even more confusingly to me, I really don't understand why they seem to standardize on tabs expanding to 8 spaces rather than 4.

The 2 spaces (of soft or hard tabs) favored by some Ruby and CoffeeScript programmers is too little, but 8 spaces is way too much.

> While I think your comment is unconstructive, I do have to say that I don't quite understand why Go decided to force hard tabs.

Since you're using goftm which imposes a strict discipline, tab-indents and space-align allows configuring tabwidth however you want locally without imposing that on other collaborators. The issue with the idea is usually doing it consistently and people properly configuring their editor (if the editor allows tab-indent+space-align at all), when the code is being hard-reformatted it's not an issue.

> Even more confusingly to me, I really don't understand why they seem to standardize on tabs expanding to 8 spaces rather than 4.

8 is the historical/default tabwidth on Unices (unconfigurable environments generally have a tabwidth of 8), using hard tabs but defaulting to anything else would be odd. And since it's tabs, you can configure your environment to whichever tabwidth you prefer (like 3 or 6, I've not seen editors with support for tabwidths in half-spaces or pixels but in theory that's also an option) (well technically the CSS tab-size property supports arbitrary <length> tabsizes but only Chrome >= 42 supports that, the rest only supports <integer> spaces, except for IE which has no support whatsoever).

A claimed benefit of 8 tabwidth is also that rightsward drift becomes a problem extremely early, the tabwidth thus acts as a check against over-nesting. Now that's inconvenient in languages with significant "natural drift" like C# (where your code lives in a method in a class in a namespace so you're already 3 indents deep before you've writing anything, class-in-files languages tend to have a tabwidth of 4 or even 2 probably for that reason), but IIRC Go only has a single "natural ident" the rest is all yours, so a tabwidth of 8 serves as a check against nesting code too much.

people properly configuring their editor

A lot of coding is reading examples online these days. Trying to read Go code on GitHub is awful since three forced tab indents feels like you're 50% across the screen already (and forget trying to read it on mobile).

Browsers don't really have a "set tab width" option that I've found (and forget trying to set user options on mobile browsers).

a check against nesting code too much.

For expert programmers coding for long-term correctness, then yes. But beginners and lean "we just gotta ship this shit" startups will just create 9 levels of unreadable cruft.

Github allows you to set the tab size to <n> when viewing code by add ing "?ts=<n>" to the end of the url. I don't know if there is a way to set it for an account.
> Browsers don't really have a "set tab width" option that I've found.

The `tab-width` CSS property is supported by all browsers except MSIE, though only for integer amount of spaces (aside from Chrome 42 which supports arbitrary widths). In most desktop browsers can setup a "user css" to set it.

> For expert programmers coding for long-term correctness, then yes. But beginners and lean "we just gotta ship this shit" startups will just create 9 levels of unreadable cruft.

Would their unreadable cruft be any more readable with a tabwidth of 4 or (god forbid) 2?

I am vastly in favor of hard tabs, as it doesn't enforce tab size. Question, why do you say that the standardize on tabs as 8 spaces? I've done all my golang programming with 4 tab spaces.
Hard tabs make "pretty/readable indent" formatting difficult too.

If you want to line up certain arguments across lines, you just can't because you're forced to an unknown width of alignment chosen by the reader. So, all your code will just be indents that ignore the specific visual alignment intentions of the author, and that reduces readability and understandability in multi-person teams (and programming is a team sport, not a one-person-does-it-all game).

> Hard tabs make "pretty/readable indent" formatting difficult too.

That's alignment not indentation. AFAIK gofmt uses spaces for vertical alignment

A significant fraction of the Go core engineers use proportional fonts when programming. On their screens, hard tabs are the only tabs that work. Spaces on proportional fonts are too tiny to be useful for moving code around.
What? Really? Who in the world would do that?
Users of Acme, written by Rob Pike. See screenshots on this page and note that the font used is not monospaced. http://acme.cat-v.org/
I love two spaces. I used to use four but switched a few years back and now anything more than two looks strange to me. It's just a preference, but it does keep your line length shorter, which is nice if you like to adhere to a maximum line length throughout your code.
> no default parameter values

This would seriously bum me out as I find the easiest to extend the functionality of an existing Python function is to add a new parameter with a default value. This way, regardless of whether the existing code base the calls the new or old version of the function, it performs the same way as it always has.

Since Go is statically-typed and compiled, it's much easier to refactor a function compared to Python. Change it and fix everywhere the compiler complains.
That's fine for internal functions, but a big problem when publishing any kind of interface. It's really nice to be able to extend an interface without breaking stuff or adding cruft.
true, forgot to think about that. But still potentially a lot of changes to fix, even if you are explicitly told what needs adjusting.
And this is where Go's tooling shines: https://golang.org/cmd/gofmt/

(Refactor automatically, not by hand.)

You can use variadic arguments with an interface type or a single struct parameter to get similar behavior.
Are they actually proud of having no generics? :o
No, the authors specifically said that they would like to have generic features but couldn't figure out a way to implement it without unacceptable performance problems.

I'm pretty sure the feature will show up in the next few minor version increments.

> I'm pretty sure the feature will show up in the next few minor version increments.

No it won't. People need to stop expecting generics because it will never happen. Not that agree with this but it was made pretty clear in the go-nuts mailing list that the Go team wants to keep Go type system "simple".

Maybe in Go 2? ;)
Never gonna happen, Go 2 is considered harmful.
This is one of the things I like about Go: it's "done." In exchange for passing on extensions that might make certain use cases easier, we'll avoid the bloat and have decades of backward compatibility.

We just came out of a decade of nifty language mania. What I learned is that languages are boring but problems are interesting. Algorithms and solutions are interesting. A great solution to a challenging problem is really interesting even if it's in the most boring language ever.

I have code on my machine written in C in the 70s because C is largely "done." People today continue to write interesting stuff in C. Neuromancer was written in the same language as Lord of the Rings and Moby Dick, too.

Basically they dismissed every implemented approach, despite generics obviously working in other systems. (And it's hardly a "new" feature unless we're counting in multiples of decades.)
> Basically they dismissed every implemented approach, despite generics obviously working in other systems.

But working at a price. And Go isn't willing to pay the price (especially in terms of compile time). If Go ever adds generics, it will be with a new approach that doesn't blow up compile times.

C# compiles and JITs incredibly fast and has generics. F# has the same generics and compiles far slower. The reason isn't compile times. (Especially since one impl of generics is just generating code, which is cheap.)

IIRC the reasons on the list were the standard tradeoffs of memory space and so on. Do they emit specialized versions for each function, or not, and so on. Again, stuff that's working fine in other platforms.

A few years ago, Andrei Alexandrescu showed that the dmd compiler was actually faster than the gc compiler, and D supports templates. Recently, with 1.5, Go has shown that it is ready to take a hit on compile-time speeds. I don't think the argument of compilation speed holds for generics. To me it sounds much more like a culture thing: generics, for better of for worse, add an extra thing to think about and I think that the Go authors and many Go developers are just not interested.
I wonder how much developer time is spent due to a lack of generics.

Developer time costs more than compiler time.

> Developer time costs more than compiler time.

I find that to be a very odd statement. Usually, the developer waits for the compiler in order to find out if the code compiles and executes properly. That is, every minute of compiler time costs a minute of developer time.

Worse, the developer time you spend due to lack of a feature, you spend while writing some code that would benefit from the feature. The compiler time you pay every time you compile - year after year, for some projects.

I imagine not that much in practice. It is not like someone is going to manually write out identical functions twenty times for each type they want to support. That's precisely what computers are good at doing and there are countless tools to do it painlessly.

The bigger problem is that Go doesn't have type inheritance or similar. Meaning, there's no great way to say that this generic function will only work with number types, for example. You leave the burden on the programmer to ensure their generalized function is applied only to types which it is intended to be used with.

While that is less than ideal, I cannot see that increasing developer time by a significant margin.

Which is why Go is advocating using "go generate" to generate code. See projects such as https://clipperhouse.github.io/gen/
Slow compilers waste developer time.
I sometimes feel that HN needs some reply bots.

For instance, when generics are mentioned in relation to Go, then it should auto-reply with this link: https://news.ycombinator.com/item?id=9622417

It would save so much time.

compare this with python, which has carefully evolved into a better language.
More like into two languages that will keep on existing for the next decade.