I feel just the opposite: Go has very few tools, which forces you to often have to solve language games instead of focusing on your problem.
You want to remove an element from a list? Instead of writing that, you need to iterate through the list and check if the element at position i has the properties you want, and if it does, copy the list from i+1 (if it wasn't the last!) over the original list. This is NOT the problem you were trying to solve.
You want to store a map from some struct to another? Instead of writing that, you have to create a key struct that is comparable for equality from your original struct, which probably involves string concatenation and great care to avoid accidentally making unequal structs have equal keys; and then two maps, one from keys to the key struct and another from keys to the value struct, and of course client code also has to know about this explicitly.
You want to pass around some large structs, or iterate over slices of them? Well, you'd better start using pointers, and start being careful about what is a copy and what isn't, otherwise you'll pay a steep performance price, without any syntactic indication.
You want to work with sets of values? You'll have to store them as map keys, perhaps doing all of the black voodoo described above. And of course, you can't do a reunion of two sets, you have to iterate through the keys of the first map and the second map and add them to a third map.
All of these things are annoying when you are writing them, and even more of a problem when you are reading the code, as you have to understand the intent from the messy internals that Go forces you to expose.
Go is basically how Java was pre-generics. People forget that early Java was much like Go in some respects. Everything casted to object. Not really sure what the object is unless you wrote the code. Lack of common patterns and a rich set of libraries (Guava, Apache Commons).
Early Java was supposed to be safe C++. Converting C++ to Java is still trivial unless there's pointer craziness. Generally copy paste the C++ and change naming conventions.
I hate working on unfamiliar Go projects. Copy pasted code everywhere. And everyone builds their own abstractions because there aren't enough.
I don't understand Go's appeal. To me it feels clunky and stuck in the 90's. Designed by C programmers for C programmers.
Like C it has no generics, bad packaging, and is a pain in the ass to cross compile. Its only killer feature, IMO, is it's fiber threading model and that's quickly being copied by other languages, even ancient Java.
What? You've obviously never seen actual C++ code to make such a ridiculous statement.
- https://blog.stalkr.net/2015/04/golang-data-races-to-break-memory-safety.html
- https://golang.org/doc/articles/race_detector#IntroductionArguably, if the project requires lots of abstractions and copy-pasted code, then the project shouldn't be written in Go. Just because you can, technically speaking, write object-oriented code in Go does not mean that it is ergonomic to do so or a recommended use-case for the language. Projects that benefit from object-oriented design should stick to languages with first-class support for it.
> a language that doesn't support dynamic dispatch.
Go's interfaces do support this enough that I consider it worth at least mentioning.
https://en.wikipedia.org/wiki/Dynamic_dispatch#Go_and_Rust_i...
Go modules are pretty good packaging IMHO. What don't you like about them?
As for cross-compiling, are you joking? It's very easy and i literally cannot imagine it being easier. What's painful about it?
The biggest reason why Go modules are a very poor packaging solution is the horrendous stupidity of tying them to source control. This makes it difficult to develop multiple modules in the same repo, it requires your source control system to know Go, it makes internal refactors a problem for all consumers of your code (e.g. if you are moving from github to gitlab, all code consuming your package will have to change their imports).
The versioning ideas of their module system, particularly around the v2+, have made it such a pain that there still isn't a single popular package that uses the proposed solution so far, not even Google ones like the Go protobuf bindings.
I think it's actually a great idea. It uses native things you use anyway, avoids an unnecessary third party ( a package repository like pypi), and adds in extra security ( remember those hijacked pypi credentials and pypi-only malicious packages that weren't in source control, so hard to verify?).
> This makes it difficult to develop multiple modules in the same repo
How so? You can justs use different folders and packages.
> requires your source control system to know Go
Not really.
> it makes internal refactors a problem for all consumers of your code
That's always the case - regardless of your package repository, if you change it, you have to update the new references everywhere. You can use go.mod to replace a reference ( e.g. use gitlab.com/me/library instead of github.com/legacy/library) without updating all the code. And then there's the goproxy protocol, which makes all of this optional.
I agree on v2 modules, that is very weird.
Not really. You may be using Git internally, but if you want to consume a module that is developed in Perforce or Subversion, you must now also install P4 and/or Subversion and configure them so that they have access to the remote repo.
It doesn't avoid a third party, it actually makes you reliant on numerous third parties, the source hosting sites for every module you use (Github is a 3rd party between me and the developers of the Go language bindings for protobuf, for example).
Also, that is false extra security, as nothing prevents the code itself from being malicious or embedding malicious files, and this can be arranged to be served only when request through Go mod (based on headers, on the HTTPS requests that only Go mod makes etc.). Especially given that go supports shell execution in source files (//go:generate rm -rf ~/).
> How so? You can justs use different folders and packages.
If you have multiple modules in the same repo, you have to tag every "release" with multiple tags, one for each module (e.g. mod1/v1.0.123, mod2/v1.0.131) and you'll quickly run into problems (if you don't sync releases, you'll have absurd combinations of module versions whenever you sync your repo, so you won't be able to rely on `replace` clauses for example).
> > requires your source control system to know Go
> Not really.
It does, if you have a dependency like "github.com/user/proj", go mod will first make an HTTPS request to this URL and expect some specific responses telling it how to get the code.
> That's always the case - regardless of your package repository, if you change it, you have to update the new references everywhere.
If I publish my modules to Maven, I can switch from Perforce to Git without any change whatsoever for anyone consuming the Maven package. I can split or merge my repos internally in any way, but as long as I produce the same Maven package, my consumers don't need to care.
Replace directives and goproxy are bandaids for a silly problem to have.
`go build -target x.y` could be considered fractionally easier than `GOOS=x GOARCH=y go build`? But yeah, this is a nonsense claim by GP.
But that's not really what the article says. As it mentions, I don't think these are insurmountable issues, and I still like and use Go: it's pretty much my go-to language these days. Overall, I think it did a lot of things right. But that doesn't mean it's perfect or can't be improved.
The blog post does admit "Are these insurmountable problems? No." Learning it in 5 minutes certainly is a stretch as you point out, but I'd still say you can learn it in a weekend to a greater degree than any other language I use.
Though you'll notice my comment after the first sentences is a way to share my own thoughts on Go, an opportunity I'm never going to neglect.
1. Lack of "manual memory management" can be taken as GC (though Rust is neither GC'd nor manual memory managed, it's declaratively memory managed) and a GC is unacceptably expensive for systems programming. Furthermore, until recently GC was also unacceptably bad for FFI. However stable Rust and Haskell with experimental features can nowadays GC across FFI without issue, but I'm still unaware of any other language that can.
2. Lack of "manual memory management" also means lacking direct memory access, etc. This is a problem in systems programming, though you do not need it for all systems programming.
3. Then there's green threads. Early on Rust had them, but they were removed when it was discovered it's theoretically impossible to implement them with acceptably low overhead for a C or even a serious C++ competitor. What's worse, you have to pay the overhead even if you don't use the feature. Nowadays, you can opt in by importing an ecosystem library that provides green threads but mostly people just use futures rather than green threads.
4. Interfaces: Even tinygo's documentation advises avoiding interfaces and tinygo isn't even a C/C++/Rust competitor, it's a micropython competitor.
5. ...
But to their point: why incur that sort of overhead for a language that doesn't necessarily give you enough conveniences to be worth it? Or rather, Rust gives you more conveniences with very little runtime overhead.
It's not exactly damning to be unable to compete with C on a microcontroller with 2K RAM, they were just pointing out the end of my comparison with C/C++.
It'd be nice to have syntactic sugar for splatting structs together, or a library that let you do it in a typesafe and efficient way.
EDIT: I will defend the random map order, though. Golang maps were only predictable up until a set number of items, which caused hard-to-detect bugs where your small test case would be in order but your large real-life data would be out of order.
The next item on my wishlist are algebraic data types. In complex projects I've opted for Rust over Go just because of the abstraction power of being able to represent things like:
Dest = Register (A | B | C)
Src = Register (A | B | C) | Immediate value
OpCode = Nop | Add(Dest, Src) | ...
In Go, I'm so tired of `interface{}`.Edit: To respond to your edit about map iter order, that's related to my complaint of Go's lack of dev vs release build. Randomizing map iter order in dev builds is acceptable to me. Go makes you pay for that in prod.
You can spell out all your types into explicit structs (where you can choose between a simple implementation with a little worse performance or to make the implementation a little more complex) and pass them around.
There are many complex code bases (kernels & databases come to mind) which use C (not C++), and they don't resort to passing void* around the "business logic" parts.
The idea that for a complex project you would choose a different language with so many different characteristics due to a minor detail about the type system (this is not exactly Haskell vs JS...). This kind of decision making would not pass in any reasonable organization...
No, it's more like Haskell vs C, considering the expressiveness of Rust’s vs Go’s type system.
An example is rolling your own discriminated union:
type Avariant struct {
a int
}
type Bvariant struct {
b string
}
type SumChoice int
const (
SumA SumChoice = 0
SumB SumChoice = 1
)
type Sum struct {
type SumChoice
A *Avariant
B *Bvariant
}
sumA := Sum{choice: SumA, A: &{a: 7} }
sumB := Sum{choice: SumB, B: &{b: "abc"} }
func foo (x Sum) {
switch(x.choice) {
case SumA: {
fmt.Printf("Value is %d", x.A.a)
}
case SumB: {
fmt.Printf("Value is \"%s\"", x.B.b)
}
}
}
As usual with Go, it's ugly and verbose and error prone, but it just about works.The previous poster was probably thinking of something similar but using interface{} (equivalent to Object or void*) instead of Sum, and then pattern matching using an explicit type switch:
func foo (x interface{}) {
switch actual := x.(type) {
case AVariant: {
fmt.Printf("Value is %d", actual.a)
}
case BVariant: {
fmt.Printf("Value is \"%s\"", actual.b)
}
}
}
This is slightly less risky and has less ceremony, but it's also less clear which types foo() supports based on its signature. Since Go has only structural subtyping [0] for interfaces, you would have to add more boilerplate to use a marker interface instead.[0] In Go, a type implements an interface if it has functions that match (name and signature) the interface functions. The name of the interface is actually irrelevant: any two interfaces with the same functions are equal. So if you declare an interface MyInterface with no methods, it is perfectly equivalent to the built-in `interface{}`: any type implements this interface, and any function which takes a MyInterface value can be called with any type in the program.
On the first approach, with "error prone" you mean the tag could be incorrect, right? (or even have an impossible variant with 0 or multiple variants set).
type Sum struct {
type SumChoice
A Avariant
B Bvariant
}
This is what I meant by saying that it depends on exactly what you mean by "sum types".Still, I think most people don't worry so much about efficient packing of sum types, and instead the safety and convenience factors are more important. Of course, YMMV based on exact usage.
I'm not in anyway claiming that Go supports sum types. Someone just asked how they may be emulated, and I don't think it should be surprising that emulation has an overhead.
As someone that was writing Arduino like code in C++ in 1993 (MS-DOS), I fail to see that.
I am fully aware of the last decade of C++, including papers written by Bjarne Stroustrup and other C++ key figures, where they uses the hated C/C++ expression, that so many happen to have issues with.
Which I can gladly point you to.
Given that security is one of my areas, I always keep up to date with C and C++ standards, even if I don't use them every day.
As for not using new and delete, I kind of agree, then again the only C++ GUI worth using has it over the place.
I rather see all those school kids learning Arduino to use new and delete than malloc() error prone size calculations, or dive into template errors due to the misuse of smart pointers.
Maybe the abbreviation should be `C & C++` then :-). But I'm not sure if infix precedences allow it to work.
Can you explain what you mean by "runtime penalty?" Last I checked, the iteration is only random wrt its starting offset; after that, it proceeds linearly through the buckets. So you only generate one random integer per `range`, which seems acceptably cheap.
https://github.com/golang/go/blob/db8142fb8631df3ee56983cbc1...
I just think we also tend to dramatize how much that matters.
I also think Go's benefit really is that it's simple and that you have very few tools that let you do anything other than focusing on solving your problem, and like the author I do think goroutines are the exception to that.
Where I work, we don't even use them. We use plain ol' mutexes instead.
When coming from higher level languages, Go does feel frustrating. In Javascript, you're writing `const [a, b, c] = await Promise.all([x(), y(), z()])`. In Go, you're writing 40 lines of WaitGroup code. It's easy to go ughhh.
But I think a nice way to appreciate Go's conservative middleground is to go back to writing some C/C++ code for a while, like some Arduino projects. Coming from that direction, Go feels like a nice incremental improvement that doesn't try to do too much (with perhaps the exception of goroutines).
Go's performance is also particularly stand-out which makes up for many of its convenience shortcomings. It's fast enough that I've written some code in Go where I would have written C not long ago. And writing C involves quite a bit more concessions than what Go gives you, so in comparison, Go kinda spoils you.
Go has plenty of annoyances too though. Not having any dev vs prod build distinction is annoying. Giving maps a runtime penalty of random iteration in order just to punish devs who don't read docs is annoying. It's annoying to have crappy implementations of things like html templating in the stdlib which steal thunder from better 3rd party libs. Not being able to shadow vars is annoying. `val, err :=` vs `val, err =` is annoying when it swivels on whether `err` has been assigned yet or not, a footgun related to the inability to shadow vars. etc. etc.
But it's too easy to overdramatize things.