文章介绍: 在学习完进阶语法后,本文基于青训营课程将讲解总结以下内容
- 接口
- 协程 Goroutine
- 管道 Channel
1. 接口
概念 :
1.1接口的使用
接口的定义
每个接口类型由任意个方法签名组成,接口的定义格式如下:
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
…
}
其中:
- 接口类型名:Go语言的接口在命名时,一般会在单词后面添加
er,如有写操作的接口叫Writer,有关闭操作的接口叫closer等。接口名最好要能突出该接口的类型含义。 - 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。
代码示例 :
package main
import "fmt"
// 定义接口
type Animal interface {
Speak() string
}
// 实现接口的类型
type Cat struct{}
type Dog struct{}
func (c Cat) Speak() string { return "喵喵喵" }
func (d Dog) Speak() string { return "汪汪汪" }
func main() {
var a Animal
a = Cat{}
fmt.Println("猫:", a.Speak())
a = Dog{}
fmt.Println("狗:", a.Speak())
}
这个示例使用 Animal 接口定义了动物的行为(发声),而不关心具体的动物类型。并且如果将来需要添加新的动物类型(例如 Bird),只需实现 Animal 接口,而不需要修改现有的代码,更加易于代码的测试和维护。
1.2 类型与接口的关系
1.2.1 一个类型实现多个接口
一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。例如狗不仅可以叫,还可以动。我们完全可以分别定义Sayer接口和Mover接口,具体代码示例如下。
// Sayer 接口
type Sayer interface {
Say()
}
// Mover 接口
type Mover interface {
Move()
}
1.2.2多种类型实现同一接口
Go语言中不同的类型还可以实现同一接口。例如在我们的代码世界中不仅狗可以动,汽车也可以动。我们可以使用如下代码体现这个关系。
// 实现Mover接口
func (d Dog) Move() {
fmt.Printf("%s会动\n", d.Name)
}
// Car 汽车结构体类型
type Car struct {
Brand string
}
// Move Car类型实现Mover接口
func (c Car) Move() {
fmt.Printf("%s速度70迈\n", c.Brand)
}
1.2 空接口
空接口 interface{} 可以接受任何类型的值。当你不知道将要处理的数据类型时,可以使用空接口来编写通用的函数。
package main
import "fmt"
func PrintType(value interface{}) {
fmt.Printf("类型: %T, 值: %v\n", value, value)
}
func main() {
PrintType("Hello, World!")
PrintType(42)
PrintType(true)
}
// 类型: string, 值: Hello, World!
// 类型: int, 值: 42
// 类型: bool, 值: true
协程
1 协程介绍
协程可以理解为用户态线程,是更微量级的线程。区别于线程,协程的调度在用户态进行,不需要切换到内核态,所以不由操作系统参与,由用户自己控制。在一些支持协程高级语言中,往往这些语言都实现了自己的协程调度器,比如go语言就有自己的协程调度器。
- 协程有独立的栈空间,但是共享堆空间。
- 一个进程上可以跑多个线程,一个线程上可以跑多个协程
2 Goroutine 介绍
Goroutine是 Go 语言支持并发的核心,在一个Go程序中同时创建成百上千个goroutine是非常普遍的,一个goroutine会以一个很小的栈开始其生命周期,一般只需要2KB。区别于操作系统线程由系统内核进行调度, goroutine 是由Go运行时(runtime)负责调度。例如Go运行时会智能地将 m个goroutine 合理地分配给n个操作系统线程,实现类似m:n的调度机制,不再需要Go开发者自行在代码层面维护一个线程池。
3 使用方法
3.1 单协程
Go语言中使用 goroutine 非常简单,只需要在函数或方法调用前加上go关键字就可以创建一个 goroutine ,从而让该函数或方法在新创建的 goroutine 中执行。
go f() // 创建一个新的 goroutine 运行函数f
匿名函数也支持使用go关键字创建 goroutine 去执行。
go func(){
// ...
}()
一个 goroutine 必定对应一个函数/方法,可以创建多个 goroutine 去执行相同的函数/方法。
3.2 多协程
在 Go 语言中实现并发就是这样简单,我们还可以启动多个 goroutine 。让我们再来看一个新的代码示例。
package main
import (
"fmt"
"sync"
)
func myGoroutine(name string, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 5; i++ {
fmt.Printf("myGroutine %s\n", name)
time.Sleep(10 * time.Millisecond)
}
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
go myGoroutine("goroutine1", &wg)
go myGoroutine("goroutine2", &wg)
wg.Wait()
}
3 管道 Channel
3.1. 管道介绍
Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
3.2. 使用
3.2.1初始化channel
声明的通道类型变量需要使用内置的make函数初始化之后才能使用。具体格式如下:
make(chan 元素类型, [缓冲大小])
其中:
- channel的缓冲大小是可选的。
栗子 :
channel_name = make(chan channel_type)
channel_name = make(chan channel_type, size)
3.2.2 channel操作
通道共有发送(send)、接收(receive)和关闭(close)三种操作。而发送和接收操作都使用<-符号。
现在我们先使用以下语句定义一个通道:
ch := make(chan int)
发送
将一个值发送到通道中。
ch <- 10 // 把10发送到ch中
接收
从一个通道中接收值。
x := <- ch // 从ch中接收值并赋值给变量x
<-ch // 从ch中接收值,忽略结果
关闭
我们通过调用内置的close函数来关闭通道。
close(ch)
3.3 管道的缓存
无缓存通道 : 通道里没有 make 建立缓存区 特点: 读入数据后没有读出时会阻塞,没有数据直接读时也会阻塞
func main() {
ch := make(chan int)
ch <- 10
fmt.Println("发送成功")
}
有缓存管道 : 有缓冲 channel 可以理解为异步模式。即写入消息之后,即使还没被消费,只要队列没满,就可继续写入。
func main() {
ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
ch <- 10
fmt.Println("发送成功")
}