主题
02 - 复合类型:数组、切片、Map、结构体
复合类型全景图
┌─────────────────────────────────────────────────────────────┐
│ Go 复合类型 │
├──────────────┬──────────────┬──────────┬────────────────────┤
│ 数组 │ 切片 │ Map │ 结构体 │
│ Array │ Slice │ │ Struct │
│ │ │ │ │
│ [5]int │ []int │ map │ type Foo struct │
│ 固定长度 │ 动态长度 │ [K]V │ { ... } │
│ 值类型 │ 引用语义 │ 引用语义 │ 值类型 │
│ 很少直接用 │ 最常用! │ 常用 │ 核心构建块 │
└──────────────┴──────────────┴──────────┴────────────────────┘1. 数组(Array)
go
package main
import "fmt"
func main() {
// 数组:固定长度,声明时就确定大小
var arr1 [5]int // [0 0 0 0 0]
arr2 := [3]string{"Go", "Java", "Python"}
arr3 := [...]int{10, 20, 30, 40} // ... 让编译器数个数
fmt.Println(arr1) // [0 0 0 0 0]
fmt.Println(arr2) // [Go Java Python]
fmt.Println(arr3) // [10 20 30 40]
fmt.Println(len(arr3)) // 4
// 注意:[3]int 和 [5]int 是不同类型!不能互相赋值
// 数组是值类型,赋值和传参会完整拷贝
}数组在 Go 中很少直接使用,因为:
┌──────────────────────────────────────┐
│ 1. 长度是类型的一部分 │
│ [3]int ≠ [5]int(不同类型!) │
│ │
│ 2. 赋值和传参都是完整拷贝 │
│ func foo(arr [1000]int) ← 拷贝! │
│ │
│ 3. 实际开发中 99% 用切片(Slice) │
└──────────────────────────────────────┘2. 切片(Slice)— Go 中最重要的数据结构之一
2.1 切片的内存结构
切片(Slice) = 指向底层数组的 "窗口"
切片变量 s:
┌──────────────────────┐
│ ptr ──────────┐ │
│ len = 3 │ │
│ cap = 5 │ │
└─────────────────┼────┘
│
▼
底层数组: ┌───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ │ │
└───┴───┴───┴───┴───┘
index: 0 1 2 3 4
◀── len ──▶
◀────── cap ──────▶
len(s) = 3 (当前元素个数)
cap(s) = 5 (底层数组从 ptr 开始到末尾的容量)2.2 创建与操作
go
package main
import "fmt"
func main() {
// 方式一:从字面量创建
s1 := []int{1, 2, 3, 4, 5}
// 方式二:make 创建(指定 len 和 cap)
s2 := make([]int, 3, 10) // len=3, cap=10
fmt.Println(s2) // [0 0 0]
// 方式三:从数组切出来
arr := [5]int{10, 20, 30, 40, 50}
s3 := arr[1:4] // [20 30 40],左闭右开
fmt.Println(s1, s3)
// append:追加元素(最常用的操作)
s := []int{1, 2, 3}
s = append(s, 4) // [1 2 3 4]
s = append(s, 5, 6, 7) // [1 2 3 4 5 6 7]
// 合并两个切片
other := []int{8, 9}
s = append(s, other...) // ... 展开切片
fmt.Println(s)
}2.3 切片的扩容机制
append 触发扩容的过程:
初始状态: len=3, cap=4
┌───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ │ ← 还有 1 个位置
└───┴───┴───┴───┘
append(s, 4): len=4, cap=4 (刚好放下)
┌───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ ← 满了!
└───┴───┴───┴───┘
append(s, 5): 容量不够 → 触发扩容!
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ 5 │ │ │ │ ← 新数组 cap=8
└───┴───┴───┴───┴───┴───┴───┴───┘
原数组被 GC 回收
扩容策略(Go 1.18+):
┌─────────────────────────────────────────┐
│ cap < 256: 新容量 = 旧容量 × 2 │
│ cap >= 256: 新容量 = 旧容量 × 1.25 + 192│
└─────────────────────────────────────────┘
⚠️ 性能提示:如果预先知道容量,用 make([]T, 0, n)
可以避免多次扩容带来的内存分配和拷贝开销2.4 切片的陷阱
go
package main
import "fmt"
func main() {
// 陷阱一:切片共享底层数组
original := []int{1, 2, 3, 4, 5}
sub := original[1:3] // [2 3]
sub[0] = 999
fmt.Println(original) // [1 999 3 4 5] ← 原始切片也被改了!
// 安全做法:用 copy 创建独立副本
src := []int{1, 2, 3}
dst := make([]int, len(src))
copy(dst, src)
dst[0] = 999
fmt.Println(src) // [1 2 3] ← 不受影响
// 陷阱二:nil 切片 vs 空切片
var s1 []int // nil 切片
s2 := []int{} // 空切片
s3 := make([]int, 0) // 空切片
fmt.Println(s1 == nil) // true
fmt.Println(s2 == nil) // false
fmt.Println(len(s1), len(s2), len(s3)) // 0 0 0
// 但 append 对两者都能正常工作!
}切片共享底层数组的示意:
original: ┌───┬─────┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ 5 │
└───┴──▲──┴───┴───┴───┘
│
sub = original[1:3]
│
sub: ┌──────┤
│ ptr ─┘
│ len = 2
│ cap = 4
└──────────
sub 和 original 指向同一块内存!修改一个影响另一个。
copy 之后:
original: ┌───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ 5 │ ← 内存区域 A
└───┴───┴───┴───┴───┘
dst: ┌───┬───┬───┐
│ 1 │ 2 │ 3 │ ← 内存区域 B(独立)
└───┴───┴───┘3. Map(映射 / 字典)
3.1 基本用法
go
package main
import "fmt"
func main() {
// 创建方式一:字面量
scores := map[string]int{
"Alice": 95,
"Bob": 87,
"Carol": 92,
}
// 创建方式二:make
ages := make(map[string]int)
ages["Alice"] = 30
ages["Bob"] = 25
// 读取
fmt.Println(scores["Alice"]) // 95
// 读取不存在的 key 返回零值(不会 panic)
fmt.Println(scores["Dave"]) // 0
// 判断 key 是否存在(comma ok 模式)
val, ok := scores["Dave"]
if ok {
fmt.Println("Dave:", val)
} else {
fmt.Println("Dave 不存在")
}
// 删除
delete(scores, "Bob")
// 遍历(注意:顺序是随机的!)
for name, score := range scores {
fmt.Printf("%s: %d\n", name, score)
}
fmt.Println("人数:", len(scores))
}Map 内部结构简化示意:
m := map[string]int{"a": 1, "b": 2, "c": 3}
┌──────────────────────────────────────────┐
│ Map Header │
│ ┌──────────────┐ │
│ │ count: 3 │ │
│ │ buckets ─────┼──┐ │
│ └──────────────┘ │ │
│ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │bucket 0 │ │bucket 1 │ │bucket 2 │ │
│ │ "a": 1 │ │ "b": 2 │ │ "c": 3 │ │
│ │ │ │ │ │ │ │
│ └─────────┘ └─────────┘ └─────────┘ │
└──────────────────────────────────────────┘
key 通过 hash 函数分配到不同的 bucket
Map 是引用类型:传递给函数时不会拷贝数据
⚠️ Map 不是并发安全的!多个 goroutine 同时读写会 panic
→ 并发场景用 sync.Map 或加 sync.RWMutex4. 结构体(Struct)
4.1 定义和使用
go
package main
import "fmt"
// 定义结构体
type User struct {
Name string
Email string
Age int
}
// 定义带嵌套的结构体
type Address struct {
City string
Country string
}
type Employee struct {
User // 嵌入(类似 Java 的继承,但更灵活)
Address // 可以嵌入多个
Department string
Salary float64
}
func main() {
// 创建方式一:按字段名(推荐)
u1 := User{
Name: "Alice",
Email: "alice@example.com",
Age: 30,
}
// 创建方式二:按顺序(不推荐,加字段会出 bug)
u2 := User{"Bob", "bob@example.com", 25}
// 创建方式三:零值 + 逐个赋值
var u3 User
u3.Name = "Carol"
u3.Age = 28
fmt.Println(u1, u2, u3)
// 嵌入结构体的使用
emp := Employee{
User: User{Name: "Dave", Email: "dave@co.com", Age: 35},
Address: Address{City: "Beijing", Country: "China"},
Department: "Engineering",
Salary: 50000,
}
// 可以直接访问嵌入字段(像 Java 继承一样)
fmt.Println(emp.Name) // "Dave"(等价于 emp.User.Name)
fmt.Println(emp.City) // "Beijing"
fmt.Println(emp.Salary) // 50000
}Go 的结构体嵌入 vs Java 的继承:
Java 继承: Go 嵌入:
┌──────────────┐ ┌──────────────────┐
│ Animal │ │ Employee │
│ - name │ │ ┌─────────────┐ │
│ - age │ │ │ User │ │
└──────┬───────┘ │ │ - Name │ │
│ extends │ │ - Email │ │
┌──────┴───────┐ │ │ - Age │ │
│ Dog │ │ └─────────────┘ │
│ - breed │ │ ┌─────────────┐ │
└──────────────┘ │ │ Address │ │
│ │ - City │ │
单继承,is-a 关系 │ └─────────────┘ │
Dog IS an Animal │ - Department │
│ - Salary │
└──────────────────┘
组合,has-a 关系
Employee HAS a User
可以嵌入多个!(Java 不行)
Go 格言:"组合优于继承"4.2 结构体方法
go
package main
import (
"fmt"
"math"
)
type Circle struct {
Radius float64
}
// 值接收者:不会修改原结构体(方法内是拷贝)
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
// 指针接收者:可以修改原结构体
func (c *Circle) Scale(factor float64) {
c.Radius *= factor
}
func main() {
c := Circle{Radius: 5}
fmt.Printf("面积: %.2f\n", c.Area()) // 78.54
c.Scale(2)
fmt.Printf("放大后面积: %.2f\n", c.Area()) // 314.16
}值接收者 vs 指针接收者:
值接收者 func (c Circle): 指针接收者 func (c *Circle):
调用时: 调用时:
┌──────────┐ 拷贝 ┌──────────┐ ┌──────────┐ 传地址 ┌────────┐
│ c={R:5} │ ────▶ │ c={R:5} │ │ c={R:5} │ ─────▶ │ c=&原c │
└──────────┘ └──────────┘ └──────────┘ └───┬────┘
原始不变 修改此拷贝 原始可能被改 │
┌──────┘
▼
修改原始数据
选择建议:
┌─────────────────────────────────────────────────┐
│ 用指针接收者 *T 的情况: │
│ ✓ 需要修改接收者 │
│ ✓ 结构体很大(避免拷贝) │
│ ✓ 一致性(如果有一个方法用指针,全都用指针) │
│ │
│ 用值接收者 T 的情况: │
│ ✓ 结构体很小(如 Point{X, Y int}) │
│ ✓ 不需要修改,且想保证不可变 │
└─────────────────────────────────────────────────┘4.3 构造函数模式
go
package main
import "fmt"
type Server struct {
Host string
Port int
MaxConn int
TLS bool
}
// Go 没有构造函数关键字,用 NewXxx 函数代替
func NewServer(host string, port int) *Server {
return &Server{
Host: host,
Port: port,
MaxConn: 100, // 默认值
TLS: false,
}
}
// 函数选项模式(更灵活的构造方式)
type Option func(*Server)
func WithMaxConn(n int) Option {
return func(s *Server) { s.MaxConn = n }
}
func WithTLS(enabled bool) Option {
return func(s *Server) { s.TLS = enabled }
}
func NewServerWithOptions(host string, port int, opts ...Option) *Server {
s := &Server{Host: host, Port: port, MaxConn: 100}
for _, opt := range opts {
opt(s)
}
return s
}
func main() {
// 简单创建
s1 := NewServer("localhost", 8080)
// 带选项创建
s2 := NewServerWithOptions("api.example.com", 443,
WithMaxConn(1000),
WithTLS(true),
)
fmt.Printf("s1: %+v\n", s1)
fmt.Printf("s2: %+v\n", s2)
}函数选项模式 (Functional Options Pattern):
这是 Go 社区非常流行的设计模式,很多知名库都在用
传统方式的问题:
NewServer("host", 8080, 100, true, false, "")
// ↑ ↑ ↑ ↑
// 啥意思???
函数选项模式:
NewServer("host", 8080,
WithMaxConn(100), ← 一看就知道什么意思
WithTLS(true),
)
┌──────────────────────────────────────────┐
│ Option = func(*Server) │
│ │
│ WithMaxConn(n) ─▶ 返回一个修改 MaxConn │
│ 的函数 │
│ WithTLS(b) ─▶ 返回一个修改 TLS │
│ 的函数 │
│ │
│ 构造时依次调用这些函数来配置 Server │
└──────────────────────────────────────────┘5. 小结
┌───────────────────────────────────────────────────────┐
│ 复合类型速查表 │
├──────────┬──────────┬─────────┬────────────────────────┤
│ 类型 │ 可变长? │ 值/引用 │ 零值 │
├──────────┼──────────┼─────────┼────────────────────────┤
│ [N]T │ 否 │ 值 │ [0,0,...0] │
│ []T │ 是 │ 引用* │ nil(可 append) │
│ map[K]V │ 是 │ 引用 │ nil(需 make 后才能写) │
│ struct │ 否 │ 值 │ 各字段都是零值 │
└──────────┴──────────┴─────────┴────────────────────────┘
* 切片本身是值类型(ptr+len+cap),但指向共享的底层数组
最常用:
┌──────────────────────────────────┐
│ []T ← 几乎所有列表场景 │
│ map[K]V ← 几乎所有字典场景 │
│ struct ← 定义所有业务对象 │
└──────────────────────────────────┘下一节: 03 - 函数与接口 — 函数、方法、接口、错误处理