写点什么

我使用 Golang 两年后的总结:优点和令人讨厌的怪癖

  • 2020-05-01
  • 本文字数:2819 字

    阅读完需:约 9 分钟

我使用Golang两年后的总结:优点和令人讨厌的怪癖

过去两年来,我就职于Assembled,一直使用 Go 工作。自公司成立以来,Go 就一直是我们的主要后端语言。在我之前的工作中,我混合着使用 Ruby 和 Scala,所以肯定有过一段适应期。总体而言,我发现 Go 的表现基本上与广告宣传的一样:尽管有些怪异,但它非常适合专业工作。

背景介绍

Assembled 是客户支持团队用来管理人员配备和分析的 Web 应用程序。从架构的角度来看,它是一个标准的 Web 应用程序:React 前端通过内部 HTTP API 访问 Go 后端,后者反过来访问 PostgreSQL 数据库。


也就是说,有几个特定的应用程序,我们也必须对其进行优化:


  • 我们维护一个外部API

  • 我们从基于 Python 的机器学习服务中生成时间序列预测

  • 我们执行相当繁重的批处理,因为我们通过 SFTP 同步请求中心系统的调度。

  • 我们运行相当复杂的优化算法;我的兄弟 John 在HN评论中对此进行了详细的分享。

  • 我们处理令人痛苦的时区和重复事件相关的逻辑

生产中的 Go

我们构建 Assembled,不使用 Python、Java、Haskell(或……),特意决定使用 Go。决定因素可归结为:静态类型、简单性、标准库和速度。在实践中,我所见过的一切都与 Go 在文档中所呈现的方式相吻合:


Go 富有表现力、简洁、干净且高效……Go 可以快速编译成机器码,同时还具有垃圾收集的便利性及运行时反射的能力。这是一种快速的、静态类型的编译语言,但它感觉起来就像是一种动态类型的解释语言。

对新手工程师来说,它很乏味,但很简单

Go 的简单性使得新手工程师可以快速使用我们的代码库,他们中的许多人以前从未使用过该语言(包括我自己)。我怀疑 Go 的设计迫使我们编写比以前更简单、更明确的代码。也就是说,简单的确会导致重复,这一点已经得到了充分的证明。截至撰写本文时,代码片段 if err != nil 在我们的代码库中出现了 2919 次。

标准库可以满足需要

Go 的标准库是一座闪亮的“灯塔”。像 bytes、encoding/json、database/sql、net/http 和 time 这样的包是全面且经过精心设计的。例如,在 Python 中,它会告诉我们第三方库(显然,三方库是好的!)采用的是 HTTP 默认缺省值。


Go 的设计显然考虑了生产代码。最引人注目的是,Assembled 有一段时间存在严重的性能问题(敲打敲打),我们可以使用 net/pprof 和 runtime/pprof 进行调试。这些功能非常强大,并且易于通过 HTTP 处理程序启用,如下所示。我唯一的遗憾是,我找到的解释该输出的最佳指南被埋没在一篇博客文章里了。


  superAdminMux.HandleFunc("/debug/pprof/heap", pprof.Handler("heap").ServeHTTP)  superAdminMux.HandleFunc("/debug/pprof/profile", pprof.Profile)
复制代码

快速、简单的构建影响一切

Go 最好的地方在于,我们可以轻松运行 go build,并且几乎无需等待,就可以可靠地期望一个可以运行的可执行文件。在没有 IntelliJ 或 Eclipse 的情况下, Java 仍然会使编译过程痛苦不堪,更不要说从 Ruby 或 Python 开始了。


快速、简单的构建体验使许多下游任务变得更容易:


  • 我们的部署命令本质上是 git pull,然后是 go install

  • 持续集成(CI),好吧……我们可以说 Go 也没有问题


主要的困难在于,采用文件监视和循环重建的方式进行本地开发。我们有多个构建目标(例如,应用程序后端和 API),并使用https://github.com/cespare/reflex,但这需要一些配合工作才能很好地在 Mac OS X 运行。

标准格式和文档

我是否提到过,Go 显然是为专业人士打造的?


  • gofmt 处理缩进和对齐;我使用vim-go插件,该插件会在我们保存.go 文件时自动应用

  • 这里有一个关于 Go 文档是如何不同的雄辩解释;我最喜欢的是标准外观,位于https://godoc.org/公共存储库中,它能在本地运行并能快速提取项目特定代码的事实。

