Envoy 这样的工程构建已经是非常复杂了,当然 Go 大型工程也不简单。 但是入门写 Go 基本就一条路,入门 C++ 就很依赖人的主观判断。
学了两天 bazel ,又学了一天 cmake ,加深了这个想法。
|      1lanlanye      2023-02-08 01:57:07 +08:00 via Android  2 难说,工程实践的结果是越写越像 Java ,然后越来越觉得那个异常处理反人类…… | 
|      2TWorldIsNButThis      2023-02-08 01:58:32 +08:00 via iPhone Java 已经在应用层革过一遍 c++的命了 | 
|      3securityCoding      2023-02-08 08:17:11 +08:00 via Android @lanlanye 已经不是反人类的问题,对代码的破坏程度简直丧心病狂 | 
|      4xuyang2      2023-02-08 09:32:27 +08:00  34 我觉得 go 的 if err != nil 没啥可黑的 层层嵌套的 throw cache 才是反人类 | 
|  |      5pursuer      2023-02-08 09:50:33 +08:00 这不是语言的问题,是生态分裂造成的,现在编译器就好几个,gcc clang msvc ,c++委员会标准库推进慢,各个平台对 c/c++的接口存在差异。 现在的新语言很多都是唯一实现 java(不考虑 android 的话) go rust 啥的。 当然也有一个分裂比较严重的 js ,好在 js 的灵活性在一定程度上减轻了这个问题,但是依然诞生的 webpack rollup swc esbuild 等一大堆构建工具。 | 
|      6TtTtTtT      2023-02-08 09:54:13 +08:00 一些编程哲学放在一边。 Goroutine 的代码实在是太难懂了,跟 Akka 一样属于好写,但是完全看不懂。 | 
|  |      78355      2023-02-08 10:05:05 +08:00  5 Go 才是现代编程语言标准 同样的需求大概率只有一种写法 而且加上代码格式化 不会让你有在魔法世界的感觉 对新手友好度很强 不需要考虑什么某某函数 某某方法有这样那样的问题 | 
|  |      10RedisMasterNode      2023-02-08 10:08:46 +08:00 @TtTtTtT 怎么会难懂呢...或许你可以举一些认为难懂的例子大家康康具体是哪里不容易阅读 | 
|      11zcreg      2023-02-08 10:10:35 +08:00  6 Go 让人舒服的一个点就是读别人的代码,不会有那些花里胡哨的写法 | 
|  |      12fioncat      2023-02-08 10:14:07 +08:00  15 我发现一堆人真的无脑黑 Go 的错误处理。 Go 错误处理的好处在于强迫你认真对待每个 error 。 某些 Javaer 一有 exception 就无脑直接 throw ,他们肯定理解不了这种设计。 | 
|  |      13libook      2023-02-08 10:18:46 +08:00 与其他语言很鲜明的区别是,Go 是专门为生产工程场景设计的一款[产品];为了解决生产过程中的痛点,牺牲了一些部分技术人员看重的爽点。 | 
|      14xxv0      2023-02-08 10:20:20 +08:00 @xuyang2 什么是 try...catch...的层层嵌套,你是指在调用方与被调用方都 try...catch...,还是说在同一个函数的 try 块里再次 try...catch...,按理说这两种写法都是不对的。 | 
|  |      15zbatman      2023-02-08 10:21:14 +08:00  2 OP 一句没提 Java ,评论句句在踩 Java ,有趣 | 
|  |      16chendy      2023-02-08 10:23:20 +08:00 所以说 go 更适合当 cpp 用,写基础设施 写业务也就图一乐 | 
|  |      18tt67wq      2023-02-08 10:29:16 +08:00 @TtTtTtT 深有同感,到处都是 channel 的异步,完全找不到消息从哪里来到哪里去,一两个模块的异步还行,有的十来个模块相互异步调用,非常令人头秃 | 
|  |      21dog82      2023-02-08 10:38:52 +08:00 没 go mod 之前,很痛苦的 | 
|      22GeruzoniAnsasu      2023-02-08 10:44:48 +08:00  2 @RedisMasterNode  语法不复杂,但是要写出 对 的程序,那可复杂到天上去了。要知道 golang 只有协程,但它不提供 **异步语法** golang 没有 await ,这意味着你要完全自己手动处理所有 chan 和 并发开始的 goroutine 的关系和时序。2023 年了连 c++都能 await 协程了,golang 却还在用 select 和 pipe 手搓异步逻辑,我愿称其为 「 unix 原教旨主义」。 举个例子好了。你有一个 spwaner , 它能并发地生成若干 worker ,worker 的执行时长不确定。 现在有一个要求,所有 worker 的执行结果要按启动顺序写回到同一个与 spwaner 共享的 chan 里,开始你的头脑风暴。 | 
|  |      23Nazz      2023-02-08 10:52:37 +08:00 @GeruzoniAnsasu async 有传染性, 同步方式写异步代码对开发者更友好, 但是牺牲了性能. | 
|  |      24RedisMasterNode      2023-02-08 10:59:41 +08:00 @GeruzoniAnsasu 我看你的描述很多时候只需要 wait group 吧。。wait group 的使用非常简单,只有需要 goroutine 间通信的时候才会需要 channel 呀,业务应用里面 goroutine 大部分场景都是用来并行做一些事情,例如并行发起 http 调用,我 golang 用了有小几年了没有感觉到什么不适而且觉得很好理解 当然你可能在描述一些多个 channel 之间共同协作,需要知道互相的结果,需要传递数据的情况,我不了解在其他语言怎么做的,但是我觉得常规开发里面写出这样的逻辑设计本来就已经对可读性不友好了,不能说只怪 golang 吧 | 
|      26yazinnnn      2023-02-08 11:15:20 +08:00 monad 鄙视一下 throw 和 try catch 也就算了, 这年头 if err != nil 都能鄙视 throw 了吗 | 
|      27star9029      2023-02-08 11:24:07 +08:00 c++ build system 是这样的,不过 cmake/xmake 的工程能 work ,而且 cmake 有大量成熟项目(这并不影响他难用) | 
|      29TtTtTtT      2023-02-08 11:53:25 +08:00 | 
|  |      30ericls      2023-02-08 11:56:38 +08:00 via iPhone 我最近才开始真正写 go, 总之很喜欢 我也说不出具体原因 tooling 也很好 想写的东西甚至乱来 根据报错也能边学边写 这种喜欢可能有一部分来自于正在学习新东西的兴奋 但也只是一小部分 | 
|  |      31wakarimasen      2023-02-08 11:57:06 +08:00  3 又到了爷最爱的斧子党和锯子党互相鄙视环节。 有这时间不如多砍两棵树。 | 
|  |      32xiangyuecn      2023-02-08 12:03:18 +08:00 @xuyang2 #4 没写过 go ,如果 编写代码漏写了 if err != nil 会产生什么有趣的问题吗? 还是说不写 if err != nil 编译不过? | 
|  |      34sadfQED2      2023-02-08 12:19:11 +08:00 via Android @xiangyuecn 跟 java 漏写 try 差不多 | 
|  |      35blankmiss      2023-02-08 12:41:58 +08:00  1 if err != nil  还不如 try catch | 
|  |      37hhjswf      2023-02-08 12:46:58 +08:00 via Android  1 如果觉得 try catch 恶心,aop 可以解决。go 有什么优雅一点异常处理 | 
|  |      38loading      2023-02-08 13:09:57 +08:00 via Android @xiangyuecn 直接全局替换 err 为 _,直接就忽略了。 | 
|      39lanlanye      2023-02-08 13:37:18 +08:00  1 @xuyang2 很有问题啊,因为大多数需要异常处理的函数都得返回至少两个结果 (result, error) ,深层调用时每一层做的事就是执行函数,如果有 error 就往上一层抛,也会导致链式调用无法正常写出来,比如 `person.Pet().Name()` ,如果 Pet()方法是一个可能失败的 lazy load ,调用时就根本写不成这样。 目前我只见过 Gorm 那样把 error 直接放进返回值结构里的做法可以缓解这个问题,或者希望 Go 学一学 Rust 。 | 
|      40lanlanye      2023-02-08 13:39:08 +08:00 @fioncat 我一开始也是这么认为的,但它确实造成了不便,可以参考我在楼上的回复。另外 Rust 的处理方式就很好,它同样强迫你处理每一个 error 。 | 
|      41lanlanye      2023-02-08 13:42:34 +08:00 @GeruzoniAnsasu 我想了想,开辟一个用于保存结果的数组,启动 worker 的时候传入对应顺序的数组下标,直接把结果写进对应位置……应该可行吧 | 
|  |      42Smilecc      2023-02-08 13:45:24 +08:00 @Nazz await 未必一定具有传染性,本质上是因为 JS 或 Rust 等语言都是无栈协程,实现方式决定具有传染性,Go 的协程是有栈的,不依赖状态机做上下文切换,我认为是可以实现的 | 
|      44leonshaw      2023-02-08 13:51:32 +08:00  1 @GeruzoniAnsasu #22 没明白你说的,对 Go 来说大部分情况只要把无栈协程模式的 await 直接改成同步调用就行了,并不需要启 goroutine 。 举的例子只要在 goroutine 把结果写到一个 slice 对应位置就可以了,“回到同一个与 spwaner 共享的 chan 里”是伪需求,因为 chan 是 Go 特有的。 | 
|      455h4nh      2023-02-08 14:33:01 +08:00 @fioncat 不赞同。我用 sourcegraph.com 查了一下 `if err != nil`,基本都是 `return err`,没有处理。而且还有不少人直接写 `_`。相反我认为 Java 的 checked exception 机制,强制要求 caller 写 try-catch 或者 caller 签名也加上,才是所谓「强迫你认真对待每个 error 」。另外,你说「某些 Javaer 一有 exception 就无脑直接 throw 」,我觉得他们如果换用 Go ,情况只会更糟糕吧.. | 
|      465h4nh      2023-02-08 14:35:53 +08:00  1 @Nazz 我也觉得 Go 的源码很难读,其中一个原因就是变量名太追求 “Unix 风格”,我觉得有点过头了。比如这个 `sudog`.. https://stackoverflow.com/questions/68569386/whats-the-mearning-of-sudog-in-the-channel-struct-in-go | 
|  |      48macscsbf      2023-02-08 14:50:23 +08:00 知乎上在哪里看到的,go 的最大特色是无聊 | 
|      49chenqh      2023-02-08 14:53:13 +08:00 既然 golang 代码看起来这么简单,为什么我看 crowedsec 看不懂呢? | 
|  |      50th00000      2023-02-08 14:56:24 +08:00  2 | 
|  |      51learningman      2023-02-08 15:00:28 +08:00 @GeruzoniAnsasu #22 启动的时候带上个序号,返回结果的时候带上序号,waitgroup 等待所有 worker 完成。 就算有 async await 不也是这么操作吗,难道你想 for 1 to n await result ?这样写是符合直觉,但又不是唯一的标准答案。 | 
|      52Slurp      2023-02-08 15:14:56 +08:00  6 靠元组实现标签联合的垃圾类型系统。基于此出来的错误处理也是一坨大便,这也有人吹? | 
|  |      53wupher      2023-02-08 17:05:49 +08:00 catch / throw 当然不完美 if err!= nil 多层嵌套有时更变态,毕竟 runtime exception 你还可以不处理。 学了 Rust 之后确实相信这才是更优雅的设计。 | 
|  |      54quicksand      2023-02-08 17:05:57 +08:00 各有千秋吧,不同人肯定喜好都不一样的,这也是语言多样性的原因,没必要强行来比较。我最近也在学 go ,感觉不舒服的点就是注释文档,感觉 javadoc 这方面做的更好一些。 | 
|      55JamesMackerel      2023-02-08 17:19:44 +08:00 看到贵贴,想来请教一下 go 的 thread local (或类似机制)的进展…… 我知道可以用 Context 然后把每个 function 都加个 Context 参数,可是除了这种方法还有没有别的办法? | 
|  |      56liuxu      2023-02-08 17:36:05 +08:00 最后发现 php 依然是最好的语言,而你们都会来写 rust | 
|      57GeruzoniAnsasu      2023-02-08 17:48:56 +08:00 @RedisMasterNode  @Nazz @leonshaw  不是的,wait group 只能在所有任务完成前一直阻塞住。而作为一个 spawner ,你需要时刻维护一个有长度的队列,当队列空出来时立即解除正在预约( schedule )任务的 routine 的阻塞,wait group 显然不合适。 注意我们的目标是,让结果按照添加的顺序依次输出,而不是一次性等待所有的结果一起输出。 有异步语法的语言,在这个场景的做法是 - 一个有长度的阻塞队列 - 当外界 scheduling 新任务时,spwaner 向队列获取一个空槽,如果队列已满,那么 spanwer 和 请求者都会被阻塞 - 如果获取了空槽,将任务放入空槽,获得一个 promise - 创建新 promise, 在这个 promise 里 { await 任务队列的尾部任务(因为我们需要按任务的添加顺序而不是任务完成顺序来返回),await 到之后返回上一步获得的 promise } - 把上面这个 promise 加到 out 队列里,每次提取结果时 await out 队列的头部 而 golang 要模拟这个做法的话,首先它没有 promise ,也没有 goroutine 的 handler ,然后要实现跟上述等价的 spawner 必须使所有调用 spanwer 的线程共享同一个 channel ,意味着 chan 要么是全局的,要么扔到 context 里。先简单考虑全局唯一 chan 的做法。(但复制 chan 用 context 传这种逆天玩意我也写过) 提取任务槽这步没问题,但怎么模拟一个 promise ? - c := make(chan,1) ; go func(){c<-do();)} 那怎么获取任务队列的尾部任务并 await 它? - 如果任务队列只是个简单的 channel 是做不到的,因此需要一个 slice + channel ,可是 slice 就没有锁了,你这时候要考虑一个可阻塞环境( chan )下的锁问题,头开始疼起来了 怎么返回 await 了 c 的新 promise ? - …… 对了,这个新 promise 还要放到 out 队列里 - ………… | 
|  |      58Nazz      2023-02-08 17:54:50 +08:00 @GeruzoniAnsasu 添加任务的时候加序列号, 线程同步后给输出结果排序 | 
|      59GeruzoniAnsasu      2023-02-08 18:00:12 +08:00 @Nazz @learningman  @leonshaw  @lanlanye  @RedisMasterNode  我提醒你们一下关于放到对应序号结果槽的实现: - 这个结果 array (它有大小,我这里用 array 来称呼,并不是指实现),是有「洞」的,需要有个机制能按顺序检查每个位置是否完成了,没完成要能阻塞住,意味着 array 里放的是锁或 chan 或任意什么东西总之是一个可锁对象,但有 promise 的情况下不需要这种可锁对象 - 我们不能一次性等待一批 worker 全部完成,而是要时刻能分派已完成的 worker 占用的任务槽 - spwaner 本身要可以等待或阻塞 | 
|  |      60Nazz      2023-02-08 18:23:43 +08:00 via Android | 
|  |      61Nazz      2023-02-08 18:26:18 +08:00 @GeruzoniAnsasu 使用有锁队列保存任务; 任务完成后去队列拿下一个任务, 递归地调用; | 
|      62CRVV      2023-02-08 19:28:08 +08:00  1 @GeruzoniAnsasu  package main import ( "fmt" "math/rand" "time" ) func worker(ch chan int, x int) { d := rand.Intn(10) time.Sleep(time.Millisecond * 10 * time.Duration(d)) ch <- x } func main() { var queue []chan int for i := 0; i < 10; i++ { ch := make(chan int) go worker(ch, i) queue = append(queue, ch) } for _, ch := range queue { fmt.Println(<-ch) } } | 
|      63CRVV      2023-02-08 19:43:07 +08:00  1 @GeruzoniAnsasu  这个东西不难写,只不过它的写法和 JavaScript 惯用的写法可能不一样。 如果你觉得我发的这个不符合你的要求,你可以先用 Promise 写一版我来翻译成 Go 总体上 Go 不用写 "await" 这几个字母,其它和带异步且多线程的语言完全一样。当然 Go 自带的语言功能少一些,不限于异步并发关系时序这些,所有方面的功能都少。 > 有 promise 的情况下不需要这种可锁对象 多线程环境下的 promise 本身也带锁或者类似的机制。单线程的 JavaScript 是另一回事。 > 我们不能一次性等待一批 worker 全部完成,而是要时刻能分派已完成的 worker 占用的任务槽 这个和 Promise 有关系么?我没觉得用 Promise 可以简化这件事情的实现,拿到一个结果了再开下一个任务,都一样吧。 > spwaner 本身要可以等待或阻塞 没看懂,什么地方可以等待?你是指可以 await spwaner() 么? | 
|  |      64lxdlam      2023-02-08 20:16:44 +08:00  1 @GeruzoniAnsasu  针对你的 task ,准备一个跟结果一直长度的 []chan 就可以了,扫一遍每个 channel 就可以针对每一个 chan 的阻塞策略,很直接。https://go.dev/play/p/YbtujcTry_J 。 至于 Promise ,你需要的只是一个 Task 结构,注意到结果本身是否 ready 可以依靠超时 + channel ,解决,封装一个类似的结构是 naive 的,在 github 上能找到非常多的类似的库。 可以去看一些官方 talk 理解 CPS 机制的原理,而不是尝试把某个机制 mapping 过来。 | 
|      65leonshaw      2023-02-08 20:23:16 +08:00 @GeruzoniAnsasu 本质上,是你后一个 promise await 了前一个。相应地 Go 里面为每个 goroutine make 一个 channel ,在写结果前等前一个 channel 就行了: in := make(chan func() any) out := make(chan any, 1) go func() { for result := range out { consume(result) } }() prevDone := make(chan struct{}) close(prevDone) for do := range in { done := make(chan struct{}) go func(do func() any, prevDone <-chan struct{}, done chan<- struct{}) { result := do() <-prevDone out <- result close(done) }(do, prevDone, done) prevDone = done } close(out) | 
|  |      66lxdlam      2023-02-08 20:35:31 +08:00 @GeruzoniAnsasu  刚注意到有个 context miss 了,我也补充几个点: - 使用 mpsc 跟 spsc 是非常简单的,一个基于 token 的 bucket 可以简单控制好 task 的数量,共用 token bucket 就可以控制每个 spawner 的数量。注意到这里也可以简单地基于 chan 封装一个,不需要所谓的阻塞队列。 - spawner/worker 基于 message passing 的 channel 可以解决所有 promise 的场景,包括超时等待等。 - 所谓的全局 channel ,js 的 microtask queue 和 marcotask queue 同样是全局的,甚至基于这两个场景你如果需要定制化 queue 的调度逻辑你需要对 runtime 有更加深入地理解,而 go 基于 token bucket 做定制可以做更多的事情。 | 
|  |      67learningman      2023-02-08 21:44:00 +08:00 @GeruzoniAnsasu #54 你别加条件,我说的实现是给你的最初版本的需求的 | 
|  |      68wangritian      2023-02-08 23:12:27 +08:00 你们不要再打了啦 | 
|  |      69swulling      2023-02-08 23:19:28 +08:00 via iPhone 编译一个 envoy 三个小时,醉了。 | 
|  |      70StevenRCE0      2023-02-09 00:12:04 +08:00  1 err 判空不算大问题,但是显然是 throwable 更适合工程啊…… 人们在改进错误处理,然后到某些选手这儿直接就说不出错误不就行了,属实流汗黄豆。 | 
|  |      71nino      2023-02-09 00:24:29 +08:00 @GeruzoniAnsasu 首先没有细看你的需求。但是 go 只是标准库不提供 async await Promise 这些并发原语而已,要模拟出来很简单的啊,有了之后不就和你写 JS 一样了。作为 JS 和 Go 都写过的人,可以负责任的讲,Go 并发这块灵活性比 JS 强多了,可以写出很有表现力的代码。 goroutine + chan + sync 包里那堆东西,什么并发程序都能写出来。 | 
|  |      73dbskcnc      2023-02-09 10:09:30 +08:00 什么舒服 /合适就用什么,大部分情况,我 go 用得挺舒服的. c/c++ 确实麻烦很多 | 
|      74xsen      2023-02-09 11:04:06 +08:00  1 @dbskcnc #72 前后用过很多语言,如 c/c++/python/java/javascript/dart 等等,到现在的 go ,相对来说用 go 的体验是最舒服的——不管是开发、调试,还是打包部署诸如此类。当然,也包括跨平台、交叉编译等 | 
|      75zxCoder      2023-02-24 16:00:03 +08:00 便捷,是大便的便吧 | 
|  |      76echoless      2023-02-28 10:29:21 +08:00 |