Go 学习笔记(三):复合数据类型

写在前面

本文是 Go 学习笔记系列的第三篇,介绍 Go 的复合数据类型:数组、切片、字典、结构体,以及 JSON 处理和字符串操作。前置知识:Go 基础语法(第二篇)。


一、数组

1.1 声明与初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 声明指定长度的数组
var nums [5]int                       // [0 0 0 0 0]
var names [3]string                   // ["" "" ""]

// 声明并初始化
colors := [3]string{"红", "绿", "蓝"}
nums := [5]int{1, 2, 3, 4, 5}

// 让编译器计算长度
nums := [...]int{1, 2, 3, 4, 5}      // 长度自动推断为 5

// 指定索引初始化
nums := [5]int{1: 10, 3: 30}         // [0 10 0 30 0]

1.2 数组操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
nums := [5]int{1, 2, 3, 4, 5}

// 访问元素
fmt.Println(nums[0])   // 1
fmt.Println(nums[4])   // 5

// 修改元素
nums[0] = 10

// 获取长度
fmt.Println(len(nums))  // 5

// 遍历
for i, v := range nums {
    fmt.Println(i, v)
}

注意:Go 数组的长度是类型的一部分,[3]int[5]int 是不同的类型,不能互相赋值。实际开发中很少直接用数组,基本都用切片。


二、切片(Slice)

切片是 Go 中最常用的数据结构,可以理解为动态数组。

2.1 创建切片

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 从数组创建
arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[1:4]            // [2 3 4],左闭右开

// 直接创建
s2 := []int{1, 2, 3}      // 切片字面量

// 用 make 创建(推荐,可指定容量)
s3 := make([]int, 5)            // 长度5,容量5,元素全为0
s4 := make([]int, 0, 10)        // 长度0,容量10(预分配)

2.2 切片三要素

1
2
3
指针 — 指向底层数组的地址
长度 — len(s),当前元素个数
容量 — cap(s),从指针位置到底层数组末尾的元素数
1
2
3
s := make([]int, 3, 5)
fmt.Println(len(s))   // 3
fmt.Println(cap(s))   // 5

2.3 添加元素

1
2
3
4
5
6
7
8
9
s := []int{1, 2, 3}

// append 返回新切片(必须接收返回值)
s = append(s, 4)          // [1 2 3 4]
s = append(s, 5, 6)       // [1 2 3 4 5 6]

// 追加另一个切片
other := []int{7, 8}
s = append(s, other...)    // [1 2 3 4 5 6 7 8]

append 可能触发扩容,扩容后地址可能改变,所以必须用 s = append(s, x) 接收返回值。

2.4 切片截取

1
2
3
4
5
6
s := []int{1, 2, 3, 4, 5}

s1 := s[1:3]     // [2 3]
s2 := s[:3]      // [1 2 3]
s3 := s[2:]      // [3 4 5]
s4 := s[:]       // [1 2 3 4 5](完整拷贝视图)

切片截取和原切片共享底层数组,修改一个会影响另一个。要完全独立需要用 copy。

2.5 复制切片

1
2
3
4
5
6
src := []int{1, 2, 3}
dst := make([]int, len(src))
copy(dst, src)        // 深拷贝,互不影响

// 或者用 append 创建独立副本
dst2 := append([]int{}, src...)

2.6 删除元素

Go 没有内置的删除方法,需要手动操作:

1
2
3
4
5
6
7
8
s := []int{1, 2, 3, 4, 5}

// 删除索引 2 的元素(保持顺序)
s = append(s[:2], s[3:]...)   // [1 2 4 5]

// 删除索引 2 的元素(不保持顺序,性能更好)
s[2] = s[len(s)-1]
s = s[:len(s)-1]

2.7 扩容机制

切片容量不够时,append 会自动扩容:

1
2
新容量 < 256     → 新容量 = 旧容量 * 2(翻倍)
新容量 >= 256    → 新容量 = 旧容量 * 1.25 + 192(渐进增长)

建议:如果知道大致元素数量,用 make([]T, 0, n) 预分配容量,避免频繁扩容。


三、字典(Map)

3.1 创建与初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 字面量创建
ages := map[string]int{
    "张三": 25,
    "李四": 30,
}

// 用 make 创建
scores := make(map[string]int)
scores["数学"] = 90
scores["英语"] = 85

// 空的 map(可以直接使用)
m := make(map[string]string)

var m map[string]string 声明的是 nil map,直接赋值会 panic。必须用 make 或字面量初始化。

3.2 基本操作

 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
m := map[string]int{"a": 1, "b": 2, "c": 3}

// 取值
fmt.Println(m["a"])    // 1
fmt.Println(m["x"])    // 0(不存在的 key 返回零值)

// 判断 key 是否存在
value, ok := m["x"]
if ok {
    fmt.Println("存在:", value)
} else {
    fmt.Println("不存在")
}

// 修改
m["a"] = 10

// 删除
delete(m, "b")

// 遍历(顺序不确定)
for key, value := range m {
    fmt.Println(key, value)
}

// 获取长度
fmt.Println(len(m))

3.3 Map 的注意事项

