Go 零基础教程

Go 结构体定义

在 Go 语言中,定义结构体 (Struct) 允许你创建自定义的数据类型,将相关的多个数据组合在一起。

通过结构体,你可以将现实世界中的实体或概念抽象为一个单一的单元,从而让代码变得更加易读、易维护且高效。

1. 认识结构体 (Struct)

结构体(Structure 的简写)是一种复合数据类型。它可以把零个或多个命名变量(称为“字段” Field)组合在一起,每个字段都可以有自己独立的数据类型。

虽然结构体看起来有点像其他面向对象编程语言中的“类 (Classes)”,但在 Go 语言中,结构体更加轻量级。Go 摒弃了传统的继承机制,专注于数据的表示(不过别担心,Go 通过“组合”机制同样能实现强大的复用,我们稍后会讲)。

1.1 定义结构体类型

要定义一个结构体,你需要使用 type 关键字,加上结构体的名字,再跟上 struct 关键字。在大括号 {} 内部,你就可以定义它的各个字段了(包含字段名和数据类型)。

package main

// 定义一个名为 Person 的结构体类型
type Person struct {
    FirstName string
    LastName  string
    Age       int
}

在这个例子中,我们定义了一个名为 Person 的结构体,它拥有三个字段:FirstName (字符串)、LastName (字符串) 和 Age (整数)。

1.2 结构体的零值 (Zero Values)

像 Go 语言里的其他数据类型一样,如果你在声明结构体变量时没有显式地给它赋初始值,Go 会自动将其所有字段初始化为对应类型的零值

package main

import "fmt"

type Person struct {
    FirstName string
    LastName  string
    Age       int
}

func main() {
    var p Person
    // %+v 可以打印出结构体的字段名和对应的值
    fmt.Printf("%+v\n", p) // 输出: {FirstName: LastName: Age:0}
}

如你所见,字符串字段 FirstNameLastName 被初始化为了空字符串 "",而整数字段 Age 被初始化为了 0

2. 创建与初始化结构体实例

在 Go 中,有多种方式可以创建和初始化结构体的实例。

2.1 使用结构体字面量 (Struct Literal)

这是日常开发中最常用、最直观的实例化方式。你可以在大括号内按名称为每个字段指定具体的值。

package main

import "fmt"

type Person struct {
    FirstName string
    LastName  string
    Age       int
}

func main() {
    // 使用结构体字面量进行初始化
    p := Person{
        FirstName: "John",
        LastName:  "Doe",
        Age:       30,
    }
    fmt.Printf("%+v\n", p) // 输出: {FirstName:John LastName:Doe Age:30}
}

避坑指南:
你也可以省略字段名,直接按结构体定义时的顺序填入数值。但极其不推荐这种写法!因为它严重降低了代码的可读性,并且一旦未来结构体增加了新字段或调整了字段顺序,这段代码直接就会报错甚至引发隐蔽的 Bug。

// 极不推荐的写法,应尽量避免
p := Person{"John", "Doe", 30}

2.2 使用 new 函数

我们在上一章学过,new 函数可以在内存中为一个新变量分配空间,并返回指向它的指针。用 new 创建的结构体,其字段全部为零值。

package main

import "fmt"

type Person struct {
    FirstName string
    LastName  string
    Age       int
}

func main() {
    p := new(Person) // 返回的是 *Person (指针)
    
    fmt.Printf("%+v\n", *p) // 输出: {FirstName: LastName: Age:0}
    
    // 给字段赋值
    p.FirstName = "John"
    p.LastName = "Doe"
    p.Age = 30
    
    fmt.Printf("%+v\n", *p) // 输出: {FirstName:John LastName:Doe Age:30}
}

2.3 使用构造函数 (Constructor Function)

Go 语言本身没有内置的“构造函数”关键字,但我们可以通过编写一个普通的函数来模拟构造函数的行为。这在需要进行复杂初始化逻辑,或者需要校验参数合法性时极其有用。惯例上,这类函数通常以 New 开头。

package main

import "fmt"

type Person struct {
    FirstName string
    LastName  string
    Age       int
}

// NewPerson 充当构造函数,返回结构体的指针
func NewPerson(firstName, lastName string, age int) *Person {
    if age < 0 {
        return nil // 参数校验:如果年龄不合法,返回 nil
    }
    return &Person{
        FirstName: firstName,
        LastName:  lastName,
        Age:       age,
    }
}

func main() {
    p := NewPerson("John", "Doe", 30)
    if p != nil {
        fmt.Printf("创建成功: %+v\n", *p) 
    }

    invalidPerson := NewPerson("Jane", "Doe", -5)
    if invalidPerson == nil {
        fmt.Println("提供的年龄无效,创建失败") 
    }
}

返回结构体的指针 (*Person) 是一个好习惯,这不仅能在创建失败时返回 nil,还能避免复制整个结构体带来的性能开销。

