当前位置: 首页 > news >正文

Go 基础:结构体与切片

第一部分:结构体(Struct)

1.1 什么是结构体

结构体是 Go 语言中用来组合多个不同类型字段的复合数据类型。你可以把它理解为一个"模板"或"蓝图",用来描述一个事物的多个属性。

package main ​ import "fmt" ​ // 定义一个结构体类型 type Vertex struct { X int Y int } ​ func main() { v := Vertex{1, 2} fmt.Println(v) // {1 2} }

底层逻辑:

  • 结构体在内存中是连续分配的,字段按照声明顺序依次排列

  • 每个字段占据固定大小的内存空间

  • 结构体是值类型:赋值时会复制整个结构体的数据

关键概念:

  • type关键字定义新类型

  • struct关键字声明这是一个结构体

  • 大写字母开头的字段名(如XY)是导出的(可被其他包访问)

  • 小写字母开头的字段名只能在当前包内访问


1.2 结构体字段访问

通过点号.来访问结构体的字段,可以读取也可以修改。

package main ​ import "fmt" ​ type Vertex struct { X int Y int } ​ func main() { v := Vertex{1, 2} ​ v.X = 4 // 修改 X 字段为 4 fmt.Println(v.X) // 输出: 4 fmt.Println(v.Y) // 输出: 2 }

底层逻辑:

  • 点号.运算符在编译时会计算字段相对于结构体起始地址的偏移量

  • 访问v.X时,Go 编译器会生成类似*(base_address + offset_of_X)的机器码

  • 结构体字段在内存中的布局与声明顺序一致


1.3 指向结构体的指针

你可以使用指针来引用结构体。指针存储的是结构体在内存中的地址,而不是结构体的副本。

package main ​ import "fmt" ​ type Vertex struct { X int Y int } ​ func main() { v := Vertex{1, 2} p := &v // p 是指向 v 的指针 ​ p.X = 1e9 // 通过指针修改字段,Go 会自动解引用 fmt.Println(v) // 输出: {1000000000 2} }

底层逻辑:

  • &v取结构体 v 的内存地址

  • p.X实际上是(*p).X的语法糖,Go 自动帮你解引用

  • 通过指针修改字段时,修改的是原始结构体,不是副本

  • 传递指针给函数时,函数内修改会影响原始结构体(避免大结构体复制的性能开销)

何时使用指针:

  • 需要在函数中修改原始结构体时

  • 结构体很大,复制成本高时

  • 需要共享同一个结构体实例时


1.4 结构体字面值(Struct Literals)

创建结构体时可以用字面值语法,有两种写法:

package main ​ import "fmt" ​ type Vertex struct { X, Y int } ​ var ( // 写法一:按字段顺序列出值(必须按声明顺序) v1 = Vertex{1, 2} // 类型是 Vertex ​ // 写法二:用字段名指定值(推荐,更清晰,顺序可以打乱) v2 = Vertex{X: 1} // Y 默认为零值 0 ​ // 写法三:完全省略字段名(按顺序,不推荐用于字段多的情况) v3 = Vertex{} // X:0, Y:0,全是零值 ​ // 写法四:指针类型 p = &Vertex{1, 2} // 类型是 *Vertex ) ​ func main() { fmt.Println(v1, v2, v3, p) }

关键规则:

  • 如果用了字段名: 值的语法,字段顺序可以随意,未指定的字段自动为零值

  • 如果不用字段名,必须严格按照声明顺序填写所有字段

  • &Vertex{1, 2}返回的是指向结构体的指针

  • 零值规则:int→0, float→0.0, string→"", bool→false, 指针→nil


第二部分:切片(Slice)

2.1 什么是切片

切片是对数组的一个连续片段的引用,它是 Go 语言中最常用的数据结构之一。

package main ​ import "fmt" ​ func main() { // 先创建一个数组 primes := [6]int{2, 3, 5, 7, 11, 13} ​ // 从数组创建一个切片:取索引 1 到 3(不含 4) var s []int = primes[1:4] ​ fmt.Println(s) // [3 5 7] }

底层逻辑(重点):切片在底层由三个部分组成(slice header):

struct SliceHeader { Data uintptr // 指向底层数组的指针 Len int // 切片的长度(当前有多少个元素) Cap int // 切片的容量(从起始位置到底层数组末尾的元素个数) }
  • 切片本身不存储数据,它只是对底层数组的"窗口"或"视图"

  • primes[1:4]表示从索引 1 开始,到索引 4 结束(不含),所以取的是primes[1]primes[2]primes[3]

  • 切片的下界可以省略(默认为 0),上界也可以省略(默认为数组长度)


2.2 切片就像数组的引用

切片不拥有数据,它引用的是底层数组。修改切片的元素会修改底层数组,反之亦然。

package main ​ import "fmt" ​ func main() { names := [4]string{ "John", "Paul", "George", "Ringo", } fmt.Println(names) ​ a := names[0:2] // [John Paul] b := names[1:3] // [Paul George] ​ fmt.Println(a, b) ​ // 修改 b[0],实际上修改的是底层数组的 names[1] b[0] = "XXX" ​ fmt.Println(a, b) fmt.Println(names) // [John XXX George Ringo] —— a 也受到了影响! }

底层逻辑:

  • ab共享同一个底层数组

  • a的 Data 指针指向names[0]b的 Data 指针指向names[1]

  • 修改b[0]就是修改底层数组的names[1]a也能看到这个变化

  • 这就是为什么切片叫"引用"——多个切片可以指向同一段内存


2.3 切片字面值

切片字面值的语法和数组类似,但不需要指定长度。

package main ​ import "fmt" ​ func main() { // 切片字面值:不需要指定长度 q := []int{2, 3, 5, 7, 11, 13} fmt.Println(q) ​ // 修改元素 q[0] = 100 fmt.Println(q) // [100 3 5 7 11 13] }

与数组的区别:

  • 数组:[6]int{...}—— 方括号里有数字,长度是类型的一部分

  • 切片:[]int{...}—— 方括号里没有数字,长度是动态的


2.4 切片的默认行为

切片时省略边界,Go 会使用合理的默认值。

package main import "fmt" func main() { s := []int{2, 3, 5, 7, 11, 13} printSlice(s) // len=6 cap=6 [2 3 5 7 11 13] // 省略下界,默认为 0 s = s[:0] printSlice(s) // len=0 cap=6 [] // 省略上界,默认为切片的长度 s = s[:4] printSlice(s) // len=4 cap=6 [2 3 5 7] // 两个都省略,等价于 s[0:len(s)] s = s[2:] printSlice(s) // len=2 cap=4 [5 7] } func printSlice(s []int) { fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) }

默认值规则总结:

写法等价于说明
s[:n]s[0:n]从头取 n 个元素
s[n:]s[n:len(s)]从 n 取到末尾
s[:]s[0:len(s)]取全部元素

2.5 切片的长度与容量

长度(len):切片当前包含的元素个数。容量(cap):从切片的第一个元素开始,到底层数组末尾的元素个数。

package main import "fmt" func main() { s := []int{2, 3, 5, 7, 11, 13} printSlice(s) // len=6 cap=6 [2 3 5 7 11 13] // 把长度设为 0,但容量不变 s = s[:0] printSlice(s) // len=0 cap=6 [] // 扩展长度到 4,容量仍然足够 s = s[:4] printSlice(s) // len=4 cap=6 [2 3 5 7] // 丢弃前两个元素,长度和容量都减少 s = s[2:] printSlice(s) // len=2 cap=4 [5 7] } func printSlice(s []int) { fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) }

底层逻辑图解:

初始: s = []int{2, 3, 5, 7, 11, 13} 底层数组: [2, 3, 5, 7, 11, 13] ↑ ↑ | | Data指向 数组末尾 len=6, cap=6 s = s[:0]: len=0, cap=6(容量不变,只是"窗口"缩小了) s = s[:4]: len=4, cap=6(扩展"窗口",但不能超过容量) s = s[2:]: len=2, cap=4(Data指针右移2位,容量从新起点算起)

关键规则:

  • 切片不能超过其容量,否则会 panic

  • len(s) <= cap(s)永远成立

  • 重新切片可以延长长度,前提是容量足够


2.6 零切片(Nil Slice)

切片的零值是nil。nil 切片没有底层数组,长度和容量都为 0。

package main import "fmt" func main() { var s []int // 没有初始化,s 是 nil fmt.Println(s, len(s), cap(s)) // [] 0 0 if s == nil { fmt.Println("nil!") // 会执行到这里 } }

注意事项:

  • var s []int声明但未初始化 → s 为 nil

  • s := []int{}s := make([]int, 0)→ s 不是 nil,只是长度为 0

  • nil 切片和空切片的区别:

    • nil 切片:没有底层数组,JSON 序列化时为null

    • 空切片:有底层数组(长度为 0),JSON 序列化时为[]

  • 对 nil 切片使用append是安全的,会自动分配底层数组


2.7 用 make 创建切片

make是 Go 的内置函数,用来创建动态大小的切片。它会在底层分配一个数组并返回指向它的切片。

package main import "fmt" func main() { // make([]类型, 长度) —— 长度=容量 a := make([]int, 5) printSlice("a", a) // a len=5 cap=5 [0 0 0 0 0] // make([]类型, 长度, 容量) —— 长度和容量不同 b := make([]int, 0, 5) printSlice("b", b) // b len=0 cap=5 [] // 扩展 b 的长度到容量 b = b[:cap(b)] printSlice("b", b) // b len=5 cap=5 [0 0 0 0 0] // 丢弃第一个元素 b = b[1:] printSlice("b", b) // b len=4 cap=4 [0 0 0 0] } func printSlice(name string, x []int) { fmt.Printf("%s len=%d cap=%d %v\n", name, len(x), cap(x), x) }

关键要点:

  • make([]int, 5)→ len=5, cap=5,元素全部为零值

  • make([]int, 0, 5)→ len=0, cap=5,适合后续用 append 添加元素

  • make分配的是归零的数组,所有元素初始值为类型的零值

  • 第三个参数(容量)是可选的,不传则容量=长度


2.8 切片的切片(多维切片)

切片可以包含任何类型,包括其他切片。这就形成了多维切片。

package main import ( "fmt" "strings" ) func main() { // 创建一个 3x3 的井字棋盘(二维切片) board := [][]string{ []string{"_", "_", "_"}, []string{"_", "_", "_"}, []string{"_", "_", "_"}, } // 玩家轮流下棋 board[0][0] = "X" board[2][2] = "O" board[1][2] = "X" board[1][0] = "O" board[0][2] = "X" // 打印棋盘 for i := 0; i < len(board); i++ { fmt.Printf("%s\n", strings.Join(board[i], " ")) } }

输出:

X _ X O _ X _ _ O

底层逻辑:

  • [][]string是一个"切片的切片",外层切片的每个元素都是一个[]string

  • 每一行(内层切片)可以有不同的长度(这就是"不规则"多维数组)

  • 访问board[i][j]时,先取外层切片的第 i 个元素(一个切片),再取这个切片的第 j 个元素


2.9 追加元素到切片(append)

Go 提供了内置函数append来向切片添加元素。

package main import "fmt" func main() { var s []int // nil 切片 printSlice(s) // len=0 cap=0 [] // append 可以作用于 nil 切片 s = append(s, 0) printSlice(s) // len=1 cap=1 [0] // 切片会根据需要自动增长 s = append(s, 1) printSlice(s) // len=2 cap=2 [0 1] // 可以一次添加多个元素 s = append(s, 2, 3, 4) printSlice(s) // len=5 cap=8 [0 1 2 3 4] } func printSlice(s []int) { fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) }

append 的函数签名:

func append(s []T, vs ...T) []T

底层逻辑(重点):

  1. append接收一个切片和若干个值,返回一个新的切片

  2. 如果底层数组还有剩余容量(len < cap),append直接在数组末尾添加元素,返回的切片指向同一个底层数组

  3. 如果容量不够(len == cap),append会:

    • 分配一个更大的新数组(通常是原来容量的 2 倍)

    • 把旧数据复制到新数组

    • 在新数组末尾添加新元素

    • 返回指向新数组的切片

  4. 重要:append可能返回一个新的切片(指向新的底层数组),所以必须用s = append(s, v)接收返回值

容量增长策略:

  • 旧容量 < 1024 时:新容量 = 旧容量 × 2

  • 旧容量 >= 1024 时:新容量 ≈ 旧容量 × 1.25


2.10 range 遍历切片

for...range循环可以遍历切片(和数组、map、字符串等)。每次迭代返回两个值:索引和元素的副本。

pack age main import "fmt" func main() { var pow = []int{1, 2, 4, 8, 16, 32, 64, 128} for i, v := range pow { fmt.Printf("2**%d = %d\n", i, v) } }

输出:

2**0 = 1 2**1 = 2 2**2 = 4 2**3 = 8 ...

range 的灵活用法:

// 只取索引,忽略值 for i, _ := range pow { // 使用 i } // 只取值,忽略索引(用 _ 占位) for _, value := range pow { // 使用 value } // 只想要索引,可以省略第二个变量 for i := range pow { // 使用 i }

底层逻辑:

  • range返回的值是元素的副本,不是引用

  • 修改v不会影响原切片中的元素

  • 如果要修改原切片的元素,需要用索引:pow[i] = newValue


2.11 range 进阶用法

package main import "fmt" func main() { // 用 make 创建一个长度为 10 的切片 pow := make([]int, 10) // 只用索引遍历,用索引计算值 for i := range pow { pow[i] = 1 << uint(i) // 1 左移 i 位,等于 2**i } // 只取值遍历,忽略索引 for _, value := range pow { fmt.Printf("%d\n", value) } }

输出:

1 2 4 8 16 32 64 128 256 512

关键技巧:

  • 1 << uint(i)是位运算,1 左移 i 位,结果是 2 的 i 次方

  • 需要把i(int 类型)转换为uint类型,因为移位运算符要求无符号整数

  • range可以省略值变量,只保留索引变量


2.12 综合练习:用切片生成图片

这是一个综合练习,演示了二维切片、make创建、range遍历、类型转换的综合运用。

package main import "golang.org/x/tour/pic" func Pic(dx, dy int) [][]uint8 { // 创建一个 dy 行的二维切片 img := make([][]uint8, dy) // 遍历每一行 for y := range img { // 每行创建 dx 个元素的切片 img[y] = make([]uint8, dx) // 遍历每一列,设置像素值 for x := range img[y] { img[y][x] = uint8(x * y) // 类型转换:int → uint8 } } return img } func main() { pic.Show(Pic) }

知识点总结:

  • make([][]uint8, dy)创建外层切片(dy 行)

  • 每行需要单独make([]uint8, dx)创建内层切片

  • uint8(x * y)是类型转换,把 int 结果转为 uint8

  • 这个练习让你理解如何在 Go 中构建和操作多维数据结构


第三部分:结构体 vs 切片 对比总结

特性结构体(Struct)切片(Slice)
本质值类型,内存连续引用类型(指向底层数组)
创建方式Vertex{1, 2}&Vertex{1, 2}make([]T, len, cap)或字面值
零值所有字段为零值nil
长度/容量固定(编译时确定)动态(len 和 cap)
扩展不支持append()自动扩展
内存分配栈或堆(取决于逃逸分析)底层数组一定在堆上
复制行为赋值时完整复制赋值时只复制 slice header(24字节)
典型用途表示有固定属性的实体表示可变长度的数据集合

第四部分:常见易错点

4.1 切片共享底层数组的陷阱

a := []int{1, 2, 3, 4, 5} b := a[:3] // [1, 2, 3] b[0] = 999 // a 也变成了 [999, 2, 3, 4, 5]!

解决:如果需要独立副本,用copy()函数。

4.2 append 可能改变底层数组

a := make([]int, 3, 5) // len=3, cap=5 b := a // b 和 a 共享底层数组 b = append(b, 4) // 容量够,共享数组 b = append(b, 5) // 容量够,共享数组 b = append(b, 6) // 容量不够!分配新数组,b 指向新数组,a 不变

解决:永远用s = append(s, v)接收返回值。

4.3 range 返回的是副本

s := []int{1, 2, 3} for _, v := range s { v = 100 // 只修改了副本,s 不变 } // s 仍然是 [1, 2, 3]

解决:用索引修改:for i := range s { s[i] = 100 }

4.4 结构体指针 vs 值

func modify1(v Vertex) { v.X = 100 } // 修改的是副本,不影响原值 func modify2(v *Vertex) { v.X = 100 } // 修改的是原值 v := Vertex{1, 2} modify1(v) // v 不变 modify2(&v) // v.X 变成 100

http://www.gsyq.cn/news/1610405.html

相关文章:

  • AI Agent 工具调用中间件:Go 实现截断、超时与熔断
  • Transformer 理解
  • Speck2f神经形态芯片与低功耗瞳孔追踪系统解析
  • Arm CCA与CAEC:机密计算中的高效内存共享技术
  • NCM音乐文件解锁神器:3分钟极速转换的终极指南
  • 医学图像分割中的域泛化挑战与SRCSM解决方案
  • 如何构建企业级数据集成管道:Pentaho Kettle核心功能深度解析
  • 批量制作门店短视频工具推荐,鹿小云混剪高效拓客
  • 保姆级教程:用群晖Drive+cpolar,把Obsidian笔记库变成你的私有云知识库
  • CrabCode v1.0.9 更新速览!一次集中打磨,体验更清爽!
  • 从GD32VF103到HPM6000:手把手教你选型国产RISC-V单片机(附开发环境清单)
  • 微服务架构迁移:后端团队应该避免的常见陷阱
  • SpringBoot+Vue 旅游出行指南_ms ()abo平台完整项目源码+SQL脚本+接口文档【Java Web毕设】
  • 从零玩转Metasploit Framework:渗透测试核心平台实战指南
  • JDspyder京东抢购脚本:3分钟快速上手指南,轻松实现茅台秒杀自动化
  • Citrix Netscaler高危漏洞CVE-2025-12101:原理、修复与加固指南
  • 量子电路优化:强化学习在NISQ时代的应用与挑战
  • 未来展望:openEuler/easybox路线图与未实现命令的优先支持计划 [特殊字符]
  • 怎样高效使用BallonTranslator:面向新手的深度学习漫画翻译方案
  • 医院信息系统(HIS)
  • 深度学习加速器架构:混合精度计算与张量核心优化
  • 如何配置Kiran会话管理器:从基础设置到高级调优的7个技巧
  • 终极指南:5分钟让PlayStation手柄在Windows游戏上完美运行
  • FPGA稀疏卷积优化:SparsePixels框架解析与应用
  • 新手自动化测试入门:5个精选练手项目与实战框架搭建指南
  • 如何快速检测微信单向好友:5分钟找出谁删除了你
  • Windows 11终极清理指南:5分钟让电脑重获新生
  • 影刀RPA新手教程:大众点评数据采集完全指南——店铺信息、用户评价与竞争对手分析
  • 影刀RPA新手教程:列表完全指南——什么是列表、怎么往里加东西、怎么取出来
  • 告别CMAC!NIST SP800-108新版密钥派生实战:手把手教你用KMAC128/256