Best practices for a new Go developer
medium.com
medium.com
> You will realize eventually that what you first thought
> was worth criticizing was actually a deep work of
> genius.
Funny, I found the more I used it the less I liked it. > I’ve seen a lot of criticism of Go’s “shortcomings”
> from people who are true experts in many languages
> other than Go.
Cool, normally I just call those people "experts". > I can’t recall similar criticism from someone who’s
> worked with Go at a very deep level for a year or two.
You would do well to understand the meaning of the term "survivorship bias".Edit: Package versioning was another pain point, as someone mentioned below. Their built in package manager is almost too simple. As with anything simple, it's great at first but not so great when more complexities arise.
"No no no no. The voice of newbies is one of your biggest assets. Why don't they understand? What is the confusion?" John Resig
But I don't think everyone using Go really believes or supports this "but in practice it's not a problem and if it's a problem then the problem is YOU!" kind of response to every criticism.
We live in a world of trade-offs. It's disingenuous to act like that wasn't the case.
I have written a fair share of Go now and have only rarely used channels or goroutines. In practice, channels are only a good model for a subset of concurrency scenarios and they come with problems.
Also, those criticizing Go are right (of course). No amount of Go usage will alleviate its shortcomings. The lack of parametric polymorphism is incredibly annoying, error handling is mediocre (Rust handles this much better), and the lack of support for even simple abstractions (algebraic data types) is jarring.
Nonetheless, I continue to use Go, because they also got a lot of things right: compile times, tooling, a very-well documented standard library, good compatibility between 1.x releases, a UNIX feel, low-barrier entry for other contributors to Go projects, and a thriving ecosystem for a relatively young language. (Of course, the JVM and Java do as well on these points, except for UNIX-feel.)
There are hundereds of tasks that request access to these services. The job of my Go code is to schedule, rate limit and coordinate access by these tasks to the network services.
I did parts of it in C++ initially (because other parts of the system are written in C++) using queues and timers (could have done the same in Java) and then I rewrote it in node.js but wasn't happy with the asychronous design or with my productivity.
So I think the reason why goroutines and channels have helped me so much is specific to this kind of project. Go's other language features (or lack thereof) wouldn't have enticed me into taking a closer look. But now that I have taken a closer look, I do like the library and tools a lot as well.
To sum it up, if a program is basically a ton of loops doing blocking reads and writes that depend on each other, then goroutines and channels make the code look very similar to the idea. Mental load drops. Life is easier.
Channels and goroutines advertised as the poster child of Go language features for simplifying concurrency since the beginning. When I tried to use them I was surprised how difficult actually it is to use them correctly, so many subtleties to deal with.
Yes they are addictive, but I suspect beginners really are not using them correctly.
By that admittedly subjective standard, goroutines and channels have been a big success (compared to asynchronous code).
Well, your barrier is also very low :). Try e.g. Erlang's actor model.
Honestly, I'm fine with actors.
Go is a good language but its for, frankly, good for a very narrow set of applications [cli; microservices]. Outside of those two applications, I find myself never wanting to use Go, honestly.
If you look at the quotes peteretep pasted, those "I-have-wisdom-that-you-don't" type of remarks can be interpreted as condescension. Therefore, it's human nature to respond to condescension with more condescension.
To be clear, the blog author (Satish Talim) didn't write those quotes -- he's quoting Baron Schwartz.
" Do not complain about something you think is missing, wrong, or stupid. You are ignorant and new to the language, no matter how much of an expert you are on programming in general."
[1] or database or framework or whatever...
More exotic languages, which take the uninitiated longer to acclimate to, don't seem to be trying to tell people they're not allowed to think deeper about and question the design of the language. Good criticisms of Go were made on day 1 and are being made to this day, Rob Pike's ego notwithstanding.
At the end of the day, if I see a problem with a developing language I am interested in, I'm going to report it. If I'm met with: "I don't know you but I'll assume you don't know what you're talking about" that's a second problem and I'm not about to double-down on it.
In general, I see "This new language I'm learning sucks because it needs to work more like the language I know best" for all kinds of languages. "Python needs to use braces instead of indentation." "$X needs to handle errors more like $Y." (lots of things can be filled in there!) Complaints about the dynamic/optional/static typing in a language. "Haskell may be pretty but isolating IO effects like that is impractical, it ought to work more like most languages."
No, it's not even remotely isolated to Go.
I ask because I've seen people explaining the tradeoffs Go makes pretty clearly online without any rudeness or condescending attitude.
That's a lot like using opaque pointers in C. What is it about Go that makes people assume it's shortcomings are beautiful designs?
I learned yesterday that `x == nil` can return false even if x is nil so long as x is an interface type. But it depends on whether x is actually nil or a nil value with a specific type.
(╯°□°)╯︵ ┻━┻
My other pet peeve is that a method with a non-pointer receiver that tries to modify the receiver object will silently drop those modifications on the ground, because the object is copied. Which makes some sense, except that Go likes to convert to pointer receivers automatically, so the caller can't tell that anything is wrong. The only difference is one character in the method definition. Everyone I know hits this bug at some point and loses half an hour before they learn to look for it. You could almost say "all method receivers must be pointers" except that you need to refer to interface types without the pointer.
(╯°□°)╯︵ ┻━┻
This is an observation and not a critique.
> I learned yesterday that `x == nil` can return false even if x is nil so long as ...
In other words:
http://play.golang.org/p/YV5ylbQN4j
Thanks for the heads-up! That was news to me.
One reason `(* T)(nil) != nil` is because a `(* T)(nil)` can still have pointer methods called on it, and it can still work fine (as long as the receiver is never dereferenced, of course): https://play.golang.org/p/rup5cqapEU
http://golang.org/doc/faq#nil_error
Basically, if you pass 'nil' of one type into an argument which accepts an interface of another type, the nil is no longer nil, but an interface wrapper around a nil (which is not nil).
So:
var buf *bufio.Reader // a nil value
if this is passed into a function with a signature of func foo(buf io.Reader)
then when in function foo: foo(buf) // valid, and buf is nil here
//inside foo:
if buf != nil { // but buf is not nil here, so this is true
buf.Read() // Panics because nil has no function Read.https://play.golang.org/p/rup5cqapEU
where calling on the nil value of T seems to work.
???
The empty interface is effectively dynamic typing, relying on runtime introspection. It does not hide any struct members (unless you make them private), you can simply enumerate them.
Opaque pointers in C provide a way to enforce static typing without exposing struct members and are effectively a method for information hiding.
So, I am wondering why you said that.
My other pet peeve is that a method with a non-pointer receiver that tries to modify the receiver object will silently drop those modifications on the ground, because the object is copied.
I agree. In fact, it is seldom that I actually want the non-pointer variant. If I don't modify the object, just give me a const modifier so that I can indicate that.
(Or reasoned from outside Go: the receiver should IMO always be passed by reference.)
There's a few interesting exceptions to this, but they're very much exceptions.
And I'm really curious what drives people in this thread that are coming to say they do not want to use Go, do not like it, and stuff like that?
I suspect it's also partly due to Go's curious mix of maturity and immaturity, in an overall promising package with lots of momentum. Here's a young language that on the one hand is productive to work with and simple to understand, yet so obstinately awkward and inelegant in several other respects, and represented by a team of designers who aren't always saying what many want to hear.
It's clear that Go was, first and foremost, designed for Google and their need for a natively-compiled, concurrent language that steers the user to a very specific path, largely defined by limitations. Its approach prevents language-theoretic cleverness that can complicate a codebase (think Boost or the STL), and it prevents a lot of foot-shooting and optimistic overdesign that newbies are prone to.
But while that's wonderful for Google, that's also a frustrating to developers who don't need this invisible hand hobbling them, and sees Go's potential subverted by the stringent design principles. I admit I'm one of those frustrated by Go's type system, for example, or its heavy-handed approach to error handling.
Part of the frustration is the lack of a clear alternative: Nimrod doesn't have the mindshare, Rust is overly complex and slow to compile, Elixir doesn't have the performance, C++ is, well, C++. Go, for better or worse, fills a kind of sweet spot that, unfortunately, also has a dash of sour. And that kind of frustration easily leads to debate whenever Go comes up.
C# is about as close as it gets to the sweet spot in the middle, IMO. But .net not being a first class citizen on all platforms kills that option. Yikes.
A whittled-down, cleaned up Scala without the JVM might also have hit that sweet spot.
I'm hoping Swift will be a contender once it's open-sourced and becomes multiplatform.
The compilation speed will be improved, and Rust does more checks during compilation than Go. Regarding Rust's complexity. Go is must easier to start with, but I heard (anecdotally) that when your honeymoon with Go is over you may find yourself bored with repeating, copy-paste and the lack of abstractions in Go. That's the price you pay for simplicity.
> Elixir doesn't have the performance
it depends what kind of application you write.
More importantly, its "advanced" nature automatically relegates it to the same land as Haskell, Ocaml or even Lisp, where it's harder to hire.
Elixir/Erlang scales better, but it's a language where you will struggle the moment you need a bit of single-core performance — which you sometimes do, even in a nicely distributed app.
Last year I wrote this article about why I didn't like Go very much. It was pretty well received here on HN, which is hopefully indicative of it being a good explanation.
Note that the Go language spec does not even contain the terms 'stack' and 'heap. Whether something is stack or heap-allocated isn't always clear. E.g. consider:
foo := &Foo{} // or: new(Foo)
This could be stack or heap-allocated (in the most popular Go implementation) depending on whether the compiler thinks it escapes or not.Yeah, this comment strikes me as deeply confused. Go does not give you precise control over what goes on the stack versus the heap: it's even in the FAQ.
There's a common misconception that stack vs. heap and value types vs. reference types represent the same distinction; they do not. For example, note that it is possible in unmanaged languages to have dangling references into the stack.
* heap-based languages (vast majority of the high-level ones), everything is treated as heap-allocated, may be stack-allocated as an optimisation (while ensuring/conserving heap semantics)[0]
* stack/heap languages, strict semantic separation between heap allocation and stack allocation, the developer decides whether values get heap-allocated or stack-allocated
Go has the semantics separation of the latter (whether something is heap-allocated or stack-allocated had strong semantic significance and is not dependent on the thing's type) but the control of the former (the runtime decides and doesn't tell you)
[0] a subset of these languages may have "value types" which have stack allocation semantics, that's linked to the type itself and the semantics are stable
> capture a reference to it in an (escaping) closure to defeat the escape analysis optimization.
the object absolutely doesn't behave as a heap-allocated value. So everything is not semantically on the heap.
1. There are many other things that can cause escape analysis to fail. You can also capture references to a value in an opaque function, send it over a channel, etc. etc.
2. There is no semantic difference between stack and heap (modulo when finalizers run). The only thing the language says is that object is guaranteed to be destroyed when it is no longer referenced. The compiler can fulfill this obligation sometimes through stack obligations, but that's done through a set of heuristics. The language says nothing about this (and the FAQ states this quite explicitly).
3. Java does the same thing. The HotSpot JVM will use escape analysis to perform SROA (which is an important optimization). Note that escape analysis is independent of value vs. reference types; Java has much fewer value types, but it can still do escape analysis. This is because value/reference and stack/heap are orthogonal.
I'm writing my current hobby project, a podcast fetcher, as my first project in Go.
The project has been going generally well but there have been a few annoyances so far:
* Why are you not able to easily version git dependencies? Go's solution to this problem is to tell you to create an entirely new git repository for each major version. Really? If they didn't want to go full blown dependency versioning with something like CocoaPods, they could at least let you specify a git branch or tag.
* The db.Sql abstraction does not support multiple result sets. Therefore database drivers, like the popular mysql driver, don't support multiple result sets. This really limits the kinds of stored procedures you can call.
* The debugger support is bad. I have to fall back to using print statements for most of my debugging.
Take a look at gopkg.in[1]. This is a service that makes branches in other repositories go-gettable. Eg. go getting gopkg.in/foo/bar/v5 will download branch v5 from github.com/foo/bar. This seems to me to be a way of dealing reasonaly well with the issue -- the go get tool itself doesn't need to understand anything at all here.
In Go I believe they use the term "idiomatic." ;-)
Would you argue that there are more robust solutions in the Haskell ecosystem? What scheme do you prefer?
This is because Go doesn't support specifying branches/tags itself when specifying a git dependency.
This workaround is contingent on gopkg.in people keeping their service maintained and running. If they went down, all your dependencies would break.
One of the many wonderful comments in there.
Those annoyances you listed aren't Go's fault. It's yours.
I just don't see what people think others would understand by teleologic statements like: "write Go the way it wants to be written"
well, this is a very general advice, approx. implying : "don't force your previous language's world view while programming in Go".
f.e. if you are coming to Go from say Java, where everything is a class and where hierarchies abound, when you try to write a 'javaesque' go, you would be fighting against the language.
however, embracing a 'goesque' worldview of composition+interfaces as primary vehicle for program design, would/should make the overall experience quite a pleasant one.
To me it means "Read what the creators thought about when creating Go and how they view it, and do the same". But really that applies to any craft that exists: instead of trying to apply and adapt what you already know, clean your mind and start from scratch.
well, with knowledge i hope there is wisdom as well to apply it in a manner befitting the context.
Why?
I've had the same dev folder structure across platforms, languages, jobs and decades. I basically had to abandon that structure when starting Go. I fought & fought and at the end of the day it is just easier to use their expected workflow. It was (is?) galling but it was the only way to stop fighting the tools and get work done.
Actually, can Go even do that? I think it can...
While a good question anyhow, it's not as bad as the comments make it out to be. Anything can be made to look bad by only looking at the negatives, anything can be made to look good by only looking at the positives. And the balance shifts depending on what sort of application you're writing. There's a reason that Go has seen a lot of success writing network servers. There's a reason why Go has no penetration into the scientific computing community, and I tend to warn away anybody even thinking about it.
In particular, a lot of the people screaming about Go do not consider some of the positives. For instance, thanks to the implicit satisfaction of interfaces, I find it one of the easiest mainstream imperative languages to still create a separation between IO and pure code, by wrapping all IO behind an interfaced object, one that I may not even have to create (i.e., Files already implement io.Reader and io.Writer, io.Reader & io.Writer also already have several test implementations available in the stdlib and it's easy to adapt them to a few more), which then allows me to write some really good testing code, almost as good as Haskell. (In fact, swapping in alternate implementations is probably easier than in Haskell.) This can be done in other languages, but it often involves jumping through hoops to create your own objects that mirror other object's methods so they can implement an interface or something; in Go it's trivially easy. Between that and the way errors are handled, I find it relatively easy to write very bullet-proof code.
And while I also consider it annoying that Go lacks half of generics (interfaces are actually half of generics, but it's missing generic types), there are also often ways of spelling your APIs so it matters less. My code actually doesn't end up with very many interface{}s in it, and many of the ones that do are "real", in that the code in question really doesn't care what's there.
If you don't put those positives onto the balance, you don't get a proper view.
It doesn't help that, frankly, this was not a very good article and I strongly agree it has tone issues. This brought out a lot of people who might otherwise have just clicked over without commenting.
I'm confused. How does not having to write "implements Reader, Writer" (25 characters to type) have anything to do with purity and IO effects?
> interfaces are actually half of generics
Interfaces aren't generics at all. Java had interfaces and Object as a supertype before it had generics, and it still had zero support for generics.
I do agree with you that Go has maybe 50% of the use cases for generics covered, but not because it has interfaces. Those don't count. It's because it has maps and growable arrays built in.
It's a definition game. One component of "generic" is "generic algorithm". Go has that covered; the Sort package provides a "generic" algorithm via an interface specification. A lot of C++ template code is built around implementing generic algorithms at compile time rather than Go's run time, for instance.
If you choose to call that "not generics", that's a valid choice of definition, and certainly makes sense in a Rust context, but it's not universally valid.
For context, I'm not trying to bend a definition to "defend Go"... I generally have a low opinion of software engineer's ability to create universal definitions of terms that apply across all languages, and I observe that pretty much any term you can imagine varies across language communities. I'm not the one creating terminology vagueness. And I have observed that every time someone contradicts me and insists some term really is rigorously defined and agreed to by all major language communities, you can count on two or three disagreements from not-me in the replies...
And then you have the faux-"fair and balanced" posts about Go by jerf, which always ends up favouring Go for some inexplicable reason.
How much more balanced do you want? If "balanced" looks like advocacy, well, maybe the problem is the community has overshot into the trough of disillusionment: https://en.wikipedia.org/wiki/Hype_cycle
If the community was over-praising Go... no, actually, when the community was over-praising Go, you know, that thing that caused the backlash we're in now... I was more negative. It wasn't the Best Language Ever. It's also not the Worst Language Ever.
And I'm generally against seeing "Library X, BUT IN $X!" on the front page of HN for all values of $X, but, that's a separate discussion.
Any language with a compiler for native code. All of them support static linking, that is nothing special in the Go toolchain.
Why have I seen several go apps that use static linking, but every C or C++ app I see uses dynamic linking?
A quick google makes me think that the main downsides to static linking are resources and upstream bugs. Those issues would apply to go the same way they'd apply to C. Right? So why does it seem (from my limited experience) that go uses static linking more than other languages do?
http://harmful.cat-v.org/software/dynamic-linking/
Modern applications use dynamic linking most of the time, because despite the possible headache with versions, it offers a much more flexible architecture.
I mean you can even get the jvm/java apps bundled as a single binary if you really want to.
There are only a few good reasons to heap allocate in C:
You're writing a library and want to be able to add fields to your data structures without breaking ABI compatibility.
You need to return dynamically sized chunk of data, whose length can't be known to the caller ahead of time, like a string. Still consider returning it by writing to a statically sized buffer passed as an argument, piecewise, as in read().
You need a growable buffer. Still consider allocating a reasonable maximum sized buffer and BOUNDS CHECKING.
Some library requires you to dynamically allocate to use its API. Oh well.
You want to maintain API consistency and that requires dynamically allocating something that doesn't strictly need to be.
Don't be dogmatic, but stack allocation, and occaisionally static allocation are much more maintainable than dynamic allocation.