Code Issues 7k+ Pull requests 401 Discussions Actions Projects Wiki Security and quality Insights
proposal: spec: add classical inheritance to Go #78847
Open JustSkiv opened this issue 8 hours ago · 22 comments
JustSkiv 8 hours ago

Abstract

This proposal introduces single classical inheritance to Go via two context-sensitive keywords: class and extends. The goal is to eliminate pervasive boilerplate associated with struct embedding when modeling type hierarchies.

Motivation

Go's current approach to code reuse through struct embedding and interfaces works well for many cases, but breaks down when modeling domains with natural type hierarchies. Consider a typical example:

type BaseEntity struct {
    ID        int64
    CreatedAt time.Time
    UpdatedAt time.Time
}

func (b *BaseEntity) Validate() error {
    if b.ID < 0 {
        return errors.New("invalid ID")
    }
    return nil
}

type User struct {
    BaseEntity
    Name  string
    Email string
}

func (u *User) Validate() error {
    if err := u.BaseEntity.Validate(); err != nil {
        return err
    }
    if u.Email == "" {
        return errors.New("email is required")
    }
    return nil
}
Copied!

This works for a single level of embedding. In practice, as hierarchies grow deeper, several problems emerge:

  • Method delegation boilerplate. When an embedded type's method signature doesn't match the outer type's interface requirements, manual wrapper methods are needed. In large codebases with 3–4 levels of embedding, this produces hundreds of trivial one-line delegation methods.
  • No polymorphic dispatch. Embedded methods don't know about the outer type. A method on BaseEntity cannot call an overridden method on User — there is no dynamic dispatch. This forces repeated reimplementation rather than extension.
  • Embedding is not subtyping. A *User cannot be passed where a *BaseEntity is expected. This leads to additional adapter code or use of interfaces solely to simulate subtype relationships, causing unnecessary heap escapes.

Proposal

Introduce class and extends as context-sensitive keywords, parsed only at the top level of type declarations.

Syntax

type Entity class {
    ID        int64
    CreatedAt time.Time
    UpdatedAt time.Time
}

func (e *Entity) Validate() error {
    if e.ID < 0 {
        return errors.New("invalid ID")
    }
    return nil
}

type User extends Entity {
    Name  string
    Email string
}

func (u *User) Validate() error {
    if err := Entity.Validate(u); err != nil {
        return err
    }
    if u.Email == "" {
        return errors.New("email is required")
    }
    return nil
}
Copied!

Semantics

  • class marks a type as inheritable. Regular structs are unaffected and cannot be extended.
  • extends establishes a single-inheritance relationship.
  • A child class inherits all fields and methods of the parent. Method override is supported via standard single-dispatch vtable lookup.
  • A *User is assignable to *Entity (true subtyping).
  • No array/slice covariance: []*User is NOT assignable to []*Entity, to preserve type safety.
  • Classes implicitly satisfy interfaces, exactly as structs do today.

What this is not

This is not an attempt to turn Go into Java. The proposal is deliberately minimal:

  • No abstract classes.
  • No access modifiers beyond existing exported/unexported rules.
  • No multiple inheritance.
  • No constructors — use factory functions as today.
  • No new super keyword. Parent methods are called explicitly via ParentType.Method(receiver) syntax, consistent with how promoted methods work today.

Compatibility

To strictly adhere to the Go 1 compatibility promise, class and extends are implemented as contextual keywords, parsed only in type declaration position. Existing code using class or extends as variable names, field names, or in any other position will continue to compile unchanged.

Update: @rsc has independently verified that no package in the public module index uses class or extends as identifiers in type declaration position. The mechanism is purely additive.

Implementation

A working prototype is available at github.com/JustSkiv/go-inherit, based on go1.26. Update: after fixes from feedback, it now passes 98.1% of the standard library tests (up from 97.3%). The remaining failures are in reflect and encoding/gob, which require updates to handle the new reflect.Class type metadata.

The implementation touches:

  • cmd/compile: parsing of contextual keywords, vtable generation for class methods
  • runtime: type metadata extensions for _type to support fast subtyping checks
  • reflect: support for class type inspection

Estimated total diff: ~3,200 lines added, ~400 removed. Compilation overhead is negligible — vtable generation adds <0.3% to compile time on the standard library benchmark.

/cc @rsc @griesemer @ianlancetaylor

👍 421 👎 97 😄 62 🎉 31 😕 58 🚀 39 ❤️ 21 👀 68
rittneje 7 hours ago

I've been writing Go professionally for 8 years — distributed systems, observability tooling, databases. Embedding + interfaces cover 99% of real-world use cases. I have never once hit a wall where inheritance would have been the answer.

Can you provide a single concrete production example where classical inheritance solves a problem that interfaces + composition genuinely cannot? Not a toy Animal/Dog hierarchy, but something from a real codebase.

The scheduler/GC arguments are interesting but very hand-wavy. "Approximately 30%" is not something you can eyeball from reading proc.go.

👍 38
andydotxyz 7 hours ago

I'll be honest — I feel the pain. We have a large codebase with deeply nested embedding, and the boilerplate for method delegation is real. Some of our types embed 3–4 structs and we end up writing dozens of pass-through methods just to satisfy interfaces.

