编程语言为什么我们更喜欢 Go 作为后端? Why we prefer Go for Backend

Posted 禅与计算机程序设计艺术

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了编程语言为什么我们更喜欢 Go 作为后端? Why we prefer Go for Backend相关的知识,希望对你有一定的参考价值。

目录

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

Performance vs development time 性能与开发时间

Small syntax 小语法

All-in-one 一体

Opinionated 自以为是

Memory management 内存管理

Great typing system 很棒的打字系统

Object Oriented, but not all the way面向对象,但不是全部

Error handling 错误处理

Green threads 绿线

Easy thread synchronization 简单的线程同步

In conclusion 综上所述


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

Why we prefer Go for Backend?

There are tons of backend programming languages out there. They all have their fair share of advantages for different situations. For example Java is great for enterprise backends, Typescript + NodeJS is useful for full stack development. There’s also php and Ruby, which… I don’t know why you’d choose those, but to each their own I guess. But on our end, whenever possible, we prefer Go for its balance of execution speed, development speed and its opinionated philosophy which leads to a more standardized code, and thus better coding practices.
那里有大量的后端编程语言。他们在不同情况下都有各自的优势。例如 Java 非常适合企业后端,Typescript + NodeJS 对全栈开发很有用。还有 PHP 和 Ruby,它们……我不知道你为什么会选择它们,但我想它们各自都有。但在我们这边,只要有可能,我们更喜欢 Go,因为它在执行速度、开发速度和其自以为是的哲学之间取得了平衡,这导致了更标准化的代码,从而更好的编码实践。

And just like our beloved language, we are also quite opinionated 🤓. So without further ado, let’s jump into why we prefer Go.
就像我们心爱的语言一样,我们也很固执己见🤓。因此,事不宜迟,让我们开始探讨为什么我们更喜欢 Go。

Performance vs development time 性能与开发时间

We found that Go hits the best balance between performance and development time. This is due to many factors, some of which we will discuss right away.
我们发现 Go 在性能和开发时间之间取得了最佳平衡。这是由许多因素造成的,我们将立即讨论其中的一些因素。

Small syntax 小语法

This point is usually regurgitated by every Go apologist, so let’s get it out of the way 😅. The fact that go has one of the smallest set of keywords and an easy syntax is great. This means that you can spend most of the time learning how to use the language instead of having to spend time learning the language.
这一点通常被每个 Go 的辩护者反驳,所以让我们把它放在一边吧。 go 具有最小的关键字集之一和简单的语法这一事实很棒。这意味着您可以将大部分时间用于学习如何使用该语言,而不必花时间学习该语言。

All-in-one 一体

This point is twofold, first of all, pretty much all the basic things you need regarding a web server is right there in the standard library, so you don’t really have to waste that much time looking for a package that does what you need (looking at you node 😠).
这一点是双重的,首先,几乎所有你需要的关于 web 服务器的基本东西都在标准库中,所以你真的不必浪费那么多时间寻找一个可以满足你需要的包(看着你节点😠)。

Speaking of which, here comes the second point. Whether it’s dependency management, a linter, a test framework, benchmark tool, race detector, and even more, it’s all included in the go CLI tool. There is even a standard formatter, gofmt. This means you don’t have to think about which package manager to use, what linter and style you’ll choose, what testing framework, etc. It’s all just there and you can jump into coding right away, and all your code will look homogeneous.
说到这里,第二点来了。无论是依赖管理、linter、测试框架、基准测试工具、竞争检测器,甚至更多,都包含在 go CLI 工具中。甚至还有一个标准格式化程序 gofmt。这意味着您不必考虑使用哪个包管理器、选择什么样的 linter 和风格、什么样的测试框架等。一切就在那里,您可以立即开始编码,您的所有代码看起来同质。

Speaking of which, this ties to our next point.
说到这里,这与我们的下一点有关。

Opinionated 自以为是

The compiler and linter are extremely obnoxious, which might sound like a bad thing, but we find it’s actually quite the opposite. Like for example with the error handling (expanding on what I already wrote). Lets say we have the following code:
编译器和 linter 非常令人讨厌,这听起来像是一件坏事,但我们发现它实际上恰恰相反。例如错误处理(扩展我已经写过的内容)。假设我们有以下代码:

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

This won’t compile because the err variable is defined but never used. In order for this to compile, you have to either use the variable, for example:
这不会编译,因为 err 变量已定义但从未使用过。为了编译它,您必须使用变量,例如:

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

