My Little World

语法 2

数组与切片

语法

数组是固定长度的、具有相同数据类型元素的有序集合。数组在Go语言中是值类型,这意味着当数组被赋值给一个新变量或传递给函数时,会创建整个数组的副本。

1
2
3
4
// 数组声明方式
var arr1 [3]int // 声明但不初始化
arr2 := [3]int{1, 2, 3} // 声明并初始化
arr3 := [...]int{1, 2, 3, 4, 5} // 编译器计算长度

切片是对数组的抽象,提供动态大小的灵活视图。切片是引用类型,包含三个组件:指向底层数组的指针、长度和容量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 切片创建方式
var slice1 []int // 声明切片
slice2 := make([]int, 3, 5) // 使用make,长度3,容量5
slice3 := []int{1, 2, 3} // 字面量声明
slice4 := arr2[1:3] // 从数组创建

s = append(s[:i], s[i+1:]...) // 删除索引为i的元素
// `append(s[:i], s[i+1:]...)` 是 原地复用底层数组 的删除方式。
// 删除后末尾会残留旧数据, i 被删除后,i+1 到最后一个位置的值往左移动,最后一个位置没有被空出来,还是原来len-1 的数据。
// eg a := []int{1, 2, 3, 4}
// 删除索引为2的元素后,a = []int{1, 2, 4}
// 删除后,底层数组是 []int{1, 2, 4, 4}
// 所以,删除后,数组的长度是 3,容量是 4。

// 如果切片里装的是 指针 ,为避免内存泄漏,建议在删除前把尾元素置空:
copy(s[i:], s[i+1:])
s[len(s)-1] = nil // 指针类型时释放引用
s = s[:len(s)-1]

切片的扩容机制:当切片容量不足时,Go会自动扩容。容量小于1024时通常翻倍,超过1024时增加25%左右。


切片的扩容机制:当切片容量不足时,Go会自动扩容。容量小于1024时通常翻倍,超过1024时增加25%左右。

1
2
3
4
5
cart := []CartItem{}
fmt.Println("购物车容量:", len(cart), cap(cart))
cart = append(cart, CartItem{"apple", 1.0, 2})
cart = append(cart, CartItem{"orange", 2.0, 1})
cart = append(cart, CartItem{"banana", 3.0, 3})
1
2
3
4
初始            len: 0  cap: 0
append apple len: 1 cap: 1
append orange len: 2 cap: 2
append banana len: 3 cap: 4 ← 这里翻倍成 4

为什么是 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
2
3
append banana   len: 3 cap: 4   扩容拷贝
(再加第4个) len: 4 cap: 4 不扩容,直接用空位
(再加第5个) len: 5 cap: 8 再次翻倍

len 和 cap 的区别

加 banana 时
len 当前实际有几个元素 3
cap 底层数组最多能装几个(不含扩容) 4

一个补充细节

扩容不是纯数学翻倍,算出 4 之后还会经过 内存分配器按 size class 取整 。
所以小类型(如int )的增长路径会不同——上面 demo 里int 第一次 append 就直接 cap=4 了,而 32 字节的CartItem 是严格的 1→2→4。
这属于运行时对小对象/no-pointer 对象的分配优化,日常开发记住” 小切片翻倍、大切片约 1.25 倍 “即可。

3. 示例代码

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

cart2 := []*CartItem{}
cart2 = append(cart2, &CartItem{"apple", 1.0, 2})
cart2 = append(cart2, &CartItem{"orange", 2.0, 1})
cart2 = append(cart2, &CartItem{"banana", 3.0, 3})
cart2 = append(cart2, &CartItem{"peach", 4.0, 5})
fmt.Println("购物车2:", cart2)

totalPrice2 := 0.0
totalQuantity2 := 0
for _, item := range cart2 {
totalPrice2 += item.Price * float64(item.Quantity)
totalQuantity2 += item.Quantity
}
fmt.Println("购物车2容量:", len(cart2), cap(cart2))
fmt.Println("购物车2总价:", totalPrice2)
fmt.Println("购物车2总数量:", totalQuantity2)