1
2
3
4
- 遍历顺序不确定(每次运行可能不同)
- Map 不是并发安全的,并发读写需要加锁或用 sync.Map
- Map  key 必须是可比较的类型(不能用 slicemapfunc 作为 key
- 取值时用 value, ok 模式判断 key 是否存在

四、结构体(Struct)

4.1 定义与实例化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 定义结构体
type User struct {
    Name string
    Age  int
}

// 实例化
u1 := User{Name: "张三", Age: 25}      // 指定字段名(推荐)
u2 := User{"李四", 30}                  // 按顺序(不推荐,不好维护)
u3 := User{}                            // 零值初始化:Name="" Age=0

// 访问和修改
fmt.Println(u1.Name)   // 张三
u1.Age = 26

// 指针结构体(避免大结构体拷贝)
u4 := &User{Name: "王五", Age: 28}
u4.Age = 29   // Go 自动解引用,不需要 ->

// new 创建(返回指针,所有字段零值)
u5 := new(User)   // *User,Name="" Age=0

4.2 结构体方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
type Circle struct {
    Radius float64
}

// 值接收者:不修改原对象,操作的是副本
func (c Circle) Area() float64 {
    return 3.14 * c.Radius * c.Radius
}

// 指针接收者:可以修改原对象
func (c *Circle) Scale(factor float64) {
    c.Radius = c.Radius * factor
}

// 调用
c := Circle{Radius: 5}
fmt.Println(c.Area())    // 78.5
c.Scale(2)
fmt.Println(c.Radius)    // 10

何时用指针接收者:需要修改对象、结构体较大(避免拷贝)、保持一致性(建议一个类型的方法统一用指针或统一用值)。

4.3 结构体嵌套

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
type Address struct {
    City    string
    Street  string
}

type Employee struct {
    Name    string
    Address            // 匿名嵌套(类似继承)
}

e := Employee{
    Name: "张三",
    Address: Address{
        City:   "北京",
        Street: "长安街",
    },
}

// 直接访问内嵌字段
fmt.Println(e.City)    // 北京(直接访问,不需要 e.Address.City)

4.4 结构体标签(Tag)

标签常用于 JSON、数据库映射:

1
2
3
4
5
6
type User struct {
    ID       int    `json:"id" db:"user_id"`
    Name     string `json:"name"`
    Email    string `json:"email,omitempty"`   // omitempty: 零值时省略
    Password string `json:"-"`                 // - 表示忽略
}

五、JSON 处理

5.1 序列化(结构体 → JSON)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import "encoding/json"

type User struct {
    Name  string `json:"name"`
    Age   int    `json:"age"`
    Email string `json:"email,omitempty"`
}

user := User{Name: "张三", Age: 25, Email: "zhangsan@example.com"}

// 转成 JSON 字符串
data, err := json.Marshal(user)
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(data))
// {"name":"张三","age":25,"email":"zhangsan@example.com"}

// 格式化输出(带缩进)
data, _ = json.MarshalIndent(user, "", "  ")
fmt.Println(string(data))

5.2 反序列化(JSON → 结构体)

1
2
3
4
5
6
7
8
9
jsonStr := `{"name":"李四","age":30}`

var user User
err := json.Unmarshal([]byte(jsonStr), &user)
if err != nil {
    log.Fatal(err)
}
fmt.Println(user.Name)  // 李四
fmt.Println(user.Age)   // 30

5.3 处理不确定结构

1
2
3
4
5
6
7
8
9
// 用 map 接收不确定结构的 JSON
var result map[string]interface{}
json.Unmarshal([]byte(jsonStr), &result)
fmt.Println(result["name"])   // 李四

// 读取嵌套字段(需要类型断言)
if data, ok := result["address"].(map[string]interface{}); ok {
    fmt.Println(data["city"])
}

六、字符串处理

6.1 strings 包

 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
import "strings"

s := "Hello, World"

// 查找
strings.Contains(s, "World")          // true
strings.HasPrefix(s, "Hello")         // true
strings.HasSuffix(s, "World")         // true
strings.Index(s, "World")             // 7

// 变换
strings.ToUpper(s)                    // "HELLO, WORLD"
strings.ToLower(s)                    // "hello, world"
strings.TrimSpace("  hi  ")          // "hi"
strings.Trim("--hi--", "-")           // "hi"

// 分割与合并
strings.Split("a,b,c", ",")           // ["a", "b", "c"]
strings.Join([]string{"a","b"}, "-")  // "a-b"

// 替换
strings.Replace("foo bar foo", "foo", "baz", 1)  // "baz bar foo"(替换1次)
strings.ReplaceAll("foo bar foo", "foo", "baz")   // "baz bar baz"

// 重复
strings.Repeat("Go", 3)               // "GoGoGo"

6.2 strconv 包

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import "strconv"

// 数字 → 字符串
strconv.Itoa(42)                            // "42"
strconv.FormatFloat(3.14, 'f', 2, 64)       // "3.14"
strconv.FormatBool(true)                    // "true"

// 字符串 → 数字
n, err := strconv.Atoi("42")               // 42
f, err := strconv.ParseFloat("3.14", 64)   // 3.14
b, err := strconv.ParseBool("true")         // true

七、小结

本文学习了 Go 的复合数据类型:

  • 数组(长度固定,实际少用)和切片(动态数组,最常用)
  • Map(字典,注意零值和并发问题)
  • 结构体(方法、嵌套、标签)
  • JSON 序列化与反序列化
  • strings 和 strconv 包

下一篇将学习接口与错误处理,这是 Go 的核心设计理念。