Or rather actively choose to ignore it with a _ instead of a name:
或者更积极地选择使用 _ 而不是名称来忽略它:

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

The constant nagging from the compiler might seem petty, but it will help you making your code shorter hence easier to read, and more importantly, it leads to a more stable code by enforcing proper error handling. We’ll dive a little more into this point in a little while
编译器的不断唠叨可能看起来微不足道,但它会帮助您缩短代码,从而更易于阅读,更重要的是,它会通过强制执行正确的错误处理来生成更稳定的代码。稍后我们将深入探讨这一点

Memory management 内存管理

For most nodeJava, etc. developers this will be either a foreign or an irrelevant concept. But when it comes to speed being able to use the memory stack in stead of allocating memory in the heap can make a huge difference. While you don’t have such a granular control as in C and C++, you do have some amount of control. If you do not use pointers, you will not allocate memory, while if you do, you might allocate memory.
对于大多数节点、Java 等开发人员来说,这将是一个陌生的或不相关的概念。但是当谈到速度时,能够使用内存堆栈而不是在堆中分配内存会产生巨大的差异。虽然您没有像在 C 和 C++ 中那样精细的控制,但您确实有一定程度的控制。如果不使用指针,则不会分配内存,而如果使用,则可能会分配内存。

“Wait, did you say pointers?!” “等等,你是说指针吗?!”

Yes, but not to worry, you don’t need to malloc nor free. In fact, most languages you use DO have pointers. Let’s think of Java and C#, in them, every instance of a class is actually a pointer to the object, that’s why you might hear that objects are “passed by reference” which is actually not true, you are just passing a copy of the pointer to an object. The difference in Go is that you are the one who chooses whether it is a pointer or not, but just as those languages, allocated memory is freed by a garbage collector.


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

Garbage collection 垃圾收集

Go is garbage collected, so you might think of it as the middle point between a C and a Java in terms of memory management. You have some control, but you leave all the hard parts to the garbage collector. Which in all honesty, has been a point for contention. Go’s garbage collector was not always great, and while it is great now, we still believe the benefits in memory management outweigh the benefits of a better garbage collection system, since in most programs you would not use that much memory allocation anyways. And any kind of garbage collection outweigh the benefits of manual memory management in most applications.
Go 是垃圾回收的,所以你可能认为它是 C 和 Java 在内存管理方面的中间点。您有一些控制权,但是您将所有困难的部分留给了垃圾收集器。老实说,这一直是争论的焦点。 Go 的垃圾收集器并不总是很棒,虽然现在很棒,但我们仍然相信内存管理的好处超过了更好的垃圾收集系统的好处,因为在大多数程序中你不会使用那么多内存分配。在大多数应用程序中,任何类型的垃圾收集都胜过手动内存管理的好处。

Great typing system 很棒的打字系统

In our eyes, having a strongly statically typed language is non-negotiable. Look everybody thinks they’ll never make a mistake in such an obvious manner as using the wrong type, but that’s bull$#!%. You’ll always have a bad day, or come to write a quick feature your PM asked you on a Friday and you just half-ass it. And having a good typing system can save you from those mistakes.
在我们看来,拥有强静态类型语言是没有商量余地的。看起来每个人都认为他们永远不会以使用错误类型这样明显的方式犯错误,但那是 bull$#!%。你总是会有糟糕的一天,或者你的 PM 在星期五要求你写一个快速的功能,而你只是半途而废。拥有一个好的打字系统可以避免这些错误。

On top of that, we actually like Go’s type system quite a bit. Being able to define interfaces only where you need them, instead of having a class implement loads of interfaces that all are subsets of the class helps a ton towards complying with ISP
最重要的是,我们实际上非常喜欢 Go 的类型系统。能够只在你需要的地方定义接口,而不是让一个类实现所有都是该类子集的接口负载,这有助于遵守 ISP

Object Oriented, but not all the way
面向对象,但不是全部

In OOP it’s often said to use composition over inheritance. In fact, according to Alan Kay (the father of OOP), it’s not even a necessary part of the definition. According to early versions of SmallTalk
在 OOP 中,人们常说使用组合而不是继承。事实上,根据 Alan Kay(OOP 之父)的说法,它甚至不是定义的必要部分。根据 SmallTalk 的早期版本

  • Objects communicate by sending and receiving messages.
    对象通过发送和接收消息进行通信。
  • Objects have their own memory. 对象有自己的记忆。
  • Every object is an instance of a class.
    每个对象都是一个类的实例。
  • The class holds the shared behavior for its instances
    该类持有其实例的共享行为

