结构体(Struct)
结构体是 Go 语言中最重要的复合数据类型,它将不同类型的字段组合成一个有意义的整体。结构体是面向对象编程在 Go 中的基础——Go 通过结构体和方法实现封装,通过嵌套实现组合,而不是传统的类继承。
结构体的定义
使用 type 关键字和 struct 关键字定义结构体:
type Person struct {
Name string
Age int
Email string
}结构体是一种聚合数据类型,它将零个或多个任意类型的命名字段组合在一起。每个字段都有名称和类型。结构体是值类型,当赋值给新变量或作为函数参数时,会进行完整的值拷贝。
定义与初始化
方式一:使用字段名初始化(推荐)
type Person struct {
Name string
Age int
Email string
}
// 指定字段名初始化(字段顺序无关)
p1 := Person{
Name: "Alice",
Age: 30,
Email: "alice@example.com",
}
// 只初始化部分字段,其余为零值
p2 := Person{
Name: "Bob",
Age: 25,
}
fmt.Printf("%+v\n", p2) // {Name:Bob Age:25 Email:}💡 最佳实践:始终使用字段名初始化结构体。这样代码更清晰,且在结构体添加新字段时不会破坏已有的初始化代码。
方式二:按顺序初始化(不推荐)
// 按字段声明顺序初始化(不推荐,容易出错)
p3 := Person{"Charlie", 35, "charlie@example.com"}⚠️ 不推荐按顺序初始化:如果结构体新增了字段,或调整了字段顺序,按顺序初始化的代码将编译失败或产生错误的结果。始终使用字段名初始化。
方式三:使用 new() 函数
// new(T) 分配一个 T 类型的零值,返回 *T
p4 := new(Person)
// p4 的类型是 *Person(指针)
// p4 指向的内存中,所有字段都是零值
fmt.Printf("%T\n", p4) // *Person
fmt.Printf("%+v\n", *p4) // {Name: Age:0 Email:}
// 通过指针访问字段(Go 允许直接用 . 访问,不需要 ->)
p4.Name = "Dave"
p4.Age = 28
fmt.Printf("%+v\n", *p4) // {Name:Dave Age:28 Email:}方式四:使用取地址操作符
p5 := &Person{} // 等价于 new(Person)
p5.Name = "Eve"
p5.Age = 22new(Person)创建一个零值的*Person,所有字段为零值&Person{Name: "Alice"}创建一个部分初始化的*PersonPerson{Name: "Alice"}创建一个值类型的Person(栈上或逃逸到堆上)
在 Go 中,&Person{Name: "Alice"} 是更常用的方式,因为它可以在创建时就初始化字段。
方式五:零值初始化
var p Person // 所有字段初始化为零值
fmt.Printf("%+v\n", p) // {Name: Age:0 Email:}访问和修改字段
使用点号 . 访问结构体字段,无论变量是值类型还是指针类型:
type Point struct {
X, Y float64
}
p := Point{3.0, 4.0}
// 访问字段
fmt.Println(p.X) // 3
fmt.Println(p.Y) // 4
// 修改字段
p.X = 10.0
p.Y = 20.0
// 通过指针访问(Go 自动解引用,不需要 -> 操作符)
pp := &p
pp.X = 100.0
fmt.Println(p.X) // 100 —— p 也被修改了💡 Go 没有 -> 操作符:与 C/C++ 不同,Go 中无论是结构体值还是结构体指针,都使用 . 来访问字段。编译器会自动处理指针的解引用。
匿名结构体
匿名结构体没有类型名称,适用于一次性使用的场景:
// 定义并初始化匿名结构体
person := struct {
Name string
Age int
}{
Name: "Anonymous",
Age: 99,
}
fmt.Printf("%+v\n", person) // {Name:Anonymous Age:99}匿名结构体常见使用场景:
// 场景一:函数返回多个值时,用匿名结构体组织
func getServerConfig() struct {
Host string
Port int
} {
return struct {
Host string
Port int
}{
Host: "localhost",
Port: 8080,
}
}
// 场景二:表驱动测试中定义测试用例
tests := []struct {
name string
input int
expected int
}{
{"positive", 5, 25},
{"zero", 0, 0},
{"negative", -3, 9},
}
for _, tc := range tests {
result := tc.input * tc.input
if result != tc.expected {
fmt.Printf("%s failed: got %d, want %d\n", tc.name, result, tc.expected)
}
}嵌套结构体
Go 通过结构体嵌套实现组合(Composition),而非传统的继承(Inheritance)。
具名嵌套
type Address struct {
City string
Country string
ZipCode string
}
type Person struct {
Name string
Age int
Address Address // 具名嵌套
}
p := Person{
Name: "Alice",
Age: 30,
Address: Address{
City: "Beijing",
Country: "China",
ZipCode: "100000",
},
}
// 访问嵌套字段
fmt.Println(p.Address.City) // Beijing
fmt.Println(p.Address.Country) // China匿名嵌套(字段提升)
当结构体嵌套一个匿名结构体(或匿名接口)时,内层结构体的字段会被”提升”到外层。可以直接通过外层结构体访问内层的字段,无需写出中间层级。这类似于面向对象中的继承,但本质上是语法糖。
type Address struct {
City string
Country string
}
type Person struct {
Name string
Age int
Address // 匿名嵌套(不写字段名)
}
p := Person{
Name: "Alice",
Age: 30,
Address: Address{
City: "Shanghai",
Country: "China",
},
}
// 字段提升:可以直接访问 City,无需 p.Address.City
fmt.Println(p.City) // Shanghai —— 等价于 p.Address.City
fmt.Println(p.Country) // China —— 等价于 p.Address.Country
// 仍然可以通过完整路径访问
fmt.Println(p.Address.City) // Shanghai
// 如果外层和内层有同名字段,外层字段优先
type Employee struct {
Name string
Address Address
Name string // 编译错误:重复字段名
}⚠️ 字段提升的歧义规则:如果外层结构体有多个匿名嵌套字段,且它们包含同名字段,则该名字段不会被提升,必须显式指定路径。
type Skill struct {
Name string
Level int
}
type Hobby struct {
Name string
Since int
}
type Person struct {
Name string
Skill // 匿名嵌套:有 Name 字段
Hobby // 匿名嵌套:也有 Name 字段
}
p := Person{
Name: "Alice",
Skill: Skill{Name: "Go", Level: 5},
Hobby: Hobby{Name: "Chess", Since: 2010},
}
fmt.Println(p.Name) // "Alice" —— 外层字段优先
// p.Name 是有歧义的(Skill.Name 和 Hobby.Name 都被提升)
// 必须显式指定
fmt.Println(p.Skill.Name) // "Go"
fmt.Println(p.Hobby.Name) // "Chess"组合 vs 继承
Go 使用组合替代继承,这是 Go 设计哲学的核心之一:
// 继承(Go 不支持):
// class Dog extends Animal { ... }
// 组合(Go 的方式):
type Animal struct {
Name string
}
func (a Animal) Speak() string {
return "..."
}
type Dog struct {
Animal // 匿名嵌套 = "继承" Animal 的字段和方法
Breed string
}
// 可以重写"父类"方法
func (d Dog) Speak() string {
return "Woof!"
}
func main() {
d := Dog{
Animal: Animal{Name: "Buddy"},
Breed: "Golden Retriever",
}
fmt.Println(d.Name) // "Buddy" —— 字段提升
fmt.Println(d.Speak()) // "Woof!" —— 方法重写
}💡 Go 设计哲学:“组合优于继承”。Go 不支持传统的类继承体系,鼓励通过小的、可组合的接口和结构体来构建系统。这避免了深层继承树带来的复杂性。
结构体的比较
可比较的结构体
如果结构体的所有字段都是可比较的,那么结构体本身也是可比较的。可以使用 == 和 != 进行比较(逐字段比较):
type Point struct {
X, Y int
}
p1 := Point{1, 2}
p2 := Point{1, 2}
p3 := Point{1, 3}
fmt.Println(p1 == p2) // true —— 所有字段都相等
fmt.Println(p1 == p3) // false —— Y 字段不同不可比较的结构体
如果结构体包含不可比较的字段(slice、map、func),则整个结构体不可比较:
type Data struct {
Name string
Items []int // slice 不可比较
}
d1 := Data{Name: "test", Items: []int{1, 2}}
d2 := Data{Name: "test", Items: []int{1, 2}}
// d1 == d2 // 编译错误:struct containing []int cannot be compared结构体标签(Struct Tags)
结构体标签是写在字段声明后面的反引号字符串,通常用于存储元数据。标签不会影响程序的逻辑行为,但可以通过反射(reflection)在运行时读取。最常见的用途是控制 JSON、YAML、数据库等格式的序列化和反序列化。
基本语法
type User struct {
ID int `json:"id" db:"user_id"`
Username string `json:"username" db:"username"`
Email string `json:"email" db:"email"`
Password string `json:"-" db:"password"` // json 中忽略此字段
Age int `json:"age,omitempty" db:"age"` // 零值时忽略
CreatedAt string `json:"created_at" db:"created_at"`
}JSON 标签详解
import "encoding/json"
type Article struct {
Title string `json:"title"` // 标准映射
Author string `json:"author,omitempty"` // 零值时忽略
Content string `json:"content,omitempty"`
Secret string `json:"-"` // 完全忽略
Published bool `json:"published,string"` // 输出为字符串
Views int `json:"views,omitempty,string"` // 零值忽略,输出为字符串
Tags []string `json:"tags,omitempty"` // slice 零值为 nil 时忽略
}
func main() {
a := Article{
Title: "Go Struct Tags",
Author: "",
Content: "Hello, World!",
Secret: "hidden",
Published: true,
Views: 0,
Tags: nil,
}
data, _ := json.MarshalIndent(a, "", " ")
fmt.Println(string(data))
// 输出(Secret 被忽略,Author 为空字符串被忽略,Views 为 0 被忽略,Tags 为 nil 被忽略):
// {
// "title": "Go Struct Tags",
// "content": "Hello, World!",
// "published": "true",
// "tags": []
// }
// 注意:Tags 为 nil 被 omitempty 忽略,但 []string{} 空切片不会被忽略
// 反序列化
jsonStr := `{"title":"Learn Go","author":"Alice","extra":"ignored"}`
var a2 Article
json.Unmarshal([]byte(jsonStr), &a2)
fmt.Printf("%+v\n", a2)
// {Title:Learn Go Author:Alice Content: Secret: Published:false Views:0 Tags:[]}
// extra 字段被忽略(不会报错)
}常用标签选项汇总
| 标签 | 选项 | 说明 |
|---|---|---|
json:"field_name" | 基本映射 | JSON 键名 |
json:"field_name,omitempty" | 零值忽略 | 字段为零值时不输出 |
json:"-" | 忽略 | 序列化和反序列化都忽略此字段 |
json:"-," | 强制输出空名 | 输出 "" 作为键名 |
json:"field,string" | 字符串化 | 数值/布尔值输出为 JSON 字符串 |
json:",omitempty,string" | 组合 | 零值忽略 + 字符串化 |
db:"column_name" | 数据库列名 | 常用于 GORM、sqlx 等 ORM |
yaml:"field_name" | YAML 映射 | 常用于配置文件解析 |
binding:"required" | 验证规则 | 常用于 Gin 框架的参数验证 |
validate:"min=1,max=100" | 验证规则 | 常用于 validator 库 |
通过反射读取标签
import (
"fmt"
"reflect"
)
type Config struct {
Host string `json:"host" yaml:"host" env:"HOST" required:"true"`
Port int `json:"port" yaml:"port" env:"PORT" required:"true"`
}
func main() {
t := reflect.TypeOf(Config{})
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf("字段名: %s\n", field.Name)
fmt.Printf(" json tag: %s\n", field.Tag.Get("json"))
fmt.Printf(" yaml tag: %s\n", field.Tag.Get("yaml"))
fmt.Printf(" env tag: %s\n", field.Tag.Get("env"))
fmt.Printf(" required: %s\n", field.Tag.Get("required"))
}
// 字段名: Host
// json tag: host
// yaml tag: host
// env tag: HOST
// required: true
// ...
}new() 与字面量初始化的区别
type Student struct {
Name string
Age int
}
// 方式一:new()
s1 := new(Student) // 返回 *Student,所有字段为零值
// 方式二:取地址
s2 := &Student{} // 返回 *Student,所有字段为零值(与 new 等价)
// 方式三:取地址并初始化
s3 := &Student{Name: "Alice", Age: 20} // 返回 *Student,字段已初始化
// 方式四:值类型字面量
s4 := Student{Name: "Bob", Age: 21} // 返回 Student(值类型)| 方式 | 返回类型 | 字段值 | 使用场景 |
|---|---|---|---|
new(Student) | *Student | 全部零值 | 兼容旧代码,较少使用 |
&Student{} | *Student | 全部零值 | 需要指针时的常用写法 |
&Student{Name: "A"} | *Student | 按指定初始化 | 需要指针且要初始化字段 |
Student{Name: "A"} | Student | 按指定初始化 | 不需要指针时推荐 |
💡 实际开发建议:Go 社区更倾向于使用字面量初始化(&Type{...} 或 Type{...}),而不是 new(Type)。字面量更直观,可以在创建时就初始化字段。
练习题
练习 1:结构体定义与方法
定义一个 Rectangle 结构体,包含 Width 和 Height 两个字段。为其添加 Area() 和 Perimeter() 方法,分别计算面积和周长。
package main
import "fmt"
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
func main() {
r := Rectangle{Width: 10, Height: 5}
fmt.Printf("面积: %.2f\n", r.Area()) // 面积: 50.00
fmt.Printf("周长: %.2f\n", r.Perimeter()) // 周长: 30.00
}练习 2:嵌套结构体与 JSON
定义一个 Employee 结构体,嵌套 Address 结构体。使用 JSON 标签将以下 JSON 反序列化为该结构体,并打印结果:
{
"name": "Alice",
"age": 30,
"address": {
"city": "Beijing",
"country": "China"
},
"salary": 50000
}package main
import (
"encoding/json"
"fmt"
)
type Address struct {
City string `json:"city"`
Country string `json:"country"`
}
type Employee struct {
Name string `json:"name"`
Age int `json:"age"`
Address Address `json:"address"`
Salary int `json:"salary"`
}
func main() {
jsonStr := `{
"name": "Alice",
"age": 30,
"address": {
"city": "Beijing",
"country": "China"
},
"salary": 50000
}`
var emp Employee
if err := json.Unmarshal([]byte(jsonStr), &emp); err != nil {
fmt.Println("解析错误:", err)
return
}
fmt.Printf("%+v\n", emp)
// {Name:Alice Age:30 Address:{City:Beijing Country:China} Salary:50000}
// 重新序列化
data, _ := json.MarshalIndent(emp, "", " ")
fmt.Println(string(data))
}练习 3:结构体比较
编写一个程序,验证两个结构体变量的比较行为:一个所有字段都是可比较类型的结构体和一个包含 slice 字段的结构体,分别尝试用 == 比较。
package main
import "fmt"
type Comparable struct {
X int
Y int
}
type NotComparable struct {
Name string
Items []int
}
func main() {
a := Comparable{X: 1, Y: 2}
b := Comparable{X: 1, Y: 2}
c := Comparable{X: 1, Y: 3}
fmt.Println(a == b) // true —— 所有字段相等
fmt.Println(a == c) // false —— Y 字段不同
m := NotComparable{Name: "test", Items: []int{1, 2}}
n := NotComparable{Name: "test", Items: []int{1, 2}}
// m == n // 编译错误:struct containing []int cannot be compared
// 不可比较的结构体可以使用 reflect.DeepEqual
// import "reflect"
// fmt.Println(reflect.DeepEqual(m, n)) // true
}