Go 语言入门指南:进阶语法 | 豆包MarsCode AI刷题

96 阅读6分钟

文章介绍: 在学习完进阶语法后,本文基于青训营课程将讲解总结以下内容

  1. 接口
  2. 协程 Goroutine
  3. 管道 Channel

1. 接口

概念 : 1704868779506.webp

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("发送成功")
}