3. 访问结构体字段

3.1 使用点号 (.) 操作符

你可以使用点号操作符轻松访问或修改结构体里的字段。

package main

import "fmt"

type Person struct {
    FirstName string
    LastName  string
    Age       int
}

func main() {
    p := Person{
        FirstName: "John",
        LastName:  "Doe",
        Age:       30,
    }
    
    fmt.Println(p.FirstName) // 输出: John
    fmt.Println(p.LastName)  // 输出: Doe
    
    p.Age = 31               // 修改字段的值
    fmt.Println(p.Age)       // 输出: 31
}

3.2 通过指针访问字段 (自动隐式解引用)

如果你手里拿的是一个指向结构体的指针,你依然可以直接使用点号操作符来访问字段。Go 语言会在底层自动帮你完成指针的解引用操作,非常贴心。

package main

import "fmt"

type Person struct {
    FirstName string
    LastName  string
    Age       int
}

func main() {
    // 拿到 Person 的指针
    p := &Person{
        FirstName: "John",
        LastName:  "Doe",
        Age:       30,
    }
    
    // Go 自动将 p.FirstName 转译为 (*p).FirstName
    fmt.Println(p.FirstName) // 输出: John 
    
    p.Age = 31               // 通过指针修改字段
    fmt.Println(p.Age)       // 输出: 31
}

4. 结构体嵌套与组合 (Composition)

Go 语言虽然没有继承,但通过在结构体中嵌套另一个结构体,实现了强大且灵活的“组合 (Composition)”模式。你可以把已有的数据结构当作积木,拼装出更复杂的模型。

4.1 嵌套结构体字段 (Embedded Fields)

package main

import "fmt"

type Address struct {
    Street  string
    City    string
    ZipCode string
}

type Person struct {
    FirstName string
    LastName  string
    Age       int
    Address   Address // 将 Address 结构体作为一个常规字段嵌套进来
}

func main() {
    p := Person{
        FirstName: "John",
        LastName:  "Doe",
        Age:       30,
        Address: Address{
            Street:  "主街 123 号",
            City:    "某某市",
            ZipCode: "12345",
        },
    }
    
    fmt.Println(p.FirstName)         // 输出: John
    fmt.Println(p.Address.Street)    // 逐级访问: 输出: 主街 123 号
    fmt.Println(p.Address.City)      // 输出: 某某市
}

4.2 匿名字段与字段提升 (Anonymous Fields)

更高级的玩法是匿名嵌套。你可以直接把另一个结构体的类型名写进来,而不给它起字段名。
此时,被嵌套结构体的内部字段会被“提升 (Promoted)”到外层结构体中,你可以直接在外层结构体上访问它们!

package main

import "fmt"

type Address struct {
    Street  string
    City    string
    ZipCode string
}

type Person struct {
    FirstName string
    LastName  string
    Age       int
    Address   // 匿名嵌套:只写类型名,不写字段名
}

func main() {
    p := Person{
        FirstName: "John",
        LastName:  "Doe",
        Age:       30,
        Address: Address{
            Street:  "主街 123 号",
            City:    "某某市",
            ZipCode: "12345",
        },
    }
    
    fmt.Println(p.FirstName)   // 输出: John
    
    // ✨ 魔法发生:Street 和 City 被提升了,可以直接通过 p 访问!
    fmt.Println(p.Street)      // 输出: 主街 123 号 
    fmt.Println(p.City)        // 输出: 某某市
    
    // 当然,完整路径依然是通的
    // fmt.Println(p.Address.Street) 
}

注意:如果内外层结构体出现了同名字段(冲突),外层的字段会优先覆盖内层的字段。为了代码清晰,建议尽量避免命名冲突。

5. 实战案例演示

示例 1:表示一个矩形

package main

import "fmt"

type Rectangle struct {
    Width  float64
    Height float64
}

func main() {
    r := Rectangle{
        Width:  10.0,
        Height: 5.0,
    }
    fmt.Println("宽度:", r.Width)   // 输出: 宽度: 10
    fmt.Println("高度:", r.Height)  // 输出: 高度: 5
}

示例 2:表示一本书及其作者信息 (组合模式)

package main

import "fmt"

type Author struct {
    Name string
    Bio  string
}

type Book struct {
    Title  string
    Author Author // 嵌套作者结构体
    Pages  int
}

func main() {
    book := Book{
        Title: "Go 语言程序设计",
        Author: Author{
            Name: "Alan Donovan & Brian Kernighan",
            Bio:  "Go 语言的经典权威著作。",
        },
        Pages: 384,
    }
    fmt.Println("书名:", book.Title)           // 输出: 书名: Go 语言程序设计
    fmt.Println("作者:", book.Author.Name)     // 输出: 作者: Alan Donovan & Brian Kernighan
}