欢迎光临
我们一直在努力

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

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

    • 目录
  • 一、结构体(struct)
    • 1.1 结构体定义语法
      • 补充规则
      • 基础示例(书籍结构体)
    • 1.2 结构体变量创建与初始化
      • 方式1:先声明,再逐个赋值(最通用)
      • 方式2:字面量初始化(按字段顺序赋值)
      • 方式3:键值对初始化(推荐,可读性强)
      • 方式4:短声明结合空结构体
    • 1.3 结构体作为函数参数
      • 示例1:值传递(无法修改原数据)
      • 示例2:指针传递(可修改原数据,工程常用)
    • 1.4 结构体指针
      • 1. 定义与使用
      • 2. 直接创建结构体指针实例
    • 1.5 结构体嵌套
    • 1.6 结构体高频踩坑
  • 二、切片(slice)
    • 2.1 切片核心概念
    • 2.2 切片创建方式(5种)
      • 方式1:直接声明(nil 切片)
      • 方式2:字面量初始化
      • 方式3:基于数组/已有切片截取
      • 方式4:make 函数创建(工程最常用)
      • 方式5:截取切片创建新切片
    • 2.3 切片常用内置函数
      • 1. append() 追加元素(动态扩容核心)
      • 2. copy() 拷贝切片
    • 2.4 切片遍历
    • 2.5 切片高频踩坑(重点)
  • 三、range 遍历语法
    • 3.1 基础语法
    • 3.2 分场景遍历示例
      • 3.1 遍历数组 & 切片(最常用)
      • 3.2 遍历字符串
      • 3.3 遍历 map
      • 3.4 遍历通道(channel)
    • 3.3 range 核心踩坑(90%新手易错)
      • 坑1:遍历变量是临时副本,修改无效
      • 坑2:遍历结构体切片,修改元素无效
      • 坑3:获取元素地址错误
      • 坑4:遍历中增删容器元素
  • 四、知识点速查表

目录

  • 结构体(struct)
  • 切片(slice)
  • range 遍历语法
  • 综合实操练习

  • 一、结构体(struct)

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

    1.1 结构体定义语法

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

    type 结构体名 struct {
    字段名1 字段类型1
    字段名2 字段类型2

    }

    补充规则

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

    package main
    import "fmt"

    // 定义书籍结构体
    type Books struct {
    title string // 书籍名称(私有,本包可见)
    Author string // 作者(大写,跨包可见)
    subject string // 科目
    bookID int // 书籍编号
    }

    func main() {
    fmt.Println("结构体定义完成")
    }

    1.2 结构体变量创建与初始化

    定义结构体后,可通过多种方式创建实例并赋值,共4种常用写法。

    方式1:先声明,再逐个赋值(最通用)

    通过 . 点运算符访问/修改结构体字段。

    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:字面量初始化(按字段顺序赋值)

    严格按照结构体字段定义顺序传值,不可跳过字段。

    book2 := Books{"Python教程", "张三", "编程", 1002}
    fmt.Println(book2)

    方式3:键值对初始化(推荐,可读性强)

    指定 字段名: 值,顺序可随意,未赋值字段自动填充零值。

    // 仅赋值部分字段,剩余字段为零值
    book3 := Books{
    title: "Java教程",
    bookID: 1003,
    }
    fmt.Println(book3)

    方式4:短声明结合空结构体

    book4 := Books{} // 所有字段为零值
    fmt.Println(book4)

    1.3 结构体作为函数参数

    结构体默认是值传递,函数内部修改的是副本,不会影响原变量;如需修改原结构体,使用结构体指针传参。

    示例1:值传递(无法修改原数据)

    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:指针传递(可修改原数据,工程常用)

    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. 定义与使用

    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. 直接创建结构体指针实例

    // 简写方式,直接分配内存并返回指针
    p := &User{name: "王五", age: 30}
    fmt.Println(p.name)

    1.5 结构体嵌套

    结构体字段可以是另一个结构体,用于描述层级关系(如用户包含地址信息)。

    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。

    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:字面量初始化

    直接指定元素,切片长度、容量等于元素个数。

    s := []int{1, 2, 3, 4}
    fmt.Printf("len=%d, cap=%d\\n", len(s), cap(s)) // len=4, cap=4

    方式3:基于数组/已有切片截取

    语法:原容器[起始索引:结束索引] 规则:左闭右开,包含起始索引,不包含结束索引;省略起始索引默认从 0 开始,省略结束索引默认到末尾。

    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),语法:

    make([]元素类型, 长度, 容量)
    // 省略容量:容量 = 长度
    make([]元素类型, 长度)

    示例:

    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:截取切片创建新切片

    s := []int{10,20,30,40}
    sNew := s[1:] // 截取原切片,共享底层数组

    2.3 切片常用内置函数

    1. append() 追加元素(动态扩容核心)

    向切片末尾追加元素,当长度超过容量时,切片自动扩容(底层新建更大数组,拷贝原数据)。 语法:切片 = append(切片, 元素1, 元素2…)

    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(目标切片, 源切片),拷贝长度以两者中较短的切片为准。

    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 基础语法

    // 标准写法:接收索引/键、值
    for 索引/,:= range 容器 {
    循环逻辑
    }

    // 只接收索引/键
    for 索引 := range 容器 {}

    // 只接收值(使用空白标识符 _ 忽略索引)
    for _,:= range 容器 {}

    3.2 分场景遍历示例

    3.1 遍历数组 & 切片(最常用)

    返回:索引、元素值

    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 类型),完美支持中文。

    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 遍历无序。

    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)

    遍历关闭后的通道,逐个读取通道数据,通道未关闭会阻塞。

    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 不会改变原容器数据。 错误示例

    package main
    import "fmt"

    func main() {
    s := []int{1, 2, 3}
    for _, v := range s {
    v = v * 2 // 仅修改副本,原切片不变
    }
    fmt.Println(s) // [1 2 3]
    }

    正确写法(通过索引修改原数据)

    s := []int{1, 2, 3}
    for idx := range s {
    s[idx] = s[idx] * 2
    }
    fmt.Println(s) // [2 4 6]

    坑2:遍历结构体切片,修改元素无效

    原理同上,结构体是值类型,迭代变量为副本:

    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 地址始终不变(共用临时变量),不能用于存储元素指针。

    // 错误示例
    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. 遍历中增删元素异常
    赞(0)
    未经允许不得转载:171主机测评 » Go 学习第6天:结构体 + 切片 + range遍历
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址