文章目录
- 一、接口和多态
-
- [1. 接口的定义和实现](#1. 接口的定义和实现)
- [2. 空接口和类型断言](#2. 空接口和类型断言)
- [3. 接口组合](#3. 接口组合)
- 二、goroutine(协程)
-
- 基础的goroutine
- WaitGroup
- [channel 通信机制](#channel 通信机制)
- [select 语句](#select 语句)
-
- [select 多路复用](#select 多路复用)
- [select 超时控制](#select 超时控制)
- [select 非阻塞操作 Non-blocking](#select 非阻塞操作 Non-blocking)
- [select 循环监听(Loop with Select)](#select 循环监听(Loop with Select))
- [select 处理退出信号](#select 处理退出信号)
- Select的注意事项
- 写在最后的话
接口和多态,协程和通道
今天的内容,感觉两个都挺重要的。。。
一、接口和多态
接口(interface)是go语言中的一个重要概念,它定义了对象的行为规范,使得对象可以以一种统一的方式进行交互;
接口也是一种抽象类型,它描述了对象应具备的方法和属性,但不提供具体的实现。
1. 接口的定义和实现
go
// ======= 定义接口 =======
type Shape interface {
Area() float64
Perimeter() float64
}
// ======= 实现接口 =======
// 隐式实现,不像java需要去声明从哪儿来的,类型的结构和功能自然地决定了它属于哪个接口
type Rectangle struct {
Width, Height float64
}
// 注意在实现接口中的函数名,是直接用的接口定义的名称"Area"
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// 这里也是"Perimeter"
func (r Rectangle) Perimeter() float64 {
return r.Width*2 + r.Height*2
}
// ======= 实现接口2 =======
// 多个类型实现同一接口
type Circle struct {
Radius float64
}
// 这里是接口2的两个方法
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return math.Pi * c.Radius * 2
}
// 调用方只关心接口,不关心具体类型,轻松实现多态。
func PrintShapeInfo(s Shape, name string) {
fmt.Printf("%s 的面积是: %f, 周长是: %f\n",
name, s.Area(), s.Perimeter())
}
func main() {
fmt.Println("==========接口示例==========")
// 形状调用
shapes := []Shape{
Rectangle{Width: 10, Height: 5},
Circle{Radius: 3},
}
name := []string{"长方形", "圆形"}
for i, shape := range shapes {
PrintShapeInfo(shape, name[i])
}
}
注意:在实现接口中的函数,不仅名称要一致,函数的数量也得一致,定义中有2个(Area、Perimeter),实现中也得有两个一样的,不能多或者少
2. 空接口和类型断言
空接口 interface{} 在 Go 1.18 之前常被用来临时存放"任意类型"
go
// 空接口可以表示任何类型
var value interface{}
value = 42
value = "hello"
value = []int{1, 2, 3}
个人理解是:在业务中会存在不知道来的是什么数据,所以无法用接口定义其类型,就只能用空接口去接这个内容
主要应用场景:通常情况下,空接口都是用来打印日志/函数的,或者用作json解析,极少情况下会使用空接口去做类型断言后再做计算
空接口是任何类型都能打印,记住是任何,能打印一切
空接口搭配类型断言
go
var value interface{}
value = 42
value = "hello"
value = []int{1, 2, 3}
// 类型断言
func doSomething(v interface{}) {
// 方式1:类型断言
if str, ok := v.(string); ok {
fmt.Println("是字符串:", str)
}
// 方式2:type switch
switch data := v.(type) {
case int:
fmt.Println("整数:", data)
case string:
fmt.Println("字符串:", data)
case []int:
fmt.Println("整数切片:", data)
default:
fmt.Println("未知类型")
}
}
func main() {
// 常见用法:从 map[string]interface{} 中取值并断言具体类型
payload := map[string]interface{}{
"id": 1001,
"name": "golang",
"extra": []string{"interface", "assertion"},
}
if id, ok := payload["id"].(int); ok {
fmt.Println("ID:", id)
}
// 断言失败时要有兜底处理
if tags, ok := payload["extra"].([]string); ok {
fmt.Println("tags:", tags)
} else {
fmt.Println("extra 字段不是期望的 []string")
}
}
3. 接口组合
首先,我定义一个接口:
go
// 定义一个接口,叫人名,他回应
type Person interface {
CallName() string
Say() string
}
type Zhangsan struct {
Mingzi string
Speak string
}
func (z Zhangsan) CallName() string {
return z.Mingzi
}
func (z Zhangsan) Say() string {
return "到!"
}
此时,来了个业务需求,我要新增一个李四,他不仅要有张三的所有功能(方法),还要新增一个表情,这个时候有两种方法实现:
方法一、在老接口中增加参数满足李四的需求,再在张三的实现接口中创建一个空的接口,确保实现接口和定义接口的三个参数是对齐的,因为接口的类型结构和功能要一致
方法二、定义一个新的接口,然后做李四的接口组合,这个方法最常用,在功能迭代上面也更灵活,也不会去动老的接口(张三)
go
// 接上面代码
// 定义新的接口--接口组合
type ExpressivePerson interface {
Person // 嵌入 Person 接口(原来的接口)
Expression() string
}
// 实现新的接口,李四
type Lisi struct {
// 因为嵌入了原来的接口Person,所以可以直接用Person的定义类型,也是一定要用的
Mingzi string
Speak string
// 新增一个方法
Biaoqing string
}
func (l Lisi) CallName() string {
return l.Mingzi
}
func (l Lisi) Say() string {
return "我在"
}
func (l Lisi) Expression() string {
return "漫不经心"
}
func main() {
fmt.Println("==========接口组合示例=======")
var p1 Person
p1 = Zhangsan{Mingzi: "张三", Speak: "张三"}
fmt.Println("张三的名字是:", p1.CallName())
fmt.Println("张三说:", p1.Say())
var p2 ExpressivePerson
p2 = Lisi{Mingzi: "李四", Speak: "李四", Biaoqing: "漫不经心"}
fmt.Println("李四的名字是:", p2.CallName())
fmt.Println("李四说:", p2.Say())
fmt.Println("李四的表情是:", p2.Expression())
}
二、goroutine(协程)
goroutine是go语言最大的亮点
什么是goroutine?通俗说:能和其他代码同时运行的"小任务",Goroutine => Go 语言实现的轻量级并发执行单元
go
// 启动一个 goroutine 只需要几 KB 内存
go func() {
fmt.Println("我很轻量!")
}()
// 可以轻松创建成千上万个
for i := 0; i < 10000; i++ {
go doWork(i) // 1 万个并发,毫无压力
}
go
对比:
线程:每个 1-2 MB → 1000 个就爆内存
Goroutine:每个 2-8 KB → 10 万个也很轻松
基础的goroutine
OK,大概对goroutine有个概念了,现在来创建一个基础的goroutine
go
// 写个打印函数
func sayHello(name string) {
fmt.Printf("Hello from %s\n", name)
}
// 基础的goroutine
func basicGoroutine() {
fmt.Println("=== 基本Goroutine ===")
// 启动goroutine
// 用go 关键字 + 函数名 就可以启动goroutine虚拟线程
go sayHello("goroutine 1")
go sayHello("goroutine 2")
go sayHello("goroutine 3")
// 等待goroutine完成
time.Sleep(time.Second)
fmt.Println()
}
func main() {
basicGoroutine()
}
// === 基本Goroutine ===
// Hello from goroutine 2
// Hello from goroutine 1
// Hello from goroutine 3
WaitGroup
Go标准库的包sync.WaitGroup
go
func waitGoroutine() {
fmt.Println("=== 等待Goroutine完成 ===")
// 甚可 sync /sɪŋk/ 同时,Go标准库的包
var wg sync.WaitGroup
// 循环,是为了生成5个
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("goroutine %d is running\n", id)
time.Sleep(time.Second)
fmt.Printf("goroutine %d is done\n", id)
}(i)
//这里的(i)是标准匿名函数的语法,表示将i的值传递给匿名函数
// 为什么要传(i)?因为goroutine是多线程,并发的,
//这里传参1是代表立即执行
//2是给并发线程都附上一个独有的id值,避免多个goroutine共享同一个变量导致数据错乱
// 定义并立即执行一个匿名函数--标准语法
//func(参数类型) {
// // 函数体,可以用参数
//}(参数值) // ← 这里的 (参数值) 就是传进去的实际值
// 这里打印是for循环体内的逻辑,当代码触 发go func函数的时候,会启动一个新线程去执行go func,
// 新线程去执行go func的同时,循环体继续往下执行,完成循环遍历
fmt.Printf("go func是新的线程,而我是循环的线程,所以我会执行\n")
}
// 等待所有线程执行完毕
wg.Wait()
fmt.Printf("全部执行完成")
}
channel 通信机制
go
// channel通信
func channelDemo() {
fmt.Println("=== channel通信示例 ===")
// 无缓冲channel
ch := make(chan int)
ch <- 66
value := <-ch
fmt.Println("从channel中获取到的值:", value)
}
// channel的关闭
func closeChannel() {
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch)
// 关闭后的channel仍然可以读取
// 但是不能再发送数据了
for value := range ch {
fmt.Println(value)
}
}
// 带缓冲的channel
func bufferedChannelDemo() {
fmt.Println("=== 带缓冲的channel ===")
// 创建一个大小为3的缓冲区
ch := make(chan int, 3)
// make(chan int, 3) 的缓冲区只能容纳 3 个元素;如果在同一个 goroutine 里连续 ch <- 1、2、3、4,
// 第四次写入时缓冲区已经满了,又没有并发的读取方,写操作就会一直阻塞。
// 由于 main goroutine 被卡住,程序到不了后面的打印语句,
// 最终触发运行时检测到 "all goroutines are asleep -- deadlock!" 的 panic。
// 要让第 4 次写入不阻塞,必须在写入时就有其他 goroutine 去消费,比如:
ch <- 1
ch <- 2
ch <- 3
// ch <- 4
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
}
// goroutine + channel线程安全通信
func bufferedChannelDemoNew() {
fmt.Println("=== goroutine + channel ===")
ch := make(chan int, 3)
defer close(ch)
// 启动读取 goroutine
go func() {
for v := range ch {
fmt.Println("routine1读取:", v)
time.Sleep(1000 * time.Millisecond) // 模拟处理耗时
}
}()
go func() {
for v := range ch {
fmt.Println("routine2读取:", v)
time.Sleep(1000 * time.Millisecond) // 模拟处理耗时
}
}()
for i := 1; i <= 40; i++ {
ch <- i
fmt.Println("写入:", i)
}
time.Sleep(300000 * time.Millisecond)
}
select 语句
什么是Select?
select 是Go语言中用于处理多个channel操作的专用语句,类似于 switch 语句,但专门用于channel通信。
核心概念:
select 允许goroutine同时等待多个channel操作
它会阻塞,直到其中一个case可以执行
如果有多个case同时就绪,会随机选择一个执行
可以用于实现超时、非阻塞操作等并发模式
基本语法:
go
select {
case msg1 := <-ch1:
// 处理ch1的消息
case msg2 := <-ch2:
// 处理ch2的消息
case ch3 <- value:
// 向ch3发送数据
default:
// 如果所有case都阻塞,执行default(可选)
}
Select的工作原理
- 执行流程:
- 检查所有case:select 会检查每个case中的channel操作是否可以立即执行
- 选择就绪的case:
- 如果有多个case就绪,随机选择一个执行
- 如果只有一个case就绪,执行该case
- 如果所有case都阻塞,执行default(如果有)
- 阻塞等待:如果没有default且所有case都阻塞,select会阻塞,直到某个case就绪
- 关键特性:
- 随机选择:当多个case同时就绪时,Go会随机选择一个,这保证了公平性
- 非阻塞:使用default可以实现非阻塞操作
- 超时控制:结合time.After可以实现超时机制
select 多路复用
go
// 同时监听多个channel,哪个先有数据就处理哪个
func selectDemo() {
fmt.Println("=== select 语句 ===")
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "from ch1"
}()
go func() {
time.Sleep(1 * time.Second)
ch2 <- "from ch2"
}()
time.Sleep(1000 * time.Millisecond)
// 随机选择一个就绪的channel
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
}
}
select 超时控制
go
// 为channel操作设置时间,避免无限等待
func timeoutDemo() {
fmt.Println("=== select 语句超时控制 ===")
ch := make(chan string)
go func() {
time.Sleep(2 * time.Second)
ch <- "result"
}()
select {
case msg := <-ch:
fmt.Println("收到:", msg)
case <-time.After(2 * time.Second):
fmt.Println("超时了")
}
fmt.Println()
}
select 非阻塞操作 Non-blocking
go
// 使用default实现非阻塞的channel操作
func nonBlockingDemo() {
fmt.Println("=== select 语句非阻塞操作 ===")
ch := make(chan int)
select {
case value := <-ch:
fmt.Println("收到:", value)
default:
fmt.Println("没有值可读(即便不接受ch,也不会阻塞)")
}
}
select 循环监听(Loop with Select)
go
// 在循环中使用select持续监听多个channel
func loopSelectDemo() {
fmt.Println("=== 循环Select示例 ===")
ch1 := make(chan int)
ch2 := make(chan string)
go func() {
for i := 0; i < 5; i++ {
ch1 <- i
time.Sleep(100 * time.Millisecond)
}
close(ch1)
}()
go func() {
for i := 0; i < 3; i++ {
ch2 <- fmt.Sprintf("msg-%d", i)
time.Sleep(150 * time.Millisecond)
}
close(ch2)
}()
for ch1 != nil || ch2 != nil {
select {
case val, ok := <-ch1:
if !ok {
ch1 = nil
fmt.Println("📝 ch1 关闭了,继续等待 ch2...")
continue
}
fmt.Println("ch1:", val)
case msg, ok := <-ch2:
if !ok {
ch2 = nil
fmt.Println("📝 ch2 关闭了,继续等待 ch1...")
continue
}
fmt.Println("ch2:", msg)
default:
fmt.Println("default-持续监听ing")
time.Sleep(50 * time.Millisecond)
}
}
fmt.Println("所有channel已关闭\n")
}
select 处理退出信号
go
func quitChannelDemo() {
fmt.Println("=== 处理退出信号 ===")
jobs := make(chan int)
quit := make(chan struct{})
go func() {
for {
select {
case job := <-jobs:
fmt.Printf("处理任务: %d\n", job)
time.Sleep(500 * time.Millisecond)
case <-quit:
fmt.Println("收到退出信号")
return
}
}
}()
for i := 0; i < 30; i++ {
jobs <- i
}
fmt.Println("结束任务")
quit <- struct{}{}
time.Sleep(100 * time.Millisecond)
fmt.Println()
}
Select的注意事项
- 避免空的select
go
// ❌ 错误:空的select会永远阻塞
select {}
// ✅ 正确:至少有一个case
select {
case <-ch:
// ...
}
- 处理关闭的channel
go
// ✅ 正确:检查channel是否关闭
select {
case val, ok := <-ch:
if !ok {
ch = nil // 设置为nil,select会忽略它
continue
}
// 处理val
}
- 避免在select中发送到nil channel
go
var ch chan int // nil channel
// ⚠️ 注意:向nil channel发送会永远阻塞
select {
case ch <- 42: // 这会永远阻塞
fmt.Println("发送成功")
default:
fmt.Println("不会执行到这里")
}
- 在循环中使用select
go
// ✅ 正确:使用break退出select,继续循环
for {
select {
case <-ch:
// 处理
break // 只退出select,不退出for循环
}
}
// ✅ 正确:使用标签退出外层循环
loop:
for {
select {
case <-ch:
break loop // 退出外层for循环
}
}
- Select的性能考虑
- select本身性能开销很小
- 但频繁使用default可能浪费CPU(忙等待)
- 在循环中使用select时,可以考虑添加适当的延迟
写在最后的话
我觉得每天这样写笔记,虽然对学习的加深很有用,但是占用了大量的时间,因为个人急着用它,所有后续应该不会这么细致的去记录整个学习的内容,也问过AI,它建议我有针对性的去做复盘,所以后续的记录应该会更简洁,更针对性的去记录我遇到的问题和解决的方法,甚至最好应该是在github上面去做每天的学习记录更新,然后转发到CSDN,这样后续也能作为你的背书...
后续的内容,就是并发、然后是各类库的调用...基础的话,到这里应该也就这么多了