Skip to content

2.1 面向对象:结构体与继承

结构体 (Struct) 是 Go 语言中一种自定义的复合数据类型,它允许您将多个不同类型(甚至相同类型)的数据项组合成一个有意义的整体。它类似于面向对象语言中的“类”,但 Go 语言本身不是严格意义上的面向对象语言。

在之前学过的数据类型中,数组与切片,只能存储同一类型的变量。若要存储多个类型的变量,就需要用到结构体,它是将多个任意类型的变量组合在一起的聚合数据类型。

可以理解为Go语言的结构体和其他语言的class有相等的地位,但是Go语言放弃大量面向对象的特性。

一、结构体的定义与声明

结构体定义了存储在其中的字段(也称为成员变量)的集合。

1. 定义结构体类型

使用 typestruct 关键字来定义一个新的结构体类型。

go
type 结构体名称 struct {
    字段名1 字段类型1
    字段名2 字段类型2
    // ... 更多字段
}

示例: 定义一个表示“用户”信息的结构体。

go
type User struct {
    ID    int       // 用户ID,整型
    Name  string    // 用户名,字符串
    Email string    // 邮箱,字符串
    Age   int       // 年龄,整型
    IsActive bool   // 是否活跃,布尔型
}

2. 声明和初始化结构体变量

定义结构体后,您可以声明该结构体类型的变量并进行初始化。

(1) 声明一个结构体变量 (零值初始化)

go
var u1 User // 声明变量u1,Go会自动将其所有字段初始化为对应类型的零值
            // int: 0, string: "", bool: false

(2) 使用字面量完整初始化 (推荐)

按字段名初始化: 这种方式最推荐,因为它不依赖字段定义的顺序,代码可读性高。

go
u2 := User{
    ID: 1001,
    Name: "Alice",
    Email: "alice@example.com",
    Age: 30,
    IsActive: true,
}

按顺序初始化: 必须严格按照结构体中字段定义的顺序,并且不能省略任何字段。

go
// 必须严格按顺序:ID, Name, Email, Age, IsActive
u3 := User{1002, "Bob", "bob@example.com", 25, false}

(3) 仅初始化部分字段

只初始化部分字段时,必须使用 字段名: 值 的方式。未初始化的字段将使用其类型的零值。

go
u4 := User{
    Name: "Charlie",
    Email: "charlie@example.com",
}
// u4.ID 为 0, u4.Age 为 0, u4.IsActive 为 false

二、结构体字段的访问

结构体变量初始化后,使用 点号 (.) 来访问其内部的字段。

go
u := User{ID: 2001, Name: "David"}

// 访问字段的值
fmt.Println("用户ID:", u.ID)    // 输出: 用户ID: 2001
fmt.Println("用户名:", u.Name)  // 输出: 用户名: David

// 修改字段的值
u.Name = "David Chen"
u.Age = 40
fmt.Println("新用户名:", u.Name) // 输出: 新用户名: David Chen

三、结构体方法

在 Go 语言中,您可以为任何自定义类型(包括结构体)定义方法。方法是绑定到特定类型的函数,它允许您像面向对象语言那样,让结构体拥有自己的行为和操作。

1. 方法的定义语法

方法和函数的定义非常相似,但它在 func 关键字和方法名之间多了一个接收者 (Receiver) 参数。

go
func (接收者变量 接收者类型) 方法名(参数列表) (返回值列表) {
    // 方法体
}
go
type User struct {
	Name string
	Age  int
}
func (u User) HappyBirthday() {
	u.Age++
	fmt.Println(u.Name, u.Age)
}

func main() {
	u := User{Name: "Alice", Age: 30}
	u.HappyBirthday()
	fmt.Println(u.Age) // 输出多少?为什么?
}

关键点:

  • 接收者变量: 类似面向对象语言中的 thisself,它代表调用该方法的结构体实例。
  • 接收者类型: 必须是您在当前包中定义的类型(如一个结构体)。

2 值类型的接收者

接收者类型可以是结构体的值类型 (T) 或指针类型 (*T)。这两种类型决定了方法对结构体实例的影响。

如果接收者是结构体的值类型T),那么在方法内部对接收者字段的修改,不会影响到原始的结构体实例。因为调用方法时,会传入结构体的一个副本

go
type User struct {
    Name string
    Age  int
}

// 值接收者:(u User)
// 接收到的是 User 实例的副本
func (u User) HappyBirthday() {
    u.Age++ // 仅修改了副本的 Age
    fmt.Printf("%s 内部:年龄改为 %d\n", u.Name, u.Age)
}

// 示例调用
func demoValueReceiver() {
    u1 := User{Name: "Alice", Age: 30}
    u1.HappyBirthday()
    fmt.Printf("Alice 外部:年龄仍是 %d\n", u1.Age) // 仍是 30
}

3. 指针类型的接收者

如果接收者是结构体的指针类型*T),那么在方法内部对接收者字段的修改,会直接影响到原始的结构体实例。

