文章目录
零、概述
特性 | 说明 |
---|---|
字段可见性 | 首字母大小写控制包访问权限 |
方法接收者 | 值接收者(操作副本) vs 指针接收者(操作原始实例) |
组合嵌套 | 命名嵌套(显式字段) vs 匿名嵌入(字段提升) |
内存对齐 | 字段顺序影响内存占用,编译器自动处理对齐 |
JSON序列化 | 通过json 标签自定义字段名和序列化规则 |
注意:
- 字段可见性控制
- 公共结构体(首字母大写)的字段若需对外暴露,设为公共字段(首字母大写)。
- 私有字段(首字母小写)通过公共方法访问,实现封装。
- 组合优于继承
利用匿名嵌入实现代码复用,避免继承带来的复杂性。
type Animal struct {
Name string
}
type Dog struct {
Animal // 嵌入Animal结构体,继承Name字段
Breed string
}
- 指针接收者的一致性
若结构体方法使用指针接收者,建议所有方法均使用指针接收者,避免值类型和指针类型实例的行为差异。
一、结构体基础:复合数据类型的基础
Go语言中的结构体(struct
)是一种用户自定义复合类型,用于将不同类型的数据组合成一个逻辑整体。它类似其他语言中的“类”,但更灵活,支持面向对象编程中的组合、方法绑定等特性,且不依赖继承机制。
1、定义语法
type 结构体名 struct {
字段名1 字段类型1
字段名2 字段类型2
...
}
- 字段命名规则:
- 首字母大写:公共字段(可跨包访问)。
- 首字母小写:私有字段(仅限包内访问)。
- 字段类型:支持内置类型(如
int
、string
)、指针、切片、接口、其他结构体等。
示例:定义人物结构体
type Person struct {
Name string // 姓名(公共字段)
age int // 年龄(私有字段,包外不可见)
Hobbies []string // 爱好(切片类型)
}
2、实例化与初始化
声明实例
var p Person // 声明实例,字段为默认零值(Name="",age=0,Hobbies=nil)
初始化方式
(1)字段名显式赋值
p := Person{
Name: "Alice",
age: 30, // 包内可访问私有字段
Hobbies: []string{"reading", "music"},
}
(2)按顺序赋值(需提供全部字段)
p := Person{"Bob", 25, []string{"sports"}} // 与字段声明顺序一致
(3)创建指针实例
p := &Person{Name: "Charlie"} // 等价于 new(Person) 并赋值
(4)匿名结构体(临时定义,无需命名)
// 直接声明并初始化一个匿名结构体实例
data := struct {
ID int
Name string
}{
ID: 1,
Name: "Temp",
}
3、字段访问与修改
访问字段
- 值类型实例:通过
.
操作符访问。p := Person{Name: "Alice"} fmt.Println(p.Name) // 输出:Alice
- 指针类型实例:自动解引用,直接通过
.
访问(无需*
)。p := &Person{Name: "Bob"} fmt.Println(p.Name) // 等价于 (*p).Name,输出:Bob
修改字段
p := Person{Name: "Alice"}
p.Name = "Bob" // 修改公共字段
// p.age = 30 // 私有字段不可直接修改(包外报错)
二、结构体方法:接收者参数 给结构体绑定方法
在 Go 语言中,接收者参数是用于将函数与结构体绑定的特殊参数,使得该函数成为类型的方法(Method)。
接收者参数是 Go 实现面向对象编程(OOP)中 “方法绑定” 的核心机制。
type 结构体名 struct { ... }
// 值接收者方法(操作副本,不影响原始实例)
func (r 结构体名) 方法名(参数列表) 返回值 { ... }
// 指针接收者方法(操作原始实例,可修改字段)
func (r *结构体名) 方法名(参数列表) 返回值 { ... }
示例:值接收者(!!!实例副本传递) vs 指针接收者
package main
type Counter struct {
Value int
}
// 值接收者:修改的是副本
func (c Counter) IncrementByValue(n int) {
c.Value += n // 原始实例不受影响
}
// 指针接收者:修改原始实例
func (c *Counter) IncrementByPointer(n int) {
c.Value += n // 直接修改原始值
}
func main() {
c := Counter{Value: 10}
c.IncrementByValue(5) // 副本修改,c.Value仍为10
println(c.Value)
c.IncrementByPointer(5) // 原始修改,c.Value变为15
println(c.Value)
}
选择接收者类型的原则:
场景 | 值接收者 | 指针接收者 |
---|---|---|
结构体体积小 | 推荐(如包含几个基础类型字段) | 可选,但无必要 |
结构体体积大 | 不推荐(拷贝开销大) | 推荐(避免内存拷贝) |
需要修改原始数据 | 不可行 | 必须使用 |
接口实现一致性 | 若使用指针接收者方法,实例需为指针 | 值类型和指针类型均实现接口 |
三、结构体组合:实现代码复用
Go语言通过组合实现代码复用,而非传统的继承。结构体可嵌套其他结构体,分为命名嵌套和匿名嵌套(嵌入)。
1. 命名嵌套(显式字段名)
package main
import "fmt"
type Address struct {
City string
State string
}
type Person struct {
Name string
Age int
Home Address // 命名嵌套:字段名为Home,类型为Address
}
func main() {
// 访问嵌套字段
p := Person{
Name: "Alice",
Home: Address{City: "Shanghai", State: "CN"},
}
fmt.Println(p.Home.City) // 输出:Shanghai
}
2. 匿名嵌套:直接声明为字段(无字段名)
将结构体类型直接声明为字段(无字段名),嵌套的字段会被提升为外层结构体的字段。
type Address struct {
City string
State string
}
type Person struct {
Name string
Age int
Address // 匿名嵌入:Address的字段提升为Person的字段
}
// 直接访问嵌入字段
p := Person{
Name: "Bob",
City: "Beijing", // 等价于 p.Address.City
State: "CN", // 等价于 p.Address.State
}
3. 冲突处理
若嵌入结构体与外层结构体有同名字段,外层字段会覆盖内层字段(就近原则)。
type A struct {
X int
}
type B struct {
A // 嵌入A
X string // 与A.X冲突,B.X覆盖A.X
}
func main() {
b := B{A: A{X: 100}, X: "hello"}
fmt.Println(b.X) // 输出:hello(外层字段)
fmt.Println(b.A.X) // 输出:100(通过嵌入结构体访问内层字段)
}
四、结构体的内存对齐
Go编译器会自动对结构体字段进行内存对齐,以提高内存访问效率。对齐规则基于字段类型的大小和对齐标签(unsafe.Alignof
)。
示例:字段顺序影响内存占用
import "unsafe"
type A struct {
a bool // 1字节,对齐边界1,地址任意
b int32 // 4字节,对齐边界4,地址是4的倍数
c string // 8字节,对齐边界8,地址是8的倍数
}
type B struct {
b int32 // 4字节,对齐边界4
c string // 8字节,对齐边界8
a bool // 1字节,对齐边界1
}
func main() {
fmt.Println(unsafe.Sizeof(A{})) // 16字节(1+3填充+4+8)
fmt.Println(unsafe.Sizeof(B{})) // 24字节(4+8+8+4填充)
}
优化建议:
- 将相同或相近大小的字段放在一起,减少填充字节。
- 按需使用
unsafe
包(谨慎使用,破坏类型安全)。
五、结构体的拷贝与比较
1. 值拷贝
结构体是值类型,赋值或传参会完整拷贝所有字段(包括嵌套结构体)。
p1 := Person{Name: "Alice"}
p2 := p1 // 拷贝p1的副本
p2.Name = "Bob" // 不影响p1的Name
2. 深度拷贝
若结构体包含指针、切片等引用类型字段,需手动拷贝其指向的数据(值拷贝仅复制指针地址)。
type Data struct {
List []int // 切片类型,值拷贝后新旧实例共享底层数组
}
func deepCopy(d *Data) Data {
newList := make([]int, len(d.List))
copy(newList, d.List) // 手动拷贝切片数据
return Data{List: newList}
}
六、结构体与JSON序列化
Go结构体可通过encoding/json
包轻松实现JSON序列化与反序列化,需注意字段可见性和标签(json
tag)。
示例:
package main
import (
"encoding/json"
"fmt"
)
type User struct {
ID int `json:"user_id"` // 标签:指定JSON字段名
Name string `json:"user_name"`
Age int `json:"age,omitempty"` // omitempty:字段为零值时不输出
}
func main() {
// 序列化(结构体→JSON)
u := User{ID: 1, Name: "Bob"}
jsonData, _ := json.Marshal(u)
fmt.Println(string(jsonData)) // 输出:{"user_id":1,"user_name":"Bob"}
// 反序列化(JSON→结构体)
var u2 User
json.Unmarshal(jsonData, &u2)
fmt.Println(u2.Name) // 输出:Bob
}
七、结构体对象
1. Car{}
:实例化对象
- 本质:它是
Car
结构体的一个实例,属于值类型。 - 示例:
type Car struct { Brand string Price int } car := Car{Brand: "Tesla", Price: 80000}
- 特点:
- 直接在内存里创建一个
Car
对象。 - 对
car
的字段进行修改时,不会影响到其他对象。
- 直接在内存里创建一个
2. &Car{}
:实例化对象的指针
- 本质:这是指向
Car
结构体实例的指针,也就是引用类型。 - 示例:
carPtr := &Car{Brand: "Tesla", Price: 80000}
- 特点:
- 先创建一个
Car
对象,接着返回该对象的内存地址。 - 通过
carPtr
对字段进行修改,会影响到原对象。 - 与手动创建对象再取地址的效果是一样的:
car := Car{Brand: "Tesla", Price: 80000} carPtr := &car
- 先创建一个
3. *Car
:指针类型
本质:它仅仅是一个指针类型的声明,并非具体的对象。
var carPtr *Car // 声明一个指向 Car 的指针,此时值为 nil
特点:
- 可用于声明函数参数、返回值或者结构体字段等。
- 在使用之前,必须先赋值,否则会出现空指针异常。
- 赋值方式有以下几种:
// 方式一:通过 &Car{} 赋值 carPtr = &Car{Brand: "Tesla", Price: 80000} // 方式二:先创建对象,再取地址 car := Car{} carPtr = &car // 方式三:使用 new 函数 carPtr = new(Car) // 等同于 &Car{}