Go 1.21 新加的 log/slog 大家开始用了吗? 写了一个输出到阿里云日志的 writer 扩展, 顺带问个跟 slog 扩展相关的问题.

226 天前
 Gota

先是分享下我写的扩展

扩展 writer 的地址: https://github.com/gota33/aliyun-log-writer

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

用的时候把参数中的 os.Stdout 换成这个 writer 就行了.

然后问两个问题

一个是 slog 想 Hook 的话就两个接口可以入手, slog.Handlerio.Writer.

我这里选择了 io.Writer, 主要是因为自己写 slog.Handler 的话, 得把 slog.commonHandler 里的逻辑再实现一遍, 实在有点麻烦, 有没有像 logrus.Hook 那样比较简单的实现方式?


还有一个问题跟 channel 有关.

有这样的工作队列, submit() 提交任务, stop() 阻止新任务提交, 并处理完存量任务后返回.

var (
	ErrClosed = errors.New("closed")
	chQuit    = make(chan struct{})
	chData    = make(chan int, 10)
)

// func start() { ... }

func submit(n int) error {
	select {
	case <-chQuit:
		return ErrClosed
	case chData <- n:
		return nil
	}
}

func stop() {
	close(chQuit)
	close(chData)
	
	for n := range chData {
		// process data
		_ = n
	}
}

但是 selectswitch 不一样, case 的选择不是有序的, 导致有时候会选到第二个 case 然后 panic.

所以后来把 stop() 改成这样了

func stop() {
	close(chQuit)

	ch := chData
	chData = make(chan int)

	for len(ch) > 0 {
		// process data
		select {
		case n := <-ch:
			_ = n
		default:
		}
	}
}

这种情况大家一般是怎么处理的?

2970 次点击
所在节点    Go 编程语言
40 条回复
hallDrawnel
226 天前
看你的逻辑是想阻止新任务提交,但是已经提交的任务要继续处理完是吧?

如果可以可以控制上游,那应该把 close chan 返回回去,让上游自己停止提交。
Gota
226 天前
@hallDrawnel 上游是 io.Writer 的 Write(), 用户用 slog 写日志的时候触发, 所以控制不了.
wentx
226 天前
close(chQuit) 改成 chQuit <- struct{}{}

试试
hsfzxjy
226 天前
chData 不要 close 呗
Gota
226 天前
#3 @wentx 只要 select 是无序的, 都有可能选到第二个. https://stackoverflow.com/questions/68650423/do-select-statements-guarantee-order-of-channel-selection

#4 @hsfzxjy 结尾那套写法就没 close, 想看看有没有其他的处理方式.
wentx
226 天前
@Gota 不不不,你 chQuit 是无缓冲的 channel, 你在往里写的时候,会阻塞,等到 submit 函数里面
case <-chQuit:
return ErrClosed

这段执行到,才会走到下一步 close(chData),这个时候 submit 函数已经退出,所以 submit 不会 panic
wentx
226 天前
@wentx 然后 stop 的最后再去 close(chQuit)
Gota
226 天前
@wentx 但 submit() 不一定只有一个线程在调用. 而且如果在 stop 清空存量任务的过程中, 有另一个 submit() 调用, 还是会走到第二个 case 的吧? 可能还有一个问题, 如果没有 submit() 直接调用 stop() 程序就卡住了.
hsfzxjy
226 天前
@wentx 要是此时没有 submit ,那 stop 就塞住了
要是有多个 submit 在并发,那只有一个是正确的
你这问题更大
wentx
226 天前
@Gota 这个是队列设计的问题了,如何保证 producer 关闭后,其他 goroutine 调用 submit 没有副作用。
wentx
226 天前
在这个场景下面,甚至都不需要使用 channel 来通知,你直接一个全局变量,在 submit 的时候,判断一下是否是 close 就可以了。
Gota
226 天前
@wentx 没听太明白, 可以具体描述下吗?
wentx
226 天前
@Gota
```
var (
ErrClosed = errors.New("closed")
chQuit bool
chData = make(chan int, 10)
)

// func start() { ... }

func submit(n int) error {
if chQuit {
return ErrClosed
}

chData <- n:

return nil
}

func stop() {
chQuit = true
close(chData)

for n := range chData {
// process data
_ = n
}
}
```
pkoukk
226 天前
stop 里不要 close(chData)啊 , defer close(chData) 不就可以了?
Gota
226 天前
#13 @wentx 嗯... 并发环境下直接用 bool 值已经很危险了. 而且即使这样写, 程序卡住和 panic 的问题依然会存在.
#14 @pkoukk defer 也不行, 哪怕 stop() 已经完全执行完了. 这时候调 submit() 还是有概率选到第二个 case.
wentx
226 天前
@Gota 会啥会卡住 和 panic 呢?
并发的话,atomic.LoadXXX 或者 atomic.StoreXXX
Gota
226 天前
@wentx

因为并发环境下函数执行随时会被挂起. 如果 submit 执行完 if 判断被挂起, 去执行 stop, 等恢复执行 submit 的时候就会 panic

即使正常执行, 如果 submit 执行到 chData <- n 如果因为 buffer 满了开始等待, 此时执行 stop, 会 100% panic.
pkoukk
226 天前
@pkoukk #14 stop 都执行完了,为什么还有线程可以调用 submit 啊?
通道关闭了,还尝试往通道写入数据也是一种 panic 行为啊
我们一般的做法是用 context.Done 作为标志,因为 contenxt 是可以逐层传递的
当 sumbit 这边收到 Done 的时候,生产者同时也应该停止了
Gota
226 天前
@pkoukk 因为这是个 Logger, 调用者从各个线程触发写日志的操作. 在主线程调用 stop() 的时候没法确保其他线程都提前停下来不写日志. 如果 writer 返回 ErrClose 的话 slog 是能处理的, 但直接 panic 掉就不行了.
qing18
226 天前
为啥要 close chData 呢?
```
func stop() {
select {
case <-chQuit:
return
default:
close(chQuit)
}
}
```

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/977965

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX