🎯 学习目标
完成本课程后,学习者将能够:
-
Go语言中数组(Array)和切片(Slice)的定义方式、数组和切片的区别
-
切片(Slice)的底层存储原理和扩容方式计算
-
结构体(struct)的定义和使用
-
包(package)的导入 modules、设置国内代理
一、Go 语言中数组(Array)的定义方式
数组是固定长度、同类型的元素序列,长度是数组类型的一部分,编译期确定,不可动态扩容。
1. 基本定义(指定长度 + 类型)
go
// 定义长度为5的int数组,元素初始化为零值(0)
var arr1 [5]int
// 定义长度为3的string数组,显式初始化
var arr2 [3]string = [3]string{"a", "b", "c"}
// 类型推导(省略等号右侧类型)
var arr3 = [3]int{1, 2, 3}
2. 自动推导长度(...)
通过初始化值的数量自动确定数组长度:
go
// 长度为4的int数组(根据初始化元素个数推导)
var arr4 = [...]int{10, 20, 30, 40}
// 长度为5的数组,指定索引初始化(未指定的索引为零值)
var arr5 = [...]int{0: 1, 3: 4, 4: 5} // [1, 0, 0, 4, 5]
3. 简短声明(仅局部作用域)
go
func main() {
arr6 := [2]bool{true, false} // 长度2的bool数组
}
二、Go 语言中切片(Slice)的定义方式
切片是动态长度的数组视图,底层指向一个数组(称为底层数组),包含:指针(指向底层数组)、长度(len)、容量(cap)。切片本身不存储数据,仅描述底层数组的一段。
1. 基本定义(空切片 / 零值切片)
go
// 零值切片:nil,len=0,cap=0,无底层数组
var s1 []int
// 空切片:len=0,cap=0,底层数组为空(非nil)
var s2 []string = []string{}
var s3 = []int{}
s4 := []bool{}
2. 基于数组 / 切片截取(最常用)
语法:[起始索引:结束索引](左闭右开,结束索引省略则到末尾,起始索引省略则从 0 开始)
go
arr := [5]int{1, 2, 3, 4, 5}
// 截取数组的[1,3)区间,s5 = [2,3],len=2,cap=4(从索引1到数组末尾)
s5 := arr[1:3]
// 截取全部:s6 = [1,2,3,4,5],len=5,cap=5
s6 := arr[:]
// 从索引2到末尾:s7 = [3,4,5],len=3,cap=3
s7 := arr[2:]
// 从开头到索引3:s8 = [1,2,3],len=3,cap=5
s8 := arr[:3]
// 切片截取切片
s9 := s5[0:1] // s9 = [2],len=1,cap=4
3. make函数创建(指定长度 / 容量)
语法:make([]T, len, cap)(cap 可选,默认等于 len)
go
// 长度3,容量3,元素初始化为零值(0)
s10 := make([]int, 3) // len=3, cap=3, [0,0,0]
// 长度2,容量5,元素初始化为零值("")
s11 := make([]string, 2, 5) // len=2, cap=5, ["", ""]
4. 直接初始化(字面量)
go
// 长度3,容量3,元素[10,20,30]
s12 := []int{10, 20, 30}
// 指定索引初始化,len=5(最大索引+1),cap=5
s13 := []int{0: 1, 2: 3, 4: 5} // [1,0,3,0,5]
三、数组与切片的核心区别
| 特性 | 数组(Array) | 切片(Slice) |
|---|---|---|
| 长度特性 | 长度固定,是类型的一部分(如[5]int和[3]int是不同类型) |
长度动态,可通过append扩容,类型仅与元素有关(如[]int) |
| 内存存储 | 直接存储元素,值类型(赋值 / 传参时拷贝整个数组) | 仅存储指针、len、cap,引用类型(赋值 / 传参时拷贝切片结构体,底层数组共享) |
| 容量(cap) | 容量 = 长度,不可变 | 容量≥长度,可通过append自动扩容(底层数组替换) |
| 零值 | 数组零值是每个元素为对应类型零值(如[3]int零值是[0,0,0]) |
切片零值是nil(len=0,cap=0,无底层数组) |
| 扩容能力 | 无,只能重新创建新数组 | 支持append扩容,扩容规则:- 原 cap<1024:新 cap=2原 cap- 原 cap≥1024:新 cap≈1.25原 cap |
| 比较操作 | 同类型数组可直接用==比较(元素全部相等则相等) |
切片不能直接用==比较(仅nil == nil),需手动遍历元素比较 |
| 底层依赖 | 无依赖,自身就是存储载体 | 依赖底层数组,切片是底层数组的 "视图" |
四、示例:直观理解区别
go
package main
import "fmt"
func main() {
// 数组:长度固定,值拷贝
arr := [3]int{1, 2, 3}
arrCopy := arr
arrCopy[0] = 100
fmt.Println(arr) // [1 2 3](原数组不变)
fmt.Println(arrCopy) // [100 2 3]
// 切片:引用类型,共享底层数组
s := arr[:]
sCopy := s
sCopy[0] = 100
fmt.Println(arr) // [100 2 3](原数组被修改)
fmt.Println(s) // [100 2 3]
fmt.Println(sCopy)// [100 2 3]
// 切片扩容:底层数组替换
s = append(s, 4)
s[0] = 200
fmt.Println(arr) // [100 2 3](原数组不变,扩容后底层数组已替换)
fmt.Println(s) // [200 2 3 4]
}
五、切片的底层存储
掌握 Go 切片核心特性(比如扩容、引用传递)的关键,搞清楚切片在内存中是如何存储、分配和管理数据。
切片不是独立的容器,而是对底层数组(underlying array) 的封装和引用。可以把切片理解为 "带了长度和容量信息的数组指针",它本身不存储数据,所有数据都存在底层数组中。
Go 的运行时源码中,切片被定义为 reflect.SliceHeader 结构体(位于 reflect 包),核心包含 3 个字段:
go
type SliceHeader struct {
Data uintptr // 指向底层数组的指针(内存地址)
Len int // 切片的长度:当前可访问的元素个数
Cap int // 切片的容量:从指针指向的位置开始,底层数组剩余的元素总数
}
内存布局示例
比如创建切片 s := []int{1,2,3,4},其内存布局如下:
go
切片s(SliceHeader) 底层数组
+---------+---------+---------+ +---+---+---+---+
| Data | Len=4 | Cap=4 | -> | 1 | 2 | 3 | 4 |
+---------+---------+---------+ +---+---+---+---+
如果对切片截取 s2 := s[1:3],内存布局变为:
go
切片s(原) 底层数组
+---------+---------+---------+ +---+---+---+---+
| Data->1 | Len=4 | Cap=4 | | 1 | 2 | 3 | 4 |
+---------+---------+---------+ +---+---+---+---+
^
切片s2(新) |
+---------+---------+---------+ |
| Data->2 | Len=2 | Cap=3 | ------+
+---------+---------+---------+
s2的Data指针指向原数组索引 1 的位置(值为 2);s2.Len=2(可访问 2、3);s2.Cap=3(从索引 1 到数组末尾共 3 个元素:2、3、4)。
切片的内存分配规则
不同创建方式,底层数组的分配规则不同:
| 创建方式 | 底层数组分配逻辑 |
|---|---|
字面量 s := []int{1,2} |
编译器自动创建匿名底层数组,切片指向该数组,Len=Cap = 元素个数 |
make 创建 s := make([]int, 3, 5) |
显式分配长度为 5 的底层数组,切片 Len=3(前 3 个元素初始化为 0),Cap=5 |
从数组截取 s := arr[1:3] |
切片复用原数组,Data 指针指向数组指定位置,Len=2,Cap = 原数组长度 - 起始索引 |
当通过 append 向切片添加元素,且 len == cap 时,切片会触发扩容,底层逻辑如下:
- 分配新数组:根据原切片的容量,计算新容量,然后分配一块新的内存空间作为新底层数组;
- 拷贝数据:将原底层数组中的数据拷贝到新数组;
- 更新切片 :切片的
Data指针指向新数组,Len增加(新增元素个数),Cap更新为新容量; - 原数组回收:如果原数组没有其他切片引用,会被 Go 的垃圾回收(GC)清理。
扩容容量计算规则(Go 1.18+)
- 若原容量
cap < 256:新容量 = 原容量 × 2; - 若原容量
cap ≥ 256:新容量 = 原容量 × 1.25(实际会对齐内存块大小,比如向上取整到最近的 8 的倍数); - 特殊情况:如果
append后需要的长度超过上述计算值,则直接以需要的长度作为新容量。
go
package main
import "fmt"
func main() {
s := make([]int, 0, 1) // Len=0, Cap=1
fmt.Printf("初始:len=%d, cap=%d\n", len(s), cap(s)) // len=0, cap=1
s = append(s, 1) // Len=1, Cap=1(未扩容)
fmt.Printf("添加1个元素:len=%d, cap=%d\n", len(s), cap(s)) // len=1, cap=1
s = append(s, 2) // Len=2, Cap=2(扩容,cap<256 翻倍)
fmt.Printf("添加第2个元素:len=%d, cap=%d\n", len(s), cap(s)) // len=2, cap=2
s = append(s, 3, 4, 5) // 需要len=5,原cap=2,计算新cap=4(2×2)不够,直接用5
fmt.Printf("添加3个元素:len=%d, cap=%d\n", len(s), cap(s)) // len=5, cap=5
}
关键特性的底层解释
1. 切片的 "引用传递"
切片作为函数参数传递时,传递的是 SliceHeader 的副本(值传递),但副本的 Data 指针仍指向原底层数组。因此:
- 修改切片元素(如
s[0] = 10)会影响原切片(因为共享底层数组); - 修改切片的
Len/Cap(如s = append(s, 1))不会影响原切片(因为副本的Data可能指向新数组)。
2. copy 函数的底层逻辑
copy(dst, src []T) 是值拷贝,会把 src 底层数组的元素逐个复制到 dst 的底层数组,不会共享数组:
- 拷贝的元素个数 = min (len (dst), len (src));
- 拷贝后,dst 和 src 的底层数组相互独立,修改一方不会影响另一方。
切片的知识点归纳
- 切片的底层核心 :切片是
Data指针 + Len + Cap的结构体,数据存储在底层数组中,切片本身仅占 24 字节(64 位系统,3 个 int 字段); - 扩容的本质:当切片长度超出容量时,会分配新的底层数组,拷贝数据并更新切片指针,原数组若无引用则被 GC 回收;
- 引用特性 :切片截取、函数传参默认共享底层数组,
copy函数可实现值拷贝,避免共享。
六、结构体
结构体通过type + 结构体名 + struct关键字定义,用于聚合多个不同类型的字段(成员变量),支持字段标签(Tag)用于序列化、ORM 映射等场景。
go
// 定义User结构体,包含基础信息和标签
type User struct {
ID int `json:"id"` // JSON序列化时映射为id
Name string `json:"name"` // JSON序列化时映射为name
Age int `json:"age"` // JSON序列化时映射为age
IsActive bool `json:"is_active"`// JSON序列化时映射为is_active
}
结构体所有实例化方式
go
// 方式1:直接声明(零值初始化)
// 所有字段会被初始化为对应类型的零值(int:0, string:"", bool:false)
var u1 User
fmt.Println("方式1-直接声明(零值):", u1)
// 手动赋值字段
u1.ID = 1
u1.Name = "张三"
u1.Age = 25
u1.IsActive = true
fmt.Println("方式1-赋值后:", u1)
// 方式2:字面量初始化(值类型)
// 指定部分字段,未指定字段为零值;字段顺序可与定义不一致
u2 := User{
ID: 2,
Name: "李四",
Age: 30,
// IsActive 未指定,默认false
}
fmt.Println("方式2-字面量(值类型):", u2)
// 方式3:字面量初始化(指针类型)【推荐】
// 直接返回结构体指针,减少值拷贝开销;语法上与值类型仅多一个&
u3 := &User{
ID: 3,
Name: "王五",
Age: 28,
IsActive: true,
}
fmt.Println("方式3-字面量(指针类型):", u3)
// 指针类型访问字段(Go自动解引用,无需(*u3).Name)
fmt.Println("方式3-访问指针字段:", u3.Name, u3.Age)
// 方式4:new函数初始化(指针类型)
// new(结构体名) 等价于 &结构体名{},返回指针,所有字段为零值
u4 := new(User)
fmt.Println("方式4-new函数(指针零值):", u4)
// 给指针实例赋值字段
u4.ID = 4
u4.Name = "赵六"
u4.Age = 35
u4.IsActive = false
fmt.Println("方式4-new函数(赋值后):", u4)
// 方式5:字面量简写(按字段定义顺序初始化,不推荐,可读性差)
// 必须严格按结构体字段定义顺序赋值,且不能省略任何字段(除非用空值)
u5 := User{5, "钱七", 22, true}
fmt.Println("方式5-字面量简写(值类型):", u5)
Go 通过结构体嵌套实现 "组合"(替代继承),支持 "匿名字段"(提升字段)。
go
// 定义地址结构体
type Address struct {
Province string
City string
}
// User嵌套Address字段
type User struct {
ID int
Name string
Address Address // 显式嵌套
}
// 使用
u := &User{
ID: 1,
Name: "张三",
Address: Address{
Province: "北京",
City: "朝阳区",
},
}
fmt.Println(u.Address.City) // 朝阳区
嵌套的结构体字段省略名称时,其内部字段会被 "提升" 为外层结构体的直接字段,可直接访问。
go
type Address struct {
Province string
City string
}
// 匿名字段嵌套
type User struct {
ID int
Name string
Address // 匿名字段,Address的字段被提升
}
// 使用
u := &User{
ID: 1,
Name: "张三",
Address: Address{
Province: "北京",
City: "朝阳区",
},
}
fmt.Println(u.City) // 直接访问提升字段,等价于u.Address.City
七、包(package)
Go 语言的是代码组织的核心单元,一个目录下的所有.go 文件必须属于同一个包,且包名建议与目录名保持一致(非强制,但符合最佳实践)。
核心定义规则:
- 每个 Go 源文件必须以
package 包名开头,声明所属包。 - 包名推荐 使用小写、简短、见名知意的名称,避免下划线、驼峰式(除非是约定俗成的如
encoding/json)。 - 包名可以与所在目录名不同,但建议保持一致(提升可读性)。
main包是特殊包:包含func main()函数,是程序的入口包,编译后生成可执行文件;非main包编译后生成库文件(.a)。- 同一个目录下的所有
.go文件必须声明为同一个包名。
Package 的导入方式
Go 支持多种灵活的包导入方式,核心关键字是 import,需写在 package 声明之后、函数 / 变量定义之前。
1、先初始化 Go 模块(确保代码可运行):
go
# 进入项目目录
cd ~/go-demo
# 初始化模块(模块名自定义,如 demo)
go mod init demo
2、文件路径:~/go-demo/main.go
go
package main
// 方式1:多行分组导入(推荐)
import (
"fmt" // 标准库包
"demo/utils" // 自定义包(基于go mod的相对路径)
)
// 方式2:单行导入(适合少量包)
// import "fmt"
// import "demo/utils"
func main() {
// 调用导入包的导出函数:包名.函数名
result := utils.Add(10, 20)
fmt.Printf("10 + 20 = %d\n", result)
}
3、别名导入
给导入的包起别名,适用于包名过长、包名冲突或简化代码的场景。
go
package main
import (
"fmt"
// 给utils包起别名u
u "demo/utils"
)
func main() {
// 通过别名调用包的函数
result := u.Add(5, 3)
fmt.Printf("5 + 3 = %d\n", result)
}
4、设置国内代理
go
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct