文章目录
- [🚀 14 - Go 结构体(struct):从基础到高级实战](#🚀 14 - Go 结构体(struct):从基础到高级实战)
- [什么是 struct?](#什么是 struct?)
- 基础定义与使用
-
- 定义结构体
- 创建结构体
-
- 按顺序初始化(不推荐)
- 指定字段(推荐)
- [new 关键字](#new 关键字)
- 访问结构体字段
- [struct 本质(重点)](#struct 本质(重点))
-
- [🔥 struct 是值类型!](#🔥 struct 是值类型!)
- [🚨 如何避免拷贝?](#🚨 如何避免拷贝?)
- 结构体指针(非常重要)
- 结构体方法(面向对象核心)
-
- [值接收者 vs 指针接收者](#值接收者 vs 指针接收者)
-
- [✅ 值接收者(不会修改原数据)](#✅ 值接收者(不会修改原数据))
- [✅ 指针接收者(可以修改原数据)](#✅ 指针接收者(可以修改原数据))
- 结构体嵌套(组合)
- [结构体标签(JSON 核心)](#结构体标签(JSON 核心))
-
- [解析 JSON](#解析 JSON)
- [struct 零值(隐藏坑)](#struct 零值(隐藏坑))
- 结构体比较(容易踩坑)
-
- [❌ 不能比较的情况](#❌ 不能比较的情况)
- [✅ 可以比较](#✅ 可以比较)
- [struct 与 map 对比](#struct 与 map 对比)
- 内存对齐(进阶)
- 最佳实践总结
- [🎯 总结](#🎯 总结)
🚀 14 - Go 结构体(struct):从基础到高级实战
在 Go 语言中,结构体(struct)是最核心的数据结构之一,它不仅承担着"对象"的角色,更是 Go 面向对象编程思想的基础。
本文将带你从底层认知到高级用法,彻底掌握 struct。
什么是 struct?
简单来说:
👉 struct 是一组字段(field)的集合
👉 用于表示一个"对象"或"实体"
📌 类似于其他语言:
| 语言 | 对应概念 |
|---|---|
| C | struct |
| Java | class |
| Python | class / dict |
基础定义与使用
定义结构体
go
type User struct {
Name string // 用户姓名
Age int // 用户年龄
}
User 表示用户实体,包含姓名和年龄两个字段。
创建结构体
按顺序初始化(不推荐)
go
package main
import "fmt"
type User struct {
Name string
Age int
}
func main() {
u := User{"John", 30}
fmt.Println(u) // 输出:{John 30}
}
👉 缺点:可读性差,字段顺序必须严格一致
指定字段(推荐)
go
package main
import "fmt"
type User struct {
Name string
Age int
}
func main() {
u := User{
Name: "John",
Age: 30,
}
fmt.Println(u) // 输出:{John 30}
}
不需要按照顺序,直白简洁
new 关键字
增加一个知识点
结构体地址:它是存储结构体数据的内存地址。
go
package main
import "fmt"
type User struct {
Name string
Age int
}
func main() {
// 创建一个User结构体指针
u := new(User)
// 创建一个User结构体实例,并初始化其字段
u2 := &User{}
// 通过指针访问结构体字段并赋值
u.Name = "John"
u.Age = 30
// u 是 *User,表示结构体地址
fmt.Println("u结构体地址:", u) // &{John 30}
// *u 是结构体本身
fmt.Println("*u结构体本身:", *u) // {John 30}
// &u 是指针变量 u 的地址(类型是 **User)
fmt.Println("&u指针变量地址:", &u) // 例如:0xc00000e028
fmt.Println("u2:", u2) // &{}
// 通过指针访问结构体字段并赋值
(*u2).Name = "John"
fmt.Println("*u2:", *u2) // {John 0}
}
输出:
bath
u结构体地址: &{John 30}
*u结构体本身: {John 30}
&u指针变量地址: 0xc00004a040
u2: &{ 0}
*u2: {John 0}
👉 返回的是指针:*User
访问结构体字段
go
package main
import "fmt"
type User struct {
Name string
Age int
}
func main() {
u := User{
Name: "John",
Age: 30,
}
fmt.Println("输出结构体", u)
fmt.Println(u.Name)
fmt.Println(u.Age)
}
输出:
bath
输出结构体 {John 30}
John
30
struct 本质(重点)
🔥 struct 是值类型!
go
package main
import "fmt"
type User struct {
Name string
Age int
}
func main() {
u1 := User{
Name: "John",
Age: 30,
}
u2 := u1
u2.Name = "Mike"
fmt.Println("输出结构体u1", u1)
fmt.Println("输出结构体u2", u2)
fmt.Println(u1.Name) // 因为u2是u1的拷贝,修改了u2.Name并不会影响u1
fmt.Println(u1.Age)
}
输出:
bath
输出结构体u1 {John 30}
输出结构体u2 {Mike 30}
John
30
👉 说明:
- struct 赋值是 值拷贝
- 修改副本不会影响原对象
🚨 如何避免拷贝?
使用指针:
go
package main
import "fmt"
type User struct {
Name string
Age int
}
// 不使用指针,每次调用都会拷贝对象
func update(u1 User) {
fmt.Println("one1:", u1)
u1.Name = "John1"
u1.Age = 1
fmt.Println("two1:", u1)
}
// 使用指针避免了拷贝,直接修改原对象
func update2(u2 *User) {
fmt.Println("one2:", u2)
u2.Name = "John2"
u2.Age = 2
fmt.Println("two2:", u2)
}
func update3(u *User) {
fmt.Println("one3:", u)
u = &User{Name: "Jack"}
fmt.Println("two3:", u)
}
func main() {
u := User{
Name: "Alice",
Age: 25,
}
update(u)
fmt.Println("update:", u.Name, u.Age)
fmt.Println("-----")
update2(&u)
fmt.Println("update2:", u.Name, u.Age)
fmt.Println("-----")
// 这里的u没有被修改,因为update3中重新赋值了u
fmt.Println("数据:", u)
update3(&u)
fmt.Println("update3:", u.Name, u.Age) // 这里的u没有被修改,因为update3中重新赋值了u ,所以这里的输出是原来的值
}
输出:
bath
one1: {Alice 25}
two1: {John1 1}
update: Alice 25
-----
one2: &{Alice 25}
two2: &{John2 2}
update2: John2 2
-----
数据: {John2 2}
one3: &{John2 2}
two3: &{Jack 0}
update3: John2 2
👉 这里其实发生了拷贝!
但:
- 拷贝的是:地址
- 不是:结构体数据
👉 Go 中:
指针拷贝的是"地址",多个指针可以指向同一块内存
结构体指针(非常重要)
go
package main
import "fmt"
type User struct {
Name string
Age int
}
func main() {
u := &User{
Name: "Alice",
Age: 25,
}
fmt.Println(u)
fmt.Println(u.Name)
}
输出:
bath
&{Alice 25}
Alice
👉 Go 语法糖:
在 Go 中,访问结构体指针的字段或方法时,编译器会自动进行解引用,因此 u.Name 等价于 (*u).Name
go
(*u).Name == u.Name
结构体方法(面向对象核心)
Go 方法的标准定义格式是:
go
func (接收者) 方法名(参数) 返回值 {
函数体
}
go
func (u User) SayHello() {
fmt.Println("Hello,", u.Name)
}
SayHello 是定义在 User 类型上的方法,其中:
u是接收者(receiver)User表示该方法属于 User 类型- 方法内部可以访问结构体字段
| 部分 | 含义 |
|---|---|
func |
声明函数 |
(u User) |
接收者(receiver) |
SayHello |
方法名 |
() |
参数 |
{} |
方法体 |
go
package main
import "fmt"
type User struct {
Name string
Age int
}
// 定义方法
func (u User) SayHello() {
fmt.Println("Hello, my name is", u.Name)
}
func main() {
u := User{
Name: "John",
Age: 30,
}
fmt.Println(u)
// 调用方法
u.SayHello()
}
输出:
bath
{John 30}
Hello, my name is John
值接收者 vs 指针接收者
✅ 值接收者(不会修改原数据)
go
package main
import "fmt"
type User struct {
Name string
Age int
}
func (u User) SetName(name string) {
fmt.Println("Setting name to:", name)
u.Name = name
fmt.Println("Name set to:", u.Name)
}
func main() {
u := User{
Name: "John",
Age: 30,
}
fmt.Println(u)
u.SetName("Jane")
fmt.Println("结果:", u)
}
输出:
bath
{John 30}
Setting name to: Jane
Name set to: Jane
结果: {John 30}
✅ 指针接收者(可以修改原数据)
go
package main
import "fmt"
type User struct {
Name string
Age int
}
// 看这里!!!!!!!!!!!!!!
func (u *User) SetName(name string) {
fmt.Println("Setting name to:", name)
u.Name = name
fmt.Println("Name set to:", u.Name)
}
func main() {
u := User{
Name: "John",
Age: 30,
}
fmt.Println(u)
u.SetName("Jane")
fmt.Println("结果:", u)
}
输出:
bath
{John 30}
Setting name to: Jane
Name set to: Jane
结果: {Jane 30}
📌 面试重点:
| 类型 | 是否修改原值 | 性能 |
|---|---|---|
| 值接收者 | ❌ | 可能拷贝 |
| 指针接收者 | ✅ | 更高效 |
值接收者会复制整个结构体,因此无法修改原值且可能有较大开销;指针接收者只复制地址,能够直接操作原始数据且性能更高。
👉 实战建议:
如果 struct 较大 或 需要修改数据 → 一律用指针接收者
👉 不是所有情况都用指针!
✅ 推荐用值接收者:
- struct 很小(比如 2~3 个字段)
- 明确不希望被修改(类似"只读")
- 类型类似
time.Time这种值语义
✅ 推荐用指针接收者:
- 需要修改数据 ✅
- struct 很大 ✅
- 避免拷贝开销 ✅
- 保持方法一致性 ✅(重要)
结构体嵌套(组合)
Go 没有继承,但有更强大的:
👉 组合(Composition)
嵌套结构体
嵌套结构体,即一个结构体内包含另一个结构体的实例
嵌套结构体可以简化代码,使得数据更加清晰
例如:用户信息中包含地址信息
普通嵌套
go
package main
import "fmt"
// Address 结构体定义
type Address struct {
City string
}
// User 结构体中包含 Address 类型字段
type User struct {
Name string
Address Address
}
func main() {
u := User{
Name: "John",
}
// 初始化 User 结构体时,Address 字段为零值
fmt.Println("one:", u)
u.Address = Address{
City: "New York",
}
// 初始化 Address 后,User 的 Address 字段不再是零值
fmt.Println("two:", u)
}
输出:
bath
one: {John {}}
two: {John {New York}}
匿名嵌套(推荐)
go
package main
import "fmt"
// 匿名结构体
type Address struct {
City string
}
// 匿名字段,匿名结构体,可以直接访问匿名结构体的字段
type User struct {
Name string
Address
}
func main() {
user := User{
Name: "张三",
Address: Address{
City: "北京",
},
}
fmt.Println(user)
// 直接访问匿名结构体的字段
fmt.Println(user.City)
}
输出:
bath
{张三 {北京}}
北京
等价理解:
go
user.City
≈
user.Address.City
Go 的匿名字段是一种结构体嵌套方式,它会将内部结构体的字段"提升"到外层,使得访问更加简洁,同时也是 Go 实现组合(替代继承)的核心机制。
结构体标签(JSON 核心)
go
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
解析 JSON
go
package main
import (
"encoding/json"
"fmt"
)
type User struct {
Name string
Age int
}
func main() {
data := `{"name":"张三","age":18}` // 字符串转json
// 解析json字符串到结构体
var u User
// 解析json字符串到结构体,需要传入[]byte类型的数据
json.Unmarshal([]byte(data), &u)
fmt.Println(u.Name, u.Age)
// 结构体转json字符串
data2, _ := json.Marshal(u)
fmt.Println(string(data2))
}
输出:
bath
张三 18
{"Name":"张三","Age":18}
📌 标签作用:
- 控制 JSON 字段名
- 控制序列化/反序列化
- ORM 映射(gorm)
struct 零值(隐藏坑)
go
var u User
fmt.Println(u.Name) // ""
fmt.Println(u.Age) // 0
👉 struct 所有字段都有默认值!
结构体比较(容易踩坑)
❌ 不能比较的情况
go
type User struct {
Name string
Tags []string
}
👉 报错:
invalid operation: struct containing slice cannot be compared
✅ 可以比较
go
type User struct {
Name string
Age int
}
👉 因为字段都是可比较类型
struct 与 map 对比
| 特性 | struct | map |
|---|---|---|
| 类型安全 | ✅ | ❌ |
| 性能 | 高 | 较低 |
| 灵活性 | 低 | 高 |
| 编译期检查 | ✅ | ❌ |
👉 实战建议:
- 固定结构 → struct
- 动态数据 → map
内存对齐(进阶)
👉 CPU 访问内存时,有一个特点:
按"对齐边界"读取会更快
比如:
int64(8字节) → 希望放在 8 的倍数地址int32(4字节) → 希望放在 4 的倍数地址
👉 如果不对齐:
- CPU 需要拆成多次读取 ❌
- 性能下降 ❌
👉 每个字段都有一个"对齐要求",通常是:
字段大小 = 对齐值(简化理解)
| 类型 | 大小 | 对齐 |
|---|---|---|
| bool | 1 | 1 |
| int32 | 4 | 4 |
| int64 | 8 | 8 |
go
type Example struct {
A bool // 1字节
B int64 // 8字节
C bool // 1字节
}
A: 1 byte
padding: 7 byte ← 为了让 B 对齐到 8
B: 8 byte
C: 1 byte
padding: 7 byte ← struct 要对齐到 8
👉 总大小:
1 + 7 + 8 + 1 + 7 = 24 bytes
👉 实际内存会对齐填充
优化写法:
go
type Example struct {
B int64
A bool
C bool
}
B: 8 byte
A: 1 byte
C: 1 byte
padding: 6 byte
👉 总大小:
8 + 1 + 1 + 6 = 16 bytes
📊 对比结果
| 写法 | 内存 |
|---|---|
| 原始 | 24 字节 ❌ |
| 优化 | 16 字节 ✅ |
📌 好处:
- 减少内存浪费
- 提升性能
大字段放前面,可以减少填充(padding)浪费
👉 struct 排序口诀:
"大字段在前,小字段在后"
✅ 推荐顺序
go
int64 → int32 → int16 → int8 → bool
验证
go
package main
import (
"fmt"
"unsafe"
)
type Example struct {
B int64
A bool
C bool
}
type Example2 struct {
A bool
B int64
C bool
}
func main() {
fmt.Println(unsafe.Sizeof(Example{})) // 输出: 16
fmt.Println(unsafe.Sizeof(Example2{})) // 输出:24
}
最佳实践总结
✅ 优先使用 struct 表达数据模型
✅ 方法尽量使用指针接收者
✅ 使用组合代替继承
✅ 合理使用 tag(json / db)
✅ 注意零值和拷贝问题
✅ 关注内存对齐(高性能场景)
🎯 总结
struct 是 Go 的核心:
👉 数据建模基础
👉 面向对象核心
👉 高性能关键
掌握 struct,本质上就是掌握 Go 的"对象模型"。