目 录CONTENT

文章目录
go

【Go】复合数据类型

hxuanyu
2025-12-26 / 0 评论 / 0 点赞 / 104 阅读 / 0 字

同构类型:集合中的元素类型相同。

异构类型:集合中的元素类型不同。


数组:同构静态复合类型

逻辑定义

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;非导出字段与复杂初始化逻辑在构造函数内部完成。

0
博主关闭了所有页面的评论