Java 转 go 学习 - 并发编程(3)

文章目录

  • [1. 惰性生成器](#1. 惰性生成器)
    • [1.1 基于通道创建](#1.1 基于通道创建)
    • [1.2 Iter.Seq 实现](#1.2 Iter.Seq 实现)
    • [1.3 自己实现一个 Seql](#1.3 自己实现一个 Seql)
    • [1.4 生成更通用的惰性生成器](#1.4 生成更通用的惰性生成器)
  • [2. Future 模式](#2. Future 模式)
    • [2.1 基础 Future 模式实现](#2.1 基础 Future 模式实现)
    • [2.2 基础使用](#2.2 基础使用)
      • [2.2.1 http 异步调用](#2.2.1 http 异步调用)
      • [2.2.2 带超时控制的 Future](#2.2.2 带超时控制的 Future)
      • [2.3 多任务并发执行](#2.3 多任务并发执行)
  • [3. 小结](#3. 小结)

本系列文章:


1. 惰性生成器

惰性生成器就是按需计算并返回值的机制,通过 channel 和 goroutine 结合实现,仅仅在需要的时候才生成下一个值,而不是一次性返回整个序列,通过这种方式可以节省内存,提高处理大数据流的效率。

具体的表现就是:

  1. 创建一个通道,调用者通过这个通道来获取生成的值。
  2. 后台通过 goroutine 来按需生成值然后发送到通道。
  3. 每次从通道读取的时候才会生成下一个值。

1.1 基于通道创建

首先来看下最基础的创建方式,基于通道和 goroutine 创建。

go 复制代码
// 返回一个只读通道
func integers() <-chan int {
	yield := make(chan int)
	count := 0
	go func() {
		for {
			yield <- count
			count++
		}
	}()
	return yield
}

func test61() {
	gen := integers()
	fmt.Println(<-gen) // 0
	fmt.Println(<-gen) // 1
	fmt.Println(<-gen) // 2
}

这种生成器的核心逻辑就是:通道阻塞 ,我们知道 channel 的特点就是如果没有缓冲区,那么往里面写入数据 必须要等到从通道获取数据之后才能继续往里面写入。

虽然 integers 方法中通过 go 启动了一个协程不断往通道写入数据,但是 yield <- count 写入第一个数据之后会直接阻塞,所以 integers 方法将这个只读通道返回了,注意返回值是 <- chan,也就是说虽然我们创建出来的 channel 是双向的,但是函数会转成 只读通道 返回。

拿到这个通道之后,只有调用 <- gen 才会从通道获取数据,然后协程才会继续往里面写入,同时由于协程阻塞会休眠,不会消耗 CPU,所以不会导致 CPU 空转。

1.2 Iter.Seq 实现

Go 1.23+ 之后官方提供了 iter 包,可以用来实现更简洁的惰性生成器。

go 复制代码
// integersSeq 返回一个基于 iter.Seq 的惰性生成器,生成 1 到 5。
func integersSeq() iter.Seq[int] {
	return func(yield func(int) bool) {
		for i := 1; ; i++ {
			if !yield(i) {
				return
			}
		}
	}
}

func test62() {
	// 直接遍历值
	//for v := range integersSeq() {
	//	fmt.Println(v)
	//	// 1
	//	// 2
	//	// 3
	//	// 4
	//	// ...
	//}

	// 也可以手动获取下一个值的函数
	next, stop := iter.Pull(integersSeq())

	fmt.Println(next()) // 1 true
	fmt.Println(next()) // 2 true
	fmt.Println(next()) // 3 true
	fmt.Println(next()) // 4 true
	fmt.Println(next()) // 5 true

	stop()

	fmt.Println(next()) // 0 false

}

下面来看下这个函数:

go 复制代码
type Seq[V any] func(yield func(V) bool) bool

这个函数接收一个 func(V) bool 类型的参数,返回 bool 的函数,当我们在函数内调用 yield,就会把值传递给消费者,比如 for-range 遍历的时候就会不断接收从 yield 方法传过来的值,next() 也是。

什么时候 yield 会返回 false,比如调用了 stop() 函数,for-range 遍历完成,或者在 for-range 中进行 break 了,上面这种写法不能用 for-range 遍历,因为生成器从 1 开始不断往里面通过 yield 传数据,所以我们可以通过 Pull 来拉取数据,每一次就 + 1,返回的第二个参数代表生成器有没有停止。

1.3 自己实现一个 Seql

我们也可以来模拟一个 yield 的写法。

首先定义迭代器类型:

go 复制代码
// 定义迭代器类型
type MySeq[V any] func(yield func(V) bool)

然后我们定义 MyPull 方法,这个方法参数是 MySeq 类型,返回值是 (next func() (V, bool), stop func()),通过 next 可以获取下一个值,通过 stop 可以关闭生成器的通道。

go 复制代码
// MyPull 把 push 风格的迭代器转成 pull 风格的 next/stop。
// stop 返回后,后续 next 一定只会得到零值和 false。
func MyPull[V any](seq MySeq[V]) (next func() (V, bool), stop func()) {
	ch := make(chan V)
	stopCh := make(chan struct{})
	done := make(chan struct{})

	go func() {
		defer close(done)
		defer close(ch)

		seq(func(v V) bool {
			select {
			case <-stopCh:
				return false
			case ch <- v:
				return true
			}
		})
	}()

	next = func() (V, bool) {
		v, ok := <-ch
		return v, ok
	}

	stop = func() {
		select {
		case <-done:
			return
		case <-stopCh:
			<-done
			return
		default:
			close(stopCh)
			<-done
		}
	}

	return next, stop
}

可以看到这个函数中 ch 就是生成数的通道,stopCh 和 done 是暂停生成的通道,在函数里面启动一个 go 协程,执行 seq 函数,由于这个函数接收参数是 yield func(V) bool,所以往通道写入的函数就是 yield,做的事情很简单,就是把 yield 的参数往 ch 通道写入,同时监听 stopCh 通道,如果发现 stopCh 关闭了就会监听到这个分支然后返回 false。

为什么要两个通道 stopCh 和 done 呢,这两个通道代表的含义不同:

  • stopCh: 你对生成器说 "别再产出了,准备停"
  • done:生成器说 "我已经退出了,后续不会再生成数据"

假设没有 done,我们可能会写成下面的写法。

go 复制代码
stop = func() {
     close(stopCh)
}

这种情况下有可能会发生下面的情况(我们考虑多个程序同时调用 next()),假设先前调用了 next,此时 goroutine 是阻塞在 select 等待进入某个 case 的:

  • 程序1 调用 stop(),此时 <-stopCh 分支就绪
  • 程序2 同时调用 next() 获取数据
  • 由于此时 close(stopCh) 和 ch <- v 都准备继续,那么 goroutine 的两个 case 都有可能进入
  • 这时候外层看来就是,明明先调用了 stop,但是还能通过 next 获取数据

但是加上 done 之后,流程就是如下了:

  • 程序1 调用 stop(),进入 default 分支,close(stopCh) 发送停止信号,然后阻塞在 <-done 上面。
  • 程序2 同时调用 next() 获取数据。
  • 由于此时 close(stopCh) 和 ch <- v 都准备继续,那么 goroutine 的两个 case 都有可能进入,此时没有语义上面的问题,因为这种情况下 stop() 卡在 <-done 上面,说明 stop 还没有调用完成,通道不关闭也是对的。
  • goroutine 假设先处理完 next,再进入 case <-stopCh 分支,此时 return false 退出 seq。
  • 假设此时还有 next() 请求到来,就会卡在 v, ok := <-ch,因为 goroutine 已经不生产数据了。
  • goroutine 执行 close(ch)、close(done),执行完成之后其他 next() 请求会返回 0, false,因为通道关闭了。
  • 到这里程序1 stop() 才收到信号,结束。

总之记住一句话:调用 close 之后从无缓冲通道获取到的就是通道类型的零值了,但是如果是有缓冲的通道就会先获取出缓冲区的值,最后再返回零值。

输出如下:

go 复制代码
func test63() {
	next, stop := MyPull(generator())
	fmt.Println(next())
	fmt.Println(next())
	fmt.Println(next())
	fmt.Println(next())

	stop()

	fmt.Println(next())
	fmt.Println(next())
	
	// 1 true
	// 2 true
	// 3 true
	// 4 true
	// 0 false
	// 0 false
}

上面解释了一些问题,下面来看下这种写法又有什么问题。

go 复制代码
func MyPull[V any](seq MySeq[V]) (next func() (V, bool), stop func()) {
	ch := make(chan V)
	stopCh := make(chan struct{})

	go func() {
		defer close(ch)

		seq(func(v V) bool {
			select {
			case ch <- v:
				return true
			default:
				return false
			}
		})
	}()

	next = func() (V, bool) {
		v, ok := <-ch
		return v, ok
	}

	stop = func() {
		stopCh <- struct{}{}
	}

	return next, stop
}

这种写法是非阻塞的,也就是 seq 函数中,如果 ch 中暂时没有值默认走的是 default 分支,我们想的是用 default 代替 <- stopCh,但是这是不对的,比如下面的语序:

  1. 主程序执行 next(),获取到 goroutine 设置到通道里面的值。
  2. 此时还没等主程序继续执行 next(),goroutine 继续遍历,走到 default 分支返回 false,退出,然后关闭 ch。
  3. 主程序此时执行了几次 next() 返回的都是 0, false。
  4. 主程序最后执行 stop() 报错 fatal error: all goroutines are asleep - deadlock!,因为这时候往 stopCh 写入的数据,阻塞住了,但是所有 goroutine 都退出了,没有协程再去处理这个通道。

输出如下:

go 复制代码
1 true
0 false
0 false
0 false
fatal error: all goroutines are asleep - deadlock!

如果 stop 改成 close(stopCh),就会输出这个:

go 复制代码
1 true
0 false
0 false
0 false
0 false
0 false

不管怎么样都不对。

1.4 生成更通用的惰性生成器

我们可以通过工厂函数创建惰性生成器。

go 复制代码
type Any interface{}
type EvalFunc func(Any) (Any, Any)

func BuildLazyIntEvaluator(evalFunc EvalFunc, initState Any) func() Any {
	// 数据写入通道
	retValChan := make(chan Any)

	loopFunc := func() {
		// 初始值
		var actState Any = initState
		// retVal 是经过我们的函数 evalFunc 计算出来的值
		var retVal Any
		// 循环写入通道
		for {
			// 将初始值传进去, 可以进行 +-*/ 等操作
			retVal, actState = evalFunc(actState)
			retValChan <- retVal
		}
	}
	// 函数, 从通道获取数据
	retFunc := func() Any {
		return <-retValChan
	}
	// 启动协程, 往 retValChan 写入数据
	go loopFunc()
	
	return retFunc
}

函数 BuildLazyIntEvaluator 接收 evalFunc 类型的参数,evalFunc 是一个函数,可以将任意类型传进去,然后计算出下一个要返回的值 retVal,最后将这个值写入通道,actState 则是实时更新。

下面来看下用法:

go 复制代码
func test65() {
	evenFunc := func(state Any) (Any, Any) {
		os := state.(int)
		ns := os + 2
		return os, ns
	}
	even := BuildLazyIntEvaluator(evenFunc, 0)
	for i := 0; i < 10; i++ {
		fmt.Printf("%vth even: %v\n", i, even())
	}
}

我们初始化函数 evenFunc,这个函数会将 state + 2 返回给 go 协程,让协程写入通道,同时记录 state + 2 的值,下一次再根据这个值再传入 evalFunc 来获取下一个生成值。

2. Future 模式

Future 模式就是:用户向系统提交一个任务,系统会通过协程去执行这个任务,然后将结果写到 Future,用户执行完其他任务之后可以通过 Future 获取到之前提交任务的执行结果,简单来说就是将一些操作解耦出来异步执行。

Go 语言中实现 Future 是 使用 goroutine 配合带缓冲的 channel,下面来看下具体实现。

2.1 基础 Future 模式实现

最常见的就是创建一个返回 <- chan Result 的函数,通过 goroutine 执行任务,然后通过带缓冲的 channel 返回结果。

go 复制代码
package main

import (
	"fmt"
	"go-learn-1/learn-4/util"
	"time"
)

type Result struct {
	Data any
	Err  error
}

func asyncCompute() <-chan Result {
	// 创建带缓冲的 channel
	ch := make(chan Result, 1)

	// 开启协程调用函数
	go func() {
		defer close(ch)
		data, err := work()
		ch <- Result{Data: data, Err: err}
	}()

	return ch
}

func work() (any, error) {
	fmt.Printf("[%s]work............\n", util.Now())
	time.Sleep(1 * time.Second)
	return "模拟异常返回结果", fmt.Errorf("模拟抛出异常")
}

func main() {
	ch := asyncCompute()

	// 阻塞
	res := <-ch
	if res.Err != nil {
		fmt.Printf("[%s]%v:%v\n", util.Now(), res.Data, res.Err)
	}
}

这里的关键点就是必须使用带缓冲的 channel(make(chan Result, 1)),否则 goroutine 可能在发送的时候阻塞。然后就是结果用一个结构体将 error 和返回结果包装起来。

最后接收方可以通过 val, ok := <-ch 判断结果是否已经送达,不需要外部调用手动 close(channel),在 goroutine 内部使用 defer close(ch) 来关闭,因为 ch 只是用来存结果,也就是只会往里面写入一次,所以直接关闭之后结果会写入缓冲区。

2.2 基础使用

2.2.1 http 异步调用

首先就是 http 异步获取结果写入 channel,下面是简单写法,可以看到我们通过 go 协程去 Get 数据,然后处理数据到 body,将结果写入 channel。

go 复制代码
func fetchURL(url string) <-chan string {
    ch := make(chan string, 1)
    go func() {
        resp, err := http.Get(url)
        if err != nil {
            ch <- ""
            return
        }
        defer resp.Body.Close()
        body, _ := ioutil.ReadAll(resp.Body)
        ch <- string(body)
    }()
    return ch
}

// 使用
html := <-fetchURL("https://example.com")

2.2.2 带超时控制的 Future

我们可以在 Future 中加上超时控制,如果处理结果超时,就往里面写入一个 Error。

go 复制代码
func asyncCompute(timeout time.Duration) <-chan Result {
	ch := make(chan Result, 1)

	go func() {
		resultCh := make(chan Result, 1)
		go func() {
			data, err := work()
			resultCh <- Result{Data: data, Err: err}
		}()

		timer := time.NewTimer(timeout)
		defer timer.Stop()

		select {
		case res := <-resultCh:
			ch <- res
		// 超时控制		
		case <-timer.C:
			ch <- Result{Err: fmt.Errorf("async compute timeout after %s: %w", timeout, context.DeadlineExceeded)}
		}
	}()

	return ch
}

可以看到这里是开了两个协程,一个协程用来控制超时,一个用来处理业务逻辑。

2.3 多任务并发执行

go 复制代码
func asyncTask(name string, duration time.Duration) <-chan Result {
	ch := make(chan Result, 1)

	go func() {
		defer close(ch)

		fmt.Printf("[%s]%s start\n", util.Now(), name)
		// 模拟任务请求耗时
		time.Sleep(duration)
		fmt.Printf("[%s]%s done\n", util.Now(), name)

		ch <- Result{
			Data: fmt.Sprintf("%s finished in %s", name, duration),
		}
	}()

	return ch
}

func waitAll(tasks ...<-chan Result) []Result {
	results := make([]Result, len(tasks))
	// 阻塞等待所有任务的返回结果
	for i, task := range tasks {
		results[i] = <-task
	}
	return results
}

多任务就是创建多个 goroutine 来执行任务,然后通过 for-range 来阻塞等待结果,下面来看下调用。

go 复制代码
func main() {

	fmt.Printf("[%s]wait all tasks\n", util.Now())
	results := waitAll(
		asyncTask("task-1", 1*time.Second),
		asyncTask("task-2", 500*time.Millisecond),
		asyncTask("task-3", 2*time.Second),
	)

	for i, result := range results {
		fmt.Printf("[%s]result-%d data=%v err=%v\n", util.Now(), i+1, result.Data, result.Err)
	}
}

输出如下:

go 复制代码
[2026-03-29 15:26:07]wait all tasks
[2026-03-29 15:26:07]task-3 start
[2026-03-29 15:26:07]task-1 start
[2026-03-29 15:26:07]task-2 start
[2026-03-29 15:26:08]task-2 done
[2026-03-29 15:26:08]task-1 done
[2026-03-29 15:26:09]task-3 done
[2026-03-29 15:26:09]result-1 data=task-1 finished in 1s err=<nil>
[2026-03-29 15:26:09]result-2 data=task-2 finished in 500ms err=<nil>
[2026-03-29 15:26:09]result-3 data=task-3 finished in 2s err=<nil>

3. 小结

好了,go 的协程先学到这里,先把后续的数据库和 web 操作学了再说。

如有错误,欢迎指出!!!

相关推荐
传奇开心果编程1 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
欣欣之王来了1 小时前
5G网络基础:5G的核心特性,eMBB、uRLLC、mMTC的应用场景
网络·学习·计算机网络·安全·教程
Xudde.2 小时前
CVE-2026-24061漏洞复现
学习·安全
传奇开心果编程2 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程2 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
一条破秋裤2 小时前
03_初始化字符设备_从cdev到应用open
学习
传奇开心果编程2 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer
一隅论数智2 小时前
RDF(Resource Description Framework)介绍和使用举例(二)
大数据·人工智能·经验分享·笔记·学习·架构·政务
海绵宝宝转agent11 小时前
learn-claude-code第1-5章开源学习笔记分享
人工智能·笔记·python·学习