likes
comments
collection
share

注意:Golang 的类型嵌入并不是继承

作者站长头像
站长
· 阅读数 11

什么是类型嵌入?

在 Golang 中,可以使用结构将一个类型嵌入到另一个类型中。这看起来像一个普通的字段声明,但没有名称。这是将一个结构嵌入另一个结构:

type inner struct {
    a int
}

type outer struct {
    inner
    b int
}

inner已嵌入到outer. 这为您提供了访问所有嵌入类型字段的语法快捷方式。例如,您可以这样做:

var x outer
fmt.Printf("inner.a is %d", x.a)

即使该outer结构没有名为a的字段,但是嵌入式inner结构有。快捷使用方式:

var x outer
fmt.Printf("inner.a is %d", x.inner.a)

但是大多数人使用此功能的原因,尤其是我们这些来自面向对象背景的人,是因为它允许您在嵌入式类型上共享方法。乍一看,它看起来像继承(但它不是)。

如果我在inner中提供一个方法,在outer实例上调用它。下面是演示此功能的完整示例:

type printer interface {
	print()
}

type inner struct {
	a int
}

func (i inner) print() {
	fmt.Printf("a is %d", i.a)
}

type outer struct {
	inner
	b int
}

func main() {
	var x printer
	x = outer{inner{1}, 2}
	x.print()
}

一切正常,并且完全符合您的期望。很整洁,对吧?您可能认为使用此特性可以节省大量代码,这是事实。但有一个巨大的警告:类型嵌入不是继承,我们得留意它是可能会导致错误的。

我们看下面的常见例子,会发现,类型嵌入和继承并不是一样的。

this很特别

这是在 Java 中:

public class Animals {
  public static void main(String[] args) {
    Animals example = new Animals();
    example.Run();
  }
	
  public void Run() {
    Animal a = new Tiger();
    a.Greet();
  }
	
  interface Animal {
    public void Speak();
    public void Greet();
  }

  class Cat implements Animal {
    public void Speak() {
      System.out.println("meow");
    }

    public void Greet() {
      this.Speak();
      System.out.println("I'm a kind of cat!");
    }
  }

  class Tiger extends Cat {
    public void Speak() {
      System.out.println("roar");
    }
  }
}

所以在这个例子中,我们有一个Animal接口声明了两个方法。然后我们有两个实现者Cat和Tiger。Tiger 扩展Cat并覆盖其中一个接口方法。当您运行它时,它会产生您期望的输出:

roar
I'm a kind of cat!

现在让我们在 Golang 中复制同样的东西,假设类型嵌入与继承相同。

type Animal interface {
	Speak()
	Greet()
}

type Cat struct {}

func (c Cat) Speak() {
	fmt.Printf("meow\n")
}

func (c Cat) Greet() {
	c.Speak()
	fmt.Printf("I'm a kind of cat!\n")
}

type Tiger struct {
	Cat
}

func (t Tiger) Speak() {
	fmt.Printf("roar\n")
}

func main() {
	var x Animal
	x = Tiger{}
	x.Greet()
}

Java工程师们应该花点时间欣赏一下有多少无用的代码和分号已被消除。但是尽管更紧凑,这段代码有一个大问题:它根本就不起作用。当你运行它时,你会得到这个输出:

meow
I'm a kind of cat!

为什么我们的老虎是喵喵叫而不是咆哮?是因为this Java中的关键字比较特殊,Golang没有。让我们将这两个Greet实现放在一起进行检查。

public void Greet() { // method in Cat class
  this.Speak();
  System.out.println("I'm a kind of cat!");
}
func (c Cat) Greet() {
	c.Speak()
	fmt.Printf("I'm a kind of cat!\n")
}

在 Java 中,this是一个特殊的隐式指针,它始终具有最初调用该方法的类的运行时类型。所以 Tiger.Greet()分派给Cat.Greet(),但后者有一个 this类型的指针Tiger,所以this.Speak()分派给 Tiger.Speak()。

在 Golang 中,这一切都不会发生。该Cat.Greet()方法没有this指针,它有一个Cat接收器。当你打电话时 Tiger.Greet(),它只是简写Tiger.Cat.Greet()。中的接收器的静态类型Cat.Greet()与其运行时类型相同,因此它分派给Cat.Speak(),而不是 Tiger.Speak()。

