My reflections on Golang

2025-05-24

我对 Golang 的思考

最初发表于deepu.tech

我喜欢Go吗?是的。我会在所有用例中使用它吗?绝对不会。

别误会,我喜欢Go,因为它本身就很可爱,但就像其他编程语言一样,它总是让人又爱又恨。没有一种编程语言是完美的,它们都有各自的优点和用例。我讨厌看到人们过度使用某些东西,而如今 Go 就存在这种现象。平心而论,在我的职业生涯中,我也有过过度使用的经历(主要是 JavaScript),我理解人们为什么会这样做。这篇博文并非要抨击 Go 或赞扬 Go,只是我使用了 9 个多月后的一些感想。在我开始吐槽 Go 的优缺点之前,先介绍一下背景。

在科技行业摸爬滚打了十多年,我觉得自己应该是​​一个务实的程序员,或者至少是越来越接近这个目标的人——这应该是程序员的涅槃。我甚至没想过要当一名程序员,如果你问18岁的我,他会说他想成为一名天体物理学家或机器人工程师(是的,制造太空机器人是我的梦想)。和大多数青少年的梦想一样,我的梦想没有实现,最终我进入了科技行业。

虽然获得IT工作纯属偶然,但编程对我来说并不陌生。高中时,为了帮助女朋友完成项目,我学过一些C/C++;大学初期,为了个人项目和博客,我涉猎了一些PHP、JavaScript、HTML和Flash(ActionScript)。所以,当我在没有任何IT背景的情况下找到一份真正的IT工作时,我和当时很多IT人士一样,根据工作任务,我首先学习了一门语言——Java。由于我学得很快,并且对C/C++的编程概念有所了解,所以Java并不难学,几个月后我就成为了一名相当不错的Java程序员。后来,我接到一个构建Web UI的任务,于是我深入研究了HTML、CSS和JavaScript,并真心实意地爱上了JavaScript,因为它灵活易用。我掌握了JQuery,很快就成为办公室里前端开发方面的专家。

那时我一点也不务实,我向所有人宣扬 JavaScript,并会与任何认为 JS 是一种糟糕的语言的人进行激烈的辩论。

快进到现在,回想起来,我已经用 C/C++、PHP、JavaScript、TypeScript、HTML、CSS、Java、Groovy、Scala、Python 以及最近的 Go 语言做过项目。我觉得这些经历可能帮助我变得更加务实,因为我开始把编程语言视为工具,每种语言都有各自的优缺点。好吧,这个故事还有更多内容,但那是下次再说了,重点是为下面的反思设定一个基准,这样我就不会听起来像一个刚接触Go就胡言乱语的人。


Go 是我最近学习和使用的语言。我已经用 Go 开发了一个 CLI 项目超过 9 个月了,和我的团队一起构建了一个强大的脚手架引擎(没错,很像JHipster),它使用 Go 模板,你可以在其中创建我们XebiaLabs中称之为蓝图的东西。所以,是的,我用 Go 做的远不止一个 Hello World 应用。

为了不浪费更多时间在不相关的事情上,这里列出了我喜欢 Go 的地方和不喜欢的地方。

我喜欢 Go 的哪些方面

简单

我喜欢 Go 的简单性(除非你做练习,否则在导览页面上浏览所有语言特性实际上需要 15 分钟),而且与 Scala、Rust 甚至 JavaScript 不同,Go 没有很多做同样事情的方法,这对于希望编写可维护代码的团队和公司中的人们来说非常有价值,即使是新加入的员工也可以阅读和理解代码而不需要太多帮助。我认为这是推动 Go 采用的最大原因之一。如果你曾经参与过大型项目,你就会知道当代码不可读并且每个新团队成员都必须花费大量时间来尝试理解一段代码的作用时是多么困难。所以当我看到 Go 没有严重依赖隐式等特性时,我真的很高兴。语言特性和概念很容易掌握,你很快就可以开始使用 Go 高效工作。唯一看起来有点复杂的概念是并发部分,但与其他语言相比,这更简单。

语言提供的代码风格和审查

