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

Go 语言核心机制深度剖析:指针、defer、多态与空接口实战指南

Go 语言基础查缺补漏

  • Go 语言基础查缺补漏
    • 指针的应用
    • defer语句
      • defer执行和执行顺序
      • defer和return谁先谁后
    • 面向对象特征
      • 表示与封装
      • 继承
      • 多态Interface
      • 空接口和断言

这份笔记,以​​直击本质的解析+手把手实战演示​​,系统梳理Go语言中最易混淆的关键特性:从指针的地址操作原理、defer 与 return 的时序陷阱,到面向对象的轻量化实现(封装、组合式“继承”、接口多态),最后深入空接口的动态类型处理与类型断言安全实践。通过​​20+可运行代码片段​​,逐层拆解设计思想,带你绕过语法糖表象,真正掌握 Go 底层机制,写出高扩展、低耦合的工业级代码。

在这里插入图片描述

指针的应用

思考一个问题:

package mainimport "fmt"func changeValue(p int) {p = 10
}func main() {var a = 1changeValue(a)fmt.Println(a)
}

我们定义了一个changeValue函数,定义p给予赋值,调用该函数会将变量的值改变。
在main函数中我定义了一个变量a初始值为1,那么我使用changeValue函数给a赋值,a是否会改变?
测试一下看看就知道了:


1进程 已完成,退出代码为 0

很明显并没有赋值成功,那为什么呢?
我是这样理解的:

我们整个程序运行的时候是在内存当中,a会有自己的内存,p也会有自己的内存,而传递的过程中, 传递并不是a的内存,传的是a的值,但p也有自己的内存,所以再怎么传a的值也不会影响p。
所以我们打印出来之后,a仍然是1。

标准的话术来说就是:
内存隔离a和p存储在不同内存地址,彼此完全独立。函数内对副本的任何操作都不会“穿透”到原始数据

那么如何通过修改p来改变a呢?

很简单:

package mainimport "fmt"func changeValue(p *int) {// *int *p = 10
}func main() {var a = 1changeValue(&a)fmt.Println(a)
}

传递地址而非值&a获取 a的地址(如 0x1000),将其赋值给指针 p。

此时 p的值是 0x1000,指向 a的真实内存。

解引用修改原始值*p = 10表示:访问 p指向的内存(0x1000)并修改值为10。由于 0x1000是 a的存储位置,a的值被直接改变
实际上就是:

func changeValue(p *int) { // p 是指针类型,存储 a 的地址*p = 10 // 解引用:修改 p 指向的内存(即 a)的值
}func main() {var a = 1changeValue(&a) // &a 获取 a 的地址,例如 0x1000fmt.Println(a) // 输出 10
}

这时我们看看是否能够正确传值:

10进程 已完成,退出代码为 0

简单的总结:

值传递:函数操作原始数据的副本,原始数据不变。

指针传递:函数通过内存地址直接操作原始数据,实现跨作用域修改。

再来一个例子:

// 练习例子
package mainimport "fmt"func swap(a *int, b *int) {var temp inttemp = *a*a = *b*b = temp//a ->temp ,temp = 10,b -> a , a = 20,temp->b ,b= 10
}func main() {var a int = 10var b int = 20swap(&a, &b)fmt.Println("a=", a, "b=", b)
}

原本会输出:

a = 10
b = 20

现在的输出是:

a= 20 b= 10

应该能够理解了吧哈哈哈。

defer语句

defer执行和执行顺序

我们先用一个例子引入一下:

package mainimport "fmt"func main() {//deferdefer fmt.Println("main end")fmt.Println("hello 1")fmt.Println("hello 2")
}

查看输出:

hello 1
hello 2
main end

defer 关键字会在defer所在函数体结束之前进行触发。

defer会延迟执行其后的函数,直到当前函数返回前(无论正常返回还是 panic),才按照后进先出顺序触发。

要是存在多个defer怎么办呢?谁会先执行呢?

试试就知道了:

