美文网首页读书简友广场想法
为什么我们更喜欢 Go 作为后端

为什么我们更喜欢 Go 作为后端

作者: 技术的游戏 | 来源:发表于2022-12-05 23:47 被阅读0次

    那里有大量的后端编程语言。他们在不同情况下都有各自的优势。例如Java非常适合企业后端,Typescript + NodeJS 对全栈开发很有用。还有 PHPRuby,它们……我不知道您为什么要选择它们,但我想它们各自适用。但在我们这边,只要有可能,我们更喜欢 Go,因为它在执行速度、开发速度和其坚持已见的哲学之间取得了平衡,这导致了更标准化的代码,从而更好的编码实践。

    image.png

    就像我们心爱的语言一样,我们也很固执己见。因此,事不宜迟,让我们开始探讨为什么我们更喜欢 Go

    性能与开发时间

    我们发现 Go 在性能和开发时间之间取得了最佳平衡。这是由许多因素造成的,我们将立即讨论其中的一些因素。

    小语法

    每一个 Go 的辩护者通常都会反驳这一点,所以让我们把它放在一边吧。go 具有最小的关键字集之一和简单的语法这一事实很棒。这意味着您可以将大部分时间用于学习如何使用该语言,而不必花时间学习该语言。

    完善的标准库

    这一点是双重的,首先,几乎所有你需要的关于 web 服务器的基本东西都在标准库中,所以你真的不必浪费那么多时间寻找一个可以满足你需要的包。

    说到这里,第二点来了。无论是依赖管理、linter、测试框架、基准测试工具、竞争检测器,甚至更多,都包含在 go CLI 工具中。甚至还有一个标准格式化程序 gofmt。这意味着您不必考虑使用哪个包管理器、您将选择哪种 linter 和样式、哪种测试框架等。一切就在那里,您可以立即开始编码,所有代码看起来是一体的。

    说到这里,这与我们的下一点有关。

    固执己见

    编译器和 linter 非常令人讨厌,这听起来像是一件坏事,但我们发现它实际上恰恰相反。例如错误处理。假设我们有以下代码:

    package foobar
    type myInterface interface{
        Foo() (int, error)
    }
    func Bar(i myInterface){
        result, err := i.Foo()
        fmt.Println(result)
    }
    

    这不会编译,因为 err 变量已定义但从未使用过。为了编译它,您必须使用变量,例如:

    func Bar(i myInterface){
        result, err := i.Foo()
        if err != nil{
            panic(err)
        }
        fmt.Println(result)
    }
    

    或者更积极地选择用 _,而不是名称来忽略它:

    func Bar(i myInterface){
        result, _ := i.Foo()
        fmt.Println(result)
    }
    

    编译器的不断唠叨可能看起来微不足道,但它会帮助您缩短代码,从而更易于阅读,更重要的是,它会通过强制执行正确的错误处理来生成更稳定的代码。稍后我们将深入探讨这一点

    内存管理

    对于大多数 nodeJava 等开发人员来说,这将是一个陌生的或不相关的概念。但是当谈到速度时,能够使用栈内存而不是在堆中分配内存会产生巨大的差异。虽然您没有像在 CC++ 中那样精细的控制,但您确实有一定程度的控制。如果不使用指针,则不会分配内存,而如果使用,则可能会分配内存。

    “等等,你是说指针吗?!”

    是的,但不用担心,您不需要 malloc 也不需要 free 。事实上,您使用的大多数语言都有指针。让我们想想 JavaC#,在它们中,类的每个实例实际上都是指向对象的指针,这就是为什么您可能会听到对象是“按引用传递”的原因,这实际上不是真的,您只是传递了一个副本指向对象的指针。Go 的不同之处在于你是选择它是否是指针的人,但就像那些语言一样,分配的内存由垃圾收集器释放。

    垃圾收集

    Go 是垃圾回收的,所以你可能认为它是 CJava 在内存管理方面的中间点。您有一些控制权,但是您将所有困难的部分留给了垃圾收集器。老实说,这一直是争论的焦点。Go 的垃圾收集器并不总是很棒,虽然现在很棒,但我们仍然相信内存管理的好处超过了更好的垃圾收集系统的好处,因为在大多数程序中你不会使用 那么多的 内存分配。在大多数应用程序中,任何类型的垃圾收集都胜过手动内存管理的好处。

    很棒的打字系统

    在我们看来,拥有强静态类型语言是没有商量余地的。看起来每个人都认为他们永远不会以使用错误类型这样明显的方式犯错误,但那是 bull$#!%。你总是会有糟糕的一天,或者你的 PM 在周五要求你写一个快速的功能,而你只是半途而废。拥有一个好的打字系统可以避免这些错误。

    最重要的是,我们实际上非常喜欢 Go 的类型系统。能够只在你需要的地方定义接口,而不是让一个类实现所有都是该类子集的接口负载,这有助于遵守 ISP

    面向对象,但不是全部

    在 OOP 中,人们常说使用组合而不是继承。事实上,根据 Alan Kay(OOP 之父)的说法,它甚至不是定义的必要部分。根据 SmallTalk 的早期版本

    • 对象通过发送和接收消息进行通信。
    • 对象有自己的记忆。
    • 每个对象都是一个类的实例。
    • 该类持有其实例的共享行为

    在这个对象和类的定义中,并没有提到继承。然而,在实践中很少出现这种情况。不仅语言在语法方面更喜欢继承而不是组合,而且大多数模式都是用继承建模的。
    老实说,里氏代换原则 (LSP) 很棒。但是你真的不需要 LSP 的继承。因此,如果您可以通过组合而不是继承来达到类似的效果,那就太好了。

    好的,那么 Go 是从哪里进入这一点的呢?在 go 中没有结构之间的继承这样的东西,但是有一种使用组合、嵌入的惊人方法。如果你使用接口,你可以毫不费力地遵循 LSP。

    type A struct{
        n int
    }
    func (a *A) SetNum(int n){
        a.n = n
    }
    func (a *A) String() string{
        return fmt.Sprintf("A: %d", n)
    }
    type B struct{
        *A
    }
    

    这里我们有A嵌入在 struct 中的结构B。那么……它有什么作用?好吧,我们现在已经定义了A可以从B对象访问的每个方法。像这样:

    func main(){
        b := &B{&A{}}
        b.SetNum(4)
        fmt.Println(b)
    }
    

    这将打印 A: 4

    等等,这不就是多了几个步骤的继承吗?

    一点都不。这是彻头彻尾的构图。这只是语言给我们的一种便利,这样我们就不需要编写除了调用另一个方法之外什么都不做的方法。事实上,当我们调用时,b.SetNum(4) 它实际上是在调用 b.A.SetNum(4). 所以我们实际上是在修改 A 对象而不是 b

    我们可以看到,如果我们添加到B具有相同名称的方法,我们仍然可以访问A的方法,并且两个对象不会相互影响。像这样:

    type B struct {
        *A
        n int
    }
    
    func (b *B) SetNum(n int) {
        b.n = n
    }
    
    func (b *B) String() string {
        return fmt.Sprintf("B: %d, A:%d", b.n, b.A.n)
    }
    
    func main(){
        b := &B{A:&A{}}
        b.A.SetNum(5)   //the A object is still accessible
        b.SetNum(4)     //we are accessing the b object
        fmt.Println(b)
    }
    

    这将打印 B:4, A:5

    这样我们就可以遵守 LSP 而无需处理那个讨厌的继承。

    错误处理

    让我们回到错误处理。软件开发中最困难的事情之一(除了命名和缓存失效)是如何处理不成功的函数调用。有两种处理方法,一种是通过异常处理,另一种是通过错误处理。异常处理是常态,但最近新的语言(例如 GoRust)选择了后者。

    但为什么?在异常处理中,您只在需要时才处理异常。这意味着您通常不会处理它,因为显然您永远不会将错误编程到您的代码库中,所以何必费心呢?或者你可能会做一些包罗万象的异常处理,只是因为没有考虑可能发生的事情。

    另一方面,在错误处理上,它是相反的方式,无论是通过 Result monad ( Rust ),还是不允许你有未使用变量的令人讨厌的编译器 ( Go ),你 必须 默认处理它,然后您可以选择忽略它。虽然这确实意味着您可能会有更多的错误处理代码(可怕的 if err != nil),但这也意味着您的代码可能不太容易出错。

    绿色线程

    Go 最大的特性之一是 goroutines,这是绿色线程的别致名称。线程和绿色(或虚拟)线程之间的区别在于,绿色线程不直接针对操作系统运行,而是依赖于运行时。运行时然后在它认为合适的时候管理本机操作系统线程。

    但是等等,这不是一件坏事吗?不一定,首先,你不依赖于操作系统的多线程能力,老实说,这在如今并不是一个大问题。

    然而,一个很大的优势是,由于这些线程不需要在操作系统级别创建一个全新的线程,因此它们可以比真正的线程轻量级得多。这意味着您可以以与本机 OS 线程相同的启动成本有效地启动更多线程。

    最后一个优点是,由于运行时本身正在管理线程,因此它可以轻松检测死锁和数据争用。Go提供了 很好 的工具。

    简单的线程同步

    说到多线程,谈到多线程时最大的障碍之一就是同步。幸运的是, Go 提供了一些开箱即用的好工具。

    • Channels:这是 Go 提供的线程之间通信和同步的最简单方法。您可以向频道发送消息,也可以从频道中读取消息。默认情况下,每次把一个 goroutine 写入一个通道时,它都会被阻塞,直到另一个 goroutine 从它读取,反之亦然。
    • WaitGroup:这个具有 3 个方法的结构体。Add(i int), Done()Wait()。您可以添加要等待的任务数,将任务标记为已完成,并等待所有任务完成。
    • Mutex、Semaphores 等:sync 标准库中的包下也提供了所有经典的同步模型。

    综上所述

    即使我们这样说它,它也不全是完美无缺的。该语言存在一些缺点,例如缺少枚举,但所有这些缺点通常都很容易处理。例如要解决枚举,您可以自定义类型与 iota

    对于您可能遇到的几乎所有问题,已经有一个已知的解决方案,并且还有一个很棒的社区,其中包含(https://github.com/avelino/awesome-go)可满足您可能需要的几乎所有内容的出色软件包。

    所以是的,我们绝对是 Go 的忠实拥护者!

    欢迎点赞,关注,转发,Happy Coding。

    相关文章

      网友评论

        本文标题:为什么我们更喜欢 Go 作为后端

        本文链接:https://www.haomeiwen.com/subject/gztwfdtx.html