fmt.Println("购物车2商品移除: orange")
if totalQuantity2 > 5 {
// 这样会左移留位
// 删前 购物车2: [0x52ecebb2e180 0x52ecebb2e1a0 0x52ecebb2e1c0 0x52ecebb2e200]
// 删后 购物车2满仓商品: [0x52ecebb2e180 0x52ecebb2e1c0 0x52ecebb2e200 0x52ecebb2e200]
// cart2 = append(cart2[:1], cart2[2:]...)
// fmt.Println("移除商品后:", cart2)

// 删前 购物车2: [0x52ecebb2e180 0x52ecebb2e1a0 0x52ecebb2e1c0 0x52ecebb2e200]
// 删后 购物车2满仓商品: [0x169da672a180 0x169da672a1c0 0x169da672a200 <nil>]
// orange 在下标1:把下标2开始的元素整体左移,覆盖到下标1
copy(cart2[1:], cart2[2:])
cart2[len(cart2)-1] = nil // 断掉尾部残留引用,让 GC 可回收
cart2 = cart2[:len(cart2)-1]
fmt.Println("移除商品后:", cart2)
}

totalPriceAfterRemove2 := 0.0
totalQuantityAfterRemove2 := 0
for _, item := range cart2 {
totalPriceAfterRemove2 += item.Price * float64(item.Quantity)
totalQuantityAfterRemove2 += item.Quantity
}
fmt.Println("购物车2移除商品后:", len(cart2), cap(cart2))
fmt.Println("购物车2移除商品后总价:", totalPriceAfterRemove2)
fmt.Println("购物车2移除商品后总数量:", totalQuantityAfterRemove2)

full2 := cart2[:4]
fmt.Println("购物车2满仓商品:", full2)

2. 应用场景

数组适用场景:

  • 存储固定大小的数据集合(如一周温度数据)
  • 内存敏感场景,数组在栈上分配
  • 加密/编码等需要精确控制内存布局的操作

切片适用场景:

  • 动态数据集合(如读取未知长度的文件内容)
  • 复用内存,避免复制(如截取日志子集)
  • 实现栈/队列数据结构
  • 高性能批量处理
    练习

映射(Map)

1. 语法讲解

映射是一种内置数据结构,用于存储键值对的无序集合。映射是引用类型,传递开销小。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 映射声明与初始化
var m1 map[string]int // 声明nil映射
m2 := make(map[string]int) // 使用make创建
m3 := map[string]int{ // 字面量创建
"apple": 5,
"banana": 10,
}

// 操作
m2["apple"] = 5 // 插入或更新
value := m2["apple"] // 查找
delete(m2, "apple") // 删除
value, exists := m2["apple"] // 检查键是否存在

并发安全问题Go的map在并发读写时不是线程安全的,需要配合sync包中的锁机制;或者使用sync.Map


并发安全问题Go的map在并发读写时不是线程安全的,需要配合sync包中的锁机制;或者使用sync.Map

2. 应用场景

  • 缓存系统:存储频繁访问的数据
  • 计数器:统计词频、访问次数等
  • 数据库查询结果索引:快速查找
  • 配置存储:键值对配置信息
  • JSON数据解析:处理JSON对象

练习

sort 包排序

1. 语法讲解

Go标准库 sort 包提供了对切片和用户定义集合进行排序的功能。

基本排序接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 排序基本类型
sort.Ints(slice []int)
sort.Float64s(slice []float64)
sort.Strings(slice []string)

// 自定义排序
sort.Slice(slice, func(i, j int) bool {
return slice[i] < slice[j] // 排序条件
})

// 实现sort.Interface接口
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
sort.Sort(slice)

当自定义struct 有 len, less, Swap 三个方法时,可以直接使用 sort.Sort 排序。

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
type Person struct {
Name string
Age int
}

type People []Person

func (p People) Len() int {
return len(p)
}

func (p People) Less(i, j int) bool {
return p[i].Age < p[j].Age
}
func (p People) Swap(i, j int) {
p[i], p[j] = p[j], p[i]
}

