Golang Go语言的特色不是语法的便捷,而是在工程

发布于 1周前 作者 yibo5220 来自 Go语言

Envoy 这样的工程构建已经是非常复杂了,当然 Go 大型工程也不简单。 但是入门写 Go 基本就一条路,入门 C++ 就很依赖人的主观判断。

学了两天 bazel ,又学了一天 cmake ,加深了这个想法。


Golang Go语言的特色不是语法的便捷,而是在工程
78 回复

难说,工程实践的结果是越写越像 Java ,然后越来越觉得那个异常处理反人类……

更多关于Golang Go语言的特色不是语法的便捷,而是在工程的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Java 已经在应用层革过一遍 c++的命了

已经不是反人类的问题,对代码的破坏程度简直丧心病狂

我觉得 go 的 if err != nil 没啥可黑的
层层嵌套的 throw cache 才是反人类

这不是语言的问题,是生态分裂造成的,现在编译器就好几个,gcc clang msvc ,c++委员会标准库推进慢,各个平台对 c/c++的接口存在差异。
现在的新语言很多都是唯一实现 java(不考虑 android 的话) go rust 啥的。
当然也有一个分裂比较严重的 js ,好在 js 的灵活性在一定程度上减轻了这个问题,但是依然诞生的 webpack rollup swc esbuild 等一大堆构建工具。

一些编程哲学放在一边。
Goroutine 的代码实在是太难懂了,跟 Akka 一样属于好写,但是完全看不懂。

Go 才是现代编程语言标准
同样的需求大概率只有一种写法 而且加上代码格式化
不会让你有在魔法世界的感觉 对新手友好度很强
不需要考虑什么某某函数 某某方法有这样那样的问题

#4 “throw cache” 那你说的一切都对🙏

正确使用的协程很好读啊,如果协程间有太多的变量、逻辑依赖,那属于滥用

怎么会难懂呢…或许你可以举一些认为难懂的例子大家康康具体是哪里不容易阅读

Go 让人舒服的一个点就是读别人的代码,不会有那些花里胡哨的写法

我发现一堆人真的无脑黑 Go 的错误处理。
Go 错误处理的好处在于强迫你认真对待每个 error 。
某些 Javaer 一有 exception 就无脑直接 throw ,他们肯定理解不了这种设计。

与其他语言很鲜明的区别是,Go 是专门为生产工程场景设计的一款[产品];为了解决生产过程中的痛点,牺牲了一些部分技术人员看重的爽点。

什么是 try…catch…的层层嵌套,你是指在调用方与被调用方都 try…catch…,还是说在同一个函数的 try 块里再次 try…catch…,按理说这两种写法都是不对的。

OP 一句没提 Java ,评论句句在踩 Java ,有趣

所以说 go 更适合当 cpp 用,写基础设施
写业务也就图一乐

Go 是最正统的 C 语言继承者. 虽然繁琐但代码更健壮, 当然要是有关键字或语法糖来抛出错误将会更好.

深有同感,到处都是 channel 的异步,完全找不到消息从哪里来到哪里去,一两个模块的异步还行,有的十来个模块相互异步调用,非常令人头秃

go 的源码实现很复杂, 一般人看不下去. map, sort, channel 皆是如此.

我会尽量使用 mutex 替代 channel, channel 明显被滥用了.

没 go mod 之前,很痛苦的



语法不复杂,但是要写出 对 的程序,那可复杂到天上去了。要知道 golang 只有协程,但它不提供 异步语法

golang 没有 await ,这意味着你要完全自己手动处理所有 chan 和 并发开始的 goroutine 的关系和时序。2023 年了连 c++都能 await 协程了,golang 却还在用 select 和 pipe 手搓异步逻辑,我愿称其为 「 unix 原教旨主义」。


举个例子好了。你有一个 spwaner , 它能并发地生成若干 worker ,worker 的执行时长不确定。 现在有一个要求,所有 worker 的执行结果要按启动顺序写回到同一个与 spwaner 共享的 chan 里,开始你的头脑风暴。

async 有传染性, 同步方式写异步代码对开发者更友好, 但是牺牲了性能.

我看你的描述很多时候只需要 wait group 吧。。wait group 的使用非常简单,只有需要 goroutine 间通信的时候才会需要 channel 呀,业务应用里面 goroutine 大部分场景都是用来并行做一些事情,例如并行发起 http 调用,我 golang 用了有小几年了没有感觉到什么不适而且觉得很好理解

当然你可能在描述一些多个 channel 之间共同协作,需要知道互相的结果,需要传递数据的情况,我不了解在其他语言怎么做的,但是我觉得常规开发里面写出这样的逻辑设计本来就已经对可读性不友好了,不能说只怪 golang 吧

#8 throw catch …

monad 鄙视一下 throw 和 try catch 也就算了, 这年头 if err != nil 都能鄙视 throw 了吗

c++ build system 是这样的,不过 cmake/xmake 的工程能 work ,而且 cmake 有大量成熟项目(这并不影响他难用)

#19
Simplicity is Complicated