Gopher ❤️

如此可爱的化身。这里有篇关于 Go gopher 起源的有趣读物:https://blog.golang.org/gopher


痛点

Go 也有令人讨厌的怪癖。其中的很多已经在其他地方被很好地记录下来了,但出于完整性的考虑,我将它们包括进来。

没有官方的包管理者故事(直到最近都没有)

从 Go 1.14(2020 年 2 月)开始,Go 模块已经准备好投入生产使用了。在此之前,这里是“荒芜的西部”:我们登陆了 dep,但没有机会迁移到模块中。 dep 是(或曾经是)一项令人钦佩的工作,但它也非常缓慢。通常的建议是将依赖项签入到存储库中(例如,在/vendor 文件夹中),这在生产环境设置中可能并不令人欣狂。

GOPATH 让人困惑

GOPATH 目录应该魔法地包含所有代码。我认为(基于Wiki的推测)这与简化从远程存储库中的获取有关,例如,go get github.com/my/repo。从理论上讲这很优雅,但在实践中却是令人困惑的,因为如果我们没有将代码放在正确的位置上,就会什么都行不通。因此,Go 给我留下了非常负面的第一印象。


现在,我工作计算机上的.profile 文件只有如下内容:


  export GOPATH=$HOME/go  export PATH="$GOPATH/bin:$PATH"  cd $GOPATH/src/github.com/assembledhq/assembled
复制代码

错误很难自省

大多数人都抱怨 Go 的错误处理太过冗长,也很难使用。在 Go 1.13(2019 年 10 月)中,添加了用于包装、拆包和比较的优秀方法,但不幸的是,采用率方面我们处于落后状态。


使用 1.13 版本之前编写的代码,不会对错误进行包装,这也特别痛苦。例如,在 Google 自己的API bindings中,请求的底层的 HTTP 错误不会被包装,因此无法作为 googleapi.RetrieveError、公共错误接口、甚至是低级的 url.Error 进行检查。唯一的选项是字符串匹配,我们这样做,通常是为了捕获类似 invalid_grant 这样的 OAuth 错误。


例如,比较一下 Scala 中模式匹配是如何进行错误处理的。

Nil versus zero values Nil 与零值

对空值或缺省后故意将其值设置为零的值进行建模,是很乏味的。例如,在 Stripe 的 Go bindings 中可以看到这种迁移。在我们的代码中,我们通常会返回一个指针和错误,例如(*string,error)。通过引入非关联 nil 指针的可能性来打破类型安全。我们可以检查 res == nil 以及 err != nil,但编译器不能把我们从健忘或懒惰中拯救出来。

缺乏面向对象的表现力

Go 建议使用接口和类型嵌入来复制有用的面向对象行为,这种行为在其他语言中是自然存在的。事实证明,这些工具具有很大的限制性,在不同的情况下,我们可以偶然绕过类型系统。这导致会诱惑。


Go 社区对此进行了详细介绍。这里有一个非常不错的总结:https://blog.golang.org/why-generics

结论

最初使用 Go,我花了一些时间来热身。正如我在 GOPATH 中所描述的那样,入门有些困惑。从诸如 Ruby 和 Scala 之类的语言转换过来,也意味着需要转变思维方式(或两种)。然而,在使用该语言的两年里,我开始真正享受它的简单和明确性了。


在 Assembled 公司,该语言确实非常适合我们的用例,这是一个主要的标准 Web 应用程序。我对这个生态系统寄予厚望,感觉就像我们正在使用经过精心设计和良好维护的工具一样。因此,在快速变更代码库的同时,提供稳定服务所需的基线工作量就更少了。


原文链接:


https://wgyn.github.io/2020/04/12/reflections-on-2-years-of-golang.html


2020-05-01 14:0010482

评论 1 条评论

发布
用户头像
go的设计哲学就不是不需要面向对象的吧,也不存在父类子类一说
2022-05-08 17:13
回复
没有更多了
发现更多内容
我使用Golang两年后的总结:优点和令人讨厌的怪癖_语言 & 开发_ryan wang_InfoQ精选文章