4. 复合数据类型:数组、切片与结构体
在 Go 语言中,复合数据类型由基本类型组合而成,用于表达更复杂的业务结构。本章聚焦于 数组、切片(slice)、映射(map) 和 结构体(struct),这些类型是构建报表接口编排中数据模型、缓存队列和请求参数的核心载体。
4.1 数组:固定长度的连续内存块
数组是长度固定的、元素类型相同的数据序列。在 Go 中,数组的长度是类型的一部分,因此 [3]int 和 [5]int 被视为不同类型。
| 特性 | 说明 | 示例 |
|---|---|---|
| 声明 | var arr [5]int | 默认元素为零值(0) |
| 初始化 | arr := [3]int{1,2,3} | 长度由编译器推导 |
| 访问 | arr[0] | 索引从 0 开始 |
| 长度 | len(arr) | 编译期常量 |
| 传递 | 值传递 | 函数内修改不影响原数组 |
避坑提示: 数组作为函数参数时,整个数组会被复制。对于大数组,应使用切片或指针避免性能开销。
// 数组声明与遍历
var days [7]string = [7]string{"Mon","Tue","Wed","Thu","Fri","Sat","Sun"}
for i, v := range days {
fmt.Printf("索引 %d: %s\n", i, v)
}
4.2 切片(slice):动态数组的底层原理与操作
切片是对数组的抽象,提供动态长度的序列视图。其底层结构包含三个字段:指针(ptr)、长度(len) 和 容量(cap)。
4.2.1 底层结构
切片本身是一个描述符,指向底层数组的某个起始位置。当切片长度超过容量时,Go 运行时会分配一个新的底层数组并将原数据复制过去。
// 切片底层结构示意(伪代码)
type slice struct {
ptr unsafe.Pointer // 指向底层数组的指针
len int // 当前元素个数
cap int // 底层数组总容量
}
关键理解: 多个切片可以共享同一个底层数组,修改一个切片中的元素会影响其他共享该数组的切片。
4.2.2 切片操作
| 操作 | 语法 | 说明 |
|---|---|---|
| 声明 | var s []int | nil 切片,无底层数组 |
| 字面量 | s := []int{1,2,3} | 自动创建底层数组 |
| make | s := make([]int, 5, 10) | len=5, cap=10 |
| 追加 | s = append(s, 4) | 可能触发扩容 |
| 截取 | sub := s[1:3] | 左闭右开,共享底层数组 |
| 复制 | copy(dst, src) | 按最小长度复制 |
// 切片扩容示例
s := make([]int, 0, 2)
fmt.Printf("初始: len=%d cap=%d\n", len(s), cap(s))
for i := 0; i < 5; i++ {
s = append(s, i)
fmt.Printf("追加 %d: len=%d cap=%d\n", i, len(s), cap(s))
}
// 输出显示 cap 在超过 2 后翻倍为 4,再翻倍为 8
4.2.3 切片与数组的关系
4.3 映射(map):键值对的无序集合
map 是 Go 内置的哈希表实现,提供 O(1) 平均复杂度的键值查找。声明时必须指定键类型和值类型,键类型必须可比较(支持 == 和 !=)。
4.3.1 声明与初始化
// 方式一:make 函数
var m1 map[string]int
m1 = make(map[string]int)
// 方式二:字面量
m2 := map[string]int{
"alice": 90,
"bob": 85,
}
// 方式三:nil map(不能直接写入)
var m3 map[string]int
// m3["key"] = 1 // panic: assignment to entry in nil map
4.3.2 常用操作
| 操作 | 代码 | 说明 |
|---|---|---|
| 插入/更新 | m["key"] = value | 键不存在则插入,存在则覆盖 |
| 读取 | v := m["key"] | 键不存在返回零值 |
| 安全读取 | v, ok := m["key"] | ok 为 bool,表示键是否存在 |
| 删除 | delete(m, "key") | 键不存在则无操作 |
| 长度 | len(m) | 返回键值对数量 |
| 遍历 | for k, v := range m {} | 遍历顺序随机,不保证有序 |
并发安全警告: map 不是并发安全的。在多个 goroutine 同时读写同一个 map 时,必须使用
sync.Mutex 或 sync.Map。
// 安全读取与删除
score := map[string]int{"zhang":95, "li":88}
if v, ok := score["wang"]; ok {
fmt.Println("wang 的分数:", v)
} else {
fmt.Println("wang 不存在")
}
delete(score, "li")
4.4 结构体(struct):自定义复合类型
结构体将多个字段组合成一个逻辑单元,是 Go 面向对象编程的基础。结构体可以定义方法,但 Go 没有类继承,而是通过组合实现复用。
4.4.1 定义与实例化
// 定义结构体
type ReportTask struct {
ID int
Name string
Endpoints []string
Timeout time.Duration
}
// 实例化方式
t1 := ReportTask{ID: 1, Name: "日报"}
t2 := ReportTask{2, "周报", nil, 30 * time.Second} // 按字段顺序
var t3 ReportTask
t3.ID = 3
4.4.2 结构体方法
方法是一种特殊的函数,拥有接收者(receiver)。接收者可以是值类型或指针类型。
| 接收者类型 | 特点 | 适用场景 |
|---|---|---|
| 值接收者 | 方法内修改不影响原实例 | 只读操作、小型结构体 |
| 指针接收者 | 方法内修改影响原实例 | 修改字段、避免复制大结构体 |
// 值接收者方法
func (t ReportTask) Duration() time.Duration {
return t.Timeout
}
// 指针接收者方法
func (t *ReportTask) SetTimeout(d time.Duration) {
t.Timeout = d
}
// 使用
task := ReportTask{Timeout: 10 * time.Second}
fmt.Println(task.Duration()) // 10s
task.SetTimeout(20 * time.Second)
fmt.Println(task.Duration()) // 20s
4.4.3 结构体嵌套与组合
Go 通过结构体嵌套实现组合,被嵌套的字段可以提升到外层直接访问。
type BaseConfig struct {
Host string
Port int
}
type ReportService struct {
BaseConfig // 字段提升
ServiceName string
}
svc := ReportService{
BaseConfig: BaseConfig{Host: "10.0.0.1", Port: 8080},
ServiceName: "daily-report",
}
fmt.Println(svc.Host) // 直接访问提升字段: 10.0.0.1
4.5 复合数据类型在报表编排中的应用
在接口编排场景中,复合数据类型用于构建请求参数、缓存中间结果和定义编排节点。
// 编排节点定义
type OrchestrationNode struct {
ID string
DependsOn []string // 依赖的上游节点 ID
Request map[string]interface{} // 请求参数
RetryPolicy struct {
MaxRetries int
Backoff time.Duration
}
}
// 报表上下文
type ReportContext struct {
TaskID string
Input map[string]interface{}
Cache map[string][]byte // 中间结果缓存
Nodes []OrchestrationNode
StartTime time.Time
}
最佳实践: 在编排引擎中,使用
map[string]interface{} 作为通用参数容器,配合结构体方法实现参数校验和默认值填充,可大幅提升代码可维护性。
4.6 本章小结
- 数组:固定长度,值传递,适合已知大小的常量集合。
- 切片:动态长度,引用底层数组,注意扩容机制和共享陷阱。
- map:无序键值对,安全读取使用
v, ok模式,非并发安全。 - 结构体:自定义类型,支持值/指针接收者方法,通过嵌套实现组合。