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

251 天前
 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:
		}
	}
}

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

3013 次点击
所在节点    Go 编程语言
40 条回复
Gota
251 天前
@qing18 不 close(chData) 就是帖子末尾处的写法.
这里的 submit 接口需要确保: 如果不返回错误的话, 写入的 data 是一定要被处理的.
所以如果不 close 也不换成一个无缓冲 channel 的话, 会出现调用者认为数据成功提交了, 但实际上却没处理的情况.
soap520
251 天前
```
func submit(n int) error {
select {
case <-chQuit:
return ErrClosed
default:
}

chData <- n
return nil

}

func stop() {
close(chQuit)

for n := range chData {
// process data
_ = n
}

close(chData)
}
```

看看这样行不行,
submit 里面先判断一下 chQuit 是不是已经 close 了。
stop 处理完再 close chData 。

一种可能让人看起来有点担心的执行顺序是,1. submit 里, 判断 chQuit 还没关闭。2. stop 里,执行 close(chQuit)。3. submit 里,接着 chData <- n 。不过应该在你的用例中年问题不大。
Gota
251 天前
@soap520 你这里把 close(chData) 放到 for 循环之后执行, 那 for 循环就永远结束不了了.
realpg
251 天前
没有
1.21 自己瞎鸡儿改 xml excel 功能都用不了,被迫退回 1.20
Gota
251 天前
@realpg 升到 1.21.1 试试呢? 一般我都等大版本之后的一个小版本才开始正式用, 最开始那个版本确实容易出一些小问题.
soap520
251 天前
@Gota 确实,我把 stop 改成这样是不是就可以了。
```
func stop() {
close(chQuit)

for {
select {
case n := <-chData:
_ = n
default:
close(chData)
return
}
}
}
```
realpg
251 天前
@Gota #25
之前说了,下个小版本会合并修复
还不是 golang 自己修的,excel 那个库大佬给提的 pr
没有特别频繁检查版本的习惯,过两天再说
Gota
251 天前
@soap520 那就剩下 #22 里你自己提到的那个 panic 问题了. 这里的用例是 slog 的 hook, 所以 submit 可能会在任意线程中被调用, 数量和时机都是没办法控制的, 也就是说 submit 里那个过了 if 之后的挂起其实很容易触发.
soap520
251 天前
@Gota 明白了,那我把 stop 最后 close(chData)去掉是不是就行了。去掉之后看起来和你 1L 的方法就差不多了,只是没有重新给 chData 赋值(我也不清楚 slog hook 的用例里需不需要再给 chData 一个 channel )。
如果要很“完美”的话,我除了弄一个锁把 submit 里的 read chDone, enqueue data 保护起来之外想不到更好的办法了。
Gota
251 天前
@soap520 哈哈, 异步相关的东西确实比较烧脑. 1L 重新赋值一个无缓冲 channel 是为了防止 stop 之后有数据进入 chData 却没人来处理, 随着主线程退出这份数据就丢掉了. 至于加锁, 不到万不得已最好别加, 否则每调一次 Log 整个应用都被锁一下, 就有点夸张了.
nuk
250 天前
显然需要一个临界区,close channel 的时候要等待其他所有线程离开临界区,不管用 channel 还是 lock 来实现代价都蛮大,单个 bool 或者单个 channel close 都不可能实现,感觉 stop 就直接 drop 掉 log 会比较容易简单。
Gota
250 天前
@nuk 直接丢数据肯定不行,正文末尾的写法也不用等其他线程呀。
nuk
250 天前
@Gota 但是这个也不能保证 close channel 之后没有线程去写啊。。
Gota
250 天前
@nuk 没有 close ,是换成了一个无缓冲的 channel ,从而确保能 select 到第一个 case 。
mapleray
248 天前
@Gota #30 如果不想加锁,又不想丢日志,只能想办法把关闭操作抛给使用者了。

目前使用方式 uber-go/atomic.Bool + 缓冲 channel ,退出时等待 channel 清理完才退出。
paceewang1
242 天前
这个场景,可以用乐观锁吧,atomic ,CAS
Jrue0011
237 天前
读写锁的场景?
stop 写锁更新状态
submit 读锁判断状态
Gota
237 天前
@paceewang1 #36
@Jrue0011 #37
见 #30 的回复,帖子末尾的写法是不需要锁的
paceewang1
233 天前
@Gota CAS 也不是在 submit 里面加锁,我是指在 stop 里面加锁然后转换状态;相当于引入一个状态机而已,submit 只需要加一个状态判断就可以了,看了一下就和#13 的代码大致一样吧,但是 stop 方法先处理 chData 再关闭:
```
func stop() {
if ok := CAS(chQuit); !ok {
// return error
}

for n := range chData {
// process data
_ = n
}

close(chData)
}
```
Gota
233 天前
@paceewang1 见#23 楼和后续的回复,还是有点问题

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

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

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

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

© 2021 V2EX