Skip to content

什么是结构体

在 Go 语言中, 并没有类/对象/继承等经典面向对象语言概念, 如: class extends 等关键字都是没有的

结构体类似于C语言中的结构体, 用于描述一组数据的结构, 将这一组数据看做一个整体, 一个自定义的数据类型

虽然没有经典的 calss 等关键字, 但是使用 struct 可以模拟面向对象的部分特性(封装 继承 多态)

定义结构体

type 结构体名称 struct { 结构体字段 结构体字段的数据类型 }

go
package main

import "fmt"

// 定义一个描述学生数据的结构体
// 将这一组数据看做一个整体(一个独立的类型)
type Student struct {
	id      string // 学号
	name    string // 姓名
	age     int    // 年龄
	subject string // 专业
}

func main() {
	// 初始化结构体
	zs := Student{
		id:      "1001",
		name:    "张三",
		age:     18,
		subject: "计算机网络与应用",
	}

	// 值:{1001 张三 18 计算机网络与应用}    类型:main.Student
	fmt.Printf("值:%v \t 类型:%T \n", zs, zs)
}

初始化结构体

  1. 直接初始化结构体并赋值
  2. 初始化多个结构体并赋值
  3. 使用 new 方法初始化结构体
go
package main

import "fmt"

type Student struct {
	id      string // 学号
	name    string // 姓名
	age     int    // 年龄
	subject string // 专业
}

func main() {
	// 1.直接初始化结构体并赋值
	zs := Student{
		id:      "1001",
		name:    "张三",
		age:     18,
		subject: "计算机网络与应用",
	}

	// 值:{1001 张三 18 计算机网络与应用}    类型:main.Student
	fmt.Printf("值:%v \t 类型:%T \n", zs, zs)

	// 2.初始化多个结构体
	studentList := []Student{
		{
			id:      "1002",
			name:    "李四",
			age:     19,
			subject: "计算机网络与应用",
		},
		{
			id:      "1003",
			name:    "王五",
			age:     20,
			subject: "计算机网络与应用",
		},
	}
	// 值:[{1002 李四 19 计算机网络与应用} {1003 王五 20 计算机网络与应用}]
	// 类型:[]main.Student
	fmt.Printf("值:%v \t 类型:%T \n", studentList, studentList)

	// 3.使用 new 方法初始化结构体
	// 返回的是一个指针,指向一个初始化后的结构体
	z6 := new(Student)
	z6.id = "1004"
	z6.name = "赵六"
	z6.age = 21
	z6.subject = "计算机网络"
	// 值:&{1004 赵六 21 计算机网络}    类型:*main.Student
	fmt.Printf("值:%v \t 类型:%T \n", z6, z6)
}

结构体的存储原理

  • unsafe.Sizeof 可以获取一个结构体的占用空间大小
go
package main

import (
	"fmt"
	"unsafe"
)

type A struct {
	a int8  // 1 byte
	b bool  // 1 byte
	c int32 // 4 byte
}

type B struct {
	a int8  // 1 byte
	b int32 // 4 byte
	c bool  // 1 byte
}

func main() {
	a := A{1, true, 2}
	fmt.Println("a 占用内存大小:", unsafe.Sizeof(a)) // 8

	b := B{1, 2, true}
	fmt.Println("b 占用内存大小:", unsafe.Sizeof(b)) // 12
}

😡

为什么同样的结构体, 仅仅是字段名和类型的顺序不同, 占用内存大小就不一样了?

  1. 开辟 4 个字节的内存空间, 如果字段能够存下则直接存
  2. 如果存不下, 那么就在再次辟 4 个字节的内存空间

memory-analysis

🤔

为什么固定开辟4个直接内存空间, 而不是根据数据类型动态的开辟刚刚好所需要的内存空间

为了内存对齐

🤔

为什么需要内存对齐?

  1. 性能提升: 现代CPU将内存视为块(例如 4、8、16字节)访问, 访问对齐数据只需一次内存操作, 访问未对齐数据可能需要两次, 并进行额外处理
  2. 硬件要求: 某些硬件平台(如ARM)强制要求数据必须对齐, 否则会引发硬件异常, 导致程序崩溃

嵌套的结构体

go
package main

import "fmt"

type Address struct {
	province string // 省份
	city     string // 城市
	area     string // 区域
	detail   string // 街道楼层门牌号详情
}

