【GoLang #7】:结构体学习

📃个人主页:island1314
⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞
- 生活总是不会一帆风顺,前进的道路也不会永远一马平川,如何面对挫折影响人生走向 – 《人民日报》
🔥 目录
一、结构体
定义:结构体是一种用户自定义的数据类型,它可以包含多个不同类型的字段(属性 field)。结构体是 Go 语言中实现面向对象编程的主要方式。
例子
// 定义一个简单的结构体
type Person struct {
Name string // 姓名
Age int // 年龄
city string // 城市
}
func main() {
// 结构体就像现实中的对象:
// 一个人有姓名、年龄、城市等属性
// 一本书有标题、作者、价格等属性
// 一辆车有品牌、型号、颜色等属性
fmt.Println("Person 结构体定义了人的基本属性")
}
- 注意:结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)
1. 结构体声明及使用
语法
type 结构体名称 struct{
field1 type
field2 type
}
- 注意:不同结构体变量的字段是独立的,互不影响,值类型
创建结构体变量
- 直接声明:var person Person
{}:var person Person = Person{}&: var person *Person = new (Person){}:var person *Person = &Person{}
说明:
- 第 3 种和第 4 种方式返回的是 结构体指针
- 结构体指针访问字段的标准方式应该是:
(*结构体指针).字段名,比如(*person).Name ="tom" - 但 go做了一个简化,也支持 结构体指针.字段名,比如
person.Name="tom"。更加符合程序员使用的习惯,go编译器底层对person.Name做了转化(*person).Name
示例如下:
type Person struct {
Name string
Age int
Active bool
}
func main() {
// 1. 零值初始化
var p1 Person
fmt.Printf("零值初始化: %+v\n", p1) // {Name: Age:0 Active:false}
fmt.Printf("零值初始化就打印值: %v\n", p1) // { 0 false}
// 2. 字面量初始化(指定字段名)
p2 := Person{
Name: "张三",
Age: 25,
Active: true,
}
fmt.Printf("字面量初始化: %+v\n", p2) // {Name:张三 Age:25 Active:true}
fmt.Printf("字面量初始化就打印值: %v\n", p2) // {张三 25 true}
// 3. 字面量初始化(按字段顺序)
p3 := Person{"李四", 30, false}
fmt.Printf("顺序初始化: %+v\n", p3)
// 4. 部分字段初始化
p4 := Person{
Name: "王五",
Age: 28,
// 未指定的字段使用零值
}
fmt.Printf("部分初始化: %+v\n", p4)
// 5. 使用 new 创建指针
p5 := new(Person)
p5.Name = "赵六"
p5.Age = 35
fmt.Printf("new 创建: %+v\n", *p5)
// 6. 指针初始化
var p6 *Person = new(Person)
(*p6).Name = "jack"
(*p6).Age = 40
fmt.Printf("%+v\n", p3)
// 7. &
var p7 *Person = &Person{}
(*p7).Name = "zfox"
(*p7).Age = 20
fmt.Println(p7)
}
访问和修改字段
// 点号 访问/修改
p1.Name = "Bob"
fmt.Println(p1.Age)
// 指针访问
p3 := &Person{Name: "Charlie"}
(*p3).Age = 40 // 标准方式
p3.Name = "Chuck" // 语法糖:自动解引用
其他类型结构体
匿名结构体 (不定义类型名)
type Employee struct {
ID int
Info struct { // 匿名结构体字段
Name string
Age int
Address string
}
Salary float64
}
func main() {
// 创建匿名结构体
anonymous := struct {
Title string
Price float64
}{
Title: "Go 语言编程",
Price: 59.9,
}
fmt.Printf("匿名结构体: %+v\n", anonymous)
}
嵌套结构体
type Address struct {
Street string
City string
Zip string
}
type Customer struct {
Name string
Address Address // 嵌套结构体
Phone string
}
func main() {
// 创建嵌套结构体
customer := Customer{
Name: "客户1",
Address: Address{
Street: "中山路123号",
City: "北京",
Zip: "100000",
},
Phone: "13800138000",
}
fmt.Printf("嵌套结构体: %+v\n", customer)
// 嵌套结构体访问
customer := Customer{
Name: "客户1",
Address: Address{
Street: "中山路123号",
City: "北京",
Zip: "100000",
},
Phone: "13800138000",
}
customer.Address.Street = "北京"
}
2. 结构体高级特性
2.1 嵌套和字段提示
type Contact struct {
Email string
Phone string
}
type Employee struct {
Person // 匿名嵌套(字段提升)
Contact Contact // 具名嵌套
EmployeeID int
}
// 使用
emp := Employee{
Person: Person{Name: "Dave"},
Contact: Contact{Email: "dave@company.com"},
}
fmt.Println(emp.Name) // 提升字段直接访问
fmt.Println(emp.Contact.Email) // 具名嵌套访问
2.2 结构体与 Json 序列化
JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号""包裹,使用冒号:分隔,然后紧接着值;多个键值之间使用英文,分隔。
还记得我之前说 结构体 大写 表示公有,小写 表示私有嘛,现在我来演示一下:
type Student struct {
Id int
name string
Sno string
}
func main() {
var s = Student{
Id: 12,
name: "zs",
Sno: "island",
}
fmt.Printf("%#v\n", s)
jsonByte, _ := json.Marshal(s)
jsonstr := string(jsonByte)
fmt.Printf("%v", jsonstr)
}
// 输出
main.Student{Id:12, name:"zs", Sno:"island"}
{"Id":12,"Sno":"island"}
- 上面 name 是小写字段,然后转化为 json 字符串后无输出
如果把 name 改成 Name 呢,输出就变成了如下:
main.Student{Id:12, Name:"zs", Sno:"island"}
{"Id":12,"Name":"zs","Sno":"island"}
这里就很明显看出了 大小写 的区别了,私有字段 在 Json 中无法访问
我们不仅可以把 结构体 转为 Json 字符串,同样也能把 Json 字符串转为 结构体 类型
type Student struct {
Id int
Name string
Sno string
}
func main() {
var str = `{"Id": 12, "Name": "zs", "Sno": "island"}`
var s Student
err := json.Unmarshal([]byte(str), &s)
if err != nil {
fmt.Println(err)
}
fmt.Printf("%#v", s)
}
// 输出如下:
main.Student{Id:12, Name:"zs", Sno:"island"}
2.3 结构体标签
结构体标签是附加在结构体字段上的元数据,它们为字段提供额外的信息,主要用于各种库和工具的处理
key1:"value1" key2:"value2"
- 结构体标签由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键值对之间使用一个空格分隔。 注意事项: 为结构体编写Tag时,必须严格遵守键值对的规则。
注意:结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在 key 和 value 之间添加 空格。
例如我们为 User 结构体 的每个字段定义 json 序列化时使用的Tag:
type User struct {
ID int `json:"id" db:"user_id"`
Username string `json:"username" validate:"required,min=3"`
Password string `json:"-" xml:"-"` // 忽略序列化
CreatedAt time.Time `json:"created_at" db:"created_at"`
}
// 标签解析如下:
func main() {
u := User{ID: 1, Username: "alice123"}
// JSON 序列化
jsonData, _ := json.Marshal(u)
fmt.Println(string(jsonData))
// 输出: {"id":1,"username":"alice123","created_at":"0001-01-01T00:00:00Z"}
// 使用反射读取标签
t := reflect.TypeOf(u)
field, _ := t.FieldByName("Username")
fmt.Println(field.Tag.Get("validate")) // 输出: required,min=3
}
2.4 结构体方法
方法集规则:
- 类型 T 的方法集包含所有值接收者方法
- 类型 *T 的方法集包含所有值接收者和指针接收者方法
// 值接收者方法
func (p Person) FullName() string {
return p.Name + " " + p.Email
}
// 指针接收者方法
func (p *Person) SetAge(age int) {
if age < 0 {
panic("年龄不能为负数")
}
p.Age = age
}
2.5 注意事项
1. 结构体所有字段在 内存 中是连续的
type Point struct {
x, y int
}
type Rect1 struct {
lu, rd Point
}
type Rect2 struct {
lu, rd *Point
}
func main() {
// r1 有四个 int, 在内存中连续分布
r1 := Rect1{Point{1, 2}, Point{3, 4}}
fmt.Printf("r1.lu.x 地址: %p, r1.lu.y 地址:%p, r1.rd.x 地址: %p, r1.rd.y 地址: %p\n",
&r1.lu.x, &r1.lu.y, &r1.rd.x, &r1.rd.y)
// r2 有两个 *Point 类型, 这两个 *Point 本身地址连续, 但指向地址不一定连续
r2 := Rect2{&Point{1, 2}, &Point{3, 4}}
fmt.Printf("r2.lu.x 地址: %p, r2.lu.y 地址:%p, r2.rd.x 地址: %p, r2.rd.y 地址: %p\n",
&r2.lu.x, &r2.lu.y, &r2.rd.x, &r2.rd.y)
fmt.Printf("r2.lu 地址: %p, r2.rd 地址: %p\n", &r2.lu, &r2.rd)
fmt.Printf("r2.lu 地址: %p, r2.rd 地址: %p\n", r2.lu, r2.rd)
}
// 输出
r1.lu.x 地址: 0xc000118040, r1.lu.y 地址:0xc000118048, r1.rd.x 地址: 0xc000118050, r1.rd.y 地址: 0xc000118058
r2.lu.x 地址: 0xc00010c0e0, r2.lu.y 地址:0xc00010c0e8, r2.rd.x 地址: 0xc00010c0f0, r2.rd.y 地址: 0xc00010c0f8
r2.lu 地址: 0xc00011c060, r2.rd 地址: 0xc00011c068
r2.lu 地址: 0xc00010c0e0, r2.rd 地址: 0xc00010c0f0
- 可以发现只差 8 个字节,是连续的
2. 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数、类型、顺序一致)
type A struct {
Num int
}
type B struct {
Num int
}
func main() {
var a A
var b B
a = A(b) // 可以转换 结构体字段 要完全相同
fmt.Println(a, b)
}
3. 结构体进行type重新定义(相当于取别名),Golang认为是新的数据类型,但是相互间可以强转
type Student struct {
Name string
Age int
}
type Stu Student
type integer int
func main() {
var stu1 Student
var stu2 Stu
stu2 = Stu(stu1) // 不能 stu2 = stu1
fmt.Println(stu1, stu2)
// 同理对内置数据类型也可以, 如下:
var i integer = 10
var j int = 20
j = int(i)
fmt.Println(i, j)
}
4. struct的每个字段上,可以写上一个tag,该tag可以通过反射机制获取,常见场景就是 序列化反序列化
演示:对 struct 变量进行 json 处理
type Person struct {
Name string
Age int
}
func main() {
var person Person
person.Name = "zs"
person.Age = 18
// 对其进行 json 格式的序列化处理
data, err := json.Marshal(person)
if err != nil {
fmt.Println("json encoding err:", err)
return
}
fmt.Println("json 后数据:", data)
fmt.Println("json 后数据:", string(data))
}
// 输出
json 后数据: [123 34 78 97 109 101 34 58 34 122 115 34 44 34 65 103 101 34 58 49 56 125]
json 后数据: {"Name":"zs","Age":18}
问题:json处理后的字段名也是首字母大写,这样如果我们是将json后的字符串返回给其它程序使用,比如 jquery,php 等,那么可能他们的不习惯这个命名方式,怎么办?
解诀方案
- 将
person的字段首字母小写,但是这个行不通,处理后返回的是空字符串,因为json.Marshal相当于在其它包访问person结构体,而首字母小写就不能在其它包访问。 - 这个时候就可以使用 tag 标签来解决
type Person struct {
Name string
Age int `json:"age"`
}
// 这个时候生成的如下:
json 后数据: {"Name":"zs","age":18}
3. 结构体底层机制
3.1 内存布局和对齐
在上面我们可以知道 结构体所有字段在 内存 中是连续的,现在来求如下结构体的总大小:
type MemoryExample struct {
A bool // 1字节
B int16 // 2字节
C float32 // 4字节
D int64 // 8字节
}
/*
内存布局(64位系统):
+---+----+-------+-------+
| A |pad | B | C |
+---+----+-------+-------+
| D |
+-------------------+
总大小:1(bool) + 1(填充) + 2(int16) + 4(float32) + 8(int64) = 16字节
*/
因此一般我们可以把内存布局优化一下,比如:
// 未优化(24字节)
type Unoptimized struct {
a bool
b int64
c bool
}
偏移量: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
内容: a [ 填充 ] [ b ] c [ 填充 ]
字段: a pad pad pad pad b b b b b b b b c pad pad pad pad pad pad pad
字节: 1 1 1 1 1 8 8 8 8 8 8 8 8 1 1 1 1 1 1 1 1 1
//总大小:1(bool) + 7(填充) + 8(int16) + 1(bool) + 7(填充) = 24字节
由于现代 CPU 访问内存时,对于某些数据类型有对齐要求。比如:
int64(8字节)通常需要在 8 字节边界上int32(4字节)通常需要在 4 字节边界上bool(1字节)可以在任何位置
因此我们可以采用如下的优化方式:
type Optimized struct {
b int64 // 8字节
a bool // 1字节
c bool // 1字节
}
偏移量: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
内容: [ b ] a c [ 填充 ]
字段: b b b b b b b b a c pad pad pad pad pad pad
字节: 8 8 8 8 8 8 8 8 1 1 1 1 1 1 1 1
// 填充 6 字节 - 为了整个结构体大小是最大对齐要求(8字节)的倍数
验证如下:
func main() {
fmt.Println("=== 结构体内存大小对比 ===")
fmt.Printf("Unoptimized 大小: %d 字节\n", unsafe.Sizeof(Unoptimized{})) // 24
fmt.Printf("Optimized 大小: %d 字节\n", unsafe.Sizeof(Optimized{})) // 16
// 详细分析每个字段的偏移量
fmt.Println("\n=== 字段偏移量分析 ===")
u := Unoptimized{}
fmt.Println("Unoptimized 结构体:")
fmt.Printf(" a 的偏移量: %d\n", unsafe.Offsetof(u.a)) // 0
fmt.Printf(" b 的偏移量: %d\n", unsafe.Offsetof(u.b)) // 8
fmt.Printf(" c 的偏移量: %d\n", unsafe.Offsetof(u.c)) // 16
o := Optimized{}
fmt.Println("Optimized 结构体:")
fmt.Printf(" b 的偏移量: %d\n", unsafe.Offsetof(o.b)) // 0
fmt.Printf(" a 的偏移量: %d\n", unsafe.Offsetof(o.a)) // 8
fmt.Printf(" c 的偏移量: %d\n", unsafe.Offsetof(o.c)) // 9
}
3.2 结构体比较
type Point struct {
X, Y int
}
p1 := Point{1, 2}
p2 := Point{1, 2}
p3 := Point{2, 3}
fmt.Println(p1 == p2) // true
fmt.Println(p1 == p3) // false
不可比较的情况:
- 包含不可比较字段(如切片、映射、函数)
- 字段包含不可比较类型的嵌套结构体
3.3 空结构体
var s struct{}
-
大小为 0 字节:
fmt.Println(unsafe.Sizeof(struct{}{})) // 输出: 0 -
没有字段,不能存储任何数据
-
可比较(所有空结构体实例都相等)
核心特性
| 特性 | 说明 |
|---|---|
| 零内存占用 | struct{}实例不占用堆或栈空间 |
| 可寻址 | 可以取地址(但地址可能相同) |
| 可比较 | struct{}{} == struct{}{}为true |
| 可作为 map 的 value | 用于模拟集合(set) |
| 可作为 channel 的元素 | 用于信号通知(不传数据) |
应用场景
场景 1:实现 Set(集合)
在 Go 中没有内置的 Set 类型,常用 map[T]struct{} 模拟:
// 创建一个字符串集合
set := make(map[string]struct{})
// 添加元素
set["apple"] = struct{}{}
set["banana"] = struct{}{}
// 检查是否存在
if _, exists := set["apple"]; exists {
fmt.Println("apple is in set")
}
// 删除元素
delete(set, "banana")
为什么用 struct{} 而不用 bool?
map[string]bool会为每个 value 分配 1 字节map[string]struct{}的 value 占用 0 字节,更省内存
💡 这是 Go 社区的标准做法!
场景 2:信号通知 Channel
当 channel 只用于“通知”而不传递数据时,用 chan struct{}:
done := make(chan struct{})
go func() {
// 做一些工作...
time.Sleep(2 * time.Second)
close(done) // 或 done <- struct{}{}
}()
<-done // 等待通知
fmt.Println("Work done!")
优势:
- 内存开销最小(
struct{}不占空间) - 语义清晰:只表示“事件发生”,不传数据
对比:
chan bool需要传true/false,而struct{}只需struct{}{}或close
场景 3:方法接收者(不需状态)
当一个类型只需要方法,不需要存储状态时:
type Logger struct{}
func (Logger) Info(msg string) {
fmt.Println("INFO:", msg)
}
func (Logger) Error(msg string) {
fmt.Println("ERROR:", msg)
}
// 使用
var log Logger
log.Info("Hello") // 无需初始化字段
场景 4:占位符 / 标记类型
用于泛型或接口实现中作为“无状态”占位:
type Cache[K comparable, V any] struct {
data map[K]V
// 用 struct{} 作为锁的占位(实际用 sync.Mutex 更好,此处仅为示例)
_ struct{} // 对齐或占位
}
常见误区
❌ 误区 1:空结构体指针地址都相同?
a := &struct{}{}
b := &struct{}{}
fmt.Println(a == b) // 可能 true,也可能 false(Go 优化)
- Go 编译器可能将所有空结构体指针指向同一个地址(因为内容无区别)
- 不要依赖此行为!地址比较无意义
❌ 误区 2:空结构体 slice 会占用大量内存?
s := make([]struct{}, 1000000)
fmt.Println(len(s), cap(s)) // 长度 100 万,但内存几乎为 0
- slice 的 底层数组不占数据内存(只有 len/cap 的元信息)
- 但 slice 本身仍有 24 字节(ptr + len + cap)
【★,°:.☆( ̄▽ ̄)/$:.°★ 】那么本篇到此就结束啦,如果有不懂 和 发现问题的小伙伴可以在评论区说出来哦,同时我还会继续更新关于【GoLang】的内容,请持续关注我 !!

更多推荐



所有评论(0)