数组与切片
语法
数组是固定长度的、具有相同数据类型元素的有序集合。数组在Go语言中是值类型,这意味着当数组被赋值给一个新变量或传递给函数时,会创建整个数组的副本。
1 | // 数组声明方式 |
切片是对数组的抽象,提供动态大小的灵活视图。切片是引用类型,包含三个组件:指向底层数组的指针、长度和容量。
1 | // 切片创建方式 |
切片的扩容机制:当切片容量不足时,Go会自动扩容。容量小于1024时通常翻倍,超过1024时增加25%左右。
切片的扩容机制:当切片容量不足时,Go会自动扩容。容量小于1024时通常翻倍,超过1024时增加25%左右。
1 | cart := []CartItem{} |
1 | 初始 len: 0 cap: 0 |
为什么是 4 不是 3
加入 banana 前,切片是len=2, cap=2 ,已经装满。再 append 需要容量 3,于是触发扩容,Go 的规则是:
当旧容量 < 256 时,新切片直接翻倍 (容量大了以后改为约 1.25 倍增长)
旧 cap = 2,翻倍 = 4 ,而不是恰好给到 3。所以你看到 3 个商品、cap 却是 4。
为什么要”浪费”这 1 个位置
假设每次只 +1,那么加入第 3、4、5…个商品时都要重新分配数组 + 拷贝全部旧元素,N 次追加就是 O(N²)。
翻倍策略把拷贝成本摊薄,让连续 append 的均摊开销降到 O(1)。多出来的空位,正是留给你 下一次 append 的 ——下次不再扩容:
1 | append banana len: 3 cap: 4 扩容拷贝 |
len 和 cap 的区别
加 banana 时len 当前实际有几个元素 3cap 底层数组最多能装几个(不含扩容) 4
一个补充细节
扩容不是纯数学翻倍,算出 4 之后还会经过 内存分配器按 size class 取整 。
所以小类型(如int )的增长路径会不同——上面 demo 里int 第一次 append 就直接 cap=4 了,而 32 字节的CartItem 是严格的 1→2→4。
这属于运行时对小对象/no-pointer 对象的分配优化,日常开发记住” 小切片翻倍、大切片约 1.25 倍 “即可。
3. 示例代码
1 |
|
2. 应用场景
数组适用场景:
- 存储固定大小的数据集合(如一周温度数据)
- 内存敏感场景,数组在栈上分配
- 加密/编码等需要精确控制内存布局的操作
切片适用场景:
- 动态数据集合(如读取未知长度的文件内容)
- 复用内存,避免复制(如截取日志子集)
- 实现栈/队列数据结构
- 高性能批量处理
练习
映射(Map)
1. 语法讲解
映射是一种内置数据结构,用于存储键值对的无序集合。映射是引用类型,传递开销小。
1 | // 映射声明与初始化 |
并发安全问题Go的map在并发读写时不是线程安全的,需要配合sync包中的锁机制;或者使用sync.Map
并发安全问题Go的map在并发读写时不是线程安全的,需要配合sync包中的锁机制;或者使用sync.Map
2. 应用场景
- 缓存系统:存储频繁访问的数据
- 计数器:统计词频、访问次数等
- 数据库查询结果索引:快速查找
- 配置存储:键值对配置信息
- JSON数据解析:处理JSON对象
sort 包排序
1. 语法讲解
Go标准库 sort 包提供了对切片和用户定义集合进行排序的功能。
基本排序接口:
1 | // 排序基本类型 |
当自定义struct 有 len, less, Swap 三个方法时,可以直接使用 sort.Sort 排序。
1 | type Person struct { |
查找某个项位置
1 |
|
2. 应用场景
- 数据展示:表格数据排序(按价格、日期、评分等)
- 搜索排名:搜索结果按相关性排序
- 数据分析:数据预处理和统计分析
- 缓存优化:LRU缓存淘汰策略
- 调度系统:任务按优先级排序执行
结构体(定义/初始化/字段访问、嵌套结构体)
1. 语法讲解
结构体是Go语言中组织相关数据的复合类型,将多个不同类型的数据组合在一起形成新的数据类型。
结构体定义语法:
1 | type 结构体名称 struct { |
结构体初始化方式:
- 使用 var 声明:
var p Person(各字段为零值) - 使用结构体字面量:
Person{字段1: 值1, 字段2: 值2} - 使用 new() 函数:
new(Person)(返回指针)
字段访问: 使用点号 . 操作符
1 |
|
2. 应用场景
真实业务场景:用户管理系统
在电商平台、社交应用等系统中,需要管理用户信息,包括基本信息、联系方式、个人资料等。结构体可以完美地组织这些相关信息
方法(绑定一个已定义类型的函数、值接收者vs指针接收者)
1. 语法讲解
方法定义:
1 | func (接收者 接收者类型) 方法名(参数列表) 返回值列表 { |
接收者类型:
- 值接收者:操作结构体的副本,不会影响原始值
- 指针接收者:操作结构体指针,可以修改原始值
如果需要修改原生类型的方法,可以现将类型重命名,然后再添加自定义方法或者覆写原生方法1
2
3
4
5
6
7
8
9
type myInt int
func (I myInt) String() string {
// 注意:不能直接 fmt.Println(I),否则打印时又会调用 I.String(),造成无限递归
// 先转回底层 int,再用 strconv 转成十进制字符串
fmt.Println("myInt.String()", int(I))
return strconv.Itoa(int(I))
}
2. 应用场景
真实业务场景:银行账户管理系统
在金融应用中,账户对象需要封装各种操作(存款、取款、查询余额),这些操作需要维护账户状态的一致性,使用方法可以很好地封装这些行为。
接口(interface)
1. 语法讲解
接口定义
接口是Go语言中实现多态的重要特性,它定义了一组方法签名,而不包含具体实现。
1 | // 一组方法签名的集合,并不实现方法,仅定义方法名,输入,输出 |
当一个类型实现了接口中所有方法,就视为实现了该接口,无需显式声明。
空接口
空接口 interface{} 不包含任何方法,可以存储任意类型的值,类似于Java中的Object或TypeScript中的any。
1 | var anything interface{} |
2. 应用场景
真实业务场景:电商支付系统
在实际电商系统中,需要接入多种支付方式(支付宝、微信、银联等),每种支付方式都有相同的核心操作(支付、查询、退款),但具体实现不同。使用接口可以统一管理这些支付方式,提高代码可扩展性和可维护性。
Go的”面向对象”特性
1. 语法讲解
结构体定义
Go使用结构体替代类的概念:
1 | type 结构体名 struct { |
方法定义
1 | func (接收者 接收者类型) 方法名(参数列表) 返回值列表 { |
接收者可以是值类型或指针类型,指针类型可修改接收者的字段。
接口定义
1 | type 接口名 interface { |
当一个类型实现了接口中所有方法,就视为实现了该接口,无需显式声明
组合替代继承
Go通过结构体嵌入实现组合,匿名嵌入的字段和方法会自动提升到外部结构体。
结构匿名套嵌直接实现继承
比如
type a 有a1,a2 方法
type b 有b1,b2 方法,并嵌套a ,b: {a}
type c 有c1,c2 方法,并嵌套b, c: {b}
那么c类型的数据 通过 套嵌b ,b 套嵌a 的链路,可以直接调用a 上面的a1/a2 方法, 即c.a1
1 | type Test1 struct { |
Go通过接口嵌入组合实现混合的更灵活的接口定义
对于字段的声明,如果是一个接口,那么只要被赋值的数据类型,有实现相关接口上声明的方法,就可以被赋值成这个字段
比如1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main
import (
"fmt"
)
type InterfaceA interface {
a1(m string) string
}
type A struct {
aa InterfaceA
}
type B struct {
bb string
}
func (b B) a1(m string) string {
fmt.Println("B a1:", m, b.bb)
return b.bb
}
type C struct {
cc []string
}
func (c C) a1(m string) string {
for i, v := range c.cc {
fmt.Println("C a1:", i, ":", v, m)
}
return c.cc[0]
}
func main() {
b := B{bb: "hello world"}
c := C{cc: []string{"hello", "world"}}
// b, c 类型结构不同,但都可以赋值给aa
tempb := A{
aa: b,
}
tempc := A{
aa: c,
}
// 注意这里不能直接调用a1,要通过aa
tempb.aa.a1("hello world bbb")
tempc.aa.a1("hello world ccc")
}
2. 应用场景
真实业务场景:电商平台用户系统
电商平台有不同类型的用户(普通用户、VIP用户),这些用户有共同属性(基础信息)和不同特性(权限、折扣等)。通过组合可以避免重复代码,同时保持类型之间的清晰关系。
包(package)与导入(import)
1. 语法讲解
包定义
每个Go源文件必须属于一个包,包声明在第一行:
1 | package 包名 |
- 可执行程序包名必须是
main,且包含main函数 - 包名使用小写字母,简洁明了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39go mod init + 名称 用于初始化模块
// 比如当前目录
1-15/
├── book/
│ ├── book.go
│ └── library.go
├── common/
│ └── logger.go
├── reader/
│ ├── reader.go
│ └── readerService.go
├── go.mod
├── go.sum
└── main.go
// 在当前1-15 目录下执行
go mod init 1-15
// 那么在1-15 中其他文件中就能互相使用1-15 文件夹下面其他的文档导出的模块(变量,方法, struct)
// 比如在reader.go 中
import (
"1-15/common"
)
common.LogOperation("CreateReader")
// 在main.go 中
import (
"1-15/book"
"1-15/reader"
)
// 同一目录所有 .go 文件必须同一个包
// 按照文件夹名称进行引用使用,同一个文件夹下面的不同文件中,package 名称 都相同,但不能有同名声明(变量,结构,函数)
library := book.NewLibrary()
// NewReaderService 和 TestMessage 位于不同文件中,但都在reader 文件夹下面
readerService := reader.NewReaderService()
fmt.Println("main fire TestMessage:", reader.TestMessage)
go mod tidy // 自动分析项目中的代码,整理并更新 go.mod 和 go.sum 文件,确保它们与代码实际需要的依赖完全一致。 查漏补缺,清理冗余
导入机制
1 | import ( |
多种导入方式:
- 单行导入:
import "fmt" - 多行导入:
1
2
3
4
5import (
"fmt"
"os"
"net/http"
)
1 | import ( |
可见性规则
- 大写字母开头:导出(public),包外可见
- 小写字母开头:非导出(private),仅包内可见
可见性规则适用范围:
struct 里面的 字段名称,
struct 上面定义的方法名称
package 里面声明测函数名称
即小写开头时,都不会被外界访问到,被类型推断为不存在
除非是在套嵌逻辑中
1 |
|
初始化
如果包里面含有init 函数,在包被引入的时候,就会立即执行
1 | var TestMessage string |
2. 应用场景
真实业务场景:电商微服务项目结构
大型电商平台采用微服务架构,不同功能模块拆分为独立包:
- user 包:用户管理
- product 包:商品管理
- order 包:订单处理
- payment 包:支付处理
- common 包:公共工具
这种分包方式提高代码可维护性,支持团队协作开发。