I don't know how people can say go "gets out of the way".
Go makes me write dozens of lines of code to do something simple that in an any modern language takes a few.
It doesn't get out of the way, it gets in the way constantly. I'm constantly thinking in any modern language I can just do X, but in Go with its myriad missing features I have to sit and think about how I'm going to do it with just loops and if statements.
It's the exact opposite of getting out the way, don't even get me started on the syntactic verbosity.
> Go makes me write dozens of lines of code to do something simple that in an any modern language takes a few.
"Getting out of the way" doesn't mean it takes fewer keystrokes - it just means that you don't have to think about it / there are no surprises. It took me a while to grok what pythonic code is and looks like, and I feel the bar for Go is even lower. Even if you're browsing an unfamiliar codebase, code is exactly where you expect it to be, and you don't have to ponder on where to make your changes. To me, that is how a language moves out of the way; it fades into the background and you mostly concern yourself with the logic.
Most programmers aren't bottlenecked by keyboard proficiency, but rather by dealing with poor tooling or gratuitously complex programs ("terse" doesn't entail "simple", and very often it's the inverse).
That's a strawman. We're not advocating for terseness in character count (otherwise we'd be using languages like APL and Jelly), but for better abstractions. There are other benefits than character count.
* Having a lot of repetitive code makes it easy to make a mistake when you edit one copy and forget about the others.
* A lack of abstraction can obscure intent, making you focus on implementation details.
* Having less code overall makes it easier to keep track of it in your head.
True, fun is certainly different for everyone! I also enjoy being able to just focus on a real world problem, but I also programmed Scala professionally for many years, and I found it a lot more fun purely from the point of view of writing code. Writing a really elegant for comprehension or using currying in clever ways to make your code "elegant" was just enjoyable in and of itself, regardless of what problem you were actually trying to solve. Rust is pretty similar to me in that regard.
I just can't stand taking three lines to unpack a value from a map or to return if error.
Why can't I just say `return if err := somefunc(); err != nil`
It's mega frustrating on top of the lack of generics and other abstractions.
And now that generics are coming about, I'm sure it will take forever until my current project can use them. My current project is in the k8s ecosystem which due to the lack of generics, implemented its own clever but awful type system.
I can't relate. Newline characters have never been burdensome to me, and they aid in visual structure (the control flow is represented by the visual structure of the program, not only for "good data" paths, but also for error paths). My programming problems are usually not related to localized keystroke boilerplate, but rather larger issues of abstraction and data modeling.
> My current project is in the k8s ecosystem which due to the lack of generics, implemented its own clever but awful type system.
The k8s ecosystem's type system is unrelated to generics. It has a concept of user-defined resource types, which means that users can provide an OpenAPI document describing their resource type which Kubernetes will then use to validate new user-provided resources of a given type. From the perspective of the Go compiler, these types are dynamic types--they can't be known at compile time. They aren't a candidate for generics in the host language.
That said, it's often tedious to write a controller for these resource types, but that's because Kubernetes' controller frameworks are really complicated. They remind me of enterprise Java code with gratuitous abstraction. Maybe that abstraction serves some purpose, but it wasn't helping me and I ended up rewriting much of it in more standard Go (I didn't release it because it was prototype code and I didn't want to support it) and it was quite a lot simpler. I don't recall seeing many places where I felt that generics would be a significant improvement, but it's been a while.
Well, if you don't know the structure of a resource ahead of time but know that it has a status.ready, I would think that would be a candidate for a generic? I haven't explored that much yet, but in retrospect I might even be able to convert
all objects to a struct that has only status.ready without generics.
I've only been in the ecosystem 6 months, but yeah larger abstractions are difficult too.
I'm not a fan of the lack of sub-classing. I like writing a base class and concrete one, and it's quite difficult in Go unless you want to make everything an interface.
Go's interfaces work fine for this case (see below), and Go's generics wouldn't help you (generic constraints operate on methods, not fields).
type Resource interface {
Status() Status
}
type Status interface {
Ready() bool
}
> I'm not a fan of the lack of sub-classing. I like writing a base class and concrete one, and it's quite difficult in Go unless you want to make everything an interface.
I've written a lot of Python, C++, Java, etc in my life (I cut my teeth on OOP). I'm thoroughly persuaded that inheritance is almost never better than composition, even in those languages where inheritance is idiomatic. Indeed, the trend in most of those languages has been away from inheritance and toward composition. Certainly in Go you'll be fighting an uphill battle by trying to make everything maximally abstract (which is a big part of why the k8s framework is so complicated per my earlier post).
Yeah, and I kind of did that. But I've found it annoying and not great.
For example in the base struct I had an interface and a ton of methods that use it.
Then when I declared the concrete struct, I have to manually point the concrete type that matches that interface to the base class's interface.
Composition doesn't really allow the same thing as inheritance. Composition typically means you'll have a motor and wheel struct in your car struct and maybe your car struct uses both in some drive method.
Inheritance is more like having a car struct with a rev engine method, but no concrete engine set.
So you can later make a Kia, set the motor to a type, and then call the base struct's rev engine method.
type Car struct {
Motor Motor
Wheels [4]Wheel
}
type Motor interface {
Rev()
}
type KiaMotor struct { ... }
func (kia *KiaMotor) Rev() {}
func NewKiaCar() Car {
return Car{Motor: &KiaMotor{ ... }}
}
Took me a second to see the difference in what you were doing between what I was doing.
In your case you're making a Kia car by making a regular car with a Kia motor.
In my case (at work) I'm making a type KiaCar struct { Car ... }.
Which is why I have to link a concrete type in the KiaCar to the Car if I want methods with Car receivers and KiaCar receivers to use the same concrete Motor.
I do like what you've written above though. I'll consider if I can use that instead of what I'm doing currently.
I'm just not entirely sure how I'd write methods for the KiaCar that knows what its concrete type is.
Yeah, I think very, very few problems (if any) are better-suited to being modeled with inheritance rather than composition. Go sort of forces you to think about composition, and once you get the hang of it I'd bet you won't go back. When you need reuse, reach for composition. When you need polymorphism, reach for interfaces/callbacks. When you need both (for example, a BookStore that works with both Postgres and SQLite backends) then you reach for both:
type BookStoreBackend interface {
GetBook(isbn string) (*Book, error)
PutBook(*Book) error
ListBooks() ([]*Book, error)
DeleteBook(isbn string) error
}
// BookStore embeds (i.e., is composed of) a Backend, which is an
// interface type.
type BookStore struct {
// ... other fields
// Backend supports PostgresBookStoreBackend, SQLiteBookStoreBackend,
// FileSystemBookStoreBackend, MemoryBookStoreBackend (for testing),
// etc.
Backend BookStoreBackend
}
The problem here is that you think "error handling" is somehow different, and probably less important, than normal logic in your codebase. But Go asserts that the "sad path" is just as important as the "happy path".
But programming languages should get on your way while you're doing things wrong. Go does not. To be fare, most mainstream languages do not: I think Rust is the best in this thing, other languages often aren't. But Go is by far the worst of all, because of its striving for "simplicity".
> But programming languages should get on your way while you're doing things wrong. Go does not. To be fare, most mainstream languages do not: I think Rust is the best in this thing, other languages often aren't. But Go is by far the worst of all, because of its striving for "simplicity".
Go typically does get in your way when you're doing things wrong, but yes, I'd like to see Go require return values be dealt with or explicitly ignored. That said, there are linters for this, but in practice it's never been a material problem for me so I haven't bothered to wire one into my project. Over time, I've learned not to be so concerned about issues which are mostly just theoretical--there are enough practical problems to deal with first.
Not usually, but the correct answer would be to either explicitly ignore the unused return values or use APIs that don't return values you don't care about.
> Go is not a fun language to program in
Not having to think about how something should be done in the most elegant way, instead focus on the problem at hand is a lot of "fun"