package mainimport "fmt"func main() {//deferdefer fmt.Println("main end 1")defer fmt.Println("main end 2")defer fmt.Println("main end 3")fmt.Println("hello 1")fmt.Println("hello 2")
}

查看结果:

hello 1
hello 2
main end 3
main end 2
main end 1

通过结果可以看到输出是一个倒序,为啥呢?

多个 defer会形成调用栈,最后注册的 defer最先执行。

简单来说,就像一个箱子里面装的书,先装进去的书就在最里面,后放进去的书就在上面,当我想把这些书拿出来的时候,很显然后进去的书就会被先拿出来,最先被放进去的书就会被最后拿出来,理解了吧。

再来一个你就懂了:

package mainimport "fmt"func a() {fmt.Println("a")
}func b() {fmt.Println("b")
}func c() {fmt.Println("c")
}func main() {//deferdefer a()defer b()defer c()
}

这回输出结果呢?

c b a 呗。c最后进去的,a最先进去的,b在他俩中间,理解了吧。

defer和return谁先谁后

既然defer和return都相当于是正个函数的末尾,那么他俩同时出现执行的顺序是谁先谁后呢?

试试就知道了呗:

package mainimport "fmt"func deferFunc() int {fmt.Println("defer func")return 0
}func returnFunc() int {fmt.Println("return func")return 0
}func returnAndDefer() int {defer deferFunc()return returnFunc()
}func main() {returnAndDefer()
}

查看一下输出结果:

return func
defer func

执行流程分三步:

赋值返回值:若为命名返回值,赋值到变量(如 result = 10);匿名返回值则存入临时变量。

执行 defer语句:按后进先出(LIFO)顺序执行延迟函数。

函数真正返回:将最终值返回给调用方。

总结:return之后的语句先执行,defer后的语句后执行。

面向对象特征

表示与封装

例子:

package mainimport "fmt"type Hero struct {Name  stringAd    intLevel int
}func (this *Hero) GetName() string {return this.Name
}func (this *Hero) SetName(newName string) {this.Name = newName
}func (this Hero) Show() {fmt.Println("Name is ", this.Name)fmt.Println("Ad is ", this.Ad)fmt.Println("Level is ", this.Level)
}func main() {hero := Hero{Name:  "zhangs",Ad:    100,Level: 10,}hero.SetName("lis")hero.Show()
}

结构体字段首字母大写(如Name)表示公开字段(包外可访问),小写则表示私有(包内访问)。GetName()和 SetName()是典型的Getter/Setter方法,用于控制对字段的访问(即使字段公开,也可通过方法添加逻辑)。Show()方法封装了打印逻辑,对外提供统一操作入口。

Go的封装是通过约定而非强制(首字母大小写)实现的轻量化数据隐藏机制。

继承

例子:

父类结构体 Hero定义基础属性 name和 age,作为被嵌入的“父类”。

子类结构体 HeroMan通过 Hero的匿名字段实现嵌入(HeroMan{Hero}),直接继承 Hero的所有字段(name, age)。

方法绑定 Print()子类 HeroMan的方法可直接访问父类字段(如 this.name),无需通过嵌套结构体名称(如 this.Hero.name),这是Go的“字段提升”特性

package mainimport "fmt"// Hero 定义父类
type Hero struct {name stringage  int
}// HeroMan 定义子类
type HeroMan struct {Herolevel int
}//定义子类打印函数
func (this *HeroMan) Print() {fmt.Println("name:", this.name)fmt.Println("age:", this.age)fmt.Println("level:", this.level)
}// NewHeroMan 工厂函数封装初始化简化复杂结构创建:
func NewHeroMan(name string, age, level int) *HeroMan {return &HeroMan{Hero{name, age}, level}
}func main() {//第一种继承方式:h := HeroMan{Hero{name: "zhangs", age: 10}, 100}h.Print()fmt.Println("=============")//第二种继承方式:var s HeroMans.name = "lis"s.age = 20s.level = 200s.Print()fmt.Println("=============")//第三类方法,组合后使用工厂函数y := NewHeroMan("wangw", 20, 100)y.Print()
}

