Golang 学习笔记07-接口和协程相关

接口

接口是一组仅包含方法名、参数、返回值的未具体实现的方法的集合

关键字interface

举例说明:有个一唱歌的方法,传入一个结构体,传的谁就会唱歌

package main

import "fmt"

// 举例说明:有个一唱歌的方法,传入一个结构体,传的谁就会唱歌

// 定义结构体
type Chicken struct {
    Name string
}

// 绑定方法
func (c Chicken) Sing() {
    fmt.Println(c.Name, "正在唱歌...")
}

// sing的方法

func sing(c Chicken) {
    c.Sing()
}

func main() {
    c := Chicken{Name: "iku"}
    sing(c)
}

此时如果有其他的对象同样要调用sing方法就会报错

type Dog struct {
	Name string
}

func (d Dog) Sing() {
	fmt.Println(d.Name, "正在唱歌...")
}
func main() {
	c := Chicken{Name: "iku"}
	sing(c)
	d := Dog{Name: "小黄"}
	sing(d)
}

image-20260124145356268

解决办法

// 定义接口
type SingInterface interface {
	Sing()
}

// sing的方法
func sing(c SingInterface) {
	c.Sing()
}

如果要在接口里面调用打印

type SingInterface interface {
	Sing()
	GetName() string
}
func (d Dog) GetName() string {
	return d.Name
}
func (c Chicken) GetName() string {
	return c.Name
}
func sing(c SingInterface) {
	c.Sing()
	fmt.Println(c.GetName())
}
func main() {
	c := Chicken{Name: "iku"}
	sing(c)
	d := Dog{Name: "小黄"}
	sing(d)
}
iku 正在唱歌...
iku
小黄 正在唱歌...
小黄

接口本身不能绑定方法

接口是值类型,保存的是:值+原始类型

类型断言

在使用接口的时候,希望此时的具体类型是什么,我们可以通过断言来获取

func sing(c SingInterface) {
    //类型断言
	c, ok := c.(Chicken)
    fmt.Println(c, ok)
	c.Sing()
	fmt.Println(c.GetName())
}
func main() {
	c := Chicken{Name: "iku"}
	sing(c)
	d := Dog{Name: "小黄"}
	sing(d)
}
{iku} true
iku 正在唱歌...
iku
{} false
 正在唱歌...

以上是一种类型的断言,如果是多种枚举出所有的类型

func sing(c SingInterface) {
	switch server := c.(type) {
	case Chicken:
		fmt.Println(server)
	case Dog:
		fmt.Println(server)
	default:
		fmt.Println("没有这个对象")
	}
}
{iku}
{小黄}

空接口

空接口可以接受任何类型

任何类型都实现了空接口的定义

// 空接口:一个打印的函数,可以接收任意类型的值
type EmptyInterface interface{}

func Print(v EmptyInterface) {
	fmt.Println(v)
}

更简洁的写法

func Print(v interface{}) {
	fmt.Println(v)
}
func Print(v any) {
	fmt.Println(v)
}

协程和channel

协程

Goroutine是Go运行时管理的轻量级线程

以下是传统的写法,现在的模式就是接力的方式

package main

import (
	"fmt"
	"time"
)

// 例子:一个耗时函数,类似于js的异步函数
func shopping(name string) {
	fmt.Printf("开始购物,用户名:%s\n", name)
	time.Sleep(time.Second * 1)
	fmt.Printf("结束购物,用户名:%s\n", name)
}

// 协程
func main() {
	strtTime := time.Now()
	shopping("张三")
	shopping("李四")
	shopping("王五")
	fmt.Println("购买完成:", time.Since(strtTime))
}
开始购物,用户名:张三
结束购物,用户名:张三
开始购物,用户名:李四
结束购物,用户名:李四
开始购物,用户名:王五
结束购物,用户名:王五
购买完成: 3.000919s

协程:类似于三个人一起购物,关键词go

func main() {
	strtTime := time.Now()
	go shopping("张三")
	go shopping("李四")
	go shopping("王五")
	fmt.Println("购买完成:", time.Since(strtTime))
}
购买完成: 0s
开始购物,用户名:张三

可以发现,以上的执行结果每次都不一样

简单的理解就是主线程结束的时候协程可能还没有结束,主线程结束的时候协程同步结束

一个简单的解决办法

声明一个全局变量wait,在协程函数里面–,main函数增加一个循环,wait为0退出循环

package main

import (
	"fmt"
	"time"
)

// 申明一个全局变量
var wait int

// 例子:一个耗时函数,类似于js的异步函数
func shopping(name string) {
	fmt.Printf("开始购物,用户名:%s\n", name)
	time.Sleep(time.Second * 1)
	fmt.Printf("结束购物,用户名:%s\n", name)
	wait--
}

