Hacker News new | ask | show | jobs
by reedjosh 1519 days ago
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.

2 comments

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).

    type Resource interface {
        Status() Status
    }

    type Status interface {
        Ready() bool
    }
This is better expressed as

    type Statuser interface { Status() Status }
    type Readyer  interface { Ready()  bool   }
Go's equivalent of sub-classing is embedding
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.

Again, it's not impossible, just really ugly.

Seems pretty straightforward to me:

    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
    }