面向Java开发人员的Go语言教程:java2go.dev

banq


关于 Go 和 Java 的介绍材料已经足够多了,这个教程目标是基于Java语言的基础并将一个人的原始感知重塑为不同的心智模型。

为什么选择Go?
答案很简单,只有一个词:复杂性。

我曾经是一名中高级 Java 开发人员,我喜欢复杂的解决方案,并经常吹嘘我曾参与并帮助构建的架构
简而言之,我曾经认为复杂性是一种荣誉勋章,而不是你应该与之抗争的东西。

Go 对稳定性和性能的关注使开发人员能够快速轻松地升级到最新版本的语言,而不必担心破坏他们的代码。

相比之下,Java 世界中的版本使用存在明显的碎片化。很少有系统在最新版本(撰写本文时为 Java 19)上运行,大多数系统分布在多个版本上,可追溯到 Java 6(截至 2022 年 12 月)。

将其与不断添加到语言中的新功能相结合,你会发现生态系统提供的内容与现有项目实际运行的内容存在很大的二分法。我有幸使用当时最新最好的 Java 版本从头开始创建项目。一开始感觉很强大,但是随着应用程序的大小和复杂性的增加,迁移到更新的 Java 版本可能会感觉像一个障碍,或者在某些情况下甚至是不可能的。除了少数例外,Go 很少出现这种情况。

由于多种原因,Go 已成为开发云基础设施的首选语言。

包结构
Go 项目应该遵循领域驱动设计(DDD),当一个项目变大时,按领域拆分是一种自然的方式来打包东西,就好像它们是自己独立的东西一样。这样,如果有需要,这些包中的每一个都可以成为自己的“东西”——服务、CLI 工具或跨多个其他项目共享的库。

Go 社区更喜欢的方法是按领域或用例分组。不是将所有模型组合在一起,而是可能有多个不同的包,例如registration, shipment,admin等,每个包可能包含其各自的数据模型、HTTP 处理程序、业务逻辑等,所有这些都包含在同一个包中并以隔离的形式出现其余的代码。

一个理想的 Go 包应该提供一组特定的功能或能力,可以被代码库的其他部分使用。它不应该简单地包含一组没有任何明确目的或功能的随机代码或函数。

因此,在命名 Go 包时,选择一个能够准确描述包提供内容的名称至关重要。这使其他开发人员可以轻松了解包的用途以及如何在自己的代码中使用它。避免使用不传达有关包功能的任何信息的模糊或通用名称。

函数和方法
Go 既支持普通的包级函数,也支持可以附加到任何类型的方法:

package foo

// A plain package-level function
func sayHello() {
    fmt.Println("Hello, World!")
}

// A type with a method
type Dog struct {}

func (d *Dog) Bark() {
    fmt.Println("bark")
}

在我看来,大多数具有 OOP 经验的开发人员都会过早地跳入将所有内容都转换为”类”,以便他们可以为其附加方法。我的建议是始终从包级函数开始,直到达到一定程度,将其转化为方法将被证明是更好的选择。

多个返回值 
Go 有一个内置的多个返回值的概念:

func foo() (int, int) {
    return 1, 2
}

// ...

a,b := foo()

这些类似于来自其他编程语言的元组,但更简单且更有限。

错误和Panic
与 Java、C# 或 Python 等流行语言不同,Go 具有明确的错误处理,因为该语言的设计者认为这种方法将使开发人员更容易编写正确、可靠的代码。在 Go 中,error类型是表示错误条件的内置接口类型。可以返回错误的函数使用以下语法:

func foo() error {
    // do something
    if (someErrorCondition) {
        return errors.New("some error message")
    }
    return nil
}

// Functions that return a result but might fail, use a multiple-value
// return style - result, error
func bar() (int, error) {
    // do something
    if (someErrorCondition) {
        return 0, errors.New("some error message")
    }
    return 42, nil
}