Golang中的关于Embedded dispatch的图解:

注意:Golang 的类型嵌入并不是继承

像 Java 这样的语言,这感觉像是一种违规行为,但 Golang 对此非常明确:接收者参数声明其静态类型,并且 go vet 会警告您不要命名接收者参数this或者是self。

每当我们尝试使用嵌入将 OOP 直觉应用于 Golang 时,同样的问题就会出现。

方法链:千万不要与Golang类型嵌入搞混淆了

另一种常见的 OOP 模式是方法链,其中每个方法都返回this指向自身的指针,这样您就可以在不开始新语句的情况下对结果调用其他方法。在 Java 中它看起来像这样:

int result = new HttpsServer().WithTimeout(30).WithTLS(true).Start().Await();

这些方法中的每一个都返回this,这就是让我们在同一条语句中对同一个对象调用一个又一个方法的神奇之处。如果有效使用,它可以节省大量空间并使代码更具可读性。

但在 Golang 中,如果你将它与类型嵌入搞混淆了,同样的模式可以成为一种陷阱。考虑以下Server 类型的玩具层次结构,一种支持 TLS,另一种不支持:

type Server interface {
	WithTimeout(int) Server
	WithTLS(bool) Server
	Start() Server
	Await() int
}

type HttpServer struct {
    ...
    timeout int
}

func (h HttpServer) WithTLS(b bool) Server {
	// no TLS support, ignore
	return h
}

func (h HttpServer) WithTimeout(i int) Server {
    h.timeout = i
	return h
}

func (h HttpServer) Start() Server {
    ...
	return h
}

func (h HttpServer) Await() int {
	...
}

type HttpsServer struct {
	HttpServer
	tlsEnabled bool
}

func (h HttpsServer) WithTLS(b bool) Server {
	h.tlsEnabled = b
	return h
}

func main() {
	HttpsServer{}.WithTimeout(10).WithTLS(true).Start().Await()
}

上面的代码可以很好地编译并运行,但它有一个致命的错误: WithTimeout调用 on HttpServer, not HttpsServer,并返回相同的结果。不仅在您的服务器上未启用 TLS,而且您正在处理与main方法中显示的完全不同的结构。

这种错误会让您花几个小时仔细debug,试图弄清楚发生了什么,因为代码在调用方面看起来是正确的,但不幸的是有缺陷。

接口嵌入:在接口中很好,在结构中很危险

上面的警告与具体类型有关,指的是有接收参数的类型。可以将一个接口嵌入到另一个接口中以表达接口扩展。 Java 和 Golang 在语义上是等价的:

public interface Animal {
    public void Eat();
}

public interface Mammal extends Animal {
    public void Lactate();
}
type Animal interface {
    Eat()
}

type Mammal interface {
    Animal
    Lactate()
}

这很好,也很理想,没有上面提到的任何问题或危险。但要注意:在结构中嵌入接口是允许的,但结果却不是你想要的。下面的代码编译没问题但在运行时产生 nil 指针错误。

type Dog struct {
	Mammal
}

func main() {
	d := Dog{}
	d.Eat()
}

可以通过创建一个NewDog() 构造函数方法来修复 nil错误,或者实现缺少的方法,如下所示:

func (d Dog) Eat() {
	println("Dog eats")
}

通常在结构中嵌入接口意图是类似这样的:Dog 实现了Mammal 并拥有了其所有方法。这是一种非常危险的技术:如果您曾经向嵌入式接口添加方法,您的代码仍会编译,但调用方法会抛出 nil 错误。如果您想要等效于 Java 的implements关键字,请改用静态接口操作。

var _ Mammal = Dog{}

这很危险,但你还是要去做

尽管类型嵌入存在问题,但编写更少代码、不再重复实在是太诱人了,让人无法抗拒。你可能最终会这样做。尽管它有缺陷,但它太有用了,无法完全避免不用。

我们知道这些错误,因为我们经历过。

了解 Golang 的局限性并在编写和code review代码时牢记这些局限性非常有帮助。