Java 开发人员眼中的 Golang——优点和缺点
我喜欢的东西
我不喜欢的事情
结论
最近我不得不学习 Go 编程语言,现在我想从 Java 开发人员的角度分享我的想法。
我们决定为 DevOpsBox ( https://www.devopsbox.io/ ) 平台创建 Kubernetes Operator(更多理由请阅读:https://dev.to/mraszplewicz/my-perfect-aws-and-kubernetes-role-based-access-control-and-the-reality-48fb)。事实证明,用 Golang 创建它们最简单——它有 Kubebuilder 和 Operator SDK 框架。唯一的问题是我们不懂 Golang,所以我不得不学习一门新的语言……
我会先从我喜欢的开始,然后再转向我不喜欢的。我会尽量专注于语言本身。
我喜欢的东西
易于学习
Golang 的学习过程令人惊叹。Go 语言导览 ( https://tour.golang.org/ ) 几乎涵盖了该语言的方方面面,其语言规范 ( https://golang.org/ref/spec ) 也相当简短易读。虽然有一些不太容易理解的特性,比如通道 (channel),但它们功能强大,而且存在是有原因的。
在 5.0 版本之前,Java 也没有这么多特性,但从来没有像 Golang 现在这么简单。
快速的开发人员反馈循环
我使用术语“开发人员反馈循环”是指从启动程序到看到其结果的时间。
有时,您可能希望等待很长时间才能启动用编译型语言编写的程序。Gomain
或单元测试在 IDE 中运行时几乎可以立即启动,因此反馈循环可以非常短。其结果与其他现代编程语言类似,即使 Go 是静态编译的,您也无需等待很长时间才能完成编译过程。
有趣的是,如今我们必须构建用设计上非静态编译的语言(例如 JavaScript)编写的程序,有时还要等待构建过程完成。
静态类型检查
Go 有非常优秀的静态类型检查系统。你甚至不需要为每个变量声明类型,只需这样写:
str := "Hello"
并且它知道变量 str 是字符串类型。当变量是函数调用的结果时,情况也是如此:
result := someFunction()
隐式接口实现
“如果它走起来像鸭子,叫起来也像鸭子,那它一定是鸭子。”
这意味着你不必显式声明你正在实现一个接口。在 Go 中,你可以这样写:
type Duck interface {
Quack()
}
type Mallard struct {
}
func(mallard *Mallard) Quack() {
}
野鸭之所以被称为鸭子,是因为它会嘎嘎叫!你可以这样写:
func main() {
var duck Duck
var mallard *Mallard
mallard = &Mallard{}
duck = mallard
duck.Quack()
}
在 Go 中,您甚至可以为现有的外部代码创建自己的接口。
多个返回值
这个很简单——你可以从一个函数返回多个值:
func Pair() (x, y int) {
return 1, 2
}
在我所知道的大多数语言中,您都必须创建一个类或结构来实现类似的功能。
但此功能还用于从函数返回错误 - 有关更多信息,请参阅“我不喜欢的事情”。
函数值
函数可以作为参数传递、分配给变量等:
func doJob(convert func(int) string) {
i := 1
fmt.Print(convert(i))
}
func main() {
convert := func(intVal int) string {
return strconv.Itoa(intVal)
}
doJob(convert)
}
它在某种程度上类似于 Java 中的方法引用或 lambda 表达式,但更“内置”于语言中。
模块
Go 有一个非常好的内置依赖管理系统。你不需要 Gradle 或 Maven 来下载依赖项,只需使用 Go 模块即可。
单元测试
单元测试支持是语言本身的一部分。您只需创建一个以“_test.go”为后缀的文件并编写测试即可。例如,对于 hello.go:
package hello
func sayHello() string {
return "Hello world!"
}
创建 hello_test.go 文件:
package hello
import "testing"
func TestSayHello(t *testing.T) {
greetings := sayHello()
if greetings != "Hello world!" {
t.Errorf("Greeting is different than expected!")
}
}
您可以从 IDE 或 CI/CD 管道运行它。
如果你想用 Golang 写出好的测试,你或许应该写“表驱动测试”。一篇关于这个主题的好文章:https://dave.cheney.net/2019/05/07/prefer-table-driven-tests
推迟
Go 提供了类似 Javafinally
关键字的功能,但在我看来,它更强大、更简单。如果你想在函数返回时运行一些代码,例如清理一些资源,可以使用以下defer
关键字:
func writeHello() {
file, err := ioutil.TempFile(os.TempDir(), "hello-file")
if err != nil {
panic(err)
}
defer os.Remove(file.Name())
file.WriteString("Hello world!")
}
这里我们创建一个临时文件,该文件将在函数执行结束时(写入“Hello world!”之后)被删除。
单二进制
Go 以生成单一二进制文件而闻名。构建程序时,您将获得一个包含所有依赖项的可执行文件。当然,您必须为每个目标平台准备一个单独的二进制文件,但与其他语言相比,程序的分发更简单。这也是 Go 经常用于创建命令行实用程序的原因之一。
眼镜蛇图书馆
第二个原因是https://github.com/spf13/cobra……它是一个非常有用的库,可以帮助编写命令行工具。在 DevOpsBox 中创建了我们的 Operator 之后,我们也希望拥有自己的 CLI,很高兴能使用 Cobra 来编写它们。
我不喜欢的事情
没有什么是完美的,Go 也不例外,它有一些我不太喜欢的功能......
错误处理
我真的不喜欢 Go 惯用的错误处理方式。主要有以下几个原因:
错误处理使代码的可读性降低
在 Go 程序中,你经常会看到这样的事情:
func doJob() ([]string, error) {
result1, err := doFirstTask()
if err != nil {
log.Error(err, "Error while doing the first task")
return nil, err
}
result2, err := doSecondTask()
if err != nil {
log.Error(err, "Error while doing the second task")
return nil, err
}
result3, err := doThirdTask()
if err != nil {
log.Error(err, "Error while doing the third task")
return nil, err
}
return []string{
result1,
result2,
result3,
}, nil
}
我认为错误处理在这里会造成很多干扰。如果没有它,这个函数会像这样:
func doJob() []string {
result1 := doFirstTask()
result2 := doSecondTask()
result3 := doThirdTask()
return []string {
result1,
result2,
result3,
}
}
Java 也存在一些问题,例如受检异常(checked exception),它会带来很多不必要的干扰。值得庆幸的是,像 Spring Framework 这样的框架可以将受检异常包装到运行时异常中,这样你就可以只捕获那些你期望的异常。
你可以忘记处理错误
考虑以下代码:
func doJob() ([]string, error) {
result1, err := doFirstTask()
if err != nil {
log.Error(err, "Error while doing the first task")
return nil, err
}
result2, err := doSecondTask()
result3, err := doThirdTask()
if err != nil {
log.Error(err, "Error while doing the third task")
return nil, err
}
return []string {
result1,
result2,
result3,
}, nil
}
怎么了?我忘了处理错误了!在稍微复杂一点的情况下,代码审查时可能会漏掉它。
在其他编程语言中,您可以拥有集中的异常处理和堆栈跟踪以用于调试目的。
你没有堆栈跟踪
我读过一些关于 Golang 中错误处理的文章,有人认为堆栈跟踪“难以阅读,难以理解”,但我已经习惯了它们,对我来说,借助堆栈跟踪很容易找到问题。
重构的问题
IDE 在重构 Golang 代码时会遇到一些问题,例如提取方法或变量时。我认为这些问题与惯用的错误处理有关。
有时太简短
有时候我觉得 Golang 太简洁了。为什么有像func
、 not 这样的关键字?为什么orfunction
中不必使用括号?为什么没有像 这样的关键字,而必须声明为大写?不过这可能只是我个人的看法……if
for
public
有时不一致
Go 不支持函数/方法重载(https://golang.org/doc/faq#overloading)。我对此表示理解,因为很多编程语言都没有重载功能。但是,为什么 Go 内置make
函数会有这么多变体呢?查看文档:
Call Type T Result
make(T, n) slice slice of type T with length n and capacity n
make(T, n, m) slice slice of type T with length n and capacity m
make(T) map map of type T
make(T, n) map map of type T with initial space for approximately n elements
make(T) channel unbuffered channel of type T
make(T, n) channel buffered channel of type T, buffer size n
难以记住的功能
任何编程语言大概都是这样,但 Go 非常简单,我以为我能轻松记住所有关键字和内置函数的使用方法。可惜,事实并非如此。每天用 Go 语言一年之后,我仍然需要查阅文档或查看示例来了解如何使用通道,或者如何使用make
。也许我脑子里装了太多不同的编程语言……
没有等效的流式 API
其他语言中有一些特性允许你使用声明式语法与集合交互,比如 Java 中的流式 API。另一方面,在 Golang 中你找不到任何类似的功能(或者也许有一些库?),而且使用for
循环是惯用的。循环本身并没有那么糟糕,但我喜欢流式 API,并且已经习惯了它。
IDE 支持不太好
我很喜欢 JetBrains 的产品,并且使用 GoLand 编写 Go 代码。我已经提到过重构的问题,这里有一个例子。如果你想从这段代码中提取一个方法:
func doJob() ([]string, error) {
result1, err := doFirstTask()
if err != nil {
log.Error(err, "Error while doing the first task")
return nil, err
}
result2, err := doSecondTask()
if err != nil {
log.Error(err, "Error while doing the second task")
return nil, err
}
result3, err := doThirdTask()
if err != nil {
log.Error(err, "Error while doing the third task")
return nil, err
}
return []string {
result1,
result2,
result3,
}, nil
}
GoLand 会这样做:
func doJob() ([]string, error) {
result1, err := doFirstTask()
if err != nil {
log.Error(err, "Error while doing the first task")
return nil, err
}
result2, result3, strings, err2 := doThirdAndSecond(err)
if err2 != nil {
return strings, err2
}
return []string{
result1,
result2,
result3,
}, nil
}
func doThirdAndSecond(err error) (string, string, []string, error) {
result2, err := doSecondTask()
if err != nil {
log.Error(err, "Error while doing the second task")
return "", "", nil, err
}
result3, err := doThirdTask()
if err != nil {
log.Error(err, "Error while doing the third task")
return "", "", nil, err
}
return result2, result3, nil, nil
}
这还不算太糟,但需要额外的手动修复。IntelliJ 在 Java 方面做得更好!
结论
Golang 虽然有很多优点,但缺点也很明显,因此我对这门语言的感受很复杂。我可以说我喜欢它,但它可能永远不会成为我的最爱。那么,究竟是哪门语言呢?C#,不过那是完全不同的故事……我认为,喜欢哪种编程语言是个人问题,而且这都是好事!
文章来源:https://dev.to/mraszplewicz/golang-through-the-eyes-of-a-java-developer-pros-and-cons-25o2