// 协程:类似于三个人一起购物
func main() {
	strtTime := time.Now()
	wait = 3
	//主线程结束,协程函数跟着结束
	go shopping("张三")
	go shopping("李四")
	go shopping("王五")
	for {
		if wait == 0 {
			break
		}
	}
	fmt.Println("购买完成:", time.Since(strtTime))
}
开始购物,用户名:张三
开始购物,用户名:李四
开始购物,用户名:王五
结束购物,用户名:张三
结束购物,用户名:李四
结束购物,用户名:王五
购买完成: 1.0004595s

更好的方法sync.WaitGroup

package main

import (
	"fmt"
	"sync"
	"time"
)

var wait sync.WaitGroup

// 例子:一个耗时函数,类似于js的异步函数
func shopping(name string) {
	fmt.Printf("开始购物,用户名:%s\n", name)
	time.Sleep(time.Second * 1)
	fmt.Printf("结束购物,用户名:%s\n", name)
	//表示结束
	wait.Done()
}

// 协程:类似于三个人一起购物
func main() {
	strtTime := time.Now()
	wait.Add(3)
	//主线程结束,协程函数跟着结束
	go shopping("张三")
	go shopping("李四")
	go shopping("王五")
	//等待协程函数
	wait.Wait()
	fmt.Println("购买完成:", time.Since(strtTime))
}

另外一种情况

如果wait是在main内部声明,shopping需要传参wait

func shopping(name string, wait sync.WaitGroup) {
	fmt.Printf("开始购物,用户名:%s\n", name)
	time.Sleep(time.Second * 1)
	fmt.Printf("结束购物,用户名:%s\n", name)
	//表示结束
	wait.Done()
}
func main() {
	var wait sync.WaitGroup
	strtTime := time.Now()
	wait.Add(3)
	//主线程结束,协程函数跟着结束
	go shopping("张三", wait)
	go shopping("李四", wait)
	go shopping("王五", wait)
	//等待协程函数
	wait.Wait()
	fmt.Println("购买完成:", time.Since(strtTime))
}

但是此时wait.Done()wait.Add(3)不是同一个wait,需要接指针

func shopping(name string, wait *sync.WaitGroup) {
	fmt.Printf("开始购物,用户名:%s\n", name)
	time.Sleep(time.Second * 1)
	fmt.Printf("结束购物,用户名:%s\n", name)
	//表示结束
	wait.Done()
}
func main() {
	var wait sync.WaitGroup
	strtTime := time.Now()
	wait.Add(3)
	//主线程结束,协程函数跟着结束
	go shopping("张三", &wait)
	go shopping("李四", &wait)
	go shopping("王五", &wait)
	//等待协程函数
	wait.Wait()
	fmt.Println("购买完成:", time.Since(strtTime))
}

channel

协程里面产生了数据,怎么传递给主线程呢?

或者是怎么传递给其他协程函数呢?

这个时候 channel来了

例子:

还是和上面一样,希望在主线程里面获取到每个人消费了多少金额

定义信道

var 信道名 chan 存储的类型

// 定义信道
var moneyChan chan int

以上是nil值

var moneyChan = make(chan int) //声明并初始化一个长度为0的信道

信道长度可以按以下理解:

  • 信道就像一个有固定容量的仓库
  • 发送方像供应商,往仓库里存放货物
  • 接收方像客户,从仓库里取走货物
  • 容量限制是仓库最多能同时存放多少货物

关键区别

  • 对于无缓冲信道(容量为0):发送和接收必须同步,没有中间存储
  • 对于有缓冲信道:发送方可以在不阻塞的情况下发送多个值,只要不超过容量限制

信道赋值语句

moneyChan <- 100
func pay(name string, money int, wait *sync.WaitGroup) {
	fmt.Printf("开始购物,用户名:%s\n", name)
	time.Sleep(time.Second * 1)
	fmt.Printf("结束购物,用户名:%s\n", name)
	//信道赋值语句
	moneyChan <- money
	//表示结束
	wait.Done()
}
func main() {
	var wait sync.WaitGroup
	startTime := time.Now()
	wait.Add(3)
	//主线程结束,协程函数跟着结束
	go pay("张三", 10, &wait)
	go pay("李四", 20, &wait)
	go pay("王五", 30, &wait)
	//等待协程函数
	wait.Wait()
	//希望在主线程里面获取到每个人消费了多少
	fmt.Println("购买完成:", time.Since(startTime))
}

如果此时直接运行会报错

fatal error: all goroutines are asleep - deadlock!

主线程接收

