Go Is Not Good – A list of articles complaining that go isn't good enough
github.com
github.com
They came up with implicit interfaces, they didn't make them covariant, and they barely used them in the std lib itself.
They came up with multiple return values, they didn't think it would have been more useful for the programmer to get tuples and destructuring in practice.
They came up with struct tags, they didn't think fully typed annotations would have been more useful.
And then they exposed all the language internals in the reflect package,to make up for Go shortcomings with serializing,deserializing data, which made refactoring go internals in a backward compatible way an impossible task.
The worst idea is go generate. It shouldn't even be here. Littering source code with pragmas and forcing people to write 3rd party executable and maintain manifests isn't simple,readable,maintainable or elegant. Macros with reification would have been a bit more honest.
Go relies way too much on runtime behavior for a statically typed compiled language, This is absolutely undeniable.
The irony is that Rust, with garbage collection instead the borrow checker, would have been a better implementation of Go than Go itself. But it seems we'll never get that language.
There has been so much controversy about generics, but one related area where Go is consistently annoying is when you want to define a sort of language of data types -- where in a different language such as Haskell would define algebraic data types. For example, an AST of syntax nodes: There's no way define this (simplified):
data Node = Literal | Identifier | And | Or | Not | EQ | NEQ | ...
Instead, you must define a bunch of structs and use a dummy interface that all nodes implement. Even then, Go has no pattern matching, so if you want to handle a node, you need a switch statement on type, then check the data with an if. And even then, there's no way to prove at compile time that the switch statement is exhaustive (you could do some runtime magic with reflection-based lookup tables, but it's still poor). If you want a generic mechanism to traverse and rewrite such deeply nested data structures, there's a lot of exhausting boilerplate in the way of the core logic, because, ahem, no generics.I'm in the middle of a project that has three sets of such data structures (a low level AST, a high level expression grammar, and a query plan graph), and it's an area where the Go development experience turns from pretty good to pretty dismal. I've considered writing a mini-DSL and using "go generate" to generate Go code, but I've not yet decided on the best design.
I am using it since 8 months and give me all those abstractions you speak about and its elegant synthax allow me to be very productive. I don't know if I am as productive as I would have been using go, but I am certainly more productive than using Java.
But this not a language as easy to pick up as go (even though I didn't have much problem to pick it up right out of school).
I also think it is much more difficult to write reliable software in go than it is in scala. The much stronger type system (generics, algebraic data type) help me to check the correctness of my program at compile time.
Haskell would be a closer match.
This point interests me. I personally eschew 'dummy' interfaces in my Go code, preferring to work with the commonalities of such an algebraic type.
For your Node example, I'd look for things that all of the nodes do or represent and work from there. The interface type built on that would have other bound functions using those commonalities to synthesize common functionality.
Remember that in Go interfaces allow one to add disjoint configurations, such as things that (to borrow from webdriver) accept Click() (like buttons) or Set() (input fields). Those interfaces can apply independently from the meat of being a webelement. By implementing (or mixing in) the behavior, you can have a richer whole.
The fact that I have to wrap them in an interface is also a performance concern, because an interface devolves to a pointer if it can't fit the contents into a single machine word. So in effect, every single struct becomes a pointer, which in my case seems unnecessary.
There are some concerns that cut across all uses of the data that could be methods, but even things like mapping over (transforming) the data shouldn't really need to be built into the data structures via interfaces. Not least because it's horrible to write. For example, consider this:
type Expression interface{}
type And struct {
LHS, RHS Expression
}
You want to be able to do something like this: newExpr := Map(expr, func(e Expression) Expression {
if and, ok := e.(And); ok {
if and.LHS == True {
return and.RHS
}
if and.RHS == True {
return and.LHS
}
}
return e
})
How do you implement Map? You have several choices. You can let Expression have Map(), and then every expression needs to implement it: type Expression interface {
Map(fn func(e Expression) Expression) Expression
}
func (e And) Map(fn func(e Expression) Expression) Expression {
e.LHS = fn(e.LHS)
e.RHS = fn(e.RHS)
return fn(e)
}
Or you could do something like: type Expression interface {
GetChildren() []Expression
SetChildren(...Expression) Expression
}
func (e And) GetChildren() {
return []interface{e.LHS, e.RHS}
}
func (e And) SetChildren(children ...Expression) Expression {
e.LHS = children[0]
e.RHS = children[1]
return e
}
Now you can implement Map() once for all expressions: func Map(e Expression, fn func(e Expression) Expression) Expression {
children := e.GetChildren()
newChildren := make([]interface{}, len(children))
for i, child := range children {
newChildren[i] = fn(child)
}
return e.SetChildren(newChildren...)
}
...but at the cost of being expensive, due to slices.Less expensive idea: Since all my expressions are nonary, unary or binary, I could have a Nonary/Unary/Binary to avoid GetChildren() and SetChildren() operating on slices, and then Map() could simply do a switch.
Or you could simply skip all of that and build a switch statement:
func Map(expr Expression, fn func(e Expression) Expression) Expression {
switch e := expr.(type) {
case And:
e.LHS = fn(e.LHS)
e.RHS = fn(e.RHS)
// ...
}
return fn(expr)
}
This is why I was considering generating code, because it's terrible having to write and quality-check all that boilerplate.(There's a lot of boilerplate: Map() is not the only function I need. I also need a top-to-bottom version (the above is depth-first) and a Find(), and multiple other things that need to recursively determine something about an expression, without knowing all the types involved.)
As with many language trade offs, at the end of the day people tend to gravitate towards languages that fit their own personal preferences
Probably compared to what they were shown in university/bootcamp/whatever.
'no programming experience' could esily mean 'no professional programming experience' which is very close to 'no programming experience'.
I am enrolled in a computer engineering course, and I can tell you the amount of code we get to write is ridiculously low.
That said, for building web applications, I still prefer Node.js.
If it's a question of versioning or package.json-style dependency management, I agree that Go still has some significant pain points that haven't been fully addressed directly.
Some package management systems can decouple what packages are called inside the code from what they're called outside.
I'm using the computer for over 30 years, writing in different languages, and I'm happy with Go, even it has his shortcomings here and there.
If I have to write some loop which talks a certain protocol over a network (servers) Go is my first choice.
The Internet is a funny thing, You will find a bunch of arguments that eggs are healthy and that they are not.
Similar with programming languages.
Coders looking for a better C are generally happy.
Coders looking for a better Java or C# are in general not happy.
I don't think its worthwhile to compare Go to dynamic languages.
I do think we should be making this comparison, because there's a trend in devops of moving from Python and Ruby (each of which have system-provided dependencies) to Go (which statically links a copy of everything).
I see this as unfortunate because devops should be promoting taking full advantage of the system's well-understood and -tested libraries and services rather than avoiding them with approaches like container isolation. If you have a reason to avoid your distro's python, fix that.