people := People{
{"Alice", 25},
{"Bob", 30},
{"Charlie", 22},
}
sort.Sort(people)
for _, person := range people {
fmt.Printf("Name: %s, Age: %d\n", person.Name, person.Age)
}

// 输出:
// Name: Charlie, Age: 22
// Name: Alice, Age: 25
// Name: Bob, Age: 30

查找某个项位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

pos := sort.Search(len(people), func(i int) bool {
return people[i].Name == "Bob"
})
fmt.Println("Position of Bob:", pos)
// 输出:
// Position of Bob: 1

names := []string{"Alice", "Bob", "Charlie"}
sort.Strings(names)
index := sort.SearchStrings(names, "Bob")
fmt.Println("Index of Bob:", index)

// 输出:
// Index of Bob: 1

2. 应用场景

  • 数据展示:表格数据排序(按价格、日期、评分等)
  • 搜索排名:搜索结果按相关性排序
  • 数据分析:数据预处理和统计分析
  • 缓存优化:LRU缓存淘汰策略
  • 调度系统:任务按优先级排序执行

练习

结构体(定义/初始化/字段访问、嵌套结构体)

1. 语法讲解

结构体是Go语言中组织相关数据的复合类型,将多个不同类型的数据组合在一起形成新的数据类型。

结构体定义语法:

1
2
3
4
5
type 结构体名称 struct {
字段1 类型1
字段2 类型2
...
}

结构体初始化方式:

  • 使用 var 声明: var p Person (各字段为零值)
  • 使用结构体字面量: Person{字段1: 值1, 字段2: 值2}
  • 使用 new() 函数: new(Person) (返回指针)

字段访问: 使用点号 . 操作符

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

type Address struct {
Province string
City string
Street string
ZipCode string
}

type UserProfile struct {
ID int
UserName string
Address // 匿名套嵌, 直接使用结构体名称,访问时直接访问当前结构体内部字段
}

userProfile := UserProfile{
ID: 1,
UserName: "admin11",
Address: Address{
Province: "北京",
City: "北京",
Street: "北京",
ZipCode: "100000",
},
}

fmt.Println("用户名:", userProfile.UserName, "城市:", userProfile.Province, "街道:", userProfile.Street)

// 指针声明与访问
userPointer := &User{
ID: 1111,
UserName: "指针用户",
Password: "123456",
Email: "1111@qq.com",
Address: Address{
Province: "北京4",
City: "北京",
Street: "北京4",
},
}
fmt.Println("用户名:", userPointer.UserName, "城市:", userPointer.Address.Province, "街道:", userPointer.Address.Street)

// 用new 声明和初始化
newUser := new(User)
newUser.UserName = "newUser"
fmt.Printf("用户名: %s, 城市: %s, 街道: %s\n", newUser.UserName, newUser.Address.Province, newUser.Address.Street)

2. 应用场景

真实业务场景:用户管理系统
在电商平台、社交应用等系统中,需要管理用户信息,包括基本信息、联系方式、个人资料等。结构体可以完美地组织这些相关信息

练习

方法(绑定一个已定义类型的函数、值接收者vs指针接收者)

1. 语法讲解

方法定义:

1
2
3
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
2
3
4
5
6
// 一组方法签名的集合,并不实现方法,仅定义方法名,输入,输出
type 接口名 interface {
方法1(参数列表) 返回值列表
方法2(参数列表) 返回值列表
// ...
}

当一个类型实现了接口中所有方法,就视为实现了该接口,无需显式声明。

空接口

空接口 interface{} 不包含任何方法,可以存储任意类型的值,类似于Java中的Object或TypeScript中的any。

1
2
3
var anything interface{}
anything = 42 // 整数
anything = "hello" // 字符串

2. 应用场景

真实业务场景:电商支付系统
在实际电商系统中,需要接入多种支付方式(支付宝、微信、银联等),每种支付方式都有相同的核心操作(支付、查询、退款),但具体实现不同。使用接口可以统一管理这些支付方式,提高代码可扩展性和可维护性。

练习

Go的”面向对象”特性

1. 语法讲解

结构体定义

