8·类型系统入门

结构体

structfieldtag

Go 幼发编程第8课:结构体 (Struct)

学习目标

学完本课,你将能够:

  1. 理解结构体的概念,并使用 struct 关键字定义自定义类型。
  2. 掌握如何创建结构体实例并访问/修改其字段。
  3. 了解如何使用结构体嵌套来组合多个类型。
  4. 学习为结构体字段添加标签(Tag)并理解其常见用途。
  5. 能够区分结构体的值语义与指针语义。

核心概念

1. 什么是结构体?

想象一下,你想要描述一个“学生”。一个学生可能有“姓名”、“年龄”、“成绩”等多个属性。单独的变量(如 name string)无法将它们关联起来。结构体就是用来解决这个问题的,它就像一张自定义的“表格模板”,你可以定义多个字段,每个字段都有名字和类型。

// 这是一张“学生”表格的模板定义
type Student struct {
    Name    string  // 字段名:Name,类型:string
    Age     int     // 字段名:Age,类型:int
    Score   float64 // 字段名:Score,类型:float64
    IsMale  bool    // 字段名:IsMale,类型:bool
}

使用 type 关键字 + 自定义名称 + struct 来定义。

2. 创建实例与访问字段

定义好模板后,就可以用它来创建具体的“学生”实例了,这个过程称为实例化

// 方法一:使用“字段名: 值”格式,顺序可以不同
student1 := Student{
    Name:   "张三",
    Age:    20,
    Score:  95.5,
    IsMale: true,
}

// 方法二:按字段定义顺序提供值(不推荐,增加字段时易出错)
student2 := Student{"李四", 21, 88.0, false}

// 访问和修改字段使用“点号”(.)
fmt.Println(student1.Name) // 输出: 张三
student1.Age = 21          // 将 student1 的年龄改为 21

3. 结构体嵌套

结构体可以包含其他结构体作为字段,就像表格里可以再嵌入一个小表格。这称为组合,是Go语言代码复用的重要方式。

type Address struct {
    City    string
    Street  string
}

type Employee struct {
    Name    string
    Age     int
    Home    Address // 将 Address 结构体作为字段
}

// 创建时需要嵌套初始化
emp := Employee{
    Name: "王五",
    Age:  30,
    Home: Address{
        City:   "北京",
        Street: "中关村大街1号",
    },
}

// 访问嵌套字段
fmt.Println(emp.Home.City) // 输出: 北京

4. 字段标签 (Tag)

标签是附加在字段后面的、用反引号包裹的字符串。它不是Go语言本身的语法,而是一个元数据,常被其他包(如 encoding/json, gorm)用来控制行为。

type User struct {
    // `json:"user_name"` 是标签
    // 意思是:当这个结构体被编码为JSON时,`Name` 字段的键名将是 "user_name"
    Name string `json:"user_name"`
    Age  int    `json:"age"`
}

代码示例

以下是一个综合性的示例,展示了结构体的定义、创建、嵌套和标签的使用。

package main

import (
    "encoding/json"
    "fmt"
)

// 1. 定义基础结构体
type Point struct {
    X int
    Y int
}

// 2. 定义包含嵌套结构体和标签的结构体
type Circle struct {
    Center Point   `json:"center"` // 嵌套 Point,并添加 json 标签
    Radius float64 `json:"radius"`
    Color  string  `json:"color,omitempty"` // omitempty 表示:如果字段为空值,则JSON编码时忽略
}

func main() {
    // 3. 创建 Circle 实例
    myCircle := Circle{
        Center: Point{X: 10, Y: 20}, // 嵌套创建 Point
        Radius: 5.0,
        Color:  "red",
    }

    // 4. 访问字段(包括嵌套字段)
    fmt.Printf("圆心坐标:(%d, %d)\n", myCircle.Center.X, myCircle.Center.Y)
    fmt.Printf("半径:%.1f, 颜色:%s\n", myCircle.Radius, myCircle.Color)

    // 5. 修改字段
    myCircle.Radius = 7.5
    myCircle.Color = "" // 将颜色置为空字符串

    // 6. 使用 json 包将结构体转换为 JSON 字符串
    // 由于 Color 为空,且标签中有 `omitempty`,该字段将不会出现在JSON中
    jsonData, err := json.MarshalIndent(myCircle, "", "  ")
    if err != nil {
        fmt.Println("JSON编码错误:", err)
        return
    }
    fmt.Println("\n转换后的JSON:")
    fmt.Println(string(jsonData))
}