匿名字段(如 Hero)的导出字段(首字母大写)会被提升到外层结构体(HeroMan),使 s.name等价于 s.Hero.name。

查看一下吧:

name: zhangs
age: 10
level: 100
=============
name: lis
age: 20
level: 200
=============
name: wangw
age: 20
level: 100

三种方法的输出都是一样的成功调用了子类。

多态Interface

Go语言中的多态通过接口(Interface) 实现,这是其面向对象设计的核心特性之一。与传统的继承多态不同,Go的多态强调行为抽象而非类型层级,通过接口统一调用不同实现类型的方法。

多态(Polymorphism) 是面向对象编程(OOP)的核心特性之一,指同一操作或接口在不同对象上表现出不同行为的能力。其本质是“同一接口,多种实现”,允许程序通过统一的接口处理不同类型的对象,而具体行为由对象自身的类型决定.

Go的多态是“通过接口实现的行为抽象”。它用轻量级接口取代传统继承,通过动态绑定实现“同一操作,不同行为”,最终达成高扩展、低耦合的架构目标

拿一个例子深入理解和使用一下意义:

package mainimport "fmt"type AnimalIF interface {Sleep()GetColor() stringGetType() string
}type Cat struct {color string
}func (this *Cat) Sleep() {fmt.Println("cat sleep")
}func (this *Cat) GetColor() string {fmt.Println("color is", this.color)return this.color
}func (this *Cat) GetType() string {fmt.Println("The type is Cat")return "Cat"
}type Dog struct {color string
}func (this *Dog) Sleep() {fmt.Println("Dog sleep")
}func (this *Dog) GetColor() string {fmt.Println("color is", this.color)return this.color
}func (this *Dog) GetType() string {fmt.Println("The type is Dog")return "Dog"
}func main() {//常规//var animal AnimalIF//animal = &Cat{"Green"}//animal.Sleep()//animal.GetType()//animal.GetColor()////fmt.Println("=============")//animal = &Dog{"Black"}//animal.GetType()//animal.GetColor()fmt.Println("=======================")//多态使用://调用函数:cat := Cat{"black"}dog := Dog{"Green"}showAnimals(&cat)fmt.Println("=============")showAnimals(&dog)
}// 定义一个多态函数
func showAnimals(animals AnimalIF) {animals.Sleep()fmt.Println("Kind:", animals.GetType())fmt.Println("Color:", animals.GetColor())
}

总结:

实现机制

  1. 接口定义行为契约:
    接口(如 AnimalIF)声明一组方法签名,不包含具体实现,仅定义规范。
  2. 隐式接口实现:
    任何类型(如结构体 Cat、Dog)只要实现了接口的全部方法,即自动满足该接口,无需显式声明(鸭子类型)。
  3. 动态绑定:
    接口变量(如 animals AnimalIF)在运行时根据实际存储的具体类型(*Cat 或 *Dog)动态调用对应方法。

与传统OOP的差异

• 无类继承:

Go通过组合而非继承实现多态,类型通过嵌入结构体复用代码,而非建立 is-a 关系。
• 轻量灵活:

接口的隐式实现和组合特性(如嵌套小接口形成大接口)使扩展新类型无需修改既有代码。

关键注意事项

• 接收者类型影响:

若方法需修改结构体状态,必须使用指针接收者(func (this *Cat) Sleep()),否则值拷贝会导致修改失效。
• 空接口与类型断言:

interface{} 可接受任意类型,但需配合类型断言(value, ok := v.(string))确保类型安全。

核心价值

• 解耦与扩展:

调用方(如函数 showAnimals(AnimalIF))仅依赖接口而非具体类型,新增类型(如 Bird)只需实现接口,无需修改调用逻辑。
• 统一处理多样性:

多态容器(如 []AnimalIF)可统一处理多种实现,替代大量 if-else 分支,提升代码简洁性。