然后由函数调用者决定如何最恰当地处理错误。这导致我们在 Go 中有一个有争议的(很多人喜欢,很多人害怕)习语:

res, err := bar()
if err != nil {
    // handler the error. Most likely, add some context and return early
    return fmt.Errorf("bar: %w", err)
}

// do something with res here

这个if err != nil习语经常被新手认为是有争议的,因为有些人认为它过于冗长并且使 Go 代码更难阅读和理解。

当然,还有更大的阵营(包括我自己)认为显式错误处理是使 Go 代码如此健壮且易于维护的原因之一。

可读性优于性能 
Go 最初是(并且在很大程度上仍然是)一种系统编程语言。C 和 C++ 的一种精神继承者。因此,它继承了前人的许多低级构造和思想是很正常的。

简单而专注
Go 遵循 UNIX 的哲学“做一件事,把它做好”。

  •  TODO:解释 UNiX 命令的概念。

这是否意味着 Go 不适合构建大型单体?一点也不,但它需要一定的结构。

指针
您不可避免地会遇到的第一个问题是何时传递/返回值以及何时返回指向它的指针。Java 和 C# 通过将所有内容都作为对象引用来简化很多事情;因此,在大多数情况下,您不必进行心理跳跃——它只是引擎盖下的一个指针。
这个设计决定不是免费的——作为 Java 开发人员,您无疑知道有多少内存可以用于支持即使是最小的 Java 应用程序。运行时通过将所有内容存储在堆上来快速扩展必要的内存容量。由垃圾收集器定期清理未使用的引用,以防止应用程序耗尽硬件的可用 RAM。

更接近底层,Go 将这个决定权交给了你。
您可以在您从 Java 和 C# 了解的所有复杂场景中使用指针,但在适当的地方放弃传递和返回值(值副本)。
结果是两者之间的健康平衡,减少了应用程序工作所需的内存和垃圾收集器的负担。

数据库 
在 Go 中使用数据库时,实际上只有 3 个可行的选项:

  • db/sql使用标准库的包直接与数据库对话。可以非常非常样板化。
  • 使用基于反射的 ORM(ORM 在 Go 世界中是一个有点脏的词,但是,是的,其中一些存在)。
  • 使用代码生成让您的生活更轻松,并将生成的样板文件收起来。

并发任务调度
虽然有几个库提供此功能,但创建一个简单的任务调度程序是一个很好的练习,任何人都可以使用它来提高他们使用 Go 的并发原语和模式的技能。

// scheduleTask executes the given task at regular intervals (specified by interval) until the given timeout is reached.
// If a task takes longer than one interval, the next one starts only after the previous task has finished.
func scheduleTask(task func() error, interval time.Duration, timeout time.Duration) {
    ctx, cancelFn := context.WithCancel(context.Background())
    defer cancelFn()
    go func() {
        loop(ctx, task, interval)
    }()
    time.Sleep(timeout)
    cancelFn()
}

func loop(ctx context.Context, task func() error, interval time.Duration) {
    newJob := make(chan struct{}, 1)
    ticker := time.NewTicker(interval)
    defer ticker.Stop()
    for {
        newJob <- struct{}{}
        go func() {
            defer func() {
                <-newJob
                if r := recover(); r != nil {
                    log.Println("Recovered from a panicking job:", r)
                }
            }()
            log.Println("New job starting")
            task()
            log.Println("New job ending")
        }()
        select {
        case <-ticker.C:
        case <-ctx.Done():
            close(newJob)
            return
        }
    }
}

使用上面的函数变得非常简单,只需使用正确的间隔和超时调用函数,并传递要执行的适当操作:

func main() {
    var a = 0

    scheduleTask(func() error {
        time.Sleep(10 * time.Millisecond)
        // Simulate a random panic
        if a == 3 {
            a++
            panic("oops")
        }
        a++
        return nil
    }, time.Millisecond*100, time.Millisecond*1000)

    log.Println(a)
}

详细点击标题