go语言学习记录-入门阶段2

文章目录

  • 一、接口和多态
    • [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的工作原理

  • 执行流程:
  1. 检查所有case:select 会检查每个case中的channel操作是否可以立即执行
  2. 选择就绪的case:
    • 如果有多个case就绪,随机选择一个执行
    • 如果只有一个case就绪,执行该case
    • 如果所有case都阻塞,执行default(如果有)
  3. 阻塞等待:如果没有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的注意事项

  1. 避免空的select
go 复制代码
// ❌ 错误:空的select会永远阻塞
select {}

// ✅ 正确:至少有一个case
select {
case <-ch:
    // ...
}
  1. 处理关闭的channel
go 复制代码
// ✅ 正确:检查channel是否关闭
select {
case val, ok := <-ch:
    if !ok {
        ch = nil  // 设置为nil,select会忽略它
        continue
    }
    // 处理val
}
  1. 避免在select中发送到nil channel
go 复制代码
var ch chan int  // nil channel

// ⚠️ 注意:向nil channel发送会永远阻塞
select {
case ch <- 42:  // 这会永远阻塞
    fmt.Println("发送成功")
default:
    fmt.Println("不会执行到这里")
}
  1. 在循环中使用select
go 复制代码
// ✅ 正确:使用break退出select,继续循环
for {
    select {
    case <-ch:
        // 处理
        break  // 只退出select,不退出for循环
    }
}

// ✅ 正确:使用标签退出外层循环
loop:
for {
    select {
    case <-ch:
        break loop  // 退出外层for循环
    }
}
  1. Select的性能考虑
  • select本身性能开销很小
  • 但频繁使用default可能浪费CPU(忙等待)
  • 在循环中使用select时,可以考虑添加适当的延迟

写在最后的话

我觉得每天这样写笔记,虽然对学习的加深很有用,但是占用了大量的时间,因为个人急着用它,所有后续应该不会这么细致的去记录整个学习的内容,也问过AI,它建议我有针对性的去做复盘,所以后续的记录应该会更简洁,更针对性的去记录我遇到的问题和解决的方法,甚至最好应该是在github上面去做每天的学习记录更新,然后转发到CSDN,这样后续也能作为你的背书...

后续的内容,就是并发、然后是各类库的调用...基础的话,到这里应该也就这么多了

相关推荐
计算机学姐2 小时前
基于SpringBoot的在线课程学习网站
java·vue.js·spring boot·后端·学习·spring·intellij-idea
Dovis(誓平步青云)2 小时前
《QT学习第一篇:QT的概述与安装、信号与槽》
开发语言·qt·学习·功能详解
2301_805962932 小时前
树莓派学习2-读取I2C设备数据
学习
2301_805962932 小时前
树莓派学习1-I2C配置与设备状态检测
嵌入式硬件·学习
摇滚侠11 小时前
如何选择 nodejs 版本,nodejs 版本号详解
学习
醇氧11 小时前
【学习】IP地址:数字世界的“门牌号”怎么读?
网络协议·学习·tcp/ip
talen_hx29612 小时前
《零基础入门Spark》学习笔记 Day 11
笔记·学习·spark
ZhiqianXia13 小时前
gem5 模拟器学习笔记(1):核心术语整理
笔记·学习