两个 goroutine 用 channel 通信,一个 goroutine 顺序发送 0,1,2,3,4 个数字,另一个 goroutine 接收并输出。 考察了 goroutine 的控制、channel 的关闭等基础知识,面试者写的代码各种问题。
上周面试 5 个人只有 1 个人一次写出了执行没问题的代码,有 1 个经过提示也没写出来,剩下的能提示后逐步修改出正确的代码。
这个题还是很经典的,不用问 GMP 、垃圾回收算法等八股文,这个题就能看出 go 基础了。
![]() |
1
NerdHND 3 天前 ![]() 拿我得指出哥们候选人真的有问题吧, 这, 这不是完全不会嘛...
|
2
hingle 3 天前 ![]() 薪资给多少?薪资低那就 “门当户对” 了,挺好的。
|
![]() |
3
kandaakihito 3 天前 ![]() 看了一下,上周那个 “一面出 LRU 算法题算难吗” 的帖子也是你发的。老哥你负责面的到底是什么类型岗位呀?怎么题目差距好像有点儿大。
而且说实话,如果给你投简历的人里面,有 80%连 chan 的基础都用不清楚。。。那么你这个岗位面试的时候应该不适合问 LRU 。 |
![]() |
4
nomagick 3 天前 ![]() 你也别有莫名的优越感然后老来收铜币;
先用务实标准筛选一下吧,统招本科四六级,3 年经验之类的 |
![]() |
5
Keystroke 3 天前
time.sleep……
|
![]() |
6
clemente 3 天前
如果说从通过率分布的角度来定义 题的难度的话
90% 应该算 hard |
7
fruitmonster 3 天前
虽然知道应该使用 waitgroup ,但是, "time sleep " 没解决没执行完就退出这个问题么?
|
![]() |
8
czfy 3 天前 via Android ![]() 钓鱼上瘾是病,得治
|
![]() |
9
xuanbg 3 天前
你这个不叫 go 基础,就是个编程基础。
|
10
jworg 3 天前
可以用第三方库不,我脑子已经被 conc 惯坏了,只记得起 conc 的 WaitGroup , 系统的 WaitGroup 好像属于 sync 包。
|
11
langhuishan 3 天前 ![]() golang 的精髓不就是并发吗?这都没掌握,等于没学
|
![]() |
12
cooooing 3 天前 via Android
@fruitmonster time sleep 只是降低出现这个问题的概率,并没有解决问题。如果操作是请求 api 或者其他耗时操作,要 sleep 多久?这种方式不好的,还是得 wait group 或者其他方式来同步
|
![]() |
13
Vegetable 3 天前
这种情况至少说明完全没做过需要 graceful shutdown 的程序,不能说判死刑,但至少对 go 不太了解吧
|
![]() |
14
lasuar 3 天前
可以说说招聘岗位要求几年经验,给多少薪资
|
![]() |
15
zljklang 3 天前
package main
import "fmt" func main() { // 创建无缓冲通道 ch := make(chan int) // 发送方 goroutine go func() { for i := 0; i < 5; i++ { ch <- i // 顺序发送数字 } close(ch) // 发送完成后关闭通道[1,7](@ref) }() // 接收方 goroutine for num := range ch { fmt.Println("Received:", num) } } 这个对吗 |
![]() |
16
givenge 3 天前 ![]() 天天发这些,不会是想转自媒体吧
|
![]() |
17
opengps 3 天前 ![]() 对于你的状态来考他们不合格,那反过来他们提问题考验你你有信心通过吗?
单个问题并不能说明人的整体能力 |
![]() |
18
hunterster 3 天前
package main
import ( "fmt" "sync" ) func writeData(c chan int, i int) { c <- i } func readData(c chan int) { for i := 0; i < 5; i++ { fmt.Println("The data is:", <-c) } } func main() { var wg sync.WaitGroup wg.Add(5) var c = make(chan int) for k := 1; k <= 5; k++ { go func() { defer wg.Done() writeData(c, k) }() } go readData(c) wg.Wait() close(c) } |
19
codersdp1 3 天前
这个确实属于 go 基础了,没掌握这些写并发功能肯定会出问题的。
|
![]() |
20
voidmnwzp 3 天前
```go
func Test2(t *testing.T) { s := make(chan int) go func() { for i := range s { fmt.Println("recv:",i) } }() for i := 0; i < 5; i++ { s <- i } close(s) } ``` 不是这和 1+。。100 求和有啥区别 很难吗 |
![]() |
21
SingeeKing 2 天前
想当年我也问过这类题,真的大量候选人答不出来😮💨
|
![]() |
22
expy 2 天前
正常,卷是真的卷,但是水平差的人也大量存在。
|
23
HappyAndSmile 2 天前
你要求什么工资?我甚至可以答得比你好很多
|
![]() |
25
bv 2 天前
@SingeeKing 真的假的?都几年经验?这种人招进去也是边学边做吧,甚至还会拖队友后腿。
|
![]() |
26
main1234 2 天前 ![]() package main
import ( "fmt" "sync" ) func main() { sw := sync.WaitGroup{} sw.Add(1) ch := make(chan int) go func() { for i := 0; i < 5; i++ { ch <- i } close(ch) }() go func() { defer sw.Done() for i := range ch { fmt.Println(i) } }() sw.Wait() fmt.Println("done") } |
30
Ayanokouji 2 天前
|
![]() |
31
Licsber 2 天前
笑死 最近写了个转发助手 和你这个题完全重叠
写出来加双端调试 部署上线测试总共两天吧 来看看我这水平能开多少( ```go // listener 省略 func handleConnection(conn net.Conn) { s := &Session{ Cmd: link_start, Remote: remote, ServerSideRemote: conn.RemoteAddr().String(), ClientID: clientID, } ctx, cancel := context.WithTimeout(context.Background(), connectionTimeout) sessionManager.Set(s.ServerSideRemote, conn) sessionManager.Bind(s.ServerSideRemote, cancel) send, _ := json.Marshal(s) token := mqttClient.Publish(controlTopic, defaultQOS, false, send) if token.Wait() && token.Error() != nil { slog.Error("Publish control packet failed:", "err", token.Error()) sessionManager.Remove(s.ServerSideRemote) return } <-ctx.Done() switch ctx.Err() { case context.Canceled: slog.Info("Link confirmed successfully:", "serversideremote", s.ServerSideRemote) case context.DeadlineExceeded: slog.Warn("Confirmation timeout after 5s:", "serversideremote", s.ServerSideRemote) sessionManager.Remove(s.ServerSideRemote) } } func runServerForwarding(s *Session) { conn, ok := sessionManager.Get(s.ServerSideRemote) if !ok { slog.Error("Connection not found:", "connID", s.ServerSideRemote) return } defer conn.Close() ctx, cancel := context.WithCancel(context.Background()) defer cancel() topic := topicPrefix + s.ClientSideLocal token := mqttClient.Subscribe(topic, defaultQOS, func(c mqtt.Client, m mqtt.Message) { select { case <-ctx.Done(): return default: slog.Debug("Recv client down:", "recv", m.Payload()) recv, err := hex.DecodeString(string(m.Payload())) if err != nil { slog.Error("HEX decode error:", "err", err) cancel() return } _, err = conn.Write(recv) if err != nil { slog.Warn("TCP write error:", "err", err) cancel() } } }) if token.Wait() && token.Error() != nil { slog.Error("Subscribe client down error:", "err", token.Error()) return } buf := make([]byte, defaultBufSize) for { select { case <-ctx.Done(): return default: conn.SetReadDeadline(time.Now().Add(readTimeout)) n, err := conn.Read(buf) if err != nil { if netErr, ok := err.(net.Error); ok && netErr.Timeout() { slog.Debug("TCP read timeout:", "connID", s.ServerSideRemote) continue } slog.Warn("TCP read error:", "err", err) return } send := hex.EncodeToString(buf[:n]) slog.Debug("Send up:", "send", send) topic := topicPrefix + s.ServerSideRemote token := mqttClient.Publish(topic, defaultQOS, false, send) if token.Wait() && token.Error() != nil { slog.Error("Publish up error:", "err", token.Error()) return } } } } ``` |
![]() |
33
yuanyao OP @hunterster 为啥要五个协程,不增加复杂度吗
|
![]() |
34
MoYi123 2 天前
我三年前写的困难版本 (多生产者多消费者关 chan) https://github.com/mmooyyii/mmooyyii/blob/master/docs/go/mpmc_channel.md
|
36
jworg 2 天前
@Ayanokouji 是的,不过基础功能的封装库并不需要经常更新,看了眼作者也还活着。
|
![]() |
37
hanxiansheng 2 天前 via Android
法拉利跑车连犁田都不会,呵呵
|
39
leehaoze98 2 天前
面外包或者校招会问两个协程交替打印,一个打印 A~Z ,一个打印 1~100 。
1. 有的人手打从'A'~'Z'的数组 2. 有的人 goroutine 没启动,主进程就退出了 3. 有的人只会处理两个协程打印数量一致的情况,一个打印 26 个,另一个打印 100 个,好多人会死锁。 |
![]() |
41
voidmnwzp 2 天前
@phpcyy #40 func Test2(t *testing.T) {
s := make(chan int) wg:=sync.WaitGroup{} wg.Add(1) go func() { for i := range s { fmt.Println("go2:",i) } fmt.Println("done") wg.Done() }() for i := 0; i < 5; i++ { s <- i } close(s) wg.Wait() } |
![]() |
42
smallparking 2 天前
```
package main import ( "fmt" "sync" ) func send(ch1 chan int) { for i := range 5 { ch1 <- i fmt.Printf("send i: %d\n", i) } close(ch1) } func recv(wg *sync.WaitGroup, ch1 chan int) { // wait receive all data and done for i := range ch1 { fmt.Printf("receive i: %d\n", i) } wg.Done() } func main() { var wg sync.WaitGroup ch1 := make(chan int) wg.Add(1) go send(ch1) go recv(&wg, ch1) wg.Wait() } ``` 确实有点难度,一般不会启两个 goroutine, 所以确实不是一下子写出来的 |
44
ChatGOP 2 天前
Gobyexample 大把例子。
|
![]() |
45
xiangxiangxiang 2 天前
写了快两年 go 。。。确实基本没用过 channel ,只有看 b 站学 go 语言的时候用过
WaitGroup 倒是工作中经常用 |
![]() |
46
k9982874 2 天前
这个不提前复习一下直接上手没 ai 辅助真写不出一次全对
|
![]() |
47
vhwwls 2 天前
运维路过,这个我觉得应该是贵司的简历筛选出了问题,一楼说的对。
|
![]() |
48
daimazha 2 天前
我觉得这也是八股文的一种
|
![]() |
50
Nazz 2 天前
我用 chan 从来不 close
|
51
zhengfan2016 2 天前 ![]() |
![]() |
52
hunterster 2 天前
@yuanyao 这个是看到要求随手写的,没注意到只要两个协程,我改一下
package main import ( "fmt" "sync" ) var wg sync.WaitGroup func init() { wg.Add(5) } func writeData(c chan int, count int) { fmt.Println("Write rountine is executed.") for k := 0; k < count; k++ { defer wg.Done() c <- k } } func readData(c chan int, count int) { fmt.Println("Read rountine is executed.") for i := 0; i < count; i++ { fmt.Println("The data is:", <-c) } } func main() { var c = make(chan int) go writeData(c, 5) go readData(c, 5) wg.Wait() close(c) } |
53
Erroad 2 天前
@zhengfan2016 #50 我建议是他先给他们公司的搞个考试,不合格的都开了再说
|
54
birdhk 2 天前
package main
import "sync" func main() { var wg sync.WaitGroup ch := make(chan int) wg.Add(2) go func() { defer wg.Done() for i := 0; i < 10000; i++ { ch <- i } close(ch) }() go func() { defer wg.Done() for i := range ch { println(i) } }() wg.Wait() } 我感觉我这样应该没问题,有大哥们 review 一下吗 |
55
Co1a 2 天前
楼主发下邮箱
|
![]() |
56
ForkNMB 2 天前
刚开始学 go 也以为是八股文看看得了 不上手试试 写的也是简单版本(因为没有具体复杂的场景 写出来当然简单)等真用 go 开发写服务 多个协程下 服务优雅关闭 资源合理释放 生产者消费者怎么稳定的转起来 还是踩了很多坑的= = 面试这个确实能筛人
|
57
sky3hao 2 天前
这个还好, 我之前出的面试题是: 用两个 channel 循环打印水分子(H2O), 分子顺序不要求, 但是每一行必须是 2 个 H 加 1 个 O 的组合
|
![]() |
59
body007 2 天前
|
60
Jinnrry 2 天前 via iPhone ![]() 我面试也问这个题目。感觉和 hr 相关,很多 hr 只看背景,不懂技术,找一堆人过来就 90%都写不出来。
我现在这里 hr 比较专业,基本上一半的人能写出来的。 想来也是搞笑,这些连个协程通信都写不明白的人,讲起垃圾回收,数组扩容,乐观锁悲观锁却能头头是道 |
![]() |
61
guanhui07 2 天前
也是八股文的一种 不过如果他能知道 waitGroup 我就让他过
|
![]() |
62
seakee 2 天前
```
func main() { num := make(chan int, 5) done := make(chan struct{}) go func() { for n := range num { fmt.Println(n) } close(done) }() go func() { for i := 0; i < 5; i++ { num <- i } close(num) }() <-done } ``` |
![]() |
63
Trim21 2 天前
这还八股啊,这不是 go 并发编程的基础知识吗 ...
|
64
kalista 2 天前
我有个问题,你们面试写代码允许用 cursor 吗
|
65
prosgtsr 2 天前
多线程的题是挺难答的。如果不经常复习的话。一段时间就忘了。甚至就连 juc 自带的几个工具叫啥都忘了。。
|
67
hxzhouh1 2 天前
大佬,求个面试机会呀
|
68
bitfly 2 天前 via Android
虽然我也不熟 但是真写过这玩意儿
在网络上并发处理随机遍历数组且不重复就需要用到这玩意 网络的不稳定性和结果返回的不确定性 的确很多 panic 修修补补不少时间配合 gpt 大法 Go 的并发网络遍历还是挺无敌的 |
![]() |
69
harryge 2 天前
package main
import ( "time" "fmt" ) func main() { ch := make(chan int) go sender(ch, 5) go receiver(ch) time.Sleep(1 * time.Second) fmt.Println("done") } func receiver(ch chan int) { for { i, ok := <- ch if !ok { fmt.Println("Chan closed") break } fmt.Println("received ", i) } } func sender(ch chan int, n int) { for i := 0; i < n; i++ { fmt.Println("sent ", i) ch <- i } close(ch) } |
![]() |
70
voidmnwzp 2 天前
@leehaoze98 func Test2(t *testing.T) {
s := make(chan int) wg := sync.WaitGroup{} wg.Add(1) go func() { var i int for { if i>=90{ break } i=<-s if i>=90{ break } i++ fmt.Printf("go1: %c\n",i) s<-i } fmt.Println("done") wg.Done() }() i := 65 fmt.Printf("main: %c\n",i) s <- i for { if i>=90{ break } i = <-s if i >= 90 { break } i++ fmt.Printf("main: %c\n",i) s <- i } wg.Wait() close(s) } |
71
securityCoding 2 天前
扔给 ai 就好了
|
72
bbao 2 天前
生产者消费者再业务开发中是非常常见的场景,考察代码能力挺好的,只是题目中有一个点会让人很奇怪,waitgroup 通常用于多个 goroutine 并发执行且等待所有 goroutine 返回结果使用。比如同时请求 HTTP 、GRPC 等请求下由多个业务接口。
[两个 goroutine 用 channel 通信] channel 原则由生产者来控制关闭。消费者直接消费,通常 v,ok:=chan 来做个逻辑判断是否关闭。 channel 另一个特点是对于已关闭的 channel 进行读时会默认返回 0 ,由第二个返回变量来判断是否关闭。所以通常消费者无需关心做额外的业务处理。 此题如果等待两个 goroutine 都结束,输出预期结果,务必需要一定的等待时间,原因在于生产者如果关闭了 channel ,则 channel 不会再有阻塞的能力。 例如: c := make(chan int, 5) go func{xxxxxx; close(c) } go func(xxxxxx;} <-c (此处不会进行任何阻塞,所以启动程序即服务进行运行结束。) waitgroup 可以,sleep 可以,context.WithTimeout 也可以。只是这三个用在这个场景中,在真实的业务里会很奇怪。一般业务也很难遇见这样的场景吧。 楼主想根据一个场景设计题同时考察 channel 、waitgroup 或 context.Withtimeout 用法之外。还是要贴合一下实际场景好一些。 比如特性特点,使用场景能很熟悉即可。是不是要用 sleep 、context.Withtimeout 、waitgroup 并不是那么重要。原因在业务代码中,如果这样出现这样的代码逻辑,是在 codreview 时打回的。 |
75
lixikei 2 天前
是不是这样子的
```go package main import ( "fmt" "sync" ) func main() { wg := sync.WaitGroup{} ch := make(chan int) wg.Add(2) go func() { defer wg.Done() defer close(ch) for i := 0; i < 5; i++ { ch <- i } }() go func() { defer wg.Done() for v := range ch { fmt.Printf("i: %d\n", v) } }() wg.Wait() } ``` |
76
zhangfuguan 2 天前
package main
import ( "fmt" "sync" ) var wg sync.WaitGroup func main() { wg.Add(2) ch := make(chan int32, 1) go send(ch) go read(ch) wg.Wait() fmt.Println("done") } func read(ch <-chan int32) { for { select { case msg := <-ch: fmt.Println(msg) if msg == 10 { wg.Done() } } } } func send(ch chan<- int32) { for i := int32(1); i <= 10; i++ { ch <- i } close(ch) wg.Done() } 这样? |
![]() |
77
CyJaySong 2 天前
帮陪朋友发的
```go package main import ( "fmt" ) func main() { ch := make(chan int) down := make(chan struct{}) go func() { for i := 0; i < 5; i++ { i := i ch <- i } close(ch) }() go func() { for num := range ch { fmt.Println("Received:", num) } down <- struct{}{} }() <-down } ``` |
![]() |
78
guanzhangzhang 2 天前
|
79
zxjxzj9 2 天前
这个确实还挺有水平的,因为一般人不写这种玩具小程序,用 goroutine 都是在 http 框架的处理器里写,确实不会在意这个退出的问题。 不过其实解决方法也很简单,就是每个 goroutine 开始前塞一个 wg.add(),运行完了塞一个 wg.done()就可以了
|
80
Huelse 2 天前
这个水平要求没啥问题,但实际上你们最后还是会选薪资要的低的
|
81
phenixc 2 天前
package main
import ( "fmt" "sync" ) func senddata(out chan <- int, wg *sync.WaitGroup) { defer wg.Done() for i := range 5 { out <- i } close(out) } func receivedata(in <- chan int, wg *sync.WaitGroup) { defer wg.Done() for i := range in { fmt.Println(i) } } func main() { var wg sync.WaitGroup ch := make(chan int) wg.Add(2) go senddata(ch, &wg) go receivedata(ch, &wg) wg.Wait() } |
![]() |
82
capgrey 2 天前
我比较好奇,为什么你懂,候选者不懂。是你们学习用的阅读材料的差异吗? op 看哪个文档的,Effective Go 吗?
|
83
caicaiwoshishui 2 天前
func main() {
ch := make(chan int, 1) wg := sync.WaitGroup{} count := 5 wg.Add(1) // Add 1 to the WaitGroup before starting the producer goroutine go func(wg *sync.WaitGroup, ch chan int, count int) { defer wg.Done() for i := 0; i <= count; i++ { ch <- i } }(&wg, ch, count) go func() { for { select { case v, ok := <-ch: if !ok { // Check if the channel is closed return } fmt.Println(v) } } }() wg.Wait() close(ch) fmt.Println("end") } |
84
sardina 2 天前 ![]() https://go.dev/play/p/Te_KHn3sWYN 只用 channel 实现
大哥,你们公司在哪个城市,我想去 |
85
2018yuli 2 天前
+1
|
![]() |
86
duty 2 天前
只能说感谢楼主,本来是一个 Go 开发,来到这家公司除了第一个项目后面全是写 python ,看到这个题的时候打开编辑器,基础语法都错了五六次,虽然最后实现了,但还是该反思自己把基础知识都整忘了,该重新抓起来了,也该开始计划换工作了,这样下去,感觉以后的路也会被糟蹋了
|
![]() |
87
hunterster 2 天前
@body007 我用的是 liteide 写的,没有这个错误提示
|
88
mayli 2 天前
正常吧,go 基础,没写过的很容易挂
|
![]() |
89
Rehtt 2 天前 via Android
也就两个 goroutine ,生产者和消费者都只有一个 goroutine 根本不需要 waitgroup ,两个 channel 就可以了,一个负载消息一个用来退出阻塞就行了
|
![]() |
90
loginv2 2 天前
v2 是回复里面不支持 md 语法么 怎么这么多人都发的没缩进了
|
![]() |
92
UN2758 1 天前
```
package main import ( "fmt" "sync" ) func producer(c chan int, wg *sync.WaitGroup) { for i := 0; i < 10000; i++ { c <- i } close(c) } func consumer(c chan int, wg *sync.WaitGroup) { for i := range c { fmt.Println("consumer", i) } wg.Done() } func main() { c := make(chan int) wg := sync.WaitGroup{} wg.Add(1) go producer(c, &wg) go consumer(c, &wg) wg.Wait() } ``` |
93
buffzty 1 天前
用不着 waitgroup ,context ,done channel 这些啊 for 循环就行了,关闭了循环就退出了
package main import "log" var ( jobs = make(chan int) ) func t2() { for v := range jobs { log.Println(v) } } func t1() { for i := 0; i < 5; i++ { jobs <- i } close(jobs) } func main() { go t1() t2() } |
94
angeni 1 天前
平台决定的候选人素质
|
![]() |
95
lesismal 1 天前 ![]() > 2. 有的 channel 不知道由生产者关闭,直接在主程序生产者还未发送结束就关闭结果 panic ;
这种面试题用一个 chan 可以,但但就这个面试题的功能的话似乎就没必要俩协程了,不需要用俩协程也就不需要用 chan 了。 所以这种题如果出给我、只是纸面作答、我是不知道怎么答只能空着,因为需求不合理。 很多基础场景生产者不是唯一的,可能会是并发多协程会生产,所以通常是应该把用于发送的和用于关闭的分开两个 chan 、用于 close 的 chan 再配个 atomic compareandswap ,避免用单个 chan 、某个地方关闭后、其他协程还在给 chan 发数据直接就 panic 了,一些粗暴的实现直接 recover 这种 panic 虽然也问题不大但毕竟它不是个好的处理方式、比如还得纠结 panic recover 是否再给调用者一个 ErrClosed 返回,还是两个 chan 好些。 另外,如果不需要清理 chan 内遗留的数,chan 本身用完之后是不需要 close 的。 |
96
gaifanking 1 天前
类似生产者消费者、状态机这种比较典型的场景/思路都有哪些呢?
|
97
sampeng 1 天前 via iPhone
说实话,我面试的时候也被问过这个类似的问题。没答上来,得到的评价是不会 golang 。这妨碍我能开发 golang 完整的项目吗?我当时回答的思路基本是自己撸一个 waitgroup ,因为当时一瞬间压根没想到 waitgroup 现成的。但我知道要有类似 waitgroup 的极致。实际开发不管是问 AI 还是 google 搜索,只要我思路有了,这也不费事。所以之后我在面试基本不会卡关键词,重点是有没思路。思路是不是自己想的和经验得来的…
|
![]() |
98
Steaven 1 天前
怎么跟我面试差不多,上周四下班后面试的
|
![]() |
99
Steaven 1 天前
两种方法,第一种:
package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup initial := make(chan struct{}) ch := make(chan int) prev := initial for i := 1; i <= 100; i++ { wg.Add(1) next := make(chan struct{}) go func(i int, prev, next chan struct{}) { <-prev ch <- i close(prev) if i < 100 { next <- struct{}{} } }(i, prev, next) prev = next } initial <- struct{}{} go func() { for i := range ch { wg.Done() fmt.Println(i) } }() wg.Wait() } 第二种(使用锁): package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup var mu sync.Mutex ch := make(chan int) cond := sync.NewCond(&mu) current := 1 for i := 1; i <= 100; i++ { wg.Add(1) go func(num int) { mu.Lock() for current != num { cond.Wait() } ch <- i current++ cond.Broadcast() mu.Unlock() }(i) } go func() { for i := range ch { wg.Done() fmt.Println(i) } }() wg.Wait() } |
100
tuxz 1 天前
```go
func main() { ch := make(chan int) doneCh := make(chan struct{}) go func(ch chan<- int) { for i := 0; i < 5; i++ { ch <- i } close(ch) }(ch) go func(ch <-chan int) { for v := range ch { fmt.Println(v) } close(doneCh) }(ch) <-doneCh } ``` |