同构类型:集合中的元素类型相同。
异构类型:集合中的元素类型不同。
数组:同构静态复合类型
逻辑定义
Go 中的数组由固定长度的同构元素组成,是一段连续的元素序列。元素类型和数组长度共同决定数组类型的声明形式:
var arr [N]T
以上代码声明了一个数组变量 arr,类型为 [N]T:元素类型为 T,长度为 N。元素类型 T 可以是任意 Go 原生类型或自定义类型,且数组长度 N 必须在编译期可确定(常量表达式)。
如果两个数组的元素类型 T 与长度 N 都相同,则这两个数组类型等价;任意一项不同,就属于不同的数组类型。例如:
func foo(arr [5]int) {}
func main() {
var arr1 [5]int
var arr2 [6]int
var arr3 [5]string
foo(arr1) // 正确
foo(arr2) // 错误:[6]int 与 [5]int 不匹配
foo(arr3) // 错误:[5]string 与 [5]int 不匹配
}
物理表现形式
Go 编译器为数组变量分配内存时,会为所有元素分配一块连续的内存空间。
Go 提供内置函数 len 获取数组长度;通过 unsafe.Sizeof 可以获得数组变量的总大小:
var arr = [6]int{1, 2, 3, 4, 5, 6}
fmt.Println("数组长度:", len(arr)) // 输出:6
fmt.Println("数组大小:", unsafe.Sizeof(arr)) // 输出:48(在常见 64 位平台上)
数组大小是所有元素大小之和(包含可能的对齐填充;对常见 [6]int 在 64 位平台上通常为 48 字节,因为 int 通常为 8 字节)。
数组支持显式初始化;未显式初始化的元素会被赋予该类型的零值。也可以使用 ... 让编译器推导长度:
var arr1 [6]int // 结果为 [0 0 0 0 0 0]
var arr2 = [6]int{
11, 12, 13, 14, 15, 16,
} // 结果为 [11 12 13 14 15 16]
var arr3 = [...]int{
21, 22, 23,
} // 结果为 [21 22 23]
fmt.Printf("%T\n", arr3) // 输出:[3]int
对于长度较大且稀疏的数组,可以通过指定下标初始化:
var arr4 = [...]int{
99: 39, // 将第 100 个元素(下标 99)设为 39,其余元素为 0
}
fmt.Printf("%T\n", arr4) // 输出:[100]int
使用数组和下标可以高效访问元素。Go 中数组下标从 0 开始;若下标越界会在运行时触发 panic(对常量下标越界,编译期可直接报错)。
多维数组
数组类型本身也可以作为元素类型,形成多维数组:
var mArr [2][3][4]int
可以将 mArr 视为包含 2 个元素、每个元素类型为 [3][4]int 的数组;mArr[0] 和 mArr[1] 的类型都是 [3][4]int(二维数组)。进一步地,[3][4]int 又可以视为包含 3 个元素、每个元素类型为 [4]int 的数组。
多维数组也可以在声明时初始化:
arr5 := [2][3][4]int{
{
{1, 2, 3, 4},
{2, 3, 4, 5},
{3, 4, 5, 6},
},
{
{4, 5, 6, 7},
{5, 6, 7, 8},
{6, 7, 8, 9},
},
}
fmt.Println(arr5)
关于多行字面量必须写逗号(分号自动插入)
Go 在词法层面有一个关键机制:分号自动插入。规范规定:在某些 token(如标识符、基本字面量、break/continue/return、)、]、} 等)之后如果遇到换行,会在换行处自动插入一个 ;。
由于 } 也会触发自动插入分号,如果你写:
var a = [2]int{
1
2
}
词法阶段大致会变成(示意):
var a = [2]int{
1;
2;
};
这会破坏“元素之间用逗号分隔”的语法:解析器看到 1; 2,但它期待 1, 2。因此 Go 规定:只要你把复合字面量写成多行(即 } 不与最后一项同一行),就必须在每一项后面加逗号。
切片:同构动态复合类型
数组长度固定且作为值类型传递时可能带来较大复制开销,因此 Go 引入了切片(slice)。
切片的声明示例:
var nums = []int{1, 2, 3, 4, 5, 6}
与数组相比,切片类型中不包含长度常量,因此更灵活。切片的长度会随着元素数量变化:
fmt.Println(len(nums)) // 输出:6
可通过内置 append 动态追加元素:
nums = append(nums, 7) // [1 2 3 4 5 6 7]
fmt.Println(len(nums)) // 输出:7
切片的实现
切片在运行时可视为一个三元组(概念结构):
type slice struct {
array unsafe.Pointer
len int
cap int
}
-
array:指向底层数组的指针 -
len:切片长度(元素数量) -
cap:从array 起到底层数组末尾的可用容量(cap >= len)
创建切片的常见方式:
方式 1:make 创建
sl := make([]byte, 6, 10) // len=6, cap=10
sl2 := make([]byte, 6) // len=6, cap=6
方式 2:数组切片化(full slice expression)
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
sl := arr[3:7:9]
sl 的 len = high - low,cap = max - low。其底层数组仍是 arr,因此修改 sl 会影响 arr:
sl[0] += 10
fmt.Println("arr[3] =", arr[3]) // 输出:14
通常省略 max,此时 max 默认为底层数组(或底层切片)的容量。
多个切片可以共享同一个底层数组,因此对共享区域的修改会互相可见。
方式 3:基于切片再切片
sl := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
sl1 := sl[2:4] // [3 4]
切片扩容
当执行 append 时,如果 len == cap(底层数组空间已满),运行时会分配新的底层数组并复制旧数据,然后返回指向新数组的切片:
var s []int
s = append(s, 11)
fmt.Println(len(s), cap(s)) // 1 1
s = append(s, 12)
fmt.Println(len(s), cap(s)) // 2 2
s = append(s, 13)
fmt.Println(len(s), cap(s)) // 3 4
s = append(s, 14)
fmt.Println(len(s), cap(s)) // 4 4
s = append(s, 15)
fmt.Println(len(s), cap(s)) // 5 8
扩容策略会随 Go 版本演进而变化:小容量通常接近 2 倍增长;更大容量时增长因子会下降(实现细节不应写死依赖)。一旦发生扩容,切片可能与原底层数组“解绑”,后续对切片的修改不再反映到原数组或其他共享切片上,这在实际编码中需要特别注意。
map 类型
类型定义
map 用于表示一组无序的键值对(key-value),且 key 唯一。类型表示由 key 类型与 value 类型共同组成:
map[key_type]value_type
例如:
map[string]string
map[int]string
若两个 map 的 key 类型相同且 value 类型相同,则它们属于同一种 map 类型。
map 的 value 类型几乎不受限制,但 key 类型必须是**可比较(comparable)**的类型,即支持 == 与 !=。因此 slice、map、func 这三类不可比较类型不能作为 key(它们只能与 nil 比较)。
声明与初始化
var m map[string]int // 零值为 nil
nil map 不能写入(写入会 panic),但可以读取、len、range、delete(删除不存在的键也安全):
var m map[string]int
_ = m["k"] // 读取安全,得到 0
fmt.Println(len(m)) // 0
delete(m, "k") // 安全
m["k"] = 1 // panic: assignment to entry in nil map
初始化方式:
方式 1:复合字面值
m := map[int]string{}
复杂示例:
m1 := map[int][]string{
1: {"val1_1", "val1_2"},
3: {"val3_1", "val3_2", "val3_3"},
7: {"val7_1"},
}
type Position struct {
x float64
y float64
}
m2 := map[Position]string{
{29.935523, 52.568915}: "school",
{25.352594, 113.304361}: "shopping-mall",
{73.224455, 111.804306}: "hospital",
}
方式 2:make 初始化(可给出容量提示)
m1 := make(map[int]string)
m2 := make(map[int]string, 8) // 容量提示(hint)
容量提示不构成上限;map 会根据需要自动增长。
基本操作
插入 / 覆盖
m := make(map[int]string)
m[1] = "value1"
m[2] = "value2"
m[3] = "value3"
若 key 已存在则覆盖旧值:
m := map[string]int{
"key1": 1,
"key2": 2,
}
m["key1"] = 11
m["key3"] = 3
键值对数量
fmt.Println(len(m))
map 不支持 cap。
查找与读取(comma ok)
直接读取不存在的 key 会得到 value 的零值:
v := m["key1"]
因此应使用 comma ok 判断 key 是否存在:
v, ok := m["key1"]
if !ok {
// key1 不存在
}
若只关心是否存在:
_, ok := m["key1"]
删除
delete(m, "key2")
即便 key 不存在也不会报错或 panic。
遍历(for range)
for k, v := range m {
_ = k
_ = v
}
只遍历 key:
for k := range m {
_ = k
}
只遍历 value:
for _, v := range m {
_ = v
}
同一个 map 多次遍历时顺序可能不同,不能依赖遍历顺序编写逻辑。
map 变量传递的开销与可见性
map 是引用语义:传参时会复制一个小的头部结构(内部指向同一份底层数据),因此开销固定且较小;在函数内部对 map 的修改在外部可见:
func foo(m map[string]int) {
m["key1"] = 11
m["key2"] = 12
}
并发访问
内置 map 不支持并发写,也不支持并发读写;在多个 goroutine 中同时读写同一个 map 会产生数据竞争,且可能触发运行时错误(例如 fatal error: concurrent map writes 或 concurrent map read and map write)。仅并发只读在没有任何写入发生时是安全的。
需要并发读写时可使用互斥锁(sync.Mutex/sync.RWMutex)保护,或使用 sync.Map(自 Go 1.9 引入)。
结构体类型
实际开发中仅靠基本类型与简单复合类型往往不够,尤其在需要对实体进行聚合抽象时,Go 使用结构体(struct)提供这一能力。
定义新类型
Go 中有两种方式定义类型。
方式 1:类型定义(定义新类型)
type T S
例如:
type T1 int
type T2 T1
底层类型:若新类型基于某个已定义类型(含原生类型)定义,则该已定义类型是新类型的底层类型。底层类型用于类型关系判断等规则。
也可以基于类型字面值定义复合类型:
type M map[int]string
type S []string
也支持 type 代码块:
type (
T1 int
T2 T1
T3 string
)
方式 2:类型别名(不定义新类型)
常用于项目重构或对外包装:
type T = S
例如:
type T = string
var s string = "hello"
var t T = s // 正确
fmt.Printf("%T\n", t) // 输出:string
定义结构体类型
常见定义方式:
type T struct {
Field1 T1
Field2 T2
// ...
}
示例:
package book
type Book struct {
Title string // 书名
Pages int // 页数
Indexes map[string]int // 索引
}
若标识符首字母大写则为导出标识符,其他包可见;首字母小写则仅包内可见。
空结构体类型
type Empty struct{}
空结构体不占用存储空间,常用作信号/事件:
var s Empty
println(unsafe.Sizeof(s)) // 0
以空结构体作为元素类型的 channel 常用于实现低内存占用的通知机制:
ch := make(chan struct{})
结构体嵌套与嵌入字段(匿名字段)
结构体可以包含另一个结构体作为字段:
type Person struct {
Name string
Phone string
Addr string
}
type Book struct {
Title string
Author Person
}
访问:
var b Book
println(b.Author.Phone)
也可以使用嵌入字段(匿名字段):
type Book struct {
Title string
Person
}
访问方式:
var b Book
println(b.Person.Phone) // 显式使用类型名字段
println(b.Phone) // 字段提升后直接访问
结构体中不允许包含自身类型的非指针字段,否则会形成无限大小:
type T struct {
t T // 编译错误:invalid recursive type
}
相互递归也不允许:
type T1 struct{ t2 T2 }
type T2 struct{ t1 T1 } // 也会非法
但可以包含指向自身的指针、以自身为元素的切片等(这些本身大小固定):
type T struct {
t *T
st []T
m map[string]T
}
结构体变量声明与初始化
type Book struct {
// ...
}
var book Book
var book2 = Book{}
book3 := Book{}
零值初始化
结构体零值表示其所有字段均为各自类型的零值:
var book Book // 零值结构体
合理设计结构体字段,使“零值可用”能显著提升使用体验;但若零值无意义或不可用,应进行显式初始化。
使用复合字面值
按字段顺序初始化:
type Book struct {
Title string
Pages int
Indexes map[string]int
}
var book = Book{"The Go Programming Language", 700, make(map[string]int)}
这种方式的问题:
- 字段顺序一旦改变或增删字段,需要同步调整初始化代码;
- 字段多时易出错、可维护性差;
- 必须提供所有字段值,否则编译错误。
更推荐使用 field: value 形式:
type T struct {
F1 int
F2 string
f3 int
F4 int
F5 int
}
var t = T{
F2: "hello",
F1: 11,
F4: 14,
}
未出现在字面值中的字段将取零值。即使需要零值,也推荐写成:
t := T{}
new 也可创建结构体指针,但并不常作为首选初始化方式(除非明确需要指针):
tp := new(T)
不能在复合字面值中设置其他包的未导出字段,否则编译错误。
若结构体包含未导出字段且零值不可用,或某些字段需要复杂初始化逻辑,通常应提供构造函数创建实例。
使用专用构造函数
标准库常用构造函数创建并初始化结构体,例如 time.NewTimer:
// $GOROOT/src/time/sleep.go
func NewTimer(d Duration) *Timer {
c := make(chan Time, 1)
t := &Timer{
C: c,
r: runtimeTimer{
when: when(d),
f: sendTime,
arg: c,
},
}
startTimer(&t.r)
return t
}
构造函数通常遵循以下模式:
func NewT(field1, field2, ...) *T {
...
}
NewT 的参数往往对应 T 的导出字段,返回一个已正确初始化的 *T;非导出字段与复杂初始化逻辑在构造函数内部完成。