Go 幼发编程第8课:结构体 (Struct)
学习目标
学完本课,你将能够:
- 理解结构体的概念,并使用
struct关键字定义自定义类型。 - 掌握如何创建结构体实例并访问/修改其字段。
- 了解如何使用结构体嵌套来组合多个类型。
- 学习为结构体字段添加标签(Tag)并理解其常见用途。
- 能够区分结构体的值语义与指针语义。
核心概念
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 函数中测试它。思考:为什么需要传指针?
常见错误
-
字段名大小写错误:Go 语言中,大写开头的字段是导出的(public),可以被其他包访问;小写开头的是未导出的(private),只能在本包内访问。初学者常因大小写不匹配导致访问失败。
type student struct { // 结构体名小写,只在本包可见 name string // 字段小写,未导出 age int } // 在其他包中无法访问 student 及其 name, age 字段 -
标签语法错误:标签必须用反引号
`包裹,而不是单引号'。标签内的键值对通常使用key:"value"格式,冒号后不能有空格(标准格式)。// 错误示范 Name string 'json:"name"' // 用了单引号 Name string `json: "name"` // 冒号后有空格 // 正确示范 Name string `json:"name"` -
值拷贝陷阱:结构体是值类型。将一个结构体变量赋值给另一个变量,或传入函数时,会拷贝整个结构体。在函数内部修改拷贝的副本,不会影响原始值。如果需要修改原始值,应传入指针。
func modifyStudent(s Student) { // s 是副本 s.Name = "Modified" } func main() { s1 := Student{Name: "Original"} modifyStudent(s1) fmt.Println(s1.Name) // 仍然是 "Original",没有改变 } -
嵌套初始化顺序错误:嵌套初始化时,内部结构体必须使用
类型名{字段: 值}的格式,不能省略类型名。// 错误 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语言中组织和管理复杂数据的能力,这是构建任何有意义程序的基础。下一课,我们将学习如何为结构体定义方法,让它不仅仅是数据的容器,更是行为的载体。