泛型:
- 泛型函数
- 泛型类型
泛型函数:
可以使用类型参数编写 Go 函数来处理多种类型。函数的类型参数出现在函数参数之前的方括号之间。语法如下:
func Index[T comparable](s []T, x T) int {
}
此声明意味着 s 是满足内置约束 comparable 的任何类型 T 的切片。x 也是相同类型的值。
comparable 约束可以让我们对任意满足该类型的值使用 == 和 != 运算符。在此示例中,使用 comparable 约束将 目标值 x 与切片的所有元素进行比较,直到找到匹配项。该 Index 函数适用于任何支持比较的类型。
示例代码:
package main
import "fmt"
// Index 返回 x 在 s 中的下标,未找到则返回 -1。
func Index[T comparable](s []T, x T) int {
for i, v := range s {
// v 和 x 的类型为 T,它拥有 comparable 可比较的约束,
// 因此我们可以使用 ==。
if v == x {
return i
}
}
return -1
}
func main() {
// Index 可以在整数切片上使用
si := []int{10, 20, 15, -10}
fmt.Println(Index(si, 15))
// Index 也可以在字符串切片上使用
ss := []string{"foo", "bar", "baz"}
fmt.Println(Index(ss, "hello"))
}
泛型类型:
除了泛型函数之外,类型也可以使用类型参数进行参数化,从而实现通用数据结构。示例代码中展示了可以保存任意类型值的单链表的声明。
package main
import (
"fmt"
"strconv"
)
// List 表示一个可以保存任何类型的值的单链表。
type List[T any] struct {
next *List[T]
val T
}
func main() {
//创建三个节点
node1 := &List[int]{val: 1}
node2 := &List[int]{val: 2}
node3 := &List[int]{val: 3}
//链接节点 1 -> 2 -> 3
node1.next = node2
node2.next = node3
//遍历链表
curr := node1
for curr != nil {
//引入 strconv 包的 Itoa 方法可以将 int 转成 string
fmt.Printf(strconv.Itoa(curr.val) + " ")
curr = curr.next;
}
}
并发
- 协程 goroutine
- 管道 channel
- 协程 goroutine 结合 Channel 管道
- 单向管道
- select 多路复用
- Golang 并发安全和锁
- Goroutine Recover 解决协程中出现的 Panic
协程 goroutine
多协程与多线程:Golang 中每个 goroutine (协程) 默认占用内存远比 Java、C 的线程少。操作系统线程一般都有固定的栈内存(通常为 2MB 左右),一个 goroutine (协程) 占用内存非常小,只有 2KB 左右,多协程 goroutine 切换调度开销方面远比线程要少。这也是为什么越来越多的大公司使用 Golang 的原因之一。
在 Go 语言中,主线程上开启多个协程,就类似 Java 中的主线程上开多个子线程一样。
举例实现:在主线程中开启一个 goroutine,该协程每隔 50 毫秒输出 ”你好 golang“。然后在主线程中同时每隔 50 毫秒输出 ”你好 golang“,输出 10 次后,退出程序。要求协程与主线程同时执行
package main
import (
"fmt"
"time"
)
func test() {
for i := 0; i < 10; i++ {
fmt.Println("test() 你好 golang")
time.Sleep(time.Millisecond * 50)
}
}
func main() {
go test()
for i := 0; i < 10; i++ {
fmt.Println("main() 你好 golang")
time.Sleep(time.Millisecond * 50)
}
}
使用关键字 go 来开启协程。
输出结果:

上述视线中存在问题,当主线程执行的时间要比协程执行的时间短,那么当主线程结束时,协程还没执行完成,程序就退出了。为了保证程序可以顺利执行,想让协程执行完毕后再执行主线程退出,这个时候可以使用 sync.WaitGroup 等待协程执行完毕。
定义一个 sync.WaitGroup 类型的全局变量,用来监听协程运行状况。
var wg sync.WaitGroup
用到的方法如下:
wg.Add(1) //协程计数器+1
wg.Done() //协程计数器-1
wg.wait() //等待所有协程运行结束
输出结果:

