Go 学习第6天:结构体 + 切片 + range遍历

Go 语言:结构体 + 切片 + range遍历

目录

  1. 结构体(struct)
  2. 切片(slice)
  3. range 遍历语法
  4. 综合实操练习

一、结构体(struct)

结构体是 Go 中自定义复合数据类型 ,可组合不同类型的字段,用来描述一组关联属性(如用户、书籍、订单),类似其他语言的实体类。结构体是值类型,也是 Go 实现面向对象思想的基础载体。

1.1 结构体定义语法

使用 type + struct 关键字定义结构体类型,语法格式:

go 复制代码
type 结构体名 struct {
    字段名1 字段类型1
    字段名2 字段类型2
    ...
}

补充规则

  1. 字段名大小写控制访问权限:首字母大写 → 跨包可访问(公开);首字母小写 → 仅当前包访问(私有);
  2. 同一结构体内部字段名不可重复;
  3. 结构体可嵌套其他结构体、指针、切片等类型。

基础示例(书籍结构体)

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 结构体高频踩坑

  1. 字段访问权限:小写字段仅本包可用,跨包(如 JSON 序列化、外部函数)无法读取,序列化场景建议字段首字母大写;
  2. 值传递误区:普通结构体传参无法修改原数据,修改必须用指针;
  3. 字面量顺序错误:顺序初始化时,值的数量、顺序必须和结构体字段完全一致;
  4. 空指针解引用:结构体指针未赋值(nil)时,直接访问字段会触发运行 panic;
  5. 结构体比较 :同类型结构体可直接用 == 比较(逐字段对比),包含切片/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 切片遍历

  1. 普通 for 循环(通过索引);
  2. for-range 遍历(Go 推荐写法,下文详细讲解)。

2.5 切片高频踩坑(重点)

  1. nil 切片与空切片区分
    • var s []int:nil 切片,s == nil 为 true;
    • s := []int{}:空切片,s == nil 为 false;
      两者都可正常使用 append
  2. 共享底层数组 :截取产生的切片会和原数组/切片共用内存,修改互相影响,需 copy 隔离数据;
  3. 索引越界 :访问 s[len(s)] 会触发运行 panic;
  4. 扩容规则:容量 < 1024 时,每次扩容 2 倍;容量 ≥ 1024 时,每次扩容 1.25 倍;
  5. make 容量小于长度:编译报错,容量必须 ≥ 长度;
  6. 函数传参:切片本身是引用类型,但切片头(指针/长度/容量)是值传递;修改切片元素全局生效,直接重新赋值切片不会影响原变量。

三、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. 遍历中增删元素异常
相关推荐
读书札记20221 小时前
Qt中windeployqt.exe工具的使用:解决使用CMake创建的项目点击exe文件后系统提示0xc000007b的问题
开发语言·qt
xiaoshuaishuai81 小时前
C# 定制化Markdown编辑器
开发语言·c#·编辑器
DogDaoDao1 小时前
C++核心技术深度剖析:从底层原理到工程实践
开发语言·c++·面试·程序员·指针·虚函数
磊 子2 小时前
C++移动语义和智能指针
java·开发语言·c++
不负岁月无痕2 小时前
C++继承与多态知识点及其高频面试问题
开发语言·c++·面试
June`2 小时前
如何组织一个并行程序
开发语言·cuda
咸甜适中2 小时前
rust语言学习笔记Trait(十七)Send、Sync(线程间数据所有权)
笔记·学习·rust
H__Rick2 小时前
C51学习-DAY7
单片机·嵌入式硬件·学习·51单片机
dtq04242 小时前
C语言刷题函数1-判断素数(分支语句,函数两种方法)
c语言·开发语言·学习