这真是太省时间了。在我看来,每种语言都应该这样做,这样你就不用浪费时间争论代码风格和设置 lint 规则了。Go 包中提供了自带的格式化、lint 和 vet 工具,Go 编译器甚至会强制执行未使用的变量之类的操作。大多数 IDE/编辑器插件也使用这些工具进行格式化和 lint,从而有助于在 Go 项目之间保持一致的代码风格,这又进一步提高了可读性和可维护性。

Goroutines 和 Channels

这是 Go 最大的优势之一。它原生支持并发和并行。这使得 Go 成为需要大量并发和/或并行处理、网络等应用的理想选择。Goroutines 可以轻松启动轻量级线程,而 Channels 则提供了一种类似消息总线的线程间通信方式。



func main() {
    messages := make(chan string)
    collected := make([]string, 2)

    go func() { messages <- "ping" }()
    go func() { messages <- "pong" }()

    collected = append(collected, <-messages)
    collected = append(collected, <-messages)
    fmt.Println(collected) // [ pong ping ]
}


Enter fullscreen mode Exit fullscreen mode

闭包和回调

如果你用过 JavaScript,你就会知道闭包和回调函数有多有用。Go 和 JavaScript 一样,将函数视为对象,因此可以将其赋值给变量、存储在 Map 中、作为函数参数传递以及从函数返回。Go 还支持创建嵌套闭包和匿名函数,这有助于封装上下文。其行为与 JavaScript 非常相似。因此,你也可以在 Go 中应用一些函数式编程概念。



func main() {
    // an unnecessarily complicated example
    type fnType = func(a int, b int) int
    fnMap := map[string]fnType{
        "ADD": func(a int, b int) int {
            return a + b
        },
        "SUB": func(a int, b int) int {
            return a - b
        },
    }

    // this is a closure
    localFn := func(method string) fnType {
        return fnMap[method] // returns a function
    }

    printer := func(fn func(method string) fnType, method string) {
        fmt.Println(fn(method)(10, 5)) // callback
    }
    // function passed as parameter
    printer(localFn, "ADD")
    printer(localFn, "SUB")
}


Enter fullscreen mode Exit fullscreen mode

类型断言和开关

Go 提供了一种很好的断言类型方法,并且可以与switch 语句一起使用,这使得反射等变得更加容易。

多次返回

这是一个非常方便的功能,就像在 Python 中一样,我们习惯于在 JavaScript 中解构对象/数组来实现这一点,而在某些语言中则使用元组等。返回值也可以命名,这有利于提高可读性。

工具

如前所述,Go 提供了用于格式化、代码检查等的标准工具,其语言设计使其易于构建工具,因此编辑器/IDE 具有测试生成、代码覆盖率等优秀的功能。例如,Go 的 VSCode 集成提供了以下选项,有助于保持一致性并减少需要手写的样板代码。

不需要运行时

Go 不需要像 JVM 或 NodeJS 那样的运行时,Go 应用程序可以使用标准 Go 工具编译为可执行的跨平台二进制文件。这使得 Go 应用程序具有可移植性和平台无关性。

我不喜欢 Go 的地方

简单

这就是我爱恨交织的开端。Go 语言简洁明了,这很好,但有时感觉过于简单和冗长。而来自 Java/JavaScript 生态系统的 Go 语言,拥有一些不错的特性和语法糖,在我看来,这些特性和语法糖让代码更具表现力,并有助于保持 DRY 原则。我最怀念的是

  • 泛型: Go 的下一个主要迭代版本正在考虑支持泛型,但在此之前,这只会造成不必要的代码重复。我已经记不清有多少次为了不同的类型重复编写相同的代码块了,而泛型本来可以让它变得简洁美观。这也是 Go 中没有 Lodash 等库的原因之一。
  • 标准错误处理:这似乎也会在 Go 的下一个主要迭代中实现if err != nil,但在它正式发布之前,我只能抱怨。任何编写 Go 代码的人都会记得在代码中做过无数次这样的事情。移除这些错误处理可能会将代码库的大小至少减少 20%。
  • 默认值:我很想在 Go 中看到这个,这很有用。也许我只是被 JS 宠坏了。

样板太多(不适合 DRY)