Go使用结构体替代类的概念:

1
2
3
4
5
type 结构体名 struct {
字段1 类型
字段2 类型
// ...
}

方法定义

1
2
3
func (接收者 接收者类型) 方法名(参数列表) 返回值列表 {
// 方法体
}

接收者可以是值类型或指针类型,指针类型可修改接收者的字段。

接口定义

1
2
3
4
5
type 接口名 interface {
方法1(参数列表) 返回值列表
方法2(参数列表) 返回值列表
// ...
}

当一个类型实现了接口中所有方法,就视为实现了该接口,无需显式声明

组合替代继承

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
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
type Test1 struct {
id string
}

func (t Test1) GetId(m string) string {
fmt.Println("test1 getId:", m, t.id)
return t.id
}

type Test2 struct {
name string
Test1
}

func (t Test2) GetName(m string) string {
fmt.Println("test2 getName:", m, t.name)
return t.name
}

type Test3 struct {
count int
Test2
}

func (t Test3) GetCount(m string) int {
fmt.Println("test3 getCount:", m, t.count)
return t.count
}

func main() {
test1 := Test1{id: "test1"}
test2 := Test2{
name: "test2",
Test1: test1
}

test3 := Test3{
count: 3,
Test2: test2,
}

test3.GetId("test1 fucntion")
}

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
    39
    go 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
2
3
4
5
import (
"标准库包"
"第三方包"
"项目内部包"
)

多种导入方式:

  • 单行导入:import "fmt"
  • 多行导入:
    1
    2
    3
    4
    5
    import (
    "fmt"
    "os"
    "net/http"
    )
1
2
3
4
5
6
7
8
9
10
11
12
import (
"fmt"
"os"
"net/http"
p "1-15/book" // 重命名
. "1-15/reader" // . 方便后续直接使用
_ "net/http/pprof" // 空白导入,实现init方法 调用

)

library := p.NewLibrary()
readerService := NewReaderService() // NewReaderService 是reader 里面声明的,因为导入时 用 . 导入,可以直接使用,不用在reader.NewReaderService

可见性规则

  • 大写字母开头:导出(public),包外可见
  • 小写字母开头:非导出(private),仅包内可见

可见性规则适用范围:
struct 里面的 字段名称,
struct 上面定义的方法名称
package 里面声明测函数名称

即小写开头时,都不会被外界访问到,被类型推断为不存在
除非是在套嵌逻辑中

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

type Reader struct {
Id int
Name string
Password string
Address Address
CreatedAt time.Time
}

func (r Reader) validatePassword() string {
if r.Password == "" {
return "密码不能为空"
}
if len(r.Password) < 6 {
return "密码长度不能小于6位"
}
return ""
}

func (r Reader) ShowReaderInfo() Reader {
fmt.Println("读者ID:", r.Id, "读者姓名:", r.Name, "读者邮箱:", r.Address.Email, "读者注册时间:", r.CreatedAt)
return r
}

type ReaderService struct {
readers []*Reader
}

func (s *ReaderService) FindReaderByName(Name string) (*Reader, int, error) {
for i, reader := range s.readers {
if reader.Name == Name {
return reader, i, nil
}
}
return nil, -1, fmt.Errorf("读者不存在 ")
}

func (s *ReaderService) ListAllReaders() {
for _, reader := range s.readers {
reader.ShowReaderInfo()
}
}

// ShowReaderInfo 如果以小写字母开头, ListAllReaders 可以正常运行
// 但是 通过FindReaderByName 返回的值Reader 指针,不能调用showReaderInfo方法

初始化

如果包里面含有init 函数,在包被引入的时候,就会立即执行

1
2
3
4
5
var TestMessage string

func init() {
TestMessage = "hello world"
}

2. 应用场景

真实业务场景:电商微服务项目结构
大型电商平台采用微服务架构,不同功能模块拆分为独立包:

  • user 包:用户管理
  • product 包:商品管理
  • order 包:订单处理
  • payment 包:支付处理
  • common 包:公共工具

这种分包方式提高代码可维护性,支持团队协作开发。

练习