Golang 编程初体验

25-05-05 编程 #code #golang

总结一下最近使用 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 判断错误,还有其他常见的坑

func do() error {
    var e *MyErr = nil
    return e         // 返回后,err != nil,因为接口包装了 *MyErr 类型
}
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 这种东西发布出来吧,把工作甩给前端开发么?

设计问题

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