Go 过于简单,这意味着你需要编写大量代码,因为该语言不提供 map、reduce 等结构。此外,由于缺乏泛型,你最终需要编写大量的实用代码,而且为了适应不同的类型,很多代码都需要重复编写。想象一下,在 Go 中编写一个 map 函数,你必须为每种可用的 map 组合都编写一个。这些因素使得在 Go 中进行 DRY 编程变得非常困难。

依赖管理

与其他主流语言相比,Go 生态系统中的依赖管理显得有些不成熟和基础。从 Git 导入软件包固然很好,但也使其更加脆弱。当你在生产应用程序中依赖 Git 分支时,可能会出现什么问题呢?没有办法使用相对依赖关系(NPM 链接无可匹敌!)。
这些问题类似于 Node 包管理器中依赖范围的问题。Glide 似乎是一个受欢迎的选择,但仍然不如其他语言的解决方案成熟。在我参与的项目中,我们将 Gradle 与Gogradle一起使用,虽然运行良好,但开发人员的体验不如在 Java 项目中使用 Gradle/Maven 或在 NodeJS 项目中使用 NPM。

GOPATH 中的源代码

Go 建议你在 GOPATH 下创建 Go 项目。也许只有我一个人这么觉得,但我不喜欢这样做,因为我通常喜欢整理代码。例如,我有一个~/workspace/文件夹,用来按组织方式整理我的项目。如果我遵循 Go 的建议,就必须把项目/home/deepu/go/src和所有下载的库源代码都放在 GOPATH 下。如果不遵循这个建议,那么大多数 Go 工具都无法使用。目前,我有一个特定的 Gradle 任务,负责将所有供应商库复制到我的本地 Gopath 中,~/workspace/XL/<project>以解决这个问题。

令人困惑的指针行为

Go 对指针的支持相当不错,默认行为是按值传递对象。如果要按引用传递,则必须特别标记。但这种行为不太一致,因为 Map 和 Slice 默认按引用传递,因此这可能会让初学者感到有些意外。

结构地狱

这更像是一个小问题。结构体是 Go 中用来创建数据结构的东西。它可能看起来像对象,但实际上并非对象。虽然结构体在功能上没问题,但在很多情况下,你最终会得到看起来像 JSON 丑陋兄弟的结构体。在实际项目中,你总是会创建复杂的结构体,尤其是在应用程序进行一些通用的 JSON 或 YAML 解析时,你的代码很快就会变成这样。这倒不是什么大问题,但每次我调试代码或编写测试时,它都会让我的眼睛很不舒服。



func main() {
    type MyYamlDoc struct {
        foo []map[interface{}][]map[interface{}]interface{}
        bar interface{}
    }

    ohno := MyYamlDoc{
        []map[interface{}][]map[interface{}]interface{}{
            {
                "Foo": {
                    {"Bar": map[interface{}][]map[interface{}]interface{}{
                        "Foo": {
                            {"Bar": map[interface{}][]map[interface{}]interface{}{
                                "Foo": {
                                    {"Bar": map[interface{}][]map[interface{}]interface{}{
                                        "Foo": {
                                            {"Bar": map[interface{}][]map[interface{}]interface{}{}},
                                        },
                                    }},
                                },
                            }},
                        },
                    }},
                },
            },
            map[interface{}][]map[interface{}]interface{}{
                "Foo": {
                    {"Bar": map[interface{}][]map[interface{}]interface{}{}},
                },
            },
        },
        map[interface{}][]map[interface{}]interface{}{
            "Foo": {
                {"Bar": map[interface{}][]map[interface{}]interface{}{}},
            },
        },
    }
    fmt.Println(ohno)
}


Enter fullscreen mode Exit fullscreen mode

奇怪的界面构造

Go 中的接口概念很奇怪。它们是 Go 中唯一隐式的构造函数。如果你之前使用过其他有接口的语言,你可能会觉得很奇怪。隐式接口意味着很容易搞砸事情。除非你有一个智能 IDE,否则重构会很麻烦,而且你可能会因为以某种方式命名方法而意外地实现别人的接口。虽然隐式接口确实有助于多态性和代码解耦,但我个人仍然更喜欢显式接口。

另一个接口陷阱是空值检查。在 Go 中,接口由类型和值两部分组成,因此nil只有当类型和值都为空时,接口才有效。这意味着你不能简单地对接口进行空值检查。这非常令人困惑,Go 对此有专门的常见问题解答。下文将对此进行更详细的解释。

