AdvanceGoLang Learn Data Day 4
GoLang似乎是很有意思的东西,写起来手感比py好多了。最近有奇思妙想,决定做一个好玩的东西出来,顺便就作为自己的简历的项目了。
个人感觉,Go语言比C++难一点,虽然有可能是错觉啦。

函数的进阶:闭包与变量赋值
1. 闭包与递归的本质界定
两者分属不同的计算机科学范畴,解决不同维度的工程问题:
- 递归(属控制流范畴): 指函数在执行流中直接或间接调用自身,核心在于逻辑的重复与分治,依赖严格的终止条件以避免调用栈溢出。
- 闭包(属状态管理范畴): 指函数与其被创建时所处的词法环境的结合体。其核心在于状态的持久化,允许函数脱离其原始执行上下文后,依然能够隐式持有并修改原作用域内的局部变量。
简单的说,闭包里的变量是可持久化的。
2. 函数作为变量赋值的底层原理
函数可以作为变量进行赋值和使用的底层原理,其核心在于:"函数本质上是内存中的一段可执行指令,而变量存储的是这段指令的入口地址"。
具体可通过以下四个技术维度进行解析:
- 内存驻留: 源代码经编译器转化为机器指令后,会被操作系统统一加载到进程内存的代码段中。每个函数在代码段中都有一个唯一的起始内存地址,即"入口点"。
- 指针本质: 当我们将函数赋值给某个变量时,底层并没有复制任何代码逻辑,而是将该函数在代码段中的入口地址(内存指针)赋值给了这个变量。此时,该变量实质上是一个函数指针(Function Pointer)。
- 跳转执行: 当通过变量名加上括号(如
f())发起调用时,编译器会将其翻译为 CPU 的间接跳转指令。CPU 读取该内存地址,建立新的函数栈帧以传递参数,并开始执行该地址处的机器指令。 - 闭包的底层扩展: 为了支持"闭包",函数变量的底层实现通常是一个复杂的结构体。它包含两部分:指令指针 (指向函数执行逻辑)和环境指针(指向该函数所捕获的外部局部变量)。
结论: 函数能作为变量赋值,是因为在计算机底层架构中,"数据"和"指令"在内存层面是统一的。变量不仅可以存储数据的值,同样也可以存储指令序列的内存地址。
切片和数组的关系
1. 传参引发的问题
go
func getall(arr []int) int {
var res = 0
for _, va := range arr {
res += va
}
return res
}
func main(){
var number1 = [...]int{1}
var number2 = [...]int{1, 2}
fmt.Println(getall(number1) + getall(number2))
}
这段代码是错误的!原因是函数形参是切片类型,而
[...]int这个东西是个数组,因此不可传递。
- 解决方法 1: 直接把
[...]里的...删掉,使其变成切片,这样就可以使用了。- 解决方法 2: 把数组变成切片再传入函数就可以了:
getall(number1[:])。
2. 切片的底层结构与机制
- 底层结构 (24字节): 切片并非真实数组,而是底层连续内存的引用描述符。固定包含 3 个字段:
Data(指针) 、Len(逻辑长度) 、Cap(物理容量)。作为参数传递时极具效率,因其仅发生这 24 字节的值拷贝。 - 动态扩容机制:
append操作未触顶时(Len < Cap),直接写入新值;触顶时(Len == Cap),触发扩容堆分配机制,申请更大内存并拷贝旧数据。 - 内存共享副作用: 极易引发状态污染。衍生新切片默认共享底层内存;
append后是否共享取决于是否触发扩容。工程上建议强制使用原变量接收返回值 ,或使用copy()进行深拷贝。
切片就像 C++ 的 vector,但是也不完全相同。由于切片类似一个视窗,并非原始数据,因此增删查改的操作非常快。内部有 len,cap,ptr,总计 24 字节。
附上 Slice 的一个快速初始化操作:
go
go// 初始化一个大小为 100100 且元素全为 1 的切片
var slicenum = make([]int, 100100)
for p := range slicenum {
slicenum[p] = 1
}
接口
1. 接口核心机制
- 隐式实现: 摒弃传统的显式继承,类型只需实现某接口定义的所有方法签名,即隐式判定实现了此接口。解耦了定义与具体实现。
- 空接口: 不包含任何方法声明的接口(
interface{}或any),所有数据类型均自动实现空接口。 - 类型断言: 针对接口变量,可通过
v, ok := x.(T)或switch v := x.(type)进行动态类型检查与向下转型。 - 工程准则: 接收接口,返回结构体。
2. 多态实战代码
go
package main
import "fmt"
// 1. 定义接口(提需求/定规则)
type Speaker interface {
Speak() string
}
// 2. 定义结构体(结构体完全不知道 Speaker 接口的存在,只管存数据)
type Dog struct { Name string }
type Cat struct { Name string }
// 3. 绑定方法(隐式实现)
func (d Dog) Speak() string {
return d.Name + ":旺旺"
}
func (c Cat) Speak() string {
return c.Name + ":咪咪"
}
// 4. 多态函数(参数类型写接口,屏蔽底层细节)
func MakeSound(s Speaker) string {
// s 究竟是狗还是猫?不重要,底层会自动找到对应的真实方法并执行
return s.Speak()
}
func main() {
gou := Dog{Name: "旺财"}
mao := Cat{Name: "汤圆"}
fmt.Println(MakeSound(gou)) // 输出:旺财:旺旺
fmt.Println(MakeSound(mao)) // 输出:汤圆:咪咪
}
泛型
下面给出一段泛型的代码:
go
package main
import "fmt"
//[T any]是泛型声明,代表T可以是任意类型
//func PrintSlice[T any](arr []T) {
func PrintSlice[T int | string](arr []T) {
for _, v := range arr {
fmt.Print(v, " ")
}
fmt.Println()
}
func main() { //泛型编程
PrintSlice([]int{1, 2, 3, 4, 5})
PrintSlice([]string{"小王", "小李"})
}
Go 的泛型底层采用 GC Shape Stenciling 与 字典传递 相结合的混合机制:
- 编译器会根据实际传入的类型局部实例化机器码。
- 为避免代码膨胀,内存布局特征完全一致的类型(GC Shape)将共享同一份底层汇编代码。
- 调用时隐式注入类型字典指针,以便在运行时动态获取真实类型的特性。
错误处理
这里给出一段基本的错误处理代码:
go
package main
import (
"errors"
"fmt"
)
// 模拟一个扣款函数
func pay(balance int, amount int) (int, error) {
if amount > balance {
//制造一个错误,正常结果返回0
return 0, errors.New("余额不足")
}
//没出错就返回空指针
return balance - amount, nil
}
func main() {
newBalance, err := pay(100, 500)
if err != nil {
fmt.Println("交易失败,原因:", err)
return
}
fmt.Println("交易成功,余额:", newBalance)
}
引入
errors,判断err是否为空(nil)就行了。
假设 有错误极为致命,需要立即退出程序,使用panic(恐慌),这个不需要errors
同样的,下面给出一段基本的代码:
go
package main
import (
"fmt"
)
// 模拟一个扣款函数
func pay(balance int, amount int) (int, error) {
if amount > balance {
//制造一个错误,正常结果返回0
panic("致命错误,没钱了")
//return 0, errors.New("余额不足")
}
//没出错就返回空指针
return balance - amount, nil
}
func main() {
newBalance, err := pay(100, 500)
//使用panic之后,再也不会执行到下面了
if err != nil {
fmt.Println("交易失败,原因:", err)
return
}
fmt.Println("交易成功,余额:", newBalance)
}
大部分情况 ,我们都不希望程序全部退出,因此使用defer+recover可以保证主程序依旧运行
同样的,下面给出一段代码:
go
package main
import "fmt"
func getaerror() {
//defer这段代码一定会在当前这段函数结束之后才会执行
//就算被panic退出也会执行
defer func() {
//尝试拦截panic
if r := recover(); r != nil {
fmt.Println("拦截到错误,错误是:", r)
}
}()
panic("越界错误")
}
func main() {
getaerror()
fmt.Println("主程序依旧执行")
}
defer会在当前所在的函数结束之后执行,recover是专门用来拦截panic的内置函数,使代码从崩溃态变成正常态,defer最后的那个括号是立即调用的意思
高并发编程
1. 协程与多线程的区别
- C++ 传统多线程:属于**内核态物理线程。由操作系统直接调度,极其笨重。创建时通常预分配 1MB~8MB 内存栈,切换开销极大。
- Go 协程:属于 用户态轻量级逻辑线程**。由 Go 的 Runtime 全权接管,初始内存栈仅 2KB(支持动态扩容)。
- 底层支撑 (GMP调度模型) :协程脱离操作系统调度,通过 M:N 的映射机制(将海量 M 个协程,复用调度到 N 个物理线程上),实现极低上下文切换开销。
- M (Machine):操作系统的物理线程。
- G (Goroutine):协程任务。
- P (Processor):逻辑处理器(装满等待执行 G 的队列)。
2. 并发同步器
**** 当主程序结束,所有协程立刻结束。因此我们需要一个工具等一等协程。
wg.Done()本质上是Add(-1)。但在不知道有多少个协程(动态数量)的情况下,绝对不能 将Add(1)写在协程内部,因为协程启动需要时间,主程序可能直接跑完了!
go
package main
import (
"fmt"
"sync" // Synchronization(同步)
"time"
)
func main() {
var wg sync.WaitGroup
wg.Add(2) // 有两个并发任务,必须写在外部
go func() {
defer wg.Done() // 该协程任务完成前汇报
fmt.Println("任务 1 正在执行...")
time.Sleep(1 * time.Second)
}()
go func() {
defer wg.Done()
fmt.Println("任务 2 正在执行...")
time.Sleep(1 * time.Second)
}()
fmt.Println("主程序等待任务完成")
wg.Wait()
fmt.Println("所有任务完成,安全退出")
}
3. 多线程的管道通信
特别注意,接收通道数据的操作是阻塞的,如果管道没有东西就会永远等待!
go
package main
import "fmt"
func main() {
// 创造一根管道,只能使 int 类型流动
ch := make(chan int)
go func() {
fmt.Println("后台协程计算中...")
result := 42
ch <- result // 将数据塞入管道
fmt.Println("后台协程结束")
}()
fmt.Println("主程序等待管道出水")
// 阻塞等待管道的数据流出
value := <-ch
fmt.Println("管道的内容:", value)
}