for {
	money, ok := <-moneyChan
	fmt.Println(money)
	if !ok {
		break
	}
}
开始购物,用户名:王五
开始购物,用户名:张三
开始购物,用户名:李四
结束购物,用户名:王五
30 true
结束购物,用户名:张三
10 true
结束购物,用户名:李四
20 true

此时还是会发生报错

fatal error: all goroutines are asleep - deadlock!

但是报错数量只有一个

解决办法:在合适的时机关闭信道

再定义一个协程函数,在协程函数里面等待结束后关闭信道

go func() {
	wait.Wait()
	close(moneyChan)
}()
开始购物,用户名:李四
开始购物,用户名:王五
开始购物,用户名:张三
结束购物,用户名:张三
结束购物,用户名:王五
结束购物,用户名:李四
10 true
30 true
20 true
0 false
购买完成: 1.0004089s

另一种写法

go func() {
	defer close(moneyChan)
	wait.Wait()
	//close(moneyChan)
}()
for money := range moneyChan {
	fmt.Println(money)
}

select

如果一个协程函数,往多个channel里面写东西,在主线程里面怎么拿数据呢?

go为我们提供了select,用于异步的从多个channel里面去取数据

继续上面的例子,在协程函数里面发送两个信道

以下是一种按照直觉的写法

var moneyChan1 = make(chan int)   //声明并初始化一个长度为0的信道
var nameChan1 = make(chan string) //声明并初始化一个长度为0的信道

func send(name string, money int, wait *sync.WaitGroup) {
	fmt.Printf("开始购物,用户名:%s\n", name)
	time.Sleep(time.Second * 1)
	fmt.Printf("结束购物,用户名:%s\n", name)
	//信道赋值语句
	moneyChan1 <- money
	nameChan1 <- name
	//表示结束
	wait.Done()
}
func main() {
	var wait sync.WaitGroup
	startTime := time.Now()
	wait.Add(3)
	//主线程结束,协程函数跟着结束
	go send("张三", 10, &wait)
	go send("李四", 20, &wait)
	go send("王五", 30, &wait)

	//再定义一个协程函数
	go func() {
		defer close(moneyChan1)
		defer close(nameChan1)
		wait.Wait()
	}()

	var moneyList []int
	var nameList []string
	go func() {
		for money := range moneyChan1 {
			//fmt.Println(money)
			moneyList = append(moneyList, money)
		}
	}()

	for name := range nameChan1 {
		//fmt.Println(money)
		nameList = append(nameList, name)
	}

	//等待协程函数
	//wait.Wait()
	//希望在主线程里面获取到每个人消费了多少
	fmt.Println("购买完成:", time.Since(startTime))
	fmt.Println("moneyList:", moneyList)
	fmt.Println("moneyList:", nameList)
}
开始购物,用户名:李四
开始购物,用户名:张三
开始购物,用户名:王五
结束购物,用户名:王五
结束购物,用户名:李四
结束购物,用户名:张三
购买完成: 1.0007567s
moneyList: [30 20 10]
moneyList: [王五 李四 张三]

以上的代码能够正常运行

可以使用select关键词

for {
	select {
	case money := <-moneyChan1:
		moneyList = append(moneyList, money)
	case name := <-nameChan1:
		nameList = append(nameList, name)
	}
}

现在并不能退出循环,需要声明一个用于关闭的信道

var doneChan = make(chan struct{}) 
go func() {
	defer close(moneyChan1)
	defer close(nameChan1)
	defer close(doneChan)
	wait.Wait()
	//close(moneyChan)
}()
for {
	select {
	case money := <-moneyChan1:
		moneyList = append(moneyList, money)
	case name := <-nameChan1:
		nameList = append(nameList, name)
	case <-doneChan:
		fmt.Println("结束")
		fmt.Println("购买完成:", time.Since(startTime))
		fmt.Println("moneyList:", moneyList)
		fmt.Println("nameList:", nameList)
		return
	}
}

现在不能在主线程拿到moneyListnameList,优化的方式,创建一个监听函数

var event = func() {
	for {
		select {
		case money := <-moneyChan1:
			moneyList = append(moneyList, money)
		case name := <-nameChan1:
			nameList = append(nameList, name)
		case <-doneChan:
			return
		}
	}
}
event()
fmt.Println("购买完成:", time.Since(startTime))
fmt.Println("moneyList:", moneyList)
fmt.Println("nameList:", nameList)

协程超时处理

var done = make(chan struct{})

func event() {
    fmt.Println("event执行开始")
    time.Sleep(2 * time.Second)
    fmt.Println("event执行结束")
}
func main() {
    go event()
    select {
    case <-done:
       fmt.Println("协程执行完毕")
    case <-time.After(1 * time.Second):
       fmt.Println("协程执行超时")
    }
}