单GC算法

Go 实现了并发三色标记 - 清除收集器作为其垃圾收集器。此特定的 GC 实现针对更好的暂停时间进行了优化,同时忽略了程序吞吐量、暂停频率和 GC 期间考虑的许多其他参数。Go 社区中的一些人声称这是有史以来最好的 GC。由于有一些 Java 背景,我不同意这种说法,因为大多数 JVM 实现提供了多种 GC 算法可供选择,其中还包括并发标记 - 清除收集器,并且其中大多数都经过平衡以处理比暂停时间更多的参数。本文对此进行了详细分析。因此,由于频繁的 GC,在某些产生大量垃圾的用例中,Go 的运行速度实际上可能比其他语言更慢。

开发人员体验

这纯粹是基于个人经验,因此会因人而异。作为一名使用过多种语言的多语言开发者,Go 的开发体验并非我体验过的最佳。JavaScript 生态系统的 DX 是我迄今为止体验过的最佳。感觉 Go 生态系统中缺少一些东西。依赖管理和工具链需要改进。如果能添加一些更合理的语言特性和语法糖就更好了。

结论

由于使用过多种主流语言,我不能只在每种用例中使用 Go,但我可以理解为什么如果人们没有使用过其他语言,他们会在每种用例中使用 Go。

那么我应该在哪里使用 Go 呢?

  • 当用例需要大量并行处理和/或并发(两者并非一回事,但关系更近)时,我肯定会使用 Go,因为您可以使用 Goroutines 来实现这一点,而且比在 Java 应用程序中管理线程或在 JavaScript 中使用回调地狱(因为 JS 实际上是单线程的)更简单、更高效。这里有一篇很好的文章解释了 Goroutines 的优势。
  • 简单的微服务,无需担心样板
  • 网络应用程序或 Web 服务器,尤其是异步工作负载,可以从 Go 中受益匪浅。平心而论,你也可以用 Java、Python、JS 等语言来实现这些功能,但 Go 最终会提供更高的效率,并且更容易实现。
  • 系统编程。虽然 Rust 或 C 语言在这方面是更好的选择,但如果这些语言不在你的掌握之中,那么 Go 是次佳选择。Go 对指针及其标准库的支持良好,因此比其他主流语言更容易编写系统程序。许多流行的系统工具,例如 Docker、Kubernetes 等,都是用 Go 编写的。

哪些情况下我不会使用 Go?

  • 复杂的 Web 应用程序:我会选择 Java 框架,例如SpringMicronaut,因为它们更易于维护且久经考验,而且你可以将更多精力放在业务逻辑上,而不是编写样板基础架构代码。反对这种技术栈的一个常见理由是它的内存占用,但使用 Spring 可以降低内存占用,而且像 Micronaut 和Quarkus这样的框架实际上承诺了 OOB(内存溢出)。
  • 用 Go 编写了一个高级 CLI 工具后,我讨厌这种体验,一直觉得用 JavaScript 写会效率提高 10 倍,体验也会更好。所以我随时都会选择在 NodeJS 上运行 JavaScript 或 TypeScript 来编写 CLI 工具。主要是因为 Go 的生态系统,以及无需花费大量时间编写样板代码就能快速完成任务的纯粹乐趣。但如果要编写的 CLI 工具是系统工具或网络工具,那么 Go 可能是一个不错的选择。

我确实希望 Go 能随着时间的推移发展成为一种通用语言,并解决其中的许多问题。与此同时,我会努力践行这一理念。

但是你总是可以选择用锤子来拧紧螺丝。


如果您喜欢这篇文章,请点赞或留言。

您可以在TwitterLinkedIn上关注我。

封面图片来源:来自egonelbre/gophers 的图片由@egonelbre创建

文章来源:https://dev.to/deepu105/my-reflections-on-golang-38jk
PREV
我的 VS Code 设置 - 充分利用 VS Code 插件终端设置结论
NEXT
我对 Rust 的第一印象 我喜欢 Rust 的哪些方面 我不喜欢 Rust 的哪些方面 挑剔之处 结论