一句话总结:Go的多态以接口为桥梁,通过隐式实现和动态绑定,用组合思维实现高扩展、低耦合的代码架构,是Go语言“少即是多”设计哲学的典型体现。

空接口和断言

空接口是不包含任何方法签名的接口,因此任何类型都自动满足其约束.

底层实现:

空接口实际由两个指针组成:动态类型指针(指向类型元数据)和动态值指针(指向实际数据)。

泛型替代:在Go 1.18前,空接口是处理通用逻辑的主要手段(如fmt.Println的实现)

一个例子来讲解:

package mainimport "fmt"func myFunc(arg interface{}) {fmt.Println(arg)value, ok := arg.(string)if !ok {fmt.Println("arg is not string")} else {fmt.Println("arg is string : ", value)}
}type Book struct {name string
}func main() {book := Book{"GoLang"}myFunc(book)myFunc(100)myFunc("Hello golang")
}

🔄 空接口 (interface{})

• 作用:作为“万能容器”,可接受任意类型的值(如 int、string、Book 结构体)。

• 原理:空接口不含任何方法,所有类型隐式实现它。底层通过动态类型指针和动态值指针存储实际数据。

• 应用场景:需处理未知类型时(如通用函数参数 myFunc(arg interface{}))。

🔍 2. 类型断言

• 作用:将空接口还原为具体类型,并安全检查类型匹配性。

• 安全写法:
value, ok := arg.(string) // 避免 panic
if ok { /* 操作 value / } else { / 处理非目标类型 */ }

• 风险写法:value := arg.(string) 失败时触发 panic,需谨慎使用。

• 典型应用:在 myFunc 中判断 arg 是否为 string,并分支处理不同类型。

💎 总结

通过 myFunc(interface{}) 函数,结合空接口的泛用性与类型断言的安全性,实现了对任意类型参数的统一接收与类型适配处理:

• 空接口统一承载 int、Book、string 等异构数据;

• 类型断言通过 , ok 模式安全提取 string 类型值,避免程序崩溃,并处理非目标类型逻辑。

http://www.lryc.cn/news/593459.html

相关文章:

  • 使用 go-redis-entraid 实现 Entra ID 无密钥认证
  • Go-Redis × RediSearch 全流程实践
  • leetcode_121 买卖股票的最佳时期
  • 力扣经典算法篇-26-长度最小的子数组(暴力求解法,左右指针法)
  • 【Java】【力扣】48.旋转图像
  • FPGA自学——整体设计思路
  • Redis数据库基础与持久化部署
  • 使用CCS6.2为C2000(DSP28335)生成.bin文件和.hex文件
  • 【LeetCode 热题 100】437. 路径总和 III——(解法一)递归递归!
  • CCF编程能力等级认证GESP—C++7级—20250628
  • STM32_Hal库学习ADC
  • IntelliJ IDEA中Mybatis的xml文件报错解决
  • SSM框架——注入类型
  • aws(学习笔记第四十九课) ECS集中练习(1)
  • Streamlit 官翻 5 - 部署、社区云 Deploy
  • Python绘制数据(三)
  • Matplotlib 30分钟精通
  • 人该怎样活着呢?55
  • Windows11下编译好的opencv4.8-mingw,可下载后直接用
  • Apache Kafka 学习笔记
  • 详细阐述 TCP、UDP、ICMPv4 和 ICMPv6 协议-以及防火墙端口原理优雅草卓伊凡
  • Python高级数据类型:字典(Dictionary)
  • Datawhale 7月学习
  • RK3568 Linux驱动学习——SDK安装编译
  • Oracle为什么需要临时表?——让数据处理更灵活
  • DAY 18 推断聚类后簇的类型
  • 【Project】kafka+flume+davinci广告点击实时分析系统
  • MySQL(145)如何升级MySQL版本?
  • 在服务器(ECS)部署 MySQL 操作流程
  • 基于单片机宠物喂食器/智能宠物窝/智能饲养