14 - Go 结构体(struct):从基础到高级实战

文章目录


🚀 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 的"对象模型"。


相关推荐
ShineWinsu1 小时前
百度搜索算法逆向思考的技术文章
开发语言
lhbian1 小时前
C# vs 汇编:编程世界的两极对比
开发语言·汇编·c#
handler011 小时前
Linux基础知识(1)
linux·服务器·c语言·开发语言·数据结构·c++
Rsun045511 小时前
12、Java 享元模式从入门到实战
java·开发语言·享元模式
枫叶丹41 小时前
【HarmonyOS 6.0】ArkWeb:Web组件销毁模式深度解析
开发语言·前端·华为·harmonyos
良木生香1 小时前
【C++ 初阶】:内存管理的迭代革新——从malloc/free 到 new/delete 的时代更迭
c语言·开发语言·c++
傻啦嘿哟1 小时前
使用 Python 管理 Word 节及页面布局设置
开发语言·python·word
XGeFei2 小时前
__init__ 初始化方法
开发语言·python
Rust研习社2 小时前
Rust 并发同步:Mutex 与 RwLock 智能指针
开发语言·后端·rust