<iframe src="https://www.youtube.com/embed/rFejpH_tAHM" class="embedded_video" allowfullscreen="" type="text/html" id="ytplayer" frameborder="0"></iframe>



最近读过的是 MinIO 的 EC pool 读写流程 hhh

我最近才开始真正写 go,

总之很喜欢 我也说不出具体原因 tooling 也很好 想写的东西甚至乱来 根据报错也能边学边写

这种喜欢可能有一部分来自于正在学习新东西的兴奋 但也只是一小部分

又到了爷最爱的斧子党和锯子党互相鄙视环节。

有这时间不如多砍两棵树。

#4 没写过 go ,如果 编写代码漏写了 if err != nil 会产生什么有趣的问题吗? 还是说不写 if err != nil 编译不过?

大项目就是本就不好读,我弄数据库的,看 tidb 也是很恼火;即使注释写的很好,不是自己写的也缺少很多上下文

跟 java 漏写 try 差不多

if err != nil 还不如 try catch

反人类的理由呢

如果觉得 try catch 恶心,aop 可以解决。go 有什么优雅一点异常处理

直接全局替换 err 为 _,直接就忽略了。

很有问题啊,因为大多数需要异常处理的函数都得返回至少两个结果 (result, error) ,深层调用时每一层做的事就是执行函数,如果有 error 就往上一层抛,也会导致链式调用无法正常写出来,比如 person.Pet().Name() ,如果 Pet()方法是一个可能失败的 lazy load ,调用时就根本写不成这样。
目前我只见过 Gorm 那样把 error 直接放进返回值结构里的做法可以缓解这个问题,或者希望 Go 学一学 Rust 。

我一开始也是这么认为的,但它确实造成了不便,可以参考我在楼上的回复。另外 Rust 的处理方式就很好,它同样强迫你处理每一个 error 。

我想了想,开辟一个用于保存结果的数组,启动 worker 的时候传入对应顺序的数组下标,直接把结果写进对应位置……应该可行吧

await 未必一定具有传染性,本质上是因为 JS 或 Rust 等语言都是无栈协程,实现方式决定具有传染性,Go 的协程是有栈的,不依赖状态机做上下文切换,我认为是可以实现的

#12 太对了
这才是严谨的体现
而不是瞎糊一通结果来个兜底

#22 没明白你说的,对 Go 来说大部分情况只要把无栈协程模式的 await 直接改成同步调用就行了,并不需要启 goroutine 。
举的例子只要在 goroutine 把结果写到一个 slice 对应位置就可以了,“回到同一个与 spwaner 共享的 chan 里”是伪需求,因为 chan 是 Go 特有的。

不赞同。我用 sourcegraph.com 查了一下 if err != nil,基本都是 return err,没有处理。而且还有不少人直接写 _。相反我认为 Java 的 checked exception 机制,强制要求 caller 写 try-catch 或者 caller 签名也加上,才是所谓「强迫你认真对待每个 error 」。另外,你说「某些 Javaer 一有 exception 就无脑直接 throw 」,我觉得他们如果换用 Go ,情况只会更糟糕吧…

我也觉得 Go 的源码很难读,其中一个原因就是变量名太追求 “Unix 风格”,我觉得有点过头了。比如这个 sudoghttps://stackoverflow.com/questions/68569386/whats-the-mearning-of-sudog-in-the-channel-struct-in-go

源码很难读是因为复杂性, 实现得异常复杂. 当然简洁不代表性能.

知乎上在哪里看到的,go 的最大特色是无聊

既然 golang 代码看起来这么简单,为什么我看 crowedsec 看不懂呢?

#12
这个逻辑反过来才是对的吧
强迫处理 error 怎么就变成优势了,这会让人增加大量的心智负担
相反让人有选择的余地,自己根据需求去决策才是好的设计吧

#22 启动的时候带上个序号,返回结果的时候带上序号,waitgroup 等待所有 worker 完成。
就算有 async await 不也是这么操作吗,难道你想 for 1 to n await result ?这样写是符合直觉,但又不是唯一的标准答案。

靠元组实现标签联合的垃圾类型系统。基于此出来的错误处理也是一坨大便,这也有人吹?

catch / throw 当然不完美

if err!= nil 多层嵌套有时更变态,毕竟 runtime exception 你还可以不处理。

学了 Rust 之后确实相信这才是更优雅的设计。

各有千秋吧,不同人肯定喜好都不一样的,这也是语言多样性的原因,没必要强行来比较。我最近也在学 go ,感觉不舒服的点就是注释文档,感觉 javadoc 这方面做的更好一些。

看到贵贴,想来请教一下 go 的 thread local (或类似机制)的进展……

我知道可以用 Context 然后把每个 function 都加个 Context 参数,可是除了这种方法还有没有别的办法?

最后发现 php 依然是最好的语言,而你们都会来写 rust



不是的,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 队列里
- …………

添加任务的时候加序列号, 线程同步后给输出结果排序



我提醒你们一下关于放到对应序号结果槽的实现:


