我们刚刚学习了方法(Methods)和接口(Interfaces),而它们几乎总是与结构体(Structs)一起使用。可以说,结构体是 Go 语言中组织数据的基石,理解它至关重要。
它扮演的角色非常像 PHP 中的 class
,但只负责封装数据(属性),不直接包含方法。
我们同样分场景,从简单到高阶来介绍它,并首先回答你最关心的问题。
核心:如何判断属性是否可被外部访问?(可见性规则)
在 PHP 中,我们用 public
, private
, protected
关键字来控制属性和方法的可见性。
Go 的规则要简单得多,并且只有一个标准:根据名称的首字母大小写来判断。
-
首字母大写 (Exported) :如果一个结构体的字段(或任何顶层变量、函数、类型)以大写字母 开头,那么它就是导出的(Exported) ,相当于 PHP 的
public
。这意味着在**任何其他包(package)**中都可以直接访问它。 -
首字母小写 (Unexported) :如果字段以小写字母 开头,那么它就是未导出的(Unexported) ,类似于 PHP 的
private
。它只能在同一个包内部被访问。
举个例子:
假设我们有一个 models
包,里面定义了一个 User
结构体:
go
// in package models
package models
type User struct {
ID int // 大写,公开的
FirstName string // 大写,公开的
lastName string // 小写,私有的
password string // 小写,私有的
}
现在,我们在 main
包(另一个包)中使用它:
go
// in package main
package main
import "myproject/models"
func main() {
u := models.User{}
u.ID = 101 // 正确!ID 是大写的,可以访问
u.FirstName = "Alice" // 正确!FirstName 是大写的,可以访问
// u.lastName = "Smith" // 编译错误!lastName 在包外不可见
// u.password = "123" // 编译错误!password 在包外不可见
}
这个规则简单、一致,适用于 Go 中所有需要控制可见性的地方。
场景一:基础用法
1. 聚合相关数据
这是 struct
最基本、最核心的用途:将一堆相关的变量组合成一个单一的、有意义的逻辑单元。
-
场景 :
- 定义一个"用户",包含姓名、年龄、邮箱。
- 定义一个数据库连接配置,包含主机、端口、用户名、密码。
- 定义一个 API 响应,包含状态码、消息和数据。
-
用法 :
gotype DBConfig struct { Host string Port int Username string Password string } func main() { // 创建并初始化一个 DBConfig 实例 config := DBConfig{ Host: "localhost", Port: 3306, Username: "root", Password: "password", } fmt.Println("Connecting to host:", config.Host) }
场景二:中阶用法
2. 结构体嵌套与组合(替代继承)
Go 语言没有 PHP 的 extends
继承。作为替代,Go 提倡使用组合(Composition) ,通过在一个结构体中嵌套另一个结构体来实现功能的复用和扩展。
-
场景:
- 一个"员工 (Employee)"拥有"个人信息 (Person)"的所有属性,同时还有额外的"职位 (Position)"属性。
- 一个"文章 (Post)"包含一个"作者 (Author)"的信息。
-
用法 :
有两种嵌套方式:
a) 作为普通字段嵌套
gotype Address struct { City string Country string } type Company struct { Name string Address Address // Address 是 Company 的一个普通字段 } func main() { c := Company{ Name: "Google", Address: Address{ City: "Mountain View", Country: "USA", }, } // 访问嵌套字段需要逐级访问 fmt.Println(c.Address.City) }
b) 匿名嵌套 (Embedding)
通过只写类型而不写字段名,可以将嵌套结构体的字段"提升"到外层,实现类似继承的效果。
gotype Person struct { Name string Age int } type Employee struct { Person // 匿名嵌套 Person Position string } func main() { e := Employee{ Person: Person{ Name: "Alice", Age: 30, }, Position: "Engineer", } // 可以直接访问 Person 的字段,就像是 Employee 自己的一样 fmt.Println(e.Name) // 直接访问,而不是 e.Person.Name fmt.Println(e.Position) }
3. 使用结构体标签 (Struct Tags)
标签是附加在结构体字段上的元数据(metadata)。它们本身对 Go 代码没有影响,但可以被其他库(如 encoding/json
)读取,用来指导这些库如何处理这个字段。
-
场景 :
- 控制一个结构体如何被编码/解码成 JSON 格式(最常用!)。
- 定义 ORM(对象关系映射)中结构体字段与数据库表列的对应关系。
- 为字段添加验证规则。
-
用法 : 标签是写在反引号 ````` 内的字符串。
gotype Product struct { ID int `json:"id"` Name string `json:"product_name"` Price float64 `json:"price,omitempty"` // omitempty 表示如果字段值为零值,则忽略它 Description string `json:"-"` // - 表示永远不要处理这个字段 }
当你使用
json.Marshal
将Product
实例转换为 JSON 时,encoding/json
包会读取这些标签,并生成对应key
的 JSON。
场景三:高阶用法
4. 零宽度结构体 struct{}
我们在用 map
实现"集合"时已经见过它。因为它不占用任何内存,所以是表达"存在性"或发送"信号"的绝佳工具。
-
场景 :
- 集合(Set) :
map[string]struct{}
,我们只关心键是否存在。 - 通道信号: 在并发编程中,有时我们只想通知另一个 Goroutine 某件事已经完成,而不需要传递任何实际数据。
- 集合(Set) :
-
用法 :
go// 使用 channel 发送一个完成信号 done := make(chan struct{}) go func() { fmt.Println("Worker is running...") // ... do some work ... // 工作完成,发送一个空结构体作为信号 done <- struct{}{} }() // main goroutine 会在这里阻塞,直到收到信号 <-done fmt.Println("Worker finished.")
总而言之,结构体是 Go 程序中数据的"骨架"。你通过定义结构体来构建你的数据模型,然后通过为它附加方法 来赋予它行为,最后通过接口来定义它的抽象契约,从而构建出清晰、健壮、可扩展的 Go 应用。