总结一下最近使用 golang 写了几个简单的工具和项目的初体验。总体感觉 golang 在语法设计上面非常糟糕,不喜欢,但是在运行时和标准库上面,确实解决了之前编程语言的很多痛点,所以目前 golang 在业务项目蚕食 Java 的地盘。但是 JVM + Kotlin 还是目前最好的业务技术栈,如果不需要计算性能的话,有了 typing 提示后简单的 web 项目用 Python 也是很好的选择,千万不要信了 golang 那些不需要 ORM/使用标准库就够了/显式处理error才是最佳实践的鬼话。
好的方面
工具链非常完善好用
虽然 maven 也有根据模板创建初始化项目的命令,但是那个命令长的根本没人记得住,所以才有 https://start.spring.io/ 这种网站,npm、cargo、uv 这种交互命令行才是人道主义,golang 在命令行方面做的非常好,go mod go tidy 等体验并没有比 cargo 差多少,当然目前接触的项目都非常简单。
标准库非常丰富
除了 python,golang 的标准库应该是最丰富的。甚至连 http 库都是自带的,在简单的命令工具场景(devops)场景下,golang 几乎可以取代 python 成为脚本运维工具,这也是很多运维 boy 转到 golang 的条件,毕竟运维环境大概率有 golang 标准库但是不能安装第三方库。
交叉编译比较简单
写了一个上传 log 到 s3 的小工具,需要编程成二进制集成到 java 项目中一块部署,开发环境是 windows,部署环境是 linux,golang 一行命令就解决了,使用 rust 没能实现 win 交叉编译到 linux,主要是 windows 需要安装的东西太多了,开发电脑不允许,在个人电脑试了一下也可以。当然使用 python 也能解决,但是体积非常大,超过了 5M 允许的限制。
编译非常快
得益于 golang 的语法简单,没有 meta programming, 所以编译非常快,可以说编译 + 启动一个 golang 项目和启动 python 小项目几乎一样快,这在 Java 项目上都是难以想象的 Java boy 需要使用破解版的 JRebel 热更新解决启动慢的问题。
启动快,初始内存小
启动内存其实对于企业 web 开发不算什么太大问题。但是对于个人项目,流量非常小的服务还是很大的优势。毕竟几百兆内存也是需要钱的。
缺点
项目写到一半我就感觉有点恶心了,可能是用过 python 和 scala 的原因,golang 的语法感觉就是半成品,连 Java 都比他严谨一点。
错误处理
golang 的错误处理有两个问题,一是繁琐,整个代码充斥的 if err != nil {},美其名曰在错误发生的地方处理错误,但是又允许通过 r, _=fn() 省略 err,这个本质上是奖励偷懒者。二是 error 是值,导致传递的信息非常有限,底层 fmt.Errorf + %w 其实是一个 error string 错误链,然后使用 error.Is 判断处理,对于 java 过来的人,没有 stack trace 而 err 只是一个 string 真的非常不习惯,感觉上了生产仅靠一行 error string 定位错误非常困难吧,虽然我还没遇到生产问题。
还有一个问题,涉及 io 的场景,每个 defer close 也是会返回一个 error 需要处理,所以一个请求 + 处理 需要处理两三个 error,没办法一次处理,对于代码可读性影响还是蛮大的。
空值设计
这个问题其实比错误处理还搞笑,一个 int 你无法判断是 nil 还是 0。还有感觉 golang 的 json 序列化就是玩的,也不知道 golang web 开发前后端是怎么约定空值的,前端还需要额外约定的 json 明显属于 json 包设计有 bug 就这么用了十几年。当然 1.24 的 omitzero 可以解决一部分问题。JSON 包新提案:用“omitzero”解决编码中的空值困局
interface 的胖指针 nil 比较问题
func IsNil(i interface{}) {
if i == nil {
fmt.Println("i is nil")
return
}
fmt.Println("i isn't nil")
}
func main() {
var sl []string
if sl == nil {
fmt.Println("sl is nil")
}
IsNil(sl)
}
上面是一个经典的 golang 面试题。接口值 interface{} 的内部表示是两部分:动态类型(type)和动态值(value),表示为 (type, value)。所以 interface 的指针比较是比较的两个信息 value 和 type,这里 sl 的 type 不是 nil 而是[]string。
除了上面函数 interface nil 判断错误,还有其他常见的坑
- 返回 error interface nil
func do() error {
var e *MyErr = nil
return e // 返回后,err != nil,因为接口包装了 *MyErr 类型
}
var m map[string]int(nil map)读取 m[“x”] 返回零值,不会 panic,但赋值 m[“x”] = 1 会 panic。使用 reflect.DeepEqual 比较 slice 是否“空,nil 和 empty slice 不同的,这个比较直观,但是 nil slice 可以 for 循环不太直观。
reflect.ValueOf 对 interface 类型 nil 判断也会有坑
import "reflect"
func IsNilInterface(i interface{}) bool {
if i == nil {
return true
}
v := reflect.ValueOf(i)
// Only these Kinds may be nil
// 这里必须使用 switch 判断,其他 Kind 调用 v.IsNil() 会 panic
switch v.Kind() {
case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Interface, reflect.Slice:
return v.IsNil()
}
return false
}
其他
time 格式、for-range 的变量捕获问题这些就不吐槽了,官方也在不停修复,但是感觉 golang 和 golang 那些项目都是发版跟玩似的(说的就是 hugo 你),严肃语言怎么也不会把 omitepmty 不支持 time 这种东西发布出来吧,把工作甩给前端开发么?
设计问题
- Context 和其他语言的 ThreadLocal/contextvars 相比,真的太丑了,整个方法链被强制污染了,但是个人还能接收,至少满足显示易懂这个点。
- slice 对于新人总是会踩坑,更多还是设计者的问题,不是初学者的问题。
- 没有默认参数,没有 computed property, 没有 set,queue 等标准库,在业务场景这些都是非常有用且高频的工具。
- 几乎所有的文件都是在文件根目录下面,_test.go 和源码相邻,很快就导致项目文件管理非常混乱。
- channel 看上去非常简单方便,实际使用的时候非常容易发生数据竞争。uber 专门写过
- panic 处理不当会炸应用,java 的绝大所数异常不影响整个应用。
- go mod 的机制导致你 fork 代码库后想改一点东西测试需要替换仓库代码所有路径名。。。没见过比这更傻了
What “sucks” about Golang? : reddit/golang
Go 错误陷阱测试题 - Go Traps
50 Shades of Go Traps GotchasandCommonMistakesforNewGolangDevs.pdf - github/GoLangBooks
Discover the Dark Side of Go: Why This Popular Language May Sucks | by Roma Gordeev | Medium
A curated list of articles complaining that go (golang) isn’t good enough
Golang is not a good language
Lies we tell ourselves to keep using Golang
My reflections on Golang - DEV Community
Why Go Is Not Good :: Will Yager