In this definition of object and class, there is no mention of inheritance. However, in practice this is rarely the case. Not only languages favor inheritance over composition syntax-wise, but also most patterns are modeled with inheritance.
在这个对象和类的定义中,并没有提到继承。然而,在实践中很少出现这种情况。不仅语言在语法方面更喜欢继承而不是组合,而且大多数模式都是用继承建模的。
Which in all honesty kind of makes sense, Liskov’s Substitution Principle (LSP) is awesome. But you don’t really need inheritance for LSP. So if you could reach something alike with composition instead of inheritance, it would be great.
老实说,里氏代换原则 (LSP) 很棒。但是你真的不需要 LSP 的继承。因此,如果您可以通过组合而不是继承来达到类似的效果,那就太好了。

Ok, so where does Go come into this point? In go there is no such thing as inheritance amongst structs, BUT there is an amazing way to use composition, embedding. And if you work with interfaces, you can follow LSP with no drama.
好的,那么 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

Here we have the struct A which is embedded inside the struct B . So… what does it do? Well, we now have every method defined for A accessible from a B object. Like so:
这里我们有一个嵌入在结构 B 中的结构 A 。那么……它有什么作用?好吧,我们现在为 A 定义的每个方法都可以从 B 对象访问。像这样:

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

This will print A: 4
这将打印 A: 4

Wait, isn’t this just inheritance with extra steps?
等等,这不就是多了几个步骤的继承吗?

No, not at all. This is composition through and through. It’s just a convenience that the language gives us so that we don’t need to write methods that do nothing but call another method. In fact, when we call b.SetNum(4) it’s acctually calling b.A.SetNum(4) . So we are actually modifying the the A object and never b.
一点都不。这是彻头彻尾的构图。这只是语言给我们的一种便利,这样我们就不需要编写除了调用另一个方法之外什么都不做的方法。事实上,当我们调用 b.SetNum(4) 时,它实际上是在调用 b.A.SetNum(4) 。所以我们实际上是在修改 A 对象而不是 b 。

We can see this if we add to B methods with the same name, we can still access A’s methods and both objects do not affect each other. Like so:
我们可以看到,如果我们向 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 := &BA:&A
    b.A.SetNum(5) //the A object is still accessible
    b.SetNum(4) //we are accessing the b object
    fmt.Println(b)

This will print B:4, A:5
这将打印 B:4, A:5

This way we can comply with LSP without needing to deal with that nasty inheritance 🤢
这样我们就可以遵守 LSP 而无需处理那个讨厌的继承🤢

Error handling 错误处理

Let’s get back to error handling. One of the hardest things in software development (besides naming and cache invalidation 🙂) is what to do on an unsuccessful function call. There are two ways to handle this, one is through exception handling, the other one is through error handling. Exception handling is the norm, but lately new languages (such as Go and Rust) opt for the latter.
让我们回到错误处理。软件开发中最困难的事情之一(除了命名和缓存失效 🙂)是如何处理不成功的函数调用。有两种处理方法,一种是通过异常处理,另一种是通过错误处理。异常处理是常态,但最近新的语言(例如 Go 和 Rust)选择了后者。

But why? In exception handling, you only handle the exception when you want to. This means you usually do not handle it, because obviously you will never program a bug into your code-base, so why even bother? Or you might do some catch-all exception handling just because without thinking much about what might happen.
但为什么?在异常处理中,您只在需要时才处理异常。这意味着您通常不会处理它,因为显然您永远不会将错误编程到您的代码库中,所以何必费心呢?或者你可能会做一些包罗万象的异常处理,只是因为没有考虑可能发生的事情。

On the other hand, on error handling, it’s the other way around, be it via a Result monad (Rust), or an obnoxious compiler that won’t let you have unused variables (Go), you have to handle it by default, and you can then choose to ignore it. While this does mean you will probably have more error handling code (the dreaded if err != nil) this also means your code will probably be less error prone.
另一方面,在错误处理上,它是相反的方式,无论是通过 Result monad (Rust),还是不允许你有未使用变量的令人讨厌的编译器 (Go),你必须默认处理它,然后您可以选择忽略它。虽然这确实意味着您可能会有更多的错误处理代码(可怕的 if err != nil ),但这也意味着您的代码可能不太容易出错。