运行输出预期:

圆心坐标:(10, 20)
半径:5.0, 颜色:red

转换后的JSON:
{
  "center": {
    "X": 10,
    "Y": 20
  },
  "radius": 7.5
}

实践练习

练习1:基础结构体

定义一个 Book 结构体,包含 Title (string), Author (string), Price (float64) 三个字段。创建一本名为《Go语言实战》、作者“John Smith”、价格 59.9 的书籍实例,并打印其所有信息。

练习2:结构体嵌套与标签

在练习1的基础上,定义一个 Library 结构体,包含 Name (string) 和 Books (一个 Book 类型的切片) 两个字段。创建一个图书馆实例,向其中添加至少两本书。最后,使用 json.Marshal 将整个图书馆结构体转换为JSON并打印,要求书籍的 Title 字段在JSON中显示为 "title"

练习3:指针与结构体

创建一个函数 growUp(p *Person),它接收一个指向 Person 结构体的指针,并将其 Age 字段增加1。在 main 函数中测试它。思考:为什么需要传指针?

常见错误

  1. 字段名大小写错误:Go 语言中,大写开头的字段是导出的(public),可以被其他包访问;小写开头的是未导出的(private),只能在本包内访问。初学者常因大小写不匹配导致访问失败。

    type student struct { // 结构体名小写,只在本包可见
        name string // 字段小写,未导出
        age  int
    }
    // 在其他包中无法访问 student 及其 name, age 字段
    
  2. 标签语法错误:标签必须用反引号 ` 包裹,而不是单引号 '。标签内的键值对通常使用 key:"value" 格式,冒号后不能有空格(标准格式)。

    // 错误示范
    Name string 'json:"name"'  // 用了单引号
    Name string `json: "name"` // 冒号后有空格
    
    // 正确示范
    Name string `json:"name"`
    
  3. 值拷贝陷阱:结构体是值类型。将一个结构体变量赋值给另一个变量,或传入函数时,会拷贝整个结构体。在函数内部修改拷贝的副本,不会影响原始值。如果需要修改原始值,应传入指针。

    func modifyStudent(s Student) { // s 是副本
        s.Name = "Modified"
    }
    
    func main() {
        s1 := Student{Name: "Original"}
        modifyStudent(s1)
        fmt.Println(s1.Name) // 仍然是 "Original",没有改变
    }
    
  4. 嵌套初始化顺序错误:嵌套初始化时,内部结构体必须使用 类型名{字段: 值} 的格式,不能省略类型名。

    // 错误
    emp := Employee{
        Name: "Alice",
        Home: {City: "Shanghai"}, // 缺少 Address 类型名
    }
    
    // 正确
    emp := Employee{
        Name: "Alice",
        Home: Address{City: "Shanghai"}, // 需要写明 Address
    }
    

小结

本节课我们学习了Go语言中最重要的复合类型——结构体

  • 定义:使用 type Name struct { ... } 定义一个包含多个字段的自定义类型。
  • 实例化:通过 TypeName{Field: Value} 或按顺序 TypeName{Value1, Value2} 创建具体实例。
  • 字段操作:使用点号 . 来访问和修改实例的字段。
  • 组合(嵌套):一个结构体可以包含另一个结构体字段,实现代码复用和更复杂的类型构建。
  • 标签:为字段添加 `key:"value"` 形式的元数据,常被序列化(如JSON)、ORM等库使用。
  • 值语义:结构体默认是值拷贝,传递给函数或赋值时会产生副本。如需共享数据或避免拷贝,应使用指向结构体的指针。

掌握了结构体,你就拥有了在Go语言中组织和管理复杂数据的能力,这是构建任何有意义程序的基础。下一课,我们将学习如何为结构体定义方法,让它不仅仅是数据的容器,更是行为的载体。

练习编辑器

go
Loading...

继续学习

完成本课后,建议继续学习下一课「方法与接收者」 以巩固所学知识。