Channel 管道
管道是 Go 中提供给 goroutine 间的通信方式,可以通过 channel 管道在多个 goroutine 之间传递信息。管道是一种引用数据类型,声明管道的时候需要为其指定元素类型。
声明管道类型的格式如下:
var 变量 chan 元素类型
举例子:
var ch1 chan int //声明一个传递整型的管道
var ch2 chan bool //声明一个传递布尔型的管道
var ch3 chan []int //声明一个传递 int 切片的管道
创建 channel
make(chan 元素类型, 容度)
channel 操作:发送 + 接收
ch <- 10 //将 10 发送到 ch 中
x := <- ch //从 ch 中接收值并赋值给变量 x
创建管道时加入元素超过了管道大小,会报出 deadlock;如果管道数据已经全部取出,再取就会报告 deadlock。
Goroutine 结合 Channel 管道
为更深入理解两者结合,有几个需求需要处理:
需求1:定义两个方法,一个方法给管道里面写数据,一个给管道里面读取数据。要求同步进行。
要求如下:
1、开启一个 fn1 的协程向管道 inChan 中写入 100 条数据
2、开启一个 fn2 的协程读取 inChan 中写入的数据
3、注意:fn1 和 fn2 同时操作一个管道
4、主线程必须等待操作完成后才可以退出
示例代码:
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func writeChan(ch chan int) {
for i := 1; i <= 10; i++ {
ch <- i
fmt.Printf("写入数据%v\n", i)
time.Sleep(time.Millisecond * 50)
}
// 只通过 for 循环遍历需要手动关闭 管道,否则会报死锁错误
close(ch)
wg.Done()
}
func readChan(ch chan int) {
for v := range ch { //range 遍历管道
fmt.Printf("读出数据%v\n", v)
time.Sleep(time.Millisecond * 50)
}
//通过 for + range 循环遍历不需要手动关闭管道
wg.Done()
}
func main() {
ch := make(chan int, 10)
wg.Add(1)
go writeChan(ch)
wg.Add(1)
go readChan(ch)
wg.Wait()
fmt.Println("结束~~")
}
输出结果:

需求2:统计 1-120000 范围内的素数,诉求是通过协程和管道实现边判断边打印素数。
小插曲:想测试代码的执行时间可以用 time 包下的方法,使用方式如下:
start := time.Now().Unix()
// 你的所有执行的代码逻辑
end := time.Now().Unix()
fmt.Print(end - start, "毫秒")
需求实现流程图如下:

示例代码:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
// 存放 1-120000 数字的 channel
func putNum(intChan chan int) {
for i := 2; i < 1200; i++ {
intChan <- i
}
//存完数据后,关闭管道
close(intChan)
wg.Done()
}
// 统计 intChan管道中 素数的 channel
func primeNum(primeChan chan int, intchan chan int, eixtChan chan bool) {
// for + range 遍历 intChan 管道
for num := range intchan {
var flag = true
for i := 2; i < num; i++ {
if num%i == 0 {
//说明不是素数
flag = false
break
}
}
//说明是素数
if flag {
primeChan <- num
}
}
eixtChan <- true
wg.Done()
//对于管道 primeChan 如果我们在这里关闭的话就会出问题
//因为我们开启了 16 个协程,在其中任何一个协程中关闭管道都会导致 其它 15 个协程无法操作 primeChan ,出现死锁问题
//close(primeChan)
// 必须等待 这 16 个线程都执行完毕才能关闭管道,那么如何判断 16 个线程都执行完毕呢
// 每执行完 一个协程后,都向 exitChan 中存入一条标记数据
}
// 打印素数的 channel
func printNum(primeNum chan int) {
for v := range primeNum {
fmt.Println(v)
}
wg.Done()
}
// 开启一个负责监控 16 个协程 执行的 primeNum 方法的 协程,为每个协程进行标记
func exitfn(exitChan chan bool, primeChan chan int) {
for i := 0; i < 16; i++ {
<-exitChan // 等待所有 primeNum 协程完成
}
// exitfn 协程结束就意味着,16 个 primeNum 协程都已执行完毕
// 关闭primeChan管道
close(primeChan)
wg.Done()
}
func main() {
intChan := make(chan int, 120000)
primeChan := make(chan int, 120000)
exitChan := make(chan bool, 16)
wg.Add(1)
go putNum(intChan)
//开启 16 个线程来并行 判断数字是否为素数
for i := 1; i <= 16; i++ {
wg.Add(1)
go primeNum(primeChan, intChan, exitChan)
}
wg.Add(1)
go printNum(primeChan)
wg.Add(1)
go exitfn(exitChan, primeChan)
wg.Wait()
fmt.Println("程序结束~~~")
}

No responses yet