But adding classical inheritance to fix this is like curing a headache with a guillotine. The problem is real, the solution isn't.

I'd rather see something like Rust's traits, or honestly just better tooling — go generate + a smart delegation generator could eliminate 90% of this boilerplate without touching the language spec at all.

👍 20 😄 6
willfaught 6 hours ago

Next proposal: add semicolons, try/catch, and public static void main to Go. Maybe a pom.xml too while we're at it.

😄 45 👍 13
apparentlymart 6 hours ago

@JustSkiv — the GuideDog extends Dog extends Animal hierarchy in your proposal is actually a textbook example of why inheritance breaks down on real domain models.

Consider what happens when product asks for a RobotGuideDog that isn't biologically a Dog at all. With inheritance you'd need to refactor the entire chain or introduce a parallel hierarchy. With interfaces, you just do this:

type Speaker interface {
    Speak() string
}

type GuideDog struct {
    Name  string
    Owner string
}

func (g GuideDog) Speak() string  { return "Woof!" }

type RobotGuide struct {
    Model string
    Owner string
}

func (r RobotGuide) Speak() string { return "Beep." }

// Both satisfy Speaker. No hierarchy needed.

This composes cleanly and doesn't lock you into an ontology you'll regret in six months.

Regarding the itab overhead claim — 12–15% only holds in tight-loop microbenchmarks where the CPU branch predictor can't warm up. In any real application with diverse call sites, inline caching brings interface dispatch within 1–2ns of a direct call. The runtime team has measured this extensively.

I appreciate the effort and the research into the runtime, genuinely. But the conclusion doesn't follow from the evidence.

👍 30
comunidadio 4 hours ago

Honestly? I've been waiting for someone brave enough to write this proposal. Every time I port a service from C# to Go I end up with a Base/Derived pattern that Go just can't express cleanly. Embedding is not the same thing.

The fact that you have a working prototype that passes 97% of stdlib is impressive. That alone deserves serious evaluation, not a knee-jerk "Go doesn't do that".

👍 10 ❤️ 4
doggedOwl 4 hours ago

The compatibility section is the part that sold me. If class and extends are truly contextual and no existing code breaks, then the risk is near zero. The Go 1 promise is preserved.

What's the compilation overhead? Does vtable generation add measurable time to cmd/compile?

👍 8
quasilyte 3 hours ago

I was skeptical when I opened this, but the more I read the more I think the design is actually well-constrained. No multiple inheritance, no constructors, no super — it avoids every trap that made C++ and Java hierarchies painful.

The key insight for me is that class/extends are strictly opt-in. You don't have to use them. Existing code that uses embedding and interfaces continues to work identically. This isn't replacing composition — it's adding a tool for cases where composition genuinely falls short.

I'd like to see how encoding/json handles class types, but that's an implementation detail, not a design blocker.

👍 17
mzattahri 3 hours ago

We maintain a large gRPC service mesh at work (~200 services). Our internal framework has a BaseService struct that every service embeds. Right now we have 14 wrapper methods per service just to satisfy the orchestration interfaces. Fourteen. Per service.

If this lands, we could replace all of that with type UserService extends BaseService and be done. That's not a toy example — that's real production code that would get dramatically simpler.

👍 24 ❤️ 4
defany 3 hours ago

For those who don't know @JustSkiv — he's done some of the best deep-dive content on Go's runtime internals I've seen anywhere. His articles on the scheduler and GC are incredibly thorough. This isn't some random feature request; the author has genuinely studied the codebase at a level most of us haven't.

I've read through the prototype diff and the changes to cmd/compile are surprisingly small for a feature this significant. Most of the heavy lifting is in the new reflect.Class type, which makes sense.

The explicit decision to NOT support slice covariance is the kind of restraint that tells me this was designed by someone who understands the pitfalls, not just the benefits.

👍 12
rsc Member 2 hours ago

I've spent some time reviewing the prototype and the proposal text. A few observations:

The contextual keyword approach is clever — it sidesteps the backward compatibility concern entirely. I verified that no package in the module index uses class or extends as identifiers in type declaration position.

The vtable implementation in the prototype is straightforward and the compiler changes are well-isolated. The reflect changes need work, but the core design is sound.

I think this deserves serious consideration. @griesemer — you've thought more about the type system implications than anyone. What's your take?

👍 172 🚀 25 ❤️ 16
griesemer Member 1 hour ago

Thanks for the ping, @rsc.

I've been reading through this since it was filed and I have to say — the type system extension is remarkably clean. Single inheritance with no covariance on slices avoids the classic Java ArrayStoreException pitfall. The ParentType.Method(receiver) syntax for calling parent methods is consistent with existing Go semantics and doesn't require a new super keyword.

The fact that this is purely additive and doesn't affect existing structs or interfaces is the key property that makes this viable.

I'm comfortable moving this to Likely Accept. We should schedule a detailed design review for the Go 1.28 cycle.

👍 224 🎉 96 🚀 49 ❤️ 44

    
Happy April Fools' Day 🐹

Bonus 🎁  ·  reload page