Green threads 绿线

One of the biggest features of Go are goroutines, which is a fancy name for a green thread. The difference between threads and green (or virtual) threads, is that green threads do not run directly against the OS, instead depending on a runtime. The runtime then managing the native OS threads as it sees fit.
Go 最大的特性之一是 goroutines,这是绿色线程的别致名称。线程和绿色(或虚拟)线程之间的区别在于,绿色线程不直接针对操作系统运行,而是依赖于运行时。运行时然后在它认为合适的时候管理本机操作系统线程。

But wait, is this not a bad thing? Not necessarily, first of all, you do not depend on the OS’s ability to multi-thread, which to be honest, nowadays is not a big issue.
但是等等,这不是一件坏事吗?不一定,首先,你不依赖于操作系统的多线程能力,老实说,现在这不是什么大问题。

However, one big advantage is that since this threads do not need to create a whole new thread at the OS level, they can be much more lightweight than real threads. This means that you can effectively launch more threads for the same startup cost as native OS threads.
然而,一个很大的优势是,由于这些线程不需要在操作系统级别创建一个全新的线程,因此它们可以比真正的线程轻量级得多。这意味着您可以以与本机 OS 线程相同的启动成本有效地启动更多线程。

The last advantage is that since the runtime itself is managing the threading, it can both detect deadlocks and data-races easily. Which Go offers great tooling for.
最后一个优点是,由于运行时本身正在管理线程,因此它可以轻松检测死锁和数据争用。 Go 提供了很好的工具。

Easy thread synchronization 简单的线程同步

Speaking of multi-threading, one of (if not the) biggest hurdle when speaking of multi-threading is synchronization. Luckily Go offers straight out of the box some great tools for this.
说到多线程,谈到多线程时最大的障碍之一(如果不是)就是同步。幸运的是,Go 提供了一些开箱即用的好工具。

  • Channels: this is the simplest way to communicate between and synchronize threads Go offers. You can send messages to channels and you can read from channels. By default, each time a goroutine writes to a channel it is blocked until another one reads from it, and vice-versa.
    通道:这是 Go 提供的线程之间通信和同步的最简单方法。您可以向频道发送消息,也可以从频道中读取消息。默认情况下,每次一个 goroutine 写入一个通道时,它都会被阻塞,直到另一个 goroutine 从它读取,反之亦然。
  • WaitGroup: this a a struct with 3 methods. Add(i int)Done() and Wait(). You can add the number of tasks you want to wait for, mark a task as done, and wait until all tasks are done.
    WaitGroup:这是一个具有 3 个方法的结构。 Add(i int) 、 Done() 和 Wait() 。您可以添加要等待的任务数,将任务标记为已完成,并等待所有任务完成。
  • Mutex, Semaphores, et al: all the classic synchronization models are also available under the sync package in the standard library.
    Mutex、Semaphores 等:所有经典的同步模型也可以在标准库的 sync 包下找到。

In conclusion 综上所述

Even though we paint it like that, it’s not all rose colored. There are some drawbacks when it comes to the language, like for example the lack of enums, but all these drawbacks are usually really easy to deal with. Like for example to solve the enum, you could just use a custom type together with iota.
即使我们这样画,它也不全是玫瑰色的。该语言存在一些缺点,例如缺少枚举,但所有这些缺点通常都很容易处理。例如解决枚举,您可以将自定义类型与 iota 一起使用。

And for pretty much any problem you may face, there is already a known solution, and also there is a great community with awesome packages for pretty much anything you might want.
对于您可能遇到的几乎所有问题,已经有一个已知的解决方案,并且还有一个很棒的社区,其中包含可满足您可能需要的几乎所有内容的出色软件包。

So yeah, we are definitely full blown Go apologists!
所以是的,我们绝对是Go的忠实拥护者!

 

以上是关于编程语言为什么我们更喜欢 Go 作为后端? Why we prefer Go for Backend的主要内容,如果未能解决你的问题,请参考以下文章

Go 语言“十诫”

使用 Go 和 ReactJS 构建聊天系统(前言)

「测试开发全栈化-Go」(1) Go语言基本了解

2018年Go语言用户调查 - Go成为开发人员最喜欢的编程语言

gRPC 101:在Python中运行Go代码

后端程序员学习Go语言的时机也许到了