结构体上的函数

我们可以把一个方法关联在一个结构体上:

type Saiyan struct {

  Name string

  Power int

}

func (s *Saiyan) Super() {

  s.Power += 10000

}

在上面的代码中,我们可以这么理解,*Saiyan 类型是 Super 方法的接受者。然后我们可以通过下面的代码去调用 Super 方法:

goku := &Saiyan{"Goku", 9001}

goku.Super()

fmt.Println(goku.Power) // 将会打印出 19001

构造器

结构体没有构造器。但是,你可以创建一个返回所期望类型的实例的函数(类似于工厂):

func NewSaiyan(name string, power int) *Saiyan {
  return &Saiyan{
    Name: name,
    Power: power,
  }
}

这种模式以错误的方式惹恼了很多开发人员。一方面,这里有一点轻微的语法变化;另一方面,它确实感觉有点不那么明显。

我们的工厂不必返回一个指针;下面的形式是完全有效的

func NewSaiyan(name string, power int) Saiyan {
  return Saiyan{
    Name: name,
    Power: power,
  }
}

结构体的字段

到目前为止的例子中,Saiyan 有两个字段 Name 和 Power,其类型分别为 string 和 int。字段可以是任何类型 -- 包括其他结构体类型以及目前我们还没有提及的 array,maps,interfaces 和 functions 等类型。

例如,我们可以扩展 Saiyan 的定义:

type Saiyan struct {   Name string   Power int   Father *Saiyan }

然后我们通过下面的方式初始化:

gohan := &Saiyan{   Name: "Gohan",   Power: 1000,   Father: &Saiyan {     Name: "Goku",     Power: 9001,     Father: nil,   }, }

New

尽管缺少构造器,Go 语言却有一个内置的 new 函数,使用它来分配类型所需要的内存。 new(X) 的结果与 &X{} 相同。

goku := new(Saiyan)
// same as
goku := &Saiyan{}

如何使用取决于你,但是你会发现大多数人更偏爱后一种写法无论是否有字段需要初始化,因为这看起来更具可读性:

goku := new(Saiyan)

goku.name = "goku"

goku.power = 9001

//vs

goku := &Saiyan {

  Name: "goku",

  Power: 9000,

}

无论你选择哪一种,如果你遵循上述的工厂模式,就可以保护剩余的代码而不必知道或担心内存分配细节

组合

Go 支持组合, 这是将一个结构包含进另一个结构的行为。在某些语言中,这种行为叫做 特质 或者 混合。 没有明确的组合机制的语言总是可以做到这一点。在 Java 中, 可以使用 继承 来扩展结构。但是在脚本中并没有这种选项, 混合将会被写成如下形式:

public class Person {

  private String name;

  public String getName() {

    return this.name;

  }

}

public class Saiyan {

  // Saiyan 中包含着 person 对象

  private Person person;

  // 将请求转发到 person 中

  public String getName() {

    return this.person.getName();

  }

  ...

}

这可能会非常繁琐。Person 的每个方法都需要在 Saiyan 中重复。Go 避免了这种复杂性:

type Person struct {

  Name string

}

func (p *Person) Introduce() {

  fmt.Printf("Hi, I'm %s\n", p.Name)

}

type Saiyan struct {

  *Person

  Power int

}

// 使用它

goku := &Saiyan{

  Person: &Person{"Goku"},

  Power: 9001,

}

goku.Introduce()

Saiyan 结构体有一个 Person 类型的字段。由于我们没有显式地给它一个字段名,所以我们可以隐式地访问组合类型的字段和函数。然而,Go 编译器确实给了它一个字段名 Person,下面这样完全有效:

goku := &Saiyan{

  Person: &Person{"Goku"},

}

fmt.Println(goku.Name)

fmt.Println(goku.Person.Name)

上面两个都打印 「Goku」。

组合比继承更好吗?许多人认为它是一种更好的组织代码的方式。当使用继承的时候,你的类和超类紧密耦合在一起,你最终专注于结构而不是行为

指针 VS 值

当你写 Go 代码的时候,很自然就会去问自己 应该是值还是指向值的指针呢? 这儿有两个好消息,首先,无论我们讨论下面哪一项,答案都是一样的:

  • 局部变量赋值

  • 结构体指针

  • 函数返回值

  • 函数参数

  • 方法接收器

第二,如果你不确定,那就用指针咯。

正如我们已经看到的,传值是一个使数据不可变的好方法(函数中改变它不会反映到调用代码中)。有时,这是你想要的行为,但是通常情况下,不是这样的。

即使你不打算改变数据,也要考虑创建大型结构体副本的成本。相反,你可能有一些小的结构:

type Point struct {

  X int

  Y int

}

这种情况下,复制结构的成本能够通过直接访问 X 和 Y 来抵消,而没有其它任何间接操作。

还有,这些案例都是很微妙的,除非你迭代成千上万个这样的指针,否则你不会注意到差异