Go 语言:结构体 + 切片 + range遍历
- 一、结构体(struct)
-
- [1.1 结构体定义语法](#1.1 结构体定义语法)
- [1.2 结构体变量创建与初始化](#1.2 结构体变量创建与初始化)
- [1.3 结构体作为函数参数](#1.3 结构体作为函数参数)
- [1.4 结构体指针](#1.4 结构体指针)
-
- [1. 定义与使用](#1. 定义与使用)
- [2. 直接创建结构体指针实例](#2. 直接创建结构体指针实例)
- [1.5 结构体嵌套](#1.5 结构体嵌套)
- [1.6 结构体高频踩坑](#1.6 结构体高频踩坑)
- 二、切片(slice)
-
- [2.1 切片核心概念](#2.1 切片核心概念)
- [2.2 切片创建方式(5种)](#2.2 切片创建方式(5种))
-
- [方式1:直接声明(nil 切片)](#方式1:直接声明(nil 切片))
- 方式2:字面量初始化
- 方式3:基于数组/已有切片截取
- [方式4:make 函数创建(工程最常用)](#方式4:make 函数创建(工程最常用))
- 方式5:截取切片创建新切片
- [2.3 切片常用内置函数](#2.3 切片常用内置函数)
-
- [1. append() 追加元素(动态扩容核心)](#1. append() 追加元素(动态扩容核心))
- [2. copy() 拷贝切片](#2. copy() 拷贝切片)
- [2.4 切片遍历](#2.4 切片遍历)
- [2.5 切片高频踩坑(重点)](#2.5 切片高频踩坑(重点))
- [三、range 遍历语法](#三、range 遍历语法)
-
- [3.1 基础语法](#3.1 基础语法)
- [3.2 分场景遍历示例](#3.2 分场景遍历示例)
-
- [3.1 遍历数组 & 切片(最常用)](#3.1 遍历数组 & 切片(最常用))
- [3.2 遍历字符串](#3.2 遍历字符串)
- [3.3 遍历 map](#3.3 遍历 map)
- [3.4 遍历通道(channel)](#3.4 遍历通道(channel))
- [3.3 range 核心踩坑(90%新手易错)](#3.3 range 核心踩坑(90%新手易错))
- 四、知识点速查表
目录
- 结构体(struct)
- 切片(slice)
- range 遍历语法
- 综合实操练习
一、结构体(struct)
结构体是 Go 中自定义复合数据类型 ,可组合不同类型的字段,用来描述一组关联属性(如用户、书籍、订单),类似其他语言的实体类。结构体是值类型,也是 Go 实现面向对象思想的基础载体。
1.1 结构体定义语法
使用 type + struct 关键字定义结构体类型,语法格式:
go
type 结构体名 struct {
字段名1 字段类型1
字段名2 字段类型2
...
}
补充规则
- 字段名大小写控制访问权限:首字母大写 → 跨包可访问(公开);首字母小写 → 仅当前包访问(私有);
- 同一结构体内部字段名不可重复;
- 结构体可嵌套其他结构体、指针、切片等类型。
基础示例(书籍结构体)
go
package main
import "fmt"
// 定义书籍结构体
type Books struct {
title string // 书籍名称(私有,本包可见)
Author string // 作者(大写,跨包可见)
subject string // 科目
bookID int // 书籍编号
}
func main() {
fmt.Println("结构体定义完成")
}
1.2 结构体变量创建与初始化
定义结构体后,可通过多种方式创建实例并赋值,共4种常用写法。
方式1:先声明,再逐个赋值(最通用)
通过 . 点运算符访问/修改结构体字段。
go
package main
import "fmt"
type Books struct {
title string
Author string
subject string
bookID int
}
func main() {
// 声明结构体变量,字段自动填充零值
var book1 Books
// 逐个给字段赋值
book1.title = "Go语言入门"
book1.Author = "菜鸟教程"
book1.subject = "编程"
book1.bookID = 1001
// 访问结构体字段
fmt.Println("书名:", book1.title)
fmt.Println("作者:", book1.Author)
}
方式2:字面量初始化(按字段顺序赋值)
严格按照结构体字段定义顺序传值,不可跳过字段。
go
book2 := Books{"Python教程", "张三", "编程", 1002}
fmt.Println(book2)
方式3:键值对初始化(推荐,可读性强)
指定 字段名: 值,顺序可随意,未赋值字段自动填充零值。
go
// 仅赋值部分字段,剩余字段为零值
book3 := Books{
title: "Java教程",
bookID: 1003,
}
fmt.Println(book3)
方式4:短声明结合空结构体
go
book4 := Books{} // 所有字段为零值
fmt.Println(book4)
1.3 结构体作为函数参数
结构体默认是值传递 ,函数内部修改的是副本,不会影响原变量;如需修改原结构体,使用结构体指针传参。
示例1:值传递(无法修改原数据)
go
package main
import "fmt"
type Books struct {
title string
price float64
}
// 值传递:接收结构体副本
func changeBook(book Books) {
book.title = "修改后的书名"
}
func main() {
b := Books{title: "原始书名", price: 39.9}
changeBook(b)
fmt.Println(b) // 原数据不变:{原始书名 39.9}
}
示例2:指针传递(可修改原数据,工程常用)
go
package main
import "fmt"
type Books struct {
title string
price float64
}
// 接收结构体指针
func changeBook(book *Books) {
book.title = "修改后的书名" // 指针访问字段,无需额外符号
}
func main() {
b := Books{title: "原始书名", price: 39.9}
change(&b) // 传递结构体地址
fmt.Println(b) // 原数据被修改:{修改后的书名 39.9}
}
1.4 结构体指针
1. 定义与使用
go
package main
import "fmt"
type User struct {
name string
age int
}
func main() {
var u User = {"李四", 25}
// 定义结构体指针,指向 u
var p *User = &u
// 指针访问字段:Go 语法糖,直接使用 .
fmt.Println(p.name, p.age)
// 通过指针修改原结构体
p.age = 26
fmt.Println(u) // {李四 26}
}
2. 直接创建结构体指针实例
go
// 简写方式,直接分配内存并返回指针
p := &User{name: "王五", age: 30}
fmt.Println(p.name)
1.5 结构体嵌套
结构体字段可以是另一个结构体,用于描述层级关系(如用户包含地址信息)。
go
package main
import "fmt"
// 地址结构体
type Address struct {
city string
addr string
}
// 用户结构体(嵌套 Address)
type User struct {
name string
age int
address Address // 嵌套结构体
}
func main() {
u := User{
name: "赵六",
age: 22,
address: Address{
city: "杭州",
addr: "XX街道",
},
}
// 多层访问字段
fmt.Println(u.address.city)
}
1.6 结构体高频踩坑
- 字段访问权限:小写字段仅本包可用,跨包(如 JSON 序列化、外部函数)无法读取,序列化场景建议字段首字母大写;
- 值传递误区:普通结构体传参无法修改原数据,修改必须用指针;
- 字面量顺序错误:顺序初始化时,值的数量、顺序必须和结构体字段完全一致;
- 空指针解引用:结构体指针未赋值(nil)时,直接访问字段会触发运行 panic;
- 结构体比较 :同类型结构体可直接用
==比较(逐字段对比),包含切片/map 的结构体不能直接比较。
二、切片(slice)
切片是 Go 核心动态数组 ,基于数组封装而成,长度可变、支持自动扩容,是开发中使用频率最高的容器。切片属于引用类型,底层依赖固定长度的数组存储数据。
2.1 切片核心概念
- 长度(len) :切片当前存储的元素个数,
len(切片)获取; - 容量(cap) :切片底层数组最大可存储元素数,
cap(切片)获取; - 底层结构:切片内部包含 指向底层数组的指针、长度、容量。
2.2 切片创建方式(5种)
方式1:直接声明(nil 切片)
仅声明不初始化,切片默认值为 nil,长度和容量均为 0。
go
package main
import "fmt"
func main() {
var s []int // 声明int类型切片,nil切片
fmt.Printf("len=%d, cap=%d, s=%v, 是否nil:%t\n", len(s), cap(s), s, s == nil)
}
输出:len=0, cap=0, s=[], 是否nil:true
方式2:字面量初始化
直接指定元素,切片长度、容量等于元素个数。
go
s := []int{1, 2, 3, 4}
fmt.Printf("len=%d, cap=%d\n", len(s), cap(s)) // len=4, cap=4
方式3:基于数组/已有切片截取
语法:原容器[起始索引:结束索引]
规则:左闭右开,包含起始索引,不包含结束索引;省略起始索引默认从 0 开始,省略结束索引默认到末尾。
go
package main
import "fmt"
func main() {
// 底层数组
arr := [6]int{0, 1, 2, 3, 4, 5}
s1 := arr[1:4] // 索引1、2、3 → [1,2,3]
s2 := arr[:3] // 从0到2 → [0,1,2]
s3 := arr[2:] // 从2到末尾 → [2,3,4,5]
fmt.Println(s1, s2, s3)
}
重点:截取的切片共享底层数组,修改一个切片会影响同源切片/数组。
方式4:make 函数创建(工程最常用)
make 专门用于创建引用类型(切片、map、channel),语法:
go
make([]元素类型, 长度, 容量)
// 省略容量:容量 = 长度
make([]元素类型, 长度)
示例:
go
package main
import "fmt"
func main() {
// 长度3,容量5,元素默认零值 0
s1 := make([]int, 3, 5)
// 长度2,容量2
s2 := make([]int, 2)
fmt.Printf("s1: len=%d, cap=%d, %v\n", len(s1), cap(s1), s1)
fmt.Printf("s2: len=%d, cap=%d, %v\n", len(s2), cap(s2))
}
方式5:截取切片创建新切片
go
s := []int{10,20,30,40}
sNew := s[1:] // 截取原切片,共享底层数组
2.3 切片常用内置函数
1. append() 追加元素(动态扩容核心)
向切片末尾追加元素,当长度超过容量时,切片自动扩容 (底层新建更大数组,拷贝原数据)。
语法:切片 = append(切片, 元素1, 元素2...)
go
package main
import "fmt"
func main() {
var s []int
s = append(s, 1) // 追加单个元素
s = append(s, 2, 3, 4) // 追加多个元素
fmt.Println(s) // [1 2 3 4]
}
2. copy() 拷贝切片
用于深拷贝 切片数据,解决共享底层数组的问题。
语法:copy(目标切片, 源切片),拷贝长度以两者中较短的切片为准。
go
package main
import "fmt"
func main() {
src := []int{1, 2, 3}
dst := make([]int, 3) // 目标切片必须提前初始化
copy(dst, src) // 拷贝 src 到 dst
dst[0] = 999
fmt.Println("源切片:", src) // [1 2 3] 不受影响
fmt.Println("目标切片:", dst) // [999 2 3]
}
2.4 切片遍历
- 普通 for 循环(通过索引);
for-range遍历(Go 推荐写法,下文详细讲解)。
2.5 切片高频踩坑(重点)
- nil 切片与空切片区分
var s []int:nil 切片,s == nil为 true;s := []int{}:空切片,s == nil为 false;
两者都可正常使用append。
- 共享底层数组 :截取产生的切片会和原数组/切片共用内存,修改互相影响,需
copy隔离数据; - 索引越界 :访问
s[len(s)]会触发运行 panic; - 扩容规则:容量 < 1024 时,每次扩容 2 倍;容量 ≥ 1024 时,每次扩容 1.25 倍;
- make 容量小于长度:编译报错,容量必须 ≥ 长度;
- 函数传参:切片本身是引用类型,但切片头(指针/长度/容量)是值传递;修改切片元素全局生效,直接重新赋值切片不会影响原变量。
三、range 遍历语法
range 是 Go 专属遍历关键字,搭配 for 使用,专门遍历 数组、切片、字符串、map、通道(channel),遍历过程会返回索引/键、元素/值。
3.1 基础语法
go
// 标准写法:接收索引/键、值
for 索引/键, 值 := range 容器 {
循环逻辑
}
// 只接收索引/键
for 索引 := range 容器 {}
// 只接收值(使用空白标识符 _ 忽略索引)
for _, 值 := range 容器 {}
3.2 分场景遍历示例
3.1 遍历数组 & 切片(最常用)
返回:索引、元素值
go
package main
import "fmt"
func main() {
nums := []int{10, 20, 30}
// 完整接收索引和值
for idx, val := range nums {
fmt.Printf("索引:%d,值:%d\n", idx, val)
}
// 忽略索引,只取值
fmt.Println("只取值:")
for _, val := range nums {
fmt.Println(val)
}
// 忽略值,只取索引
fmt.Println("只取索引:")
for idx := range nums {
fmt.Println(idx)
}
}
3.2 遍历字符串
返回:字节索引、Unicode 字符(rune 类型),完美支持中文。
go
package main
import "fmt"
func main() {
str := "Go语言"
for idx, char := range str {
fmt.Printf("索引:%d,字符:%c\n", idx, char)
}
}
3.3 遍历 map
返回:键(key)、值(value) ,map 遍历无序。
go
package main
import "fmt"
func main() {
m := map[string]int{
"张三": 20,
"李四": 22,
}
for k, v := range m {
fmt.Printf("键:%s,值:%d\n", k, v)
}
}
3.4 遍历通道(channel)
遍历关闭后的通道,逐个读取通道数据,通道未关闭会阻塞。
go
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 100
ch <- 200
close(ch) // 必须关闭通道,否则遍历阻塞
for val := range ch {
fmt.Println(val)
}
}
3.3 range 核心踩坑(90%新手易错)
坑1:遍历变量是临时副本,修改无效
for _, v := range 切片 中,v 是每次迭代的临时拷贝 ,修改 v 不会改变原容器数据。
错误示例
go
package main
import "fmt"
func main() {
s := []int{1, 2, 3}
for _, v := range s {
v = v * 2 // 仅修改副本,原切片不变
}
fmt.Println(s) // [1 2 3]
}
正确写法(通过索引修改原数据)
go
s := []int{1, 2, 3}
for idx := range s {
s[idx] = s[idx] * 2
}
fmt.Println(s) // [2 4 6]
坑2:遍历结构体切片,修改元素无效
原理同上,结构体是值类型,迭代变量为副本:
go
package main
import "fmt"
type User struct {
name string
age int
}
func main() {
users := []User{{"小明", 18}, {"小红", 20}}
// 错误:修改副本
for _, u := range users {
u.age = 21
}
fmt.Println(users) // 数据不变
// 正确:使用索引
for idx := range users {
users[idx].age = 21
}
fmt.Println(users)
}
坑3:获取元素地址错误
迭代变量 v 地址始终不变(共用临时变量),不能用于存储元素指针。
go
// 错误示例
s := []int{1,2,3}
var ptrs []*int
for _, v := range s {
ptrs = append(ptrs, &v) // 所有指针指向同一个临时变量
}
// 正确示例:取原切片元素地址
for idx := range s {
ptrs = append(ptrs, &s[idx])
}
坑4:遍历中增删容器元素
遍历切片/map 时,不要同时增删元素,会导致遍历结果异常、漏元素或重复遍历。
四、知识点速查表
| 模块 | 核心要点 | 关键坑点 |
|---|---|---|
| 结构体 | 1. 复合值类型;2. 字段大小写控制访问;3. 值传递/指针传递;4. 支持嵌套 | 1. 小写字段跨包不可访问;2. 值传递无法修改原数据;3. 含切片/map 不能用 == 比较 |
| 切片 | 1. 引用类型,动态扩容;2. len(长度)、cap(容量);3. append 追加、copy 拷贝;4. 截取共享底层数组 | 1. 截取切片互相影响;2. 索引越界panic;3. nil/空切片区分 |
| range | 1. 遍历数组/切片/字符串/map/通道;2. 支持忽略索引/值;3. 迭代变量为临时副本 | 1. 直接修改遍历值无效;2. 遍历取地址出错;3. 遍历中增删元素异常 |