- 这个结果 array (它有大小,我这里用 array 来称呼,并不是指实现),是有「洞」的,需要有个机制能按顺序检查每个位置是否完成了,没完成要能阻塞住,意味着 array 里放的是锁或 chan 或任意什么东西总之是一个可锁对象,但有 promise 的情况下不需要这种可锁对象

- 我们不能一次性等待一批 worker 全部完成,而是要时刻能分派已完成的 worker 占用的任务槽

- spwaner 本身要可以等待或阻塞

不知道我写的这个库是否满足你的需求
https://github.com/lxzan/concurrency

使用有锁队列保存任务; 任务完成后去队列拿下一个任务, 递归地调用;



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)
}

}



这个东西不难写,只不过它的写法和 JavaScript 惯用的写法可能不一样。
如果你觉得我发的这个不符合你的要求,你可以先用 Promise 写一版我来翻译成 Go
总体上 Go 不用写 “await” 这几个字母,其它和带异步且多线程的语言完全一样。当然 Go 自带的语言功能少一些,不限于异步并发关系时序这些,所有方面的功能都少。

> 有 promise 的情况下不需要这种可锁对象
多线程环境下的 promise 本身也带锁或者类似的机制。单线程的 JavaScript 是另一回事。

> 我们不能一次性等待一批 worker 全部完成,而是要时刻能分派已完成的 worker 占用的任务槽
这个和 Promise 有关系么?我没觉得用 Promise 可以简化这件事情的实现,拿到一个结果了再开下一个任务,都一样吧。

> spwaner 本身要可以等待或阻塞
没看懂,什么地方可以等待?你是指可以 await spwaner() 么?



针对你的 task ,准备一个跟结果一直长度的 []chan 就可以了,扫一遍每个 channel 就可以针对每一个 chan 的阻塞策略,很直接。https://go.dev/play/p/YbtujcTry_J

至于 Promise ,你需要的只是一个 Task 结构,注意到结果本身是否 ready 可以依靠超时 + channel ,解决,封装一个类似的结构是 naive 的,在 github 上能找到非常多的类似的库。

可以去看一些官方 talk 理解 CPS 机制的原理,而不是尝试把某个机制 mapping 过来。

本质上,是你后一个 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)



刚注意到有个 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 做定制可以做更多的事情。

#54 你别加条件,我说的实现是给你的最初版本的需求的

你们不要再打了啦

编译一个 envoy 三个小时,醉了。

err 判空不算大问题,但是显然是 throwable 更适合工程啊……
人们在改进错误处理,然后到某些选手这儿直接就说不出错误不就行了,属实流汗黄豆。

首先没有细看你的需求。但是 go 只是标准库不提供 async await Promise 这些并发原语而已,要模拟出来很简单的啊,有了之后不就和你写 JS 一样了。作为 JS 和 Go 都写过的人,可以负责任的讲,Go 并发这块灵活性比 JS 强多了,可以写出很有表现力的代码。
goroutine + chan + sync 包里那堆东西,什么并发程序都能写出来。

#43
现在 golang 也有大量瞎糊一通,各种 panic ,然后 recovery 兜底

什么舒服 /合适就用什么,大部分情况,我 go 用得挺舒服的. c/c++ 确实麻烦很多

#72 前后用过很多语言,如 c/c++/python/java/javascript/dart 等等,到现在的 go ,相对来说用 go 的体验是最舒服的——不管是开发、调试,还是打包部署诸如此类。当然,也包括跨平台、交叉编译等

便捷,是大便的便吧

#46 sudog 这命名真是妙啊, 没有多少英文背景的人是无法体会的.

然而, 能与之媲美的就是 身份证 代码里写成 sfz 了.

#26 纯手工, 鄙视你们这些投机取巧的

在探讨Golang(又称Go语言)的特色时,确实需要超越语法层面的便捷性,深入到其在工程实践中的卓越表现。Go语言自诞生以来,就以其独特的设计理念在工程领域赢得了广泛的认可与赞誉。

首先,Go语言在设计上强调了简洁与高效,这不仅体现在语法上,更体现在其底层实现和运行时性能上。这使得Go语言在构建高性能、高并发的服务器端应用时具有得天独厚的优势。

其次,Go语言的工具链极为强大,包括编译器、构建工具、调试器和性能分析工具等,这些工具为开发者提供了全方位的支持,极大地提高了开发效率和代码质量。

再者,Go语言对并发编程提供了原生支持,通过goroutine和channel等机制,开发者可以轻松地编写出高效且易于维护的并发代码。这一特性使得Go语言在构建分布式系统和微服务架构时表现出色。

最后,Go语言拥有活跃的社区和丰富的第三方库,这为开发者提供了丰富的资源和支持。无论是学习交流、问题解决还是代码复用,Go语言的社区都为其用户提供了极大的便利。

综上所述,Go语言的特色不仅在于其语法的便捷性,更在于其在工程实践中的高效性、稳定性、并发编程能力和丰富的生态系统。这些特性使得Go语言成为现代软件开发中不可或缺的一员,尤其适合用于构建高性能、高并发的服务器端应用和分布式系统。

回到顶部