pjzyr
Published on 2025-02-18 / 17 Visits
0
0

Go 继承 vs. Java 继承:动态调度的实现对比

在 Java 或 .NET 这样的面向对象语言里,基类的方法可以调用子类重写的方法,这是通过 虚方法表(VTable) 机制实现的。例如,Java 代码如下:

abstract class BaseModel {
    // 子类必须实现的方法
    public abstract String getTableName();

    // 生成 SQL 查询
    public String queryOne() {
        return "SELECT * FROM " + getTableName() + " LIMIT 1";
    }
}

class UserModel extends BaseModel {
    private String tableName;

    public UserModel(String tableName) {
        this.tableName = tableName;
    }

    @Override
    public String getTableName() {
        return tableName;
    }
}

public class Main {
    public static void main(String[] args) {
        BaseModel user = new UserModel("user");
        System.out.println(user.queryOne()); // 输出: SELECT * FROM user LIMIT 1
    }
}

Java 允许基类 BaseModel 直接调用 getTableName(),并能正确调用子类 UserModel 实现的 getTableName(),这是因为 Java 通过虚方法表进行动态调度。

Go的实现方式

Go 不是典型的面向对象语言,它没有继承,而是使用 组合 来复用代码,因此无法直接实现类似 Java 的动态方法调用。例如:

package main

import "fmt"

type Tabler interface {
	TableName() string
}

type BaseModel struct {
	tabler Tabler
}

func (b *BaseModel) QueryOne() string {
	return fmt.Sprintf("SELECT * FROM %s LIMIT 1", b.tabler.TableName())
}

type UserModel struct {
	BaseModel
}

func NewUserModel() *UserModel {
	u := &UserModel{}
	u.tabler = u // 把自身赋值给 BaseModel.tabler
	return u
}

func (u *UserModel) TableName() string {
	return "users"
}

func main() {
	u := NewUserModel()
	fmt.Println(u.QueryOne()) // 输出: SELECT * FROM users LIMIT 1
}

在 Go 里,BaseModel不能直接调用 TableName(),因为它并不知道子类 UserModelTableName()。为了实现 Java 类似的效果,我们引入了 Tabler 接口,并在 NewUserModel() 里手动把 UserModel 赋值给 BaseModeltabler 字段。

为什么 Go 这样设计?

Go 这样设计的原因主要是为了简化继承带来的复杂性,比如:

  • 避免菱形继承问题(多个基类继承同一个父类,可能导致方法解析冲突)

  • 去除 VTable,提高运行效率(Go 采用接口和组合实现动态行为)

  • 显式地表达对象关系(在 Go 里,你必须手动赋值 tabler,这样可以避免某些隐藏的继承行为)

结论

  1. Java 通过继承和虚方法表(VTable)实现动态方法调用

  2. Go 没有继承,采用组合 + 接口方式

  3. 在 Go 里,基类无法直接访问子类方法,必须使用接口来间接实现

  4. 如果可能,直接用接口代替继承

虽然 Go 不能像 Java 那样优雅地处理继承,但它通过接口提供了一种更显式、更可控的方式来实现类似功能。


Comment