Go基础:结构体

140 阅读5分钟

本文正在参加金石计划

「前言」

结构体

什么是结构体

结构体是一种数据集合,它可以封装不同类型的数据成员,并使这些成员组成一个逻辑上相关联的整体,同时Go也允许为其绑定方法。结构体与方法的关系实质上就是数据与行为的关系,这也是面向对象的重要思想之一。

怎么声明一个结构体

结构体声明与函数声明类似,也分为有名结构体(自定义结构体)与匿名结构体,如下:

type Person struct {
    name string
    age  int
}

struct{
  id int
}

这里我们定义了一个名为Person的结构体,包含两个字段:nameage,分别表示人的姓名与年龄。并且定义了一个匿名结构体,它只包含一个id字段。需要注意的是,结构体类型和结构体字段都应该遵循 Go 语言的命名规范,即使用驼峰式命名,且首字母大写表示外部可见的字段。

结构体中的字段可以是任何合法的 Go 类型,包括内置类型、自定义类型和其他结构体类型等。并且Go允许直接通过类型名称与指向类型的指针进行嵌套

type Name struct{
  surname string
  personalName string
}

type Age struct{
  birthday string
  age int
}

type Person struct{
  Name
  *Age
}

这里我们定义了三个类型,分别是用于表示姓名与年龄的结构体——NameAge,还有用于表示人的Person结构体。Name中包含两个字段,surname表示姓,personalName表示名;Age中包含两个字段,birthday表示生日,age表示世界年龄。而Person分别用嵌套类型名字与指向类型的指针分别嵌套了NameAge,这使得Person将会包含他们的字段。

怎么初始化一个结构体变量

初始化一个结构体变量也被称为初始化一个实例。对于如何初始化这个问题,在「变量」那章有提到基本的赋值条件的基本逻辑:

用字面常量
var p = Person{ name:"Tomy", age:18 }
用相同类型的
var p = Person{ name:"Tomy", age:33 }
var father Preson = p
用类似的

⚠️结构体之间是不能隐式转换的,即使它们的字段完全一样。不同的结构体类型在使用时需要显式地进行类型转换。(关于这点将会在后续文章提到,此处会更新链接)

结构体有什么用

结构体作为一种数据的集合,它不但能使若干个字段组成一个逻辑上相关联的整体,还能绑定自己的行为。

使用字段

对于普通的字段,我们能够通过用实例.的方式对字段赋值或取值:

var p1 Person

p1.name = "Alice"
p1.age = 20

fmt.Printf("%s:%d\n", p1.name, p1.age)

假如通过类型名称与指向类型的指针进行嵌套,我们不需要显式地通过详细的引用来访问被嵌入的结构体中的字段:

type Person struct {
    name string
    age  int
}

type Employee struct {
    Person
    salary float64
}

func main() {
    e := Employee{
        Person: Person{
            name: "Alice",
            age:  30,
        },
        salary: 5000.0,
    }
    fmt.Println(e.name, e.age, e.salary) // Alice 30 5000
}

在这个例子中,我们定义了两个结构体类型:PersonEmployeeEmployee 结构体中嵌入了 Person 结构体,使得 Employee 结构体中包含了 Person 结构体的所有字段和方法。这种方式可以看做是一种间接的结构体类型转换,使得我们可以在 Employee 结构体中直接访问 Person 结构体中的字段。

main 函数中,我们创建了一个名为 eEmployee 类型的变量,并使用结构体字面量的形式来初始化它的字段。在访问 e 变量的 nameage 字段时,我们不需要显式地通过 e.Person.namee.Person.age 来访问 Person 结构体中的字段,而是可以直接使用 e.namee.age 访问。

绑定方法

方法与结构体的绑定是通过 Go 语言中的接收者来实现的。

接收者可以理解为一个方法的第一个参数,它可以是一个值类型或者一个指针类型。在将方法绑定到结构体上时,我们需要为方法添加一个接收器,这个接收器指定了这个方法所属的结构体类型。在别的语言中,我们经常用this或self指定使用的接收者。

下面是一个例子,其中我们为一个结构体类型 Person 添加了一个名为 SayHello 的方法:

type Person struct {
    name string
    age  int
}

func (p Person) SayHello() {
    fmt.Printf("Hello, my name is %s and I am %d years old\n", p.name, p.age)
}

在这个例子中,我们定义了一个结构体类型 Person,其中包含了两个字段 nameage。我们为 Person 类型添加了一个 SayHello 方法,它打印出 Person 对象的姓名和年龄。

方法的接收器 (p Person) 表示这个方法是绑定到 Person 类型上的。这个接收器是一个值类型,也可以使用指针类型作为接收器,比如:

func (p *Person) SetAge(age int) {
    p.age = age
}

在这个例子中,我们定义了一个名为 SetAge 的方法,它接收一个指向 Person 类型的指针作为接收器。这个方法可以修改 Person 对象的年龄字段。

我们该怎么选择接收者何时使用值类型何时使用指针类型呢?

  1. 需要修改接收器的值或者访问接收器的指针时,就必须使用指针类型作为接收者;
  2. 不需要修改接收器的值或者访问接收器的指针,就可以使用值类型作为接收者;
  3. 当我们使用值类型作为接收器时,方法被调用时,接收者的值会被复制一份。使用指针类型作为接收者可以避免在方法内部对其复制所产生的性能损耗,从而提高程序的效率