这是修改结构体字段值的首选方式。

go
// 指针接收者:(u *User)
// 接收到的是 User 实例的内存地址
func (u *User) ChangeName(newName string) {
    (&u).Name = newName // 直接修改了原始结构体实例的 Name ,或者使用 u.Name = newName 也是可以的,事实上更推荐使用 u.Name = newName 的方式,因为更简洁,更符合Go语言的惯用写法  
    fmt.Printf("内部:名字已改为 %s\n", u.Name)
}

// 示例调用
func demoPointerReceiver() {
    u2 := User{Name: "Bob", Age: 25}
    u2.ChangeName("Robert")
    fmt.Printf("Bob 外部:名字已是 %s\n", u2.Name) // 已是 Robert
}

💡 Go 的语法糖 (Syntactic Sugar):

无论是值变量还是指针变量,都可以使用 . 来调用使用指针接收者的方法。Go 编译器会自动处理地址转换。

  • u2.ChangeName(...) (u2 是值) 会自动被转换为 (&u2).ChangeName(...)
  • p.ChangeName(...) (p 是指针) 保持不变

4. 为什么选择指针接收者?

在为结构体定义方法时,选择指针接收者通常有以下几个原因:

场景原因推荐接收者
需要修改接收者状态指针接收者可以直接修改原始结构体实例的字段。指针
结构体体积较大传递指针只需要复制一个内存地址(通常 8 字节),避免了复制整个大型结构体的开销。指针
保持一致性如果结构体的一部分方法使用了指针接收者,那么所有相关方法通常都应使用指针接收者,以保持代码一致性。指针
仅读取数据结构体较小,且方法仅用于读取数据或执行计算,不需要修改状态。

约定俗成: 在实际开发中,除非结构体非常小且确定不需要修改其状态,否则大部分时间都使用指针接收者

四、结构体指针

Go 语言中,结构体变量本身是值类型。当将一个结构体变量赋值给另一个变量或作为函数参数传递时,会进行值拷贝

通常我们更常使用结构体指针

1. 获取结构体指针

使用 取地址符 & 来获取结构体变量的内存地址,即结构体指针。

go
u := User{Name: "Eve"}
p := &u // p 是一个 *User 类型的指针,指向 u

2. 通过指针访问字段 (自动解引用)

Go 语言允许直接使用 点号 (.) 通过结构体指针来访问其字段,Go 编译器会自动进行解引用操作。

go
p.Age = 28          // 等价于 (*p).Age = 28
fmt.Println(p.Name) // 等价于 fmt.Println((*p).Name)

当你对象是结构体对象的指针时,你想要获取字段属性时,按照常规理解应该这么做

go
type User struct {
	Name string
}
func main() {
	p1 := &User{"sixue"}
  	fmt.Println((*p1).Name)  // output: sixue
}

但还有一个更简洁的做法,可以直接省去 * 取值的操作,选择器 . 会直接解引用,示例如下

go
type User struct {
	Name string
}
func main() {
	p1 := &User{"sixue"}
	fmt.Println(p1.Name)  // output: sixue
}

3. 使用 new() 创建结构体指针

可以使用内置的 new() 函数为结构体分配内存,并返回一个指向该结构体实例的指针,其字段会被初始化为零值。

go
p2 := new(User) // p2 是 *User 类型,所有字段为零值
p2.Name = "Frank"
fmt.Println(p2.Name) // 输出: Frank

4. 使用 & 创建结构体

可以使用内置的 new() 函数为结构体分配内存,并返回一个指向该结构体实例的指针,其字段会被初始化为零值。

go
var xm *User = &User{}
fmt.Println(xm)
xm.Name = "sixue"   // 或者 (*xm).name = "sixue"
fmt.Println(xm)

五、匿名结构体

匿名结构体是没有名字的结构体,通常只在临时使用定义局部变量时使用。

go
// 声明并初始化一个匿名结构体变量
var p1 = struct {
    Name string
    Age int
}{
    Name: "Grace",
    Age: 22,
}
fmt.Println(p1.Name) // 输出: Grace
go
var user struct{Name string; Age int}
user.Name = "pprof.cn"
user.Age = 18
fmt.Printf("%#v\n", user)

六、结构体嵌套 (匿名/具名)

结构体可以包含其他结构体作为字段,这称为嵌套。

1. 具名嵌套 (推荐)

将一个结构体作为另一个结构体的字段,访问时需要通过字段名。

go

type User struct {
	Name string
	Age  int
}

type Address struct {
    City string
    PostalCode string
}

type Employee struct {
    ID int
    Info User       // 具名嵌套:User类型
    Location Address // 具名嵌套:Address类型
}

e := Employee{
    ID: 101,
    Info: User{Name: "Henry"},
    Location: Address{City: "Beijing"},
}

// 访问嵌套字段
fmt.Println(e.Info.Name)      // 输出: Henry
fmt.Println(e.Location.City)  // 输出: Beijing

