在这里插入图片描述

📃个人主页:island1314

⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞

  • 生活总是不会一帆风顺,前进的道路也不会永远一马平川,如何面对挫折影响人生走向 – 《人民日报》


一、结构体

定义:结构体是一种用户自定义的数据类型,它可以包含多个不同类型的字段(属性 field)。结构体是 Go 语言中实现面向对象编程的主要方式。

例子

// 定义一个简单的结构体
type Person struct {
    Name string  // 姓名
    Age  int     // 年龄
    city string  // 城市
}

func main() {
    // 结构体就像现实中的对象:
    // 一个人有姓名、年龄、城市等属性
    // 一本书有标题、作者、价格等属性
    // 一辆车有品牌、型号、颜色等属性
    
    fmt.Println("Person 结构体定义了人的基本属性")
}
  • 注意:结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)

1. 结构体声明及使用

语法

type 结构体名称 struct{
    field1 type
    field2 type
}
  • 注意:不同结构体变量的字段是独立的,互不影响,值类型
创建结构体变量
  1. 直接声明:var person Person
  2. {}:var person Person = Person{}
  3. &: var person *Person = new (Person)
  4. {}: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时,必须严格遵守键值对的规则。

注意:结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在 keyvalue 之间添加 空格

例如我们为 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 等,那么可能他们的不习惯这个命名方式,怎么办?

解诀方案

  1. person 的字段首字母小写,但是这个行不通,处理后返回的是空字符串,因为 json.Marshal 相当于在其它包访问 person 结构体,而首字母小写就不能在其它包访问。
  2. 这个时候就可以使用 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】的内容,请持续关注我 !!

在这里插入图片描述

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