泛型:

  • 泛型函数
  • 泛型类型

泛型函数:

可以使用类型参数编写 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("程序结束~~~")

}

Categories:

Tags:

No responses yet

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注