Go语言核心三剑客:数组、切片与结构体使用指南

🎯 学习目标

完成本课程后,学习者将能够:

  • 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   | ------+
+---------+---------+---------+
  • s2Data 指针指向原数组索引 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 时,切片会触发扩容,底层逻辑如下:

  1. 分配新数组:根据原切片的容量,计算新容量,然后分配一块新的内存空间作为新底层数组;
  2. 拷贝数据:将原底层数组中的数据拷贝到新数组;
  3. 更新切片 :切片的 Data 指针指向新数组,Len 增加(新增元素个数),Cap 更新为新容量;
  4. 原数组回收:如果原数组没有其他切片引用,会被 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 的底层数组相互独立,修改一方不会影响另一方。

切片的知识点归纳

  1. 切片的底层核心 :切片是 Data指针 + Len + Cap 的结构体,数据存储在底层数组中,切片本身仅占 24 字节(64 位系统,3 个 int 字段);
  2. 扩容的本质:当切片长度超出容量时,会分配新的底层数组,拷贝数据并更新切片指针,原数组若无引用则被 GC 回收;
  3. 引用特性 :切片截取、函数传参默认共享底层数组,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
相关推荐
華勳全栈21 分钟前
两天开发完成智能体平台
java·spring·go
程序新视界26 分钟前
为什么不建议基于Multi-Agent来构建Agent工程?
人工智能·后端·agent
Victor35638 分钟前
Hibernate(29)什么是Hibernate的连接池?
后端
Victor35638 分钟前
Hibernate(30)Hibernate的Named Query是什么?
后端
源代码•宸1 小时前
GoLang八股(Go语言基础)
开发语言·后端·golang·map·defer·recover·panic
czlczl200209251 小时前
OAuth 2.0 解析:后端开发者视角的原理与流程讲解
java·spring boot·后端
颜淡慕潇1 小时前
Spring Boot 3.3.x、3.4.x、3.5.x 深度对比与演进分析
java·后端·架构
布列瑟农的星空1 小时前
WebAssembly入门(一)——Emscripten
前端·后端
小突突突3 小时前
Spring框架中的单例bean是线程安全的吗?
java·后端·spring
iso少年3 小时前
Go 语言并发编程核心与用法
开发语言·后端·golang