These are my notes/thoughts while reading The Go Programming Language, from the lens of a long-time JVM programmer.
Note that I donât make constant Java comparisons to seem competitive, anti-Go, and pro-Java; instead I think mapping new concepts (e.g. Go syntax/features) onto your known/existing concepts (which for me is generally Java/JVM things), and then realizing how they are either slightly or significantly different from each other is just a good way to learn.
This is not meant as a Go tutorial, so I assume familiarity with Go, although Iâll try and explain things here and there.
Wtf GOPATH
It took me awhile to figure out the GOPATH
thing.
My usual approach is to checking out code is have a top-level home directory, like ~/code
or ~/company
, and then clone projects into ~/code/project
or ~/company/project
.
Simple and it works for every language/project Iâve ever tried to build.
In Go, this is the Wrong Thing To Do.
The Go tooling, e.g. go build
and go test
, will refuse to work on any project that is cloned like this.
Instead, it expects a GOPATH
to own all of your Go code, e.g. ~/go
by convention, and you use go get github.com/org/project
to have the project automatically cloned in ~/go/src/github.com/org/project
.
And then the builds will work.
Bizarre.
Iâm not sure why Go needs to be opinionated about this, especially when having a single ~/go
GOPATH
(as recommended by convention) is committing the same âsingle/global package namespaceâ fiasco that many other languages have learned the hard way to avoid, as per-project versioning is necessary on basically any non-toy project.
Which, granted, sounds like govendor
and similar tools now exist (post-Go 1.6), with first-class support for a per-project vendor/
directory in the Go tooling (where you can put specific versions of dependencies that are scoped to your project), but the legacy GOPATH
still being around seems super-odd and, AFAICT?, doesnât provide any value?
If it does, let me know in the comments.
Pointers Are A Thing Again?
Pointers seems like a step backwards, but I read a good SO post that it allows efficient memory layout.
E.g. structs are created contiguously in memory, including any fields that are nested structs. E.g. you can have an Employee
struct with a field Address
struct inside of it, and theyâll be contiguous memory, not just the Employee
âs primitives + a pointer for Address
at some random spot on the heap (which is what Java ends up with).
To get the contiguous memory, assigning values to the structs/nested-structs needs to be by-value/copy, so that those physical memory locations are actually populated.
Which is great, but as GOPL points out, you donât always want that (e.g. for cyclic structures like tree nodes), and so pointers let you opt out of that.
Fair enough, but it seems odd that structs are call-by-value by default, so in the majority of cases (I assert) where you really want call-by-reference, you have to opt-in via the boilerplate of pointers.
Also, maps and channels (and slices!) are actually call-by-reference (okay, call-by-reference-ish), because, yes, that âmakes senseâ as to what you usually want (I agree!), but itâs not consistent with how structs are handled, where I also assert âby reference by defaultâ makes the most sense.
I believe GOPL explained this (that maps/channels/slices are references) as a historical inconsistency, which is fair, but GOPL also had a blurb about âthe strict gofmt
rules allow deterministic AST parsing for source code transformationsâ, so maybe they should just change it with a codemod (tongue in cheek). :-)
Also, IANAE, but Iâve read that C# and Swift have solved this value/stack vs. reference/heap issue without pointers, which seems preferable.
E.g. I know Scala/the JVM have considered allowing the stack vs. heap decision to be made at type declaration time, which generally seems less boilerplately than having to decide, at each usage site, which semantics you want. This is similar to how, IMO, Scalaâs declaration-site variance is preferable/less boilerplately than Javaâs usage-site variance.
So, dunno, Iâm sure I would get used to pointers and could live with them, but seems like an odd choice.
âNo Genericsâ
Go doesnât have generics âbecause you donât need themââŚexcept for maps and channelsâŚand arrays/slicesâŚ
Basically, the language designers get generics, but you donât.
Which is generally the opposite direction of âallow user code to have as much power as the compilerâ that other new/advanced languages like Rust, Scala, etc. are going with macros/meta-programming.
Maps have a syntax of map[key]value
, e.g. map[string]int
, which is odd as the trailing value
type is outside the last ]
âŚthis is very special-cased for âthe last type parameter is a return typeâ and doesnât seem like it would generalize well to N
-arity generics.
I suppose you can treat map[key]value
as a âbuilt-in syntaxâ, similar to arrays, and no one complains about arrays having a built-in syntax, e.g. int[]
in C/C++/Java/etc.
However, arrays are fundamentally tied to memory layout, vs. just being a batteries-included data structure, so Iâm not sure map
is that special. Also, I did really like Scalaâs consistency of going the other way, and modeling arrays as âjust another genericâ, e.g. Array[Int]
.
Channels also have their own one-off generic syntax of chan string
, which is odd as it has no brackets at all, which is fine/Haskell-like, but again seems hard to eventually generalize into first-class generics.
âNo Inheritanceâ
Go structs donât have inheritance because âinheritance is evilââŚand yet you can embed structs in another to get basically the same effect, e.g.
struct Employee {
SSN string
}
struct Account {
// note there is no field name here
Employee
}
Having only the Employee
type, with no explicit field name, means all of Employee's
fields become auto-aliased and can be referenced like account.SSN
âŚhey look, we âinheritedâ the SSN
field from Employee
.
(Disclaimer: Account
and Employee
are really bad examples here.)
My understanding is the Account.employee
field still exists under the covers, and account.SSN
is just compiler syntax sugar that expands into account.Employee.SSN
.
I need to learn more about this, but my initial reaction is that this seems similar to the JS proclamations of âprototypes are superior to classesâ, when, at the end of the day, theyâre so similar that most people solve/models problems the same way anyway.
My assumption is that struct embedding would end up being the same âeveryone uses it as faux inheritanceâ, at which point does it really matter whether itâs inheritance or not?
E.g. Go seems to assert âstructs must always be âhas aââ, and âonly interfaces can do âis aâââŚbut at the end of the day, you often need to your data (structs) to model âis-aâ relationshipsâŚsoâŚ
Iâm not sure yet.
Seems somewhat similar to Protobuf/GRPC (which is another Google project) ânot supporting inheritanceâ, but really it just means people model inheritance in an informal, adhoc way anyway.
(Can I claim a Greenspun-style law? âAny language/framework that doesnât support inheritance, will see its users implement adhoc, informally-specified, bug-ridden approaches of half of proper inheritance?â)
Upper Case for Public Access
In Go, the Employee
type is exported from its package, because of the upper-case E
, but the employee
type is not. Same with functions/methods like Name
vs. name
.
Using upper case for public
access is cute, admittedly very succinct, and I think I could like it.
However, one major downside is that interferes with the nearly-ubiquitous âtype names are upper case, variable names are lower caseâ idiom from other languages.
E.g. in Go you might have type foo struct { ... }
to represent a package-internal type, and so have methods like func add(f foo)
, which, dunno seems weird to me.
Iâm not sure the succinctness is worth giving up the type/variable name convention, vs. just having something like default-public + a private
keyword.
Field Tags are Annotations
Field tags in Go are ways to add metadata to structs/fields, which is great, I like metadata, e.g.:
struct Employee {
FirstName string `json:first_name`
}
The json:first_name
string is the field tag (which the JSON library will pick up to customize its handling of the FirstName
field).
Field tags are dramatically simpler than annotations, as they are just key-value pairs, and even that is only by convention/encoding the key/value pair in a string. Which seems maybe a little too reductionist.
That said, I get using âitâs just a stringâ likely dramatically simplifies (and speeds up) the compiler, because it does not have to go find and type-check a user-supplied annotation type, e.g @MyAnnotation("foo_bar")
, and make sure the annotation type parameters are appropriate.
Which is slower, but checking those things is generally the point of a type-safe language/compiler.
âNo Exceptionsâ
Goâs approach to errors/exceptions seems reasonable; I was initially thrown off by the âno exceptionsâ label, but it does have panics which are extremely similar (e.g. panics unwind the stack, can be caught (in a defer
function), etc.), so much so that Iâd be tempted to just call them the same thing (from a mechanism perspective, I get the convention is different).
(Although note that in Go truly bad things like OOMEs are not panics, they are just exits, which I think is nice/fine, as the JVM mixing java.lang.Error
into java.lang.Throwable
can occasionally cause confusion, granted most of that was Scala specific.)
Per GOPL, the biggest difference between exceptions and panics is that Go has stronger conventions around when to use/not use panics (e.g. in the JVM, catching exceptions is common, in Go catching panics should be rare).
Specifically, the convention is that error codes are similar to (well-used) checked exceptions in Java: only for âexpected errorsâ like I/O.
Panics are reserved for âunexpected errorsâ, e.g. bugs, e.g. runtime exceptions in Java.
So, while Java has been all over the place in its conventions (started out with lots of checked exception abuse, and has retreated to runtime exceptions for all things), Go is starting out with strong conventions on âexpected errors == codes vs. unexpected errors == panicsâ, which to me is more important than the semantics of exceptions vs. return values.
For example, a common complaint about checked exceptions is that they âlitterâ the call chain up the stack until they are handled; however, Goâs error codes would similarly need to be percolated up the call chain until they are handled (e.g. if youâre 5 methods deep, and now make a call that returns error
, you likely need to change your own return type, and all the return types above you, to pass the error
up the stack). And, if anything, in a more verbose way since this percolation is not handled by stack unwinding.
So, at that point, whether your method has throws FooException
or return (... error)
seems somewhat of a wash, at least in terms of boilerplate/annoyance.
That said, while Javaâs checked exceptions are âmust always be handledâ enforced, you can opt-out of this with Goâs error codes by just ignoring the 2nd/error code return value. So that is nice.
Anyway, my simplistic view for JVM programmers: error codes are checked exceptions (theyâre documented/percolated in the API), while panics are runtime exceptions (theyâre unpredictable).
And, again, the technical distinction is somewhat moot, IMO, vs. the bigger factor that Go has had strong/consistent conventions applied from day 1.
So, seems fine, and I do like how the convention handles layering of error code information as it manually unwinds the stack.
Method Declarations
Go has methods, e.g. you can do employee.Name()
(where employee
is an Employee
struct instance), but methods are just special function declarations that are not enclosed within the traditional Employee
class or struct block.
E.g. the definition for Name()
that allows employee.Name()
is:
func (e *Employee) Name() string { ... }
This is cute because it highlights that methods are just functions with a special 1st argument (similar to Pythonâs self
argument), called the receiver.
And it also highlights that structs are open, e.g. anyone can add their own new methods to existing structs. Which I think in traditional OO is not considered kosher, but since youâre limited to the external/public API anyway, I donât mind the syntax sugar. E.g. extension methods in C# are similar and nifty.
However, I wonder whether the boilerplate of writing (e *Employee)
over and over, for each method declaration, is worth the cuteness vs. just giving a syntax sugar for:
class Employee {
func Name() string { ... }
}
Okay, I get it, class
is a dirty word, how about:
methods Employee {
func Name() string { ... }
}
Granted, the notation needs to handle pointers vs. values, but dunno, just seems repetitive to re-type that first (e *Employee)
each time (especially as type names get longer, which they invariably do in large/complex project).
But perhaps the mental clarity is worth it.
Odd interface{}
syntax
In Go, you can accept interface{}
as a type, and it basically means âthis interface could be anythingâ, e.g.:
func doSomething(foo interface{}) string {
...
}
Which, I get it, interface{}
means âthe empty interfaceâ, but other languages give their similar top types a dedicated name like any
or Object
.
Granted, this is slightly different as itâs not actually the top type, as interfaces lives in a different namespace than types themselves (e.g. there is no object hierarchy between int
and float
and struct Employee
, this is only for interfaces).
Dunno, just seems worth a keyword to avoid the {}
after the type name. Who knows, maybe youâll want to use {}
for generics someday. :-)
Structural Typing
Similar to TypeScript, Goâs interfaces are structurally matched, e.g. a struct Employee
doesnât have to have an implements Printable
in its declaration to be cast/accepted as the Printable
type.
Instead, Go just needs to be able to match the Employee
shape to the Printable
shape (e.g. they both have the PrintOut()
method).
Furthermore, per earlier, since you can declare your own methods for structs, you can retrofit existing/3rd-party structs into whatever interface/shape you want.
Which I think is great; initially I was skeptical that âinterfaces are not just shape, they are semanticsâ, but Iâve come around, itâs really handy to adapt 3rd-party/uncontrolled objects to your own abstractions.
There was a proposal to add something similar to the JVM, interface injection, granted as a tool for language implementors, and not as part of the Java language proper.
And, of course, itâs not implemented yet.
But, a win for Go here, I like it.
Type Switches
Go allows switching on the concrete type of an interface, similar to instanceof
in Java, e.g.:
func doSomething(foo: interface{}) {
switch foo.(type) {
case int: ...
case string: ...
}
}
And within each case statement, the foo
variable will be implicitly cast to the matching type. Which is handy.
This seems like a useful language constructor.
That said, Iâll nit pick :-) that the GOPL book asserted this allows Go to have discriminated unions/ad hoc polymorphism (which is where instead of handling polymorphism through âdifferent behavior is in different methods in my centralized class hierarchyâ, you handle polymorphism by âdifferent behavior is in different case statements throughout my user programâ; there are trade-offs to each).
Definitely agreed this allows ad-hoc polymorphism, but IMO itâs not discriminated unions, because there is no way to restrict the foo
type to only the subset of types we actually handle (e.g. int
and string
in this case).
In other languages, again picking TypeScript, discriminated unions are modeled like foo: int | string
.
Goâs approach is basically passing around java.lang.Object
, and so not type-safe.
Which is fine, itâs what youâd do to achieve ad hoc polymorphism in Java as well, but Java doesnât claim to have discriminated unions.
(Admittedly, this was just one sentence in GOPL, and not the Go marketing website or what not.)
Go Channels
Perhaps simplistic, but go channels are just queues: either unbuffered (e.g. Javaâs SynchronousQueue or LinkedTransferQueue) or buffered (e.g. Javaâs LinkedBlockingQueue).
A quick example:
words := make(chan string)
// fire up a new goroutine to read words
go func() {
// read/process a word
word := <-words
}()
// write some words
words <- "first"
words <- "second"
My initial skepticism is that go routines/collections should be a library rather a platform feature.
However, that is likely (okay, definitely) wrong, because the crux of goroutines is not thread-safe collections, itâs high-volume scheduling (e.g. N
fibers running on M
threads where N >> M
), which is inherently a platform/runtime feature.
Iâm not sure the latest/best way to do this on the JVM; Quasar looks dead, Clojure has a new core.async but I havenât found what it uses for underlying scheduling.
I suppose Akka is most likely the JVM equivalent, but it assumes actors, which is a slightly different model than typed queues.
The runtime support aside (haha, that is a big disclaimer), I donât see anything that makes currency from a language/type-system perspective easier to do in Go.
E.g. the one-way channel syntax (which you can use to pass around read-only or write-only channel variables, to prevent code from accidentally reading from something itâs only supposed to write to) , <-chan int
and chan<- int
, again seems like a cute language-specific syntax hack for what generics would handle with a WriteOnlyChannel[int]
or what not.
Hm, well, I might be wrong: the select
that blocks on N
channels looks unique/canât do that with a library:
// waits for either channelOne or channelTwo
select {
case <-channelOne: ...
case <-channelTwo: ...
}
Doing a blocking read from two queues like this is hard in vanilla Java. However, it is pretty close to an actor responding to N
messages.
Granted, the channels are typed, and communication is more about âpublishing to a queueâ than âpublishing to an actorâ, which personally I prefer.
So, nit picking their special syntax status aside, channels look nice/well done.
Other interesting notes:
- Every channel operation (read/write) flushes memory barriers, so you can use them as synchronization primitives.
- Channel scheduling is done via language/runtime hooks, e.g. waking up from a blocked read on a channel doesnât require a hardware interrupt, instead the channel that youâre blocking on knows via bookkeeping that youâre waiting for it, so youâll get implicitly/automatically scheduled, all in-process (I think).
Other Misc Pros
Things I like:
-
Static binaries are great.
I like deploying as static binaries. I wish Javaâs GraalVM would have come out ~5+ years ago.
Given my previous nitpicks with the language above, I can see deployment simplicity being a large factor in why itâs become popular, especially in the cloud tooling world.
-
The compiler speed/productivity is amazing.
I made a quick VS Code project for an existing, medium-ism Go project, and it had all of the nice IDE features with little hassle (
GOPATH
aside) and very quick, very solid.Thatâs great.
Obviously a huge win, albeit the âspeed is non-negotiableâ stick is also what they beat down features like generics with. But at least it really is fast.
Other Misc Cons
Things I donât like:
-
Unlikely to have cross-platform ambititions.
Go seems unashamedly a server-side language + runtime. Which is fine, specialization is good, although as a nit-pick I think itâs only really necessary for the runtime requirements of goroutines (e.g. the
m:n
scheduling infrastructure). (E.g. if you gave up goroutines, the rest of the language could be cross-platform.)But, in a perfect world, Iâd prefer using the same language on mobile, web, and server. It just lessens the cognitive overhead vs. a team/org having to support
N
platforms withN
distinct super-unique codebases.Note, I said âperfect worldâ, as weâre not always there yet, but I think languages that move us closer to that (primarily TypeScript via JavaScript eating the world, but also Kotlin via JVM/JS tranpilation, and others) are great to see.
-
No functional programming.
I like the OO/FP blend of Scala, and attempts to do similar in other languages, e.g.
map
andfilter
show up everywhere these days (not to insinuate Scala invented them, itâs just where I was exposed to them first).However, Go sucks for functional collections, e.g. theyâre not built-in, nor can you roll-your-own due to the lack of generics.
Also, the community, at least in 2014, seemed borderline rude about it: âNewbies should learn the language theyâre trying to learnâ.
I get that FP-a-la Java Streams is way more expensive than
for
loops, and Scala had itâs fair share of âoops, rewrite the slow FP as fast imperitiveâ, but seems like that should be a challenge to solve, not dismiss as âif you want/like FP, you must be using Go wrongâ.
Conclusion
So, those are my notes/thoughts.
I donât think I personally would choose Go for a new personal project; to me there are enough language warts that are not offset by enough âonly available in Goâ features.
But I also donât think Iâd hate working on a Go codebase, as, to their credit, the stake-in-the-ground around compiler performance and associated developer and tooling productivity is very nice.
I good see that ending up changing my mind.
Fast feedback loops can make up for a lot of deficiencies.
And the opposite is not true, e.g. perfection with a slow feedback loop is not actually perfection, and can actually be dramatically worse.