I've done video4linux stuff in Go, and passing an unsafe.Pointer to a Go struct in an ioctl() worked fine, which tells me that Go structs are isomorphic to C structs. Even though Go has garbage collection, it allocates everything it can on the stack, so only long-lived shared-between-goroutines objects are subject to garbage collection.
Go abstracts concurrency, completely removing all concurrent features from a language except for the "go" keyword (that launches a goroutine - which is basically a tiny virtual thread), channels (which are selectable queues) and "select" keyword that waits for the first "input" from a static set of channels.
Go strikes me as one of the best "good enough" languages we have right now. You're not going to do HPC in Go, but it's performant enough to run circles around a lot of high level languages and dynamic languages. It abstracts away the stuff that's super error prone about manual memory management, and it's so brutally simple that it's hard for one of your colleagues to write code you're not going to be able to understand.
There's a few features that could improve it, like proper ADT's, and it's a bit lacking in expressiveness for me to choose it for personal hobby projects, but I would recommend it any time for general-case professional software development.
Go can be faster at small, specific programs where memory lifetimes are deterministic and you can use value types. Otherwise, Java will beat every other managed language by a huge margin when it comes to GC-related workflows. Sure, it does so at higher memory usage, but that is a good tradeoff for many use-cases (especially server).
So all in all, for bigger programs it is hard to do a good comparison, but there is exactly where JIT compilers shine and the memory tradeoff and the like brings their return.
Isn't a lot of server code these days small programs that just pull data out of a database and feed it to an HHTPS response?
It seems like it's a pretty good option to have your infrastructure code implemented in a systems programming language like C or Rust (probably AWS or GCP is doing this for you), and just implement your business logic in Go as the type of small, well-defined programs you're talking about.
But then why go? You can also implement that logic in a likely much more readable way in Python (as at that point performance doesn’t matter), or just write the whole thing in Java/C#/Scala/Kotlin whatever, which in my opinion are more expressive for business logic.
Go is much faster than Python, uses less memory, and compiles down to a statically-linked native binary, making containerization trivial. And (IMHO) it's even more readable than Python - nowdays Python code is as easily turned into an unreadable mess as Java or C# code. Just try reading Python standard library and Go standard library - the difference is monumental.
We are talking about business logic. The infrastructure is already in a lower level language, so the performance is not a concern.
And we will have to disagree on C#/Java/Python being unreadable mess. In my experience all 3 can be written in a really well maintainable way. I don’t have much experience with Go, but out of these, I would vote for it as the least maintainable (as just because each line is trivial to understand, doesn’t make the whole program flow easy to read. Otherwise why not just write assembly, every line is even more trivial there)
> In my experience all 3 can be written in a really well maintainable way.
That's true, but that is generally true of any (non-toy) language. But in the modern world of rapid development, it matters how hard is it to write code in a non-maintainable way - i.e. how well it tolerates modifications by different people. And to me, it seems easier to write readable code in Go than it is to write unreadable code.
It seems to come from the lack of features - Java, Python and C# have too many features, and any problem can be solved in N different ways, each one with its own warts. If you want to work on a wide range of codebases, you have to know each one of the approaches and their warts and footguns.
Meanwhile, Go feels like it really reached the "there should be one obvious way to do it" ideal of Python, while Python has over the years evolved into something more Perl-like. Want to build a concurrent application? Chose your tradeoff - either you get CPU scalability (multiprocessing) but lose memory sharing, or you get a simple concurrent model (threading) that isn't scalable, or you get I/O scalability (asyncio) at the cost of function coloring, error-proneness and a single-threadedness. Go solved the whole thing with the goroutine model - internally it multiplexes coroutines onto a set of OS threads, but all blocking calls are wrapped by Go runtime which makes every coroutine behave and feel like an ordinary thread, without the massive memory use of OS threads.
The number of ways to write something is only very loosely correlated with maintainability. The ease of maintenance is IMHO more a function of how much information about the properties of the code you can easily read from the text, and how well the abstractions in the code map to the abstractions you'd use when describing the solution to a friend. Lack of features doesn't help in that regard. That's why probably Go has just added generics, despite the long tradition of Go promoters claiming "lack of generics is a good thing" ;)
Languages with very little type information, e.g. dynamic ones, tend to be quite hard to maintain, unless the original developers kept the discipline of good naming and verbose commenting. Go and Java with their somewhat static, but limited typing and elements of dynamism (interface{}, Object, reflection), sit somewhere in the middle between PHP/JS and Rust/Scala/Haskell.
Languages with little expressive / abstraction power, so the ones limited in features or low-level are also often hard to maintain, because you have to reverse-engineer the high-level stuff from all the details you see. Take assembly as an example - while it may be quite obvious what the program is doing at the bits and bytes level, understanding the sense of that bit-level manipulation may be a much harder task. The assembly language might actually be very simple, but that does not help. I remember when we had a MIPS class, the whole specs was just a few pages, could be learned in an hour.
Could you expand a bit on why do you think Java has too many features? It is a very small language, that is often berated because it picks up features way too slowly if anything.
I would go as far to claim that Java is an easier language than Go, or at least in the same ballpark.
> Could you expand a bit on why do you think Java has too many features?
I think you're asking for technical details, but I'm afraid I don't know Java well enough to do an objective comparison. I'll try with a subjective explanation or why I think so.
I've learned Go in 20 minutes following the Go Tour. Few months later, I feel like there isn't a single thing I don't know about Go. It's dead simple. When I open a Go repository, it's easy for me to get into the codebase, as all code is more-or-less the same.
I've learned Java back in high school, and to this day I don't feel like I "know" the language. I've tried reading some Java repositories, and every time I feel like there's some kind of friction - some implicit knowledge about it that I just don't understand.
Maybe it's just me, and I haven't spent enough time learning Java. But then again, I've spent even less time learning Go, and yet I have a much easier time using it. That's what I mean by "a very small language".
Performance is always a concern. For instance, if running a python interpreter introduces latency for each request, that can add up to perceptively worse performance when applied throughout a product.
In my experience, Python is far less readable than languages like go. The information density and semantic whitespace of Python really hurts readability.
Having read the rules it's difficult to know what's considered "fair" for this test - all GC tuning is off the table, sure. But what's bugging me is "Leaf nodes must be the same as interior nodes - the same memory allocation." So what constitutes "the same memory allocation" - literally the exact same call to some opaque internal allocator? If so, shouldn't Java also have to disable JIT to be fair?
Let me offer an alternate interpretation: I will do the same memory allocation if I need to allocate a node, but if my language lets me not allocate a node yet still use that node why should I? Or an alternate argument if you don't like that one: Why must my "node" be `Tree`, rather than `*Tree`?
A central idiom of Go is that zero-values of a type can be useful; a two-line change, no new special-cases, no pooling or such gauche hacks:
// Count the nodes in the given complete binary tree.
func (t *Tree) Count() int {
if t == nil {
return 1
}
return 1 + t.Right.Count() + t.Left.Count()
}
// Create a complete binary tree of `depth` and return it as a pointer.
func NewTree(depth int) *Tree {
if depth > 0 {
return &Tree{Left: NewTree(depth - 1), Right: NewTree(depth - 1)}
} else {
return nil
}
}
I'm sure someone will tell me I "optimized away the work" - but in the end I believe I'm making exactly the same number of method calls on the same type of receiver. If that's not the work, what is?
And then the Java etc programs are re-written to do the same and the test value is increased (to compensate for the reduced memory allocation) and we're back where we started?
- Practically, Java would rather have to `return 3` when it detects a null child, effectively precomputing the penultimate level.
- Semantically, Java could no longer distinguish between an absent child and a child with no children.
Honestly, I have other variants that still don't use pooling but are less idiomatic; I find this exercise is begging the question hard. Any tools, however idiomatic, the language is giving you to reduce the effects of allocation seem to be off-limits for GCd languages. Whereas then e.g. C can just throw them all in a third-party pool library. And JIT languages are presumably allowed to fuse anything they want.
The answer to "… but if my language lets me not allocate a node yet still use that node why should I?" is — Because allocate a node is the basis of comparison with the other programs!
Change that for the Go programs and you change that for all the other programs; otherwise just special pleading for Go lang.
Wow, I had no idea that java was so fast. One thing I like about go is that you can cram many thousands of concurrent requests into the same process. But it looks like java has some pretty robust async tools... So I bet you could do something similar
The JVM is a real beast, which makes sense as a good chunk of the whole internet runs on top of it (almost every big corp has plenty of infrastructure running Java), so it had plenty of engineering time poured into it.
Regarding concurrency, I wouldn’t choose existing reactive frameworks and what not for a new system. Java will soon get Project Loom, which will introduce Go-like virtual threads - so that one can write a web server that spawns a new thread for each request as well. Since the Java ecosystem is very purely written almost exclusively in Java itself (no FFI), basically everything will turn automagically non-blocking.
He said "easy to read" not "filled with weird syntax choices that make anyone from a C background barf". What the hell are those channel arrows and why do they point the wrong way.
Channel arrows are basically just read/write operations.
If `ch` is a channel, then this expression means "value obtained from reading from the channel":
<-ch
And this expression means "write value x into the channel":
ch <- x
Both expressions can be used as a case inside select statement:
select {
case val := <-ch:
// ...
case ch <- val:
// ...
}
Which will execute exactly one case, depending on which channel becomes "ready" first - channel is ready for reading if there is another goroutine blocked on a write operation, and ready for writing if there is another goroutine blocked on a read operation.
You say you're from C background - if you've ever worked will file descriptors you will notice that channels are basically userspace file descriptors. Channel reading and writing is isomorphic to read() and write() syscalls, and select keyword is isomorphic to select() syscall.
Hopefully this clears up the whole channel syntax thing. I just hope you weren't trolling.
> Channel reading and writing is isomorphic to read() and write() syscalls
This is a very bad mental model because channels operations cannot be canceled (without using select on two channels) or return any error status (at all).
While technically true, I don't really see the impact of the difference. It is idiomatic to use contexts for any kind of cancellation during channel operations, and it works well.
The syscall comparison was made to give intuition about general behavior of channels to someone with a background in C. Of course it's not completely identical.
The ability of read/write to communicate via errors as well as the actual data transferred is significant - there's a reason Go's i/o model is io.Reader/io.Writer and not chan []byte.
You might as well explain channels in terms of any blocking operation if the bar for "isomorphic" (now backtracked to "intuitively" I guess) is that low.
I don't know what's the bar for "isomorphism", but I know that the word literally means "same shape", so just because some nerd that got killed in a duel over a girl used the same word in a mathematical context doesn't give him dibs over its general use.
Unless, of course, system calls can be modeled as operators over sets. In which case, please tell me how.
Well, in my case I used it to signify same behavior in terms of process/goroutine communication. I think it's "specific enough" to warrant the use of word "isomorphic".
I've done video4linux stuff in Go, and passing an unsafe.Pointer to a Go struct in an ioctl() worked fine, which tells me that Go structs are isomorphic to C structs. Even though Go has garbage collection, it allocates everything it can on the stack, so only long-lived shared-between-goroutines objects are subject to garbage collection.
Go abstracts concurrency, completely removing all concurrent features from a language except for the "go" keyword (that launches a goroutine - which is basically a tiny virtual thread), channels (which are selectable queues) and "select" keyword that waits for the first "input" from a static set of channels.