- 作者:老汪软件技巧
- 发表时间:2024-11-27 17:05
- 浏览量:
在你编程之路的某个阶段,你的程序可能已经涉及到一些与I/O相关的任务,比如创建和删除文件和目录,可能还包括协调新进程的创建、执行其他程序,甚至促进同一计算机上线程和进程之间、以及通过网络连接的不同计算机之间的通信。
当我们的程序聚焦于使用一组低级任务时,我们将其归类为系统编程。
有人认为系统编程是枯燥乏味的,但我完全不同意!事实上,情况恰恰相反——它是一种愉快而充满乐趣的体验,就像一个魔术师。你可以控制操作系统和硬件,做出在其他语言中无法实现的事情。
在本章中,我们将讨论为什么Go语言非常适合用于构建高效且高性能的系统软件,以支持现实世界中的应用场景。
本章将涵盖以下主要内容:
通过本章的学习,你将了解Go在系统编程生态中的位置,Go的并发模型在构建高效和高性能系统软件中的重要性,Go与操作系统交互的方式,Go的跨平台开发方法,以及Go内建工具中的主要命令。
选择Go语言
如今,系统编程领域有许多编程语言:一些是历史悠久的,如C和C++;一些是新兴的,如Zig、Rust和Odin;还有一些声称自己是“C/C++的杀手”,并以其卓越的性能为卖点。
当然,我们可以使用这些语言中的任何一种,取得出色的结果。然而,我们也可能会陷入一些隐性的陷阱,例如陡峭的学习曲线、高认知负担、缺乏社区支持、不一致的API和频繁的破坏性更新、以及采用率低等问题。
Go语言的设计理念强调简洁、表达力、稳健性和效率。Go对并发的支持、强大的依赖管理以及注重组合的特性,使其成为系统编程的一个有力选择。Go的创造者旨在构建一种语言,提供强大的构建块而不增加不必要的复杂性,这使得编写、阅读、理解和维护系统级代码变得更加容易。具有编程经验的人通常需要两周时间来熟悉Go,虽然他们可能还不能算是专家,但他们可以自信地阅读标准Go代码,并编写从基础到中等复杂度的程序,而不感到困难。
此外,Go非常适合系统编程,因为它具有类Unix的设计理念,符合简洁性要求。许多精通Python和Ruby的程序员通常会转向Go,因为它让他们在保持语言表达力的同时,能够获得更高的性能并具备并发编程的能力。
值得注意的是,Go的设计哲学并不优先考虑CPU使用的零成本,而是着重于减少程序员的工作量,认为这是更为重要的目标,作为副产品,它也使得编程体验变得更加愉悦。
使用Go进行系统编程的一个主要批评点是垃圾回收器(GC),特别是它的暂停和显式的内存限制。如果你对Go的这一特性仍然心存不满,不必担心。在第6章中,我们将看到,从Go 1.20及以后版本开始,Go提供了更精细的内存管理。
注意:
在GC暂停的最坏情况下,"停止世界"的时间通常不到100微秒。
并发与Goroutines
Go语言最重要的特性之一就是其并发模型。并发是指能够同时运行多个任务。在系统编程中,执行多个任务并行是提升程序性能和响应能力的关键。
并发
实时系统要求高精度,其中并发是一个至关重要的因素。这些系统需要协调任务的执行,尤其是在毫秒级别的时间差也至关重要的场景下。并发通过提高吞吐量(即系统在一定时间内能够处理的信息单位数量)并减少任务完成时间,提供了显著的优势。现实生活中的实例展示了并发如何提高系统响应能力,使得系统更具灵活性,任务更为高效。此外,并发的隔离能力通过防止干扰来保证数据完整性。
系统编程涉及多种类型的任务,从CPU密集型到I/O密集型。并发通过允许CPU密集型任务在等待I/O资源的同时继续进行,协调了这种任务的多样性。
稍后在第10章中,我们将讨论分布式系统,并发的重要性将会更加突出。它协调了跨应用程序甚至不同节点的任务,非常适合处理大规模并发。
Goroutines
Go的并发模型依赖于goroutines和通道。Goroutines是轻量级的执行线程,通常被称为绿色线程。创建它们的成本很低。与传统线程不同,goroutines表现出卓越的效率,使得数千个goroutines可以在少量操作系统线程上同时运行。
另一方面,通道提供了一种机制,让goroutines可以在不依赖锁的情况下进行通信和同步。这种方法借鉴了Communicating Sequential Process (CSP) (~crary/819-…)形式化理论,强调并发组件之间的协调互动。
与许多其他编程语言依赖外部库或线程构造来实现并发不同,Go将并发无缝地融入其核心语言设计中。这一设计决策使得代码不仅更容易理解,也减少了错误的可能性,因为线程的复杂性被抽象化。
受CSP启发的模型
Go的并发模型受CSP的启发,CSP是一种描述并发系统的形式语言。CSP专注于并发执行实体之间的通信和同步。与传统的多线程编程不同,CSP和Go通过通道进行通信,而不是使用共享内存,从而降低了复杂性和潜在的危险。同步和协调至关重要,CSP使用通道进行进程同步,而Go使用类似的通道来协调goroutines。安全性和隔离性是关键,因为这两种语言都通过通道确保安全的交互,从而增强了可预测性和可靠性。Go的通道直接实现了CSP的基于通信的方式,为goroutines提供了一个安全的交换数据的途径,避免了共享内存和锁的陷阱。
通过通信共享
Go的名言“不要通过共享内存来通信,应该通过通信来共享内存”常常成为讨论和误解的源头。其实,更精确的理解应该是:“通过通信来共享,而不是通过锁定”,特别是因为主流编程语言通常依赖锁来保护共享数据,从而导致潜在的问题,比如死锁和竞态条件。Go提倡一种不同的范式:通过通道发送和接收消息来共享数据。这种“通过通信共享”的理念减少了显式锁的需求,并促进了更安全的并发环境。
如果你对函数式编程感兴趣,那么我有个好消息。在Go中,数据不会隐式地在goroutines之间共享。换句话说,数据是被复制的。你可能会担心数据的不可变性吗?这与那些默认使用共享内存作为线程间通信模式的语言形成了鲜明对比。Go通过强调通过通道显式通信,有效避免了传统线程模型中可能出现的意外数据共享和竞态条件。这个模型的另一个好处是,没有回调地狱,因为与并发代码的每次交互通常是以过程化的方式来读取的。一个普通的Go函数可以在过程化代码中使用,而不需要附加额外的关键字来绑定签名。
注意:
回调地狱,也称为“恐怖的金字塔”,是编程中的一个术语,描述了嵌套和相互依赖的回调函数使得代码变得难以阅读、理解和维护的情况。通常出现在异步编程环境中,如JavaScript,其中回调用于处理异步操作。
在下一章,我们将刷新并发的所有概念及其构建模块,为与操作系统接口的交互做好准备。
除了并发模型,Go还提供了与操作系统低级别交互的方式。这对于系统编程至关重要,因为你经常需要控制操作系统和硬件。
与操作系统的交互
Go语言在系统调用方面的处理方式,旨在保证安全和高效,特别是在其并发模型的背景下。
在Go中,系统调用相较于某些其他编程语言来说,更接近底层。如果你需要对系统资源进行精细控制,这种特性非常有用,但它也意味着你需要处理更多的底层细节。
进行系统调用通常需要理解底层操作系统的API和约定。这一副作用是,如果你是系统编程或低级开发的新手,可能会面临更陡峭的学习曲线。
不熟悉系统调用?别担心!本书的第二部分将详细探讨和实验系统调用,涵盖我们在系统编程过程中需要掌握的主要内容。
工具
Go就像一个工具箱,提供了构建优秀软件所需的一切,因此我们不需要额外的工具,只需使用其标准工具即可创建程序。
让我们来探索一些主要的工具,这些工具有助于构建、测试、运行、错误检查和代码格式化。
go build
go build命令用于将Go代码编译成可执行的二进制文件,之后你可以运行该二进制文件。
来看一个例子:
假设你有一个名为main.go的Go源文件,其中包含以下代码:
package main
import "fmt"
func main() {
fmt.Println("Hello, Go!")
}
你可以使用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
}
你可以创建一个名为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)
}
}
然后,你可以使用go test命令来运行这些测试:
go test
go run
go run命令允许你直接运行Go代码,而不需要显式地将其编译成可执行文件。
来看这个例子:
假设你有一个名为hello.go的Go源文件,其中包含以下代码:
package main
import "fmt"
func main() {
fmt.Println("Hello, Go!")
}
你可以使用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)
}
你可以使用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)
}
你可以使用go fmt命令格式化代码:
go fmt unformatted.go
它会更新代码,使其符合标准的格式化约定:
package main
import "fmt"
func main() {
msg := "Hello"
fmt.Println(msg)
}
现在,我们已经掌握了基本工具的使用,可以开始了解Go的跨平台能力。
跨平台开发与Go
使用Go进行跨平台开发非常简单。你可以轻松地编写在多种操作系统和架构上运行的代码。
Go的跨平台开发可以通过使用GOOS和GOARCH环境变量来实现。GOOS环境变量指定你想要的目标操作系统,而GOARCH环境变量指定目标架构。
例如,假设你有一个名为main.go的Go源文件:
package main
import "fmt"
func main() {
fmt.Println("This program runs in any OS!")
}
为了将代码编译为Linux平台上的应用,你需要将GOOS环境变量设置为linux,将GOARCH环境变量设置为amd64:
GOOS=linux GOARCH=amd64 go build
此命令将编译代码以供Linux使用。
你还可以使用GOOS和GOARCH环境变量在不同的平台上运行代码。例如,假设你已为Linux编译了代码,现在你想在macOS上运行它,你可以将GOOS环境变量设置为darwin,将GOARCH环境变量设置为amd64:
GOOS=darwin GOARCH=amd64 go run
此命令将在macOS上运行代码。
注意
虽然Go努力在不同平台间实现可移植性,但通过系统调用与操作系统交互本质上会将你的代码与特定的操作系统特性绑定。大量依赖这些操作的代码可能需要在面向不同平台时进行条件编译或调整。
利用Go中的构建标志,你可以根据特定条件(如目标操作系统或架构)有选择性地编译代码的特定部分。这在创建与/x/sys包交互的程序时特别有用,特别是当目标是Windows和类Unix系统时。
假设你有两个Go源文件,分别命名为main_windows.go和main_linux.go,并且你想通过构建标签来确保代码的分割。
以下是一个使用构建标签分割Windows平台代码的例子:
// go:build windows
package main
import "fmt"
func main() {
fmt.Println("This is Windows!")
}
同样的,我们可以为Linux平台编写类似的代码:
// go:build linux
package main
import "fmt"
func main() {
fmt.Println("This is Linux!")
}
你可以使用以下命令来分别编译这些程序:
GOOS=windows go build -o app.exe
GOOS=linux go build -o app
当我们在Linux环境中执行app时,它应该打印This is Linux!。同时,在Windows系统中运行app.exe将显示This is Windows!。
总结
本章提供了关于为什么Go是系统编程的最佳选择的全面指南,并深入探讨了Go的设计哲学,强调了简洁性、稳健性和效率。我们了解了Go的并发模型、与操作系统交互的方式,以及如何使用工具进行跨平台开发。这些内容为我们提供了必要的知识,使我们能够使用Go编写、阅读和维护系统级代码,从而提升性能并有效处理并发任务。
在下一章中,我们将进一步探讨并发的相关概念,复习所有相关的基础知识和构建模块,为与操作系统接口的更高级互动做好准备,从而增强我们创建强大且响应迅速的程序的能力。