Go 零基础教程

Go 结构体字段访问

在 Go 语言中,定义和初始化了结构体 (Struct) 之后,下一步自然就是读取或者修改里面存储的数据。访问结构体字段是操作自定义数据类型最核心、最频繁的动作。

本章我们将全面梳理在 Go 语言中访问结构体字段的各种姿势,包括基础的点操作符访问、极其便利的指针自动解引用,以及在处理“嵌套”和“组合(匿名嵌套)”时字段访问的特殊规则。

1. 基础访问:使用点号 (.) 操作符

最常见、最直白访问结构体字段的方式就是使用 点操作符 (.)。只要你手里有一个结构体的实例,你就可以通过 实例名.字段名 的方式来读取或修改它。

1.1 基础示例

来看看最简单的 Person 结构体:

package main

import "fmt"

type Person struct {
    FirstName string
    LastName  string
    Age       int
}

func main() {
    person := Person{FirstName: "John", LastName: "Doe", Age: 30}
    
    // 使用点操作符读取字段
    fmt.Println("名:", person.FirstName)
    fmt.Println("姓:", person.LastName)
    fmt.Println("年龄:", person.Age)
    
    // 使用点操作符修改字段
    person.Age = 31
    fmt.Println("过完生日后的年龄:", person.Age) // 输出: 31
}

1.2 链式访问嵌套结构体 (Nested Structs)

当一个结构体把另一个结构体作为自己的常规字段时(这叫嵌套),你可以顺藤摸瓜,通过连续使用点操作符进行链式访问

package main

import "fmt"

type Address struct {
    Street  string
    City    string
    Country string
}

type Employee struct {
    ID      int
    Name    string
    Address Address // 将 Address 作为常规字段嵌套进来
}

func main() {
    employee := Employee{
        ID:   123,
        Name: "Alice Smith",
        Address: Address{
            Street:  "123 Main St",
            City:    "Anytown",
            Country: "USA",
        },
    }
    
    // 链式访问 Address 内部的字段
    fmt.Println("员工街道:", employee.Address.Street)
    fmt.Println("员工城市:", employee.Address.City)
    
    // 修改嵌套层级里的字段
    employee.Address.City = "New York"
    fmt.Println("员工搬家后的城市:", employee.Address.City)
}

2. 进阶访问:通过指针操作字段

我们在前面的指针章节提过,如果要想在函数间传递大型结构体并修改它,我们需要传递结构体的指针。那么,拿到指针后,我们该如何访问字段呢?

2.1 Go 的贴心魔法:自动解引用

在 C 或 C++ 中,如果你有一个指向结构体的指针,你需要使用特殊的 -> 操作符来访问字段。
但在 Go 语言中,你依然可以使用普通的点操作符 (.)!Go 编译器在底层极其贴心地为你自动完成了解引用。

package main

import "fmt"

type Book struct {
    Title  string
    Author string
    Pages  int
}

func main() {
    book := Book{Title: "Go 语言程序设计", Author: "Alan Donovan", Pages: 384}
    
    bookPtr := &book // bookPtr 是指向 book 的指针 (*Book)
    
    // 即使 bookPtr 是个指针,依然可以直接用 . 访问字段!
    fmt.Println("书名:", bookPtr.Title)
    fmt.Println("作者:", bookPtr.Author)
    
    // 通过指针修改字段,原实例也会被修改
    bookPtr.Pages = 400
    fmt.Println("修改后的页数:", bookPtr.Pages) // 输出: 400
    fmt.Println("原实例的页数也被改了:", book.Pages)  // 输出: 400
}

2.2 严格的显式解引用 (极其罕见)

虽然 Go 支持自动解引用,但从底层语法上讲,严格的写法应该是先用 * 将指针解引用为结构体实例,然后再用 . 访问字段。因为 . 的优先级高于 *,所以必须加括号 (*pointer).Field

package main

import "fmt"

type Car struct {
    Make  string
    Model string
    Year  int
}

func main() {
    car := Car{Make: "Toyota", Model: "Camry", Year: 2022}
    carPtr := &car
    
    // 显式解引用的写法 (虽然合法,但太啰嗦,日常开发中几乎没人这么写)
    fmt.Println("汽车品牌:", (*carPtr).Make)
    fmt.Println("汽车型号:", (*carPtr).Model)
}

结论: 忘记 (*pointer).Field 吧,永远使用 pointer.Field,享受 Go 带来的简洁。

3. 组合模式下的访问:匿名字段与提升

我们在上一章学过,如果在结构体中进行匿名嵌套 (Anonymous fields),也就是只写被嵌套的结构体类型名,不写字段名,那么被嵌套结构体的字段会被**“提升 (Promoted)”**到外层结构体中。

这使得 Go 语言以一种非常优雅的方式实现了类似面向对象中“继承”的效果(在 Go 中称为“组合 Composition”)。

3.1 字段提升 (Field Promotion) 示例

package main

import "fmt"

type Engine struct {
    Cylinders  int
    Horsepower int
}

type Vehicle struct {
    Make  string
    Model string
    Engine // 匿名嵌套 Engine,触发字段提升
}

func main() {
    vehicle := Vehicle{
        Make:  "Ford",
        Model: "Mustang",
        Engine: Engine{
            Cylinders:  8,
            Horsepower: 450,
        },
    }
    
    // 可以像访问 Vehicle 自己的字段一样,直接访问 Engine 的字段!
    fmt.Println("汽车品牌:", vehicle.Make)
    fmt.Println("气缸数:", vehicle.Cylinders)    // 发生了字段提升
    fmt.Println("马力:", vehicle.Horsepower)   // 发生了字段提升
    
    // 当然,显式地按层级访问也绝对没问题
    // fmt.Println(vehicle.Engine.Cylinders) 
}

3.2 解决同名字段冲突 (Name Collisions)

如果外层结构体和匿名嵌套的内层结构体,恰好有一个同名的字段,会发生什么?
规则是:外层的字段优先级更高(会遮蔽内层的同名字段)。

如果你依然想访问内层那个被遮蔽的字段,你就必须写出完整的层级路径。

package main

import "fmt"

type Inner struct {
    Value int
}

type Outer struct {
    Value int // 与 Inner 里的 Value 重名了!
    Inner     // 匿名嵌套
}

func main() {
    outer := Outer{
        Value: 10, // 属于 Outer 自己的 Value
        Inner: Inner{
            Value: 20, // 属于 Inner 的 Value
        },
    }
    
    // 直接访问,优先拿到的是外层的字段
    fmt.Println("直接访问 outer.Value:", outer.Value)       // 输出: 10
    
    // 必须写全路径,才能访问到内层被遮蔽的字段
    fmt.Println("完整路径访问 outer.Inner.Value:", outer.Inner.Value) // 输出: 20
}