第1章 为什么选择Go语言?
# 第1章 为什么选择Go语言?
在编程的过程中,你的程序可能执行过与输入/输出(I/O)相关的任务,比如创建和删除文件及目录。程序或许还能创建新进程、执行其他程序,甚至实现同一台计算机上的线程与进程之间,以及通过网络连接的不同计算机上的进程之间的通信。
当程序主要进行底层任务时,我们将其归类为系统编程。
有人说系统编程很枯燥,但我完全不这么认为!事实上,恰恰相反,它是一种令人愉悦且有趣的体验。这就像成为一名魔术师,你能够控制操作系统和硬件,实现其他语言难以达成的功能。
在本章中,我们将探讨为何Go语言非常适合构建高效、高性能的系统软件,以满足实际应用场景的需求。
本章将涵盖以下主要内容:
- 选择Go语言的原因
- 并发与goroutine
- 与操作系统交互
- 工具
- 使用Go进行跨平台开发
学习完本章后,你将了解Go语言在系统编程领域的地位、Go并发模型对构建高效高性能系统软件的重要性、Go语言与操作系统交互的方式、Go语言实现跨平台开发的方法,以及Go内置工具中的主要命令。
# 选择Go语言
如今,系统编程领域有众多编程语言:有些已十分成熟,如C和C++;有些是新兴语言,如Zig、Rust和Odin;还有些语言号称是“C/C++杀手”,宣称性能卓越。
当然,使用这些语言都能取得出色的成果。然而,我们可能会陷入一些潜在的困境,例如学习曲线陡峭、认知负担重、缺乏社区支持、应用程序编程接口(API)不一致且频繁变更,以及应用范围有限等问题。
Go语言的设计理念强调简单性、表达性、健壮性和高效性。它对并发的支持、强大的依赖管理,以及对组合的重视,使其成为系统编程的有力选择。Go语言的创造者致力于构建一种语言,提供强大的构建模块,同时避免不必要的复杂性,从而使编写、阅读、理解和维护系统级代码变得更加容易。有编程经验的人通常只需两周就能熟悉Go语言。虽然他们可能还算不上专家,但能够轻松阅读标准的Go代码,并编写基础到中等复杂度的程序。
此外,Go语言非常适合系统编程,因为其设计理念深受Unix思想的影响,充分体现了简单性。许多精通Python和Ruby的程序员常常转向学习Go语言,因为Go语言在保持他们编程表达能力的同时,还能提升性能并支持并发编程。
值得注意的是,Go语言的设计理念并不追求CPU使用成本为零,而是旨在减轻程序员的编程负担,这一点被认为更为重要,同时也让编程过程更加愉快。
使用Go语言进行系统编程的一个主要争议点在于垃圾回收器(Garbage Collector,GC),特别是它的暂停时间和明确的内存限制。如果你对此感到困扰,别担心。在第6章中,你会了解到从Go 1.20及更高版本开始,Go语言提供了更精细的内存管理功能。
![注意]
在垃圾回收器暂停的最坏情况下,“世界停止”(stop-the-world)时间通常不到100微秒。
# 并发与goroutine
Go语言最重要的特性之一是它的并发模型。并发是指能够同时运行多个任务的能力。在系统编程中,并行执行多个任务对于提高程序的性能和响应速度至关重要。
# 并发
实时系统对精度要求极高,并发是其中的关键因素。这些系统能够精确协调任务的执行时间,在一些场景中,即使是毫秒级的时间差异也至关重要。并发具有显著优势,它可以提高吞吐量(衡量系统在给定时间内能够处理的信息量),同时缩短任务完成时间。实际案例表明,并发能够提升系统的响应速度,使其更加灵活,任务执行效率更高。此外,并发的隔离能力可以防止干扰,确保数据的完整性。
系统编程涉及各种不同类型的任务,从CPU密集型任务到I/O密集型任务。并发机制能够协调这些不同类型的任务,使CPU密集型任务在I/O密集型任务等待资源时继续执行。在第10章讨论分布式系统时,并发的重要性将更加凸显。它能够在整个应用程序甚至网络中的不同节点间协调任务,非常适合管理大规模的并发操作。
# goroutine
Go语言的并发模型依赖于goroutine和通道(channel)。goroutine是轻量级的执行线程,常被称为绿色线程。创建goroutine的成本很低,与传统线程不同,它们的效率极高,仅需少量操作系统线程就能同时运行数千个goroutine。
另一方面,通道为goroutine提供了一种无需使用锁就能进行通信和同步的机制。这种方法受到通信顺序进程(Communicating Sequential Process,CSP)(https://www.cs.cmu.edu/~crary/819-f09/Hoare78.pdf (opens new window))形式化理论的启发,强调并发组件之间的协同交互。
与许多依赖外部库或线程构造来实现并发的编程语言不同,Go语言将并发功能无缝集成到其核心语言设计中。这一设计决策不仅使代码更易于理解,还减少了出错的可能性,因为线程相关的复杂性被抽象化了。
# 受CSP启发的模型
Go语言的并发模型受CSP的启发,CSP是一种用于描述并发系统的形式化语言。CSP专注于并发执行实体之间的通信和同步。与传统的多线程编程不同,CSP和Go语言都优先通过通道进行通信,而不是共享内存,从而降低了复杂性和潜在风险。同步和协调至关重要,CSP使用通道进行进程同步,Go语言则使用类似的通道来协调goroutine。安全性和隔离性是关键,这两种语言都通过通道确保安全交互,提高了可预测性和可靠性。Go语言的通道直接实现了CSP基于通信的方法,为goroutine提供了一种安全的数据交换方式,避免了共享内存和锁带来的问题 。
# 通过通信共享数据
Go语言中有一句著名的谚语:“不要通过共享内存来通信,而要通过通信来共享内存”,这句话常常引发讨论和误解。更准确地理解应该是“通过通信而不是加锁来共享数据”,这主要是因为主流编程语言通常依赖锁来保护共享数据,这可能会导致死锁和竞态条件等潜在问题。Go语言倡导一种不同的编程范式:通过通道发送和接收消息来共享数据。这种“通过通信共享数据”的理念减少了对显式锁的需求,促进了更安全的并发环境。
如果你喜欢函数式编程,那还有个好消息。在Go语言中,数据不会在goroutine之间隐式共享,换句话说,数据是被复制的。你是否关注过数据的不可变性?这与那些默认以共享内存作为线程间通信方式的语言形成鲜明对比。Go语言强调通过通道进行显式通信,有助于避免传统线程模型中可能出现的意外数据共享和竞态条件。这种模型的另一个好处是,不存在“回调地狱”,因为与并发代码的每次交互通常都以过程式的方式编写。普通的Go函数可以在过程式代码中使用,无需额外的关键字来修饰函数签名。
注意 “回调地狱”,也称为“厄运金字塔”,是编程术语,用于描述嵌套且相互依赖的回调函数使代码难以阅读、理解和维护的情况。这种情况通常发生在异步编程环境中,例如JavaScript,其中回调用于处理异步操作。 |
---|
在下一章中,我们将回顾并发及其基础概念,为你后续与操作系统接口的交互做好准备。
除了并发模型,Go语言还提供了一种与操作系统进行底层交互的方式。这对于系统编程至关重要,因为在系统编程中,你常常需要控制操作系统和硬件。
# 与操作系统交互
Go语言的系统调用设计旨在确保安全性和高效性,特别是在其并发模型的背景下。
与某些其他编程语言相比,Go语言中的系统调用相对更底层。如果你需要对系统资源进行精细控制,这会很有帮助,但也意味着你需要处理更多底层细节。
进行系统调用通常需要了解底层操作系统的API和约定。其副作用是,如果你是系统编程或底层开发的新手,可能会面临更陡峭的学习曲线。
如果不熟悉系统调用也不用担心!本书的第二部分将详细探讨和实践系统调用,涵盖系统编程过程中需要掌握的主要方面。
# 工具
Go语言就像一个工具箱,拥有构建优秀软件所需的一切工具。因此,我们无需其他工具,仅使用Go的标准工具就能创建程序。
下面我们来探索一下用于构建、测试、运行、错误检查和代码格式化的主要工具。
# go build
go build
命令用于将Go代码编译成可执行二进制文件,以便运行。来看一个示例。
假设你有一个名为main.go
的Go源文件,包含以下代码:
package main
import "fmt"
func main() {
fmt.Println("Hello, Go!")
}
2
3
4
5
6
7
你可以使用go build
命令进行编译:
go build main.go
这将生成一个名为main
的可执行二进制文件(在Windows系统上为main.exe
)。然后,你可以运行该二进制文件查看输出:
./main
# go test
go test
命令用于对Go代码进行测试。它会自动查找测试文件并运行相关的测试函数。
以下是一个示例。
假设你有一个名为math.go
的Go源文件,其中包含一个用于计算两个数之和的函数:
package math
func Add(a, b int) int {
return a + b
}
2
3
4
5
你可以创建一个名为math_test.go
的测试文件,为Add
函数编写测试代码:
package math
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Expected 5, but got %d", result)
}
}
2
3
4
5
6
7
8
9
10
使用go test
命令运行测试:
go test
# go run
go run
命令允许你直接运行Go代码,而无需将其显式编译成可执行文件。
我们通过一个示例来了解一下。
假设你有一个名为hello.go
的Go源文件,其中包含以下代码:
package main
import "fmt"
func main() {
fmt.Println("Hello, Go!")
}
2
3
4
5
6
7
你可以使用go run
命令直接运行代码:
go run hello.go
这将执行代码,并在控制台打印出“Hello, Go!”。
# go vet
我们使用go vet
命令来检查Go代码中潜在的错误或可疑结构。它使用的启发式方法不能确保所有报告的问题都是实际存在的问题,但它可以发现编译器未捕获的错误。
以下是一个示例。
假设你有一个名为error.go
的Go源文件,其中故意包含以下错误代码:
package main
import "fmt"
func main() {
movie_year := 1999
movie_title := "The Matrix"
fmt.Printf("In %s, %s was released.\n", movie_year, movie_title)
}
2
3
4
5
6
7
8
9
你可以使用go vet
命令检查错误:
go vet error.go
它可能会报告如下警告:Printf format %s has arg 1999 of wrong type int
。
# go fmt
go fmt
命令用于根据Go编程风格指南格式化你的Go代码。它会自动调整代码的缩进、间距等。
我们也来看一个示例。
假设你有一个名为unformatted.go
的Go源文件,其中的代码格式不正确:
package main
import "fmt"
func main() {
msg:="Hello"
fmt.Println(msg)
}
2
3
4
5
6
你可以使用go fmt
命令格式化代码:
go fmt unformatted.go
它会更新代码,使其符合标准的格式化约定:
package main
import "fmt"
func main() {
msg := "Hello"
fmt.Println(msg)
}
2
3
4
5
6
7
8
现在我们已经很好地掌握了这些基本工具,接下来可以开始熟悉Go的跨平台能力。
# 使用Go进行跨平台开发
使用Go进行跨平台开发非常轻松。你可以轻松编写在各种操作系统和架构上运行的代码。
使用Go进行跨平台开发可以通过设置GOOS
和GOARCH
环境变量来实现。GOOS
环境变量指定你要目标的操作系统(operating system,OS),GOARCH
环境变量指定目标架构。
例如,假设你有一个名为main.go
的Go源文件:
package main
import "fmt"
func main() {
fmt.Println("This program runs in any OS!")
}
2
3
4
5
6
7
要为Linux编译代码,你需要将GOOS
环境变量设置为linux
,将GOARCH
环境变量设置为amd64
:
GOOS=linux GOARCH=amd64 go build
这个命令会为Linux编译代码。
你还可以使用GOOS
和GOARCH
环境变量在不同平台上运行代码。例如,要在macOS上运行你为Linux编译的代码,你需要将GOOS
环境变量设置为darwin
,将GOARCH
环境变量设置为amd64
。
GOOS=darwin GOARCH=amd64 go run
这个命令会在macOS上运行代码。
注意 虽然Go致力于在各种平台上实现可移植性,但通过系统调用与操作系统交互本质上会使你的代码依赖于特定的操作系统特性。严重依赖这些操作的代码在针对不同平台时可能需要进行条件编译或调整。在Go中使用构建标志(build flags)可以让你根据特定条件(如目标操作系统或架构)选择性地编译代码的特定部分。 在创建与 golang.org/x/sys 包(用于Windows和类Unix系统)交互的程序时,这会很有用。 |
---|
假设你有两个Go源文件,分别名为main_windows.go
和main_linux.go
,并且你想使用构建标签(build tags)来确保代码的区分。
以下是一个使用构建标签为Windows区分代码的示例:
// go:build windows
package main
import "fmt"
func main() {
fmt.Println("This is Windows!")
}
2
3
4
5
6
7
8
我们也可以为Linux做同样的事情:
// go:build linux
package main
import "fmt"
func main() {
fmt.Println("This is Linux!")
}
2
3
4
5
6
7
8
分别使用以下命令来编译这些程序:
GOOS=windows go build -o app.exe
GOOS=linux go build -o app
2
当我们在Linux环境中执行app
时,它应该打印出“This is Linux!”。同时,在Windows系统上运行app.exe
将显示“This is Windows!”。
# 总结
本章全面介绍了Go为何是系统编程的首选语言,并深入讲解了Go的设计理念,强调了其简单性、健壮性和高效性。我们学习了Go的并发模型、与操作系统交互的方式,以及如何使用工具进行跨平台开发。这些知识有助于我们掌握用Go编写、阅读和维护系统级代码所需的技能,从而提高性能并具备处理并发的能力。
在下一章中,我们将探讨并发概念,复习所有相关概念和基础内容。这将为我们更深入地与操作系统接口交互做好准备,提升我们创建强大且响应迅速的程序的能力。