2. 匿名嵌套 (也称为“继承”或“组合”)

在结构体中,只写字段类型而不写字段名,该字段就是匿名嵌套字段。

  • 特性: 外部结构体可以直接访问内部结构体的字段。
go

type User struct {
	Name string
	Age  int
}

type Student struct {
    User // 匿名嵌套,字段名默认为类型名 User
    Major string
}

s := Student{
    User: User{Name: "Ivy", Age: 20},
    Major: "Computer Science",
}

// 直接访问 User 的字段 (提升字段/Promoted Fields)
fmt.Println(s.Name) // 输出: Ivy
fmt.Println(s.Age)  // 输出: 20

// 也可以通过具名方式访问
fmt.Println(s.User.Name) // 输出: Ivy

七、结构体标签 (Struct Tag)

结构体标签是附加在结构体字段上的字符串元数据,它不影响程序的运行逻辑,但常用于:

  • JSON/XML 序列化与反序列化 (如指定 JSON 字段名)。
  • 数据库 ORM 映射 (如指定数据库列名)。
  • 表单验证等。

标签格式为:反引号包围的字符串,内部是 key:"value" 键值对,多个键值对用空格分隔。

go
type Product struct {
    ID    int     `json:"product_id" db:"id"`       // 定义JSON和DB标签
    Name  string  `json:"name,omitempty" db:"name"` // omitempty 表示若 Name 零值时,不输出到 JSON
    Price float64 `json:"price"`
}

详细内容请参考 面向对象:结构体里的 Tag 用法

八、构造函数

Go语言的结构体没有构造函数,我们可以自己实现。 例如,下方的代码就实现了一个person的构造函数。 因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。

go
package main

import "fmt"

// Person 结构体:字段均为包内私有(首字母小写)
type Person struct {
    name string 
    age  int8   
}

func NewPerson(name string, age int8) (*Person) {
    return &Person{
        name: name,
        age:  age,
    }
}

func (p Person) GetInfo() string {
    // 通过方法,外部可以访问到私有字段 name 和 age
    return fmt.Sprintf("姓名: %s, 年龄: %d", p.name, p.age)
}

func (p *Person) Birthday() {
    p.age++ // 直接修改原始结构体实例的 age 字段
    fmt.Printf("%s 过了生日,现在 %d 岁了。\n", p.name, p.age)
}

func main() {
    // 1. 使用构造函数创建实例
    p1:= NewPerson("王小明", 28)
    fmt.Println(p1.GetInfo()) 
    p1.Birthday()
    fmt.Println(p1.GetInfo())
}

通过这个完整的例子,我们可以看到结构体、构造函数和方法的协同工作:

  • 构造函数 (NewPerson) 负责创建实例、校验输入并隐藏字段的初始化细节。
  • 值接收者方法 (GetInfo) 负责安全读取结构体的内部状态(包括私有字段)。
  • 指针接收者方法 (Birthday) 负责安全修改结构体的内部状态。

这种模式是 Go 语言中实现数据封装和行为绑定的标准做法。

九、结构体的组合与“继承”

Go 语言通过 结构体嵌入(匿名嵌套) 实现代码复用和类似“继承”的行为,核心思想是 组合优于继承

1. 实现“继承”

将一个结构体作为另一个结构体的 匿名字段,即可实现字段和方法的“提升”。

go
// 基础结构体 (Parent)
type Animal struct {
    Name string
}

func (a Animal) Move() { /* ... */ }

// 组合结构体 (Child)
type Dog struct {
    Animal      // 匿名嵌入 Animal (即“继承”)
    Breed string 
}

func main() {
    d := Dog{
        Animal: Animal{Name: "旺财"},
        Breed: "金毛",
    }
    
    // 访问提升的字段 (Animal.Name)
    fmt.Println(d.Name) 
    
    // 调用提升的方法 (Animal.Move())
    d.Move() 
}

2. 方法的覆盖(Override)

如果外部结构体(如 Dog)定义了与嵌入结构体(如 Animal)同名的方法,则外部结构体的方法会 覆盖 嵌入结构体的方法。

go
// Dog 自己的 Move 方法
func (d Dog) Move() {
    fmt.Printf("%s 正在快速奔跑!\n", d.Name)
}

func main() {
    // ...
    d.Move() // 调用的是 Dog 的 Move() 方法 (覆盖了 Animal 的 Move())
}

Go 提倡的是组合 (Composition) 优于继承 (Inheritance)。通过结构体嵌入,Go 实现了代码复用,同时避免了传统继承带来的复杂性和紧耦合问题。

十、结构体字段的可见性

Go语言中,结构体字段的可见性是通过字段名的首字母大小写来控制的。

  • 如果字段名的首字母是大写,则该字段是可导出的,可以在包外访问。
  • 如果字段名的首字母是小写,则该字段是不可导出的,不能在包外访问(仅在定义当前结构体的包中可访问)。