type LogisticsInfo struct {
	orderId     int     // 订单id
	productName string  // 商品名称
	address     Address // 收件人地址信息
}

func main() {
	info := LogisticsInfo{
		orderId:     1001,
		productName: "HUAWEI Meta80 Pro",
		address: Address{
			province: "湖南省",
			city:     "衡阳市",
			area:     "蒸湘区",
			detail:   "解放路幸福小区5栋2单元2801室",
		},
	}
	// 值:{1001 HUAWEI Meta80 Pro {湖南省 衡阳市 蒸湘区 解放路幸福小区5栋2单元2801室}}
	// 类型:main.LogisticsInfo
	fmt.Printf("值:%v \n类型:%T \n", info, info)
}

给结构体绑定方法

注意点

由于在Go语言中, 默认情况下 struct 是值传递, 要改变结构体的值必须引用传递(也就是必须传递指针)

go
package main

import "fmt"

type Student struct {
	id   int
	name string
	age  int
}

// 获取值: 使用值/引用都可以, 因为都可以获取到
// 虽然可以可以通过值来获取值, 但为了风格统一
// 还是推荐使用: 引用传递的方式来获取值
func (s Student) getId() int {
	return s.id
}

func (s *Student) getName() string {
	// 推荐
	return s.name
}

// 修改值: 必须使用引用传递
func (s Student) setName1(name string) {
	// 错误示范
	s.name = name
}

func (s *Student) setName(name string) {
	s.name = name
}

func main() {
	zs := Student{id: 1001, name: "张三", age: 18}
	//// 读 ///
	id := zs.getId()
	fmt.Printf("id:%d\n", id)

	name := zs.getName()
	fmt.Printf("name:%s\n", name)

	//// 写 ///

	// 并没有改变, 因为是值传递, 函数修改的是"副本"
	zs.setName1("张仨")
	fmt.Printf("数据:%v  类型:%T\n", zs, zs)

	// name字段被修改, 因为是引用传递, 函数修改的是"原件"
	zs.setName("张3")
	fmt.Printf("数据:%v  类型:%T\n", zs, zs)
}

泛型结构体

与泛型函数类似, 在定义时并不确定类型的具体类型, 只有在初始化时才确定字段的具体类型

go
package main

import "fmt"

// 栈 数据结构
type Stack[T any] struct {
	elements []T
}

func (s *Stack[T]) Size() int {
	return len(s.elements)
}

func (s *Stack[T]) Push(value T) {
	s.elements = append(s.elements, value)
}

func (s *Stack[T]) Pop() T {
	lastIndex := s.Size() - 1
	lastElement := s.elements[lastIndex]
	s.elements = s.elements[:lastIndex]
	return lastElement
}

func main() {
	s := Stack[int]{}
	s.Push(1)
	s.Push(2)
	s.Push(3)
	fmt.Printf("数据:%v \n", s)
	fmt.Printf("类型:%T \n", s)

	fmt.Println(s.Pop())
	fmt.Println(s.Pop())
	fmt.Println(s.Pop())
}
go
package main

import "fmt"

type Node[T any] struct {
	value T
	prev  *Node[T]
	next  *Node[T]
}

// 双向链表 数据结构
type LinkedList[T any] struct {
	head *Node[T]
	tail *Node[T]
}

func (ll *LinkedList[T]) Append(value T) {
	newNode := &Node[T]{value: value}

	if ll.head == nil {
		// 空链表, 新节点既是头也是尾
		ll.head = newNode
		ll.tail = newNode
		return
	}

	// 将新节点添加到尾部
	ll.tail.next = newNode
	newNode.prev = ll.tail
	ll.tail = newNode
}

func (ll *LinkedList[T]) ToString() string {
	if ll.head == nil {
		return "[]"
	}

	str := ""
	node := ll.head

	// 处理第一个节点
	str += fmt.Sprintf("[%v]", node.value)
	node = node.next

	// 处理后续节点
	for node != nil {
		str += fmt.Sprintf(" -> [%v]", node.value)
		node = node.next
	}

	return str
}

func main() {
	list := &LinkedList[int]{}
	list.Append(2)
	list.Append(4)
	list.Append(6)

	fmt.Println(list.ToString()) // 输出: [2] -> [4] -> [6]
}

Released under the MIT License.