diff options
| author | Eltrac <hi@guhub.cn> | 2025-11-24 12:03:38 +0800 |
|---|---|---|
| committer | Eltrac <hi@guhub.cn> | 2025-11-24 12:03:38 +0800 |
| commit | d82feefccc423c39008a2a7d0a764d5a0742c858 (patch) | |
| tree | 8fe61a6ccd1d2ae5382f6aefdfae93d1cc4fffed /content/_drafts | |
| parent | 638902527b4e26b6d5ca3d5e7256d56323b0dad6 (diff) | |
docs: update draft
Former-commit-id: 8fec4157243c89cbbf7523baffa825313ad88625
Diffstat (limited to 'content/_drafts')
| -rw-r--r-- | content/_drafts/go-philosophy.md | 208 |
1 files changed, 205 insertions, 3 deletions
diff --git a/content/_drafts/go-philosophy.md b/content/_drafts/go-philosophy.md index 0a1e6f58..3cbc732a 100644 --- a/content/_drafts/go-philosophy.md +++ b/content/_drafts/go-philosophy.md @@ -83,7 +83,7 @@ stat = NewStatistics(<-fmlChan, <-caskChan, pkgName, <-rvsChan) 编排这个词很有意思,它的原文是 orchestration,又名 choreography,意思分别是「编配管弦乐曲」和「编舞」。如果你理解概念,就会觉得这个描述非常生动形象。这个词在不同的语境下的意思有一些区别,但一般来说,是指自动化的协调管理、统一掌控和分工协作。在管弦乐(orchestration)中,各个演奏者各司其职,但由指挥家统一主导;在舞蹈(choreography)中,舞者之间需要相互沟通,分工协作。这引申到编程中,对应了不同的编程结构,下面用简单的例子来解释。 -假设有一项操作需要大量计算,这些计算被分散开,启动多个 `goroutines` 执行,最后由主线程将计算的结果整合起来,Go 语言代码可以这样写。 +假设有一项操作需要大量计算,这些计算被分散开,启动多个 `goroutines` 执行,最后由主线程将计算的结果整合起来,Go 语言代码可以这样写: ```go // 有 100 项作业需要执行,相应地有 100 项结果需要接收 @@ -109,8 +109,210 @@ for _ := range 100 { } ``` -这是一个典型的 Worker Pool。这个例子中,主线程就是指挥,而 Workers 就是乐队成员,整体上,这就是一种编排。 +这是一个典型的 Worker Pool。这个例子中,主线程就是指挥,而 Workers 就是乐队成员,整体上,这就是一种编排(orchestration)。 + +假设有三个作业(A、B、C),C 作业依赖 A 和 B 的结果,而 B 依赖 A 的结果,要让这三个作业相互配合完成工作,可以这样写: + +```go +a := make(chan int) +b := make(chan int) +c := make(chan int) + +// A +go func() { + result := doSomething() + a <- result +}() + +// B +go func() { + result := doSomething(<-a) + b <- result +}() + +// C +go func() { + result := doSomething(<-a, <-b) + c <- result +}() +``` + +三个作业各司其职,使用 `channel` 相互通信完成了工作,A、B 和 C 都可以被视作相互配合的舞者,这也是一种编排(choreography)。 + +Go 语言还提供 `select` 控制结构,用于监听和操作 `channel`。在线程等待多个操作返回结果的时候,可以使用 `select`,数据先从哪个管道过来,就先处理哪一个。 + +```go +// 例子来源:https://gobyexample.com/select +c1 := make(chan string) +c2 := make(chan string) + +go func() { + time.Sleep(1 * time.Second) + c1 <- "one" +}() +go func() { + time.Sleep(2 * time.Second) + c2 <- "two" +}() + +for range 2 { + select { + case msg1 := <-c1: + fmt.Println("received", msg1) + case msg2 := <-c2: + fmt.Println("received", msg2) + } +} +``` + +`select` 和 `channel` 为 Go 语言提供了十分强大的编排能力,允许程序员在大多数情况下高效处理业务,而不是机械地等待上一个操作完成再做下一个。当然,严格的串行也是存在的,通常是为了保证某一操作的**原子性**(Atomic),即不可被打断性,这个时候就要用到锁。 + +在 Go 中使用锁需要用到标准库 `sync`,一个常见的做法是把 `sync.Mutex` 放到使用它的结构体中。以下例子实现了一个计数器容器,一个 `Container` 中有多个计数器(`counter`),`inc`(自增)方法用于自增指定计数器。默认情况下 `++` 是一个非原子(non-atomic)操作,如果多个 `goroutine` 都在对同一个计数器自增,极有可能会相互影响——例如,A 读取到计数器的值是 5,自增后写回 6;B 在 A 写回之前也读取了计数器,值也是 5,也写回 6,这就覆盖了 A 的操作;两次自增的结果本应该是 7,但由于操作的非原子性,变成了错误的 6。 + +为了保证自增操作的原子性,可以在自增时加锁,不允许其他 `goroutine` 读写。 + +```go +// 例子来源:https://gobyexample.com/mutexes +type Container struct { + mu sync.Mutex + counters map[string]int +} + +func (c *Container) inc(name string) { + c.mu.Lock() + defer c.mu.Unlock() + c.counters[name]++ +} +// ... +``` + +注意 `defer c.mu.Unlock()` 这一行代码。`defer` 关键词表示这行代码会在其周边代码执行完毕之后被执行,用在 `Unlock()` 和 `Close()` 这样的操作前面,就可以把它们紧挨着 `Lock()` 和 `Open()`,防止忘记关锁。`defer` 还会在程序 `panic`(遇到无法处理的错误)时执行,因此 `defer` 也经常被用来恢复错误。 + +以上操作也可以直接用 Go 标准库提供的原子计数器,不需要自己封装。 + +```go +var ops atomic.Uint64 +ops.Add(1) +``` + +总而言之,`channel` 用于高效地编排程序,而 `mutex`(互斥锁)用于严格地保证串行。 + +## The bigger the interface, the weaker the abstraction. + +这句话直接翻译过来是:接口越大,抽象越弱。 + +熟悉面向对象编程的程序员应该不会对 `interface`(接口)感到陌生,它用于规定一个类应该实现什么方法,也让静态类型语言的类型系统变得更强大。例如,Java 中的 `List` 接口规定了列表应该实现 `add()` 等方法,同时也让底层实现不同的列表(`ArrayList`、`LinkedList`...)可以通用——如果一个方法要求传入一个 `List` 作为参数,无论传入 `ArrayList` 还是 `LinkedList` 都是可以接受的。 + +Go 语言鼓励程序员编写「小巧」的接口,就像这样: + +```go +type Writer interface { + Write() +} +``` + +只有一两个方法的接口看起来很没用,但语义非常明确:Writers write; Dancers dance; Eaters eat. 用 -er 后缀结尾的词命名接口几乎是 Go 语言的惯例。小巧的抽象是强大的,它能让程序员更灵活地使用 Go 语言的类型系统。 + +例如,这是一个日志接口: + +```go +type Logger interface { + Log() +} +``` + +在某个系统中,日志可能有三种输出方式:直接输出到终端、写入文件和发送 HTTP 请求给远程服务器。 + +```go +type StdoutLogger struct{} + +func (s StdoutLogger) Log(msg string) { + fmt.Println(msg) +} +``` + +```go +type FileLogger struct { + f *os.File +} + +func (fl FileLogger) Log(msg string) { + fmt.Fprintln(fl.f, msg) +} +``` + +```go +type HttpLogger struct { + endpoint string +} + +func (h HttpLogger) Log(msg string) { + http.Post(h.endpoint, "text/plain", strings.NewReader(msg)) +} +``` + +现在,我们正常编写业务逻辑,在处理某个数据的时候,突然有了记录日志的需求。由于我们定义了 `Logger` 接口,处理数据时不需要思考应该用哪种方式输出日志,只需要接收一个 `Logger`,直接 `Log()` 就好。调用这个函数时再传入具体的 `StdoutLogger` `FileLogger` 或者 `HttpLogger`。 + +```go +func Process(data string, logger Logger) { + logger.Log("processing: " + data) +} +``` + +短小的接口看起来没用,但接口完全可以像搭积木一样把方法堆叠起来,比如下面这个类型就既是 `Writer` 又是 `Closer` 还是 `Logger`。 + +```go +type FileWriter struct { + f *os.File +} + +func (*fw FileWriter) Write() { ... } +func (*fw FileWriter) Close() { ... } +func (*fw FileWriter) Log() { ... } +``` + +`interface` 实际上是对类型的「限制」,规定的方法越多,接口越大,要实现这个接口就越难,应用的场景也就越局限。Go 语言不是严格意义上的面向对象编程语言,它更关注**类型**,而短小的接口使得程序员可以更灵活地使用类型系统。 + +顺带一提,和面向对象编程语言不同,Go 语言不需要显式地指定实现哪些接口,只要包含了接口规定的方法的类型,都会被归为这个接口。 + +```java +// 在 Java 中实现接口 +class FileWriter implements Writer { + public void write() { ... } +} +``` + +```go +// 在 Go 中实现接口 +type FileWriter struct { ... } +func (*fw FileWriter) Write() { ... } +``` + +## Make the zero value useful. + +这句话直接翻译过来是:让零值变得有用。 + +Zero value 指的是变量被声明但尚未初始化(赋值),类型系统自动为变量填充的值。对字符串来说,这个值是 `""`,整数是 `0`,浮点数是 `0.0`,布尔值是 `false`,指针是 `nil`。结构体被声明时,其中的变量也会被填充零值。 + +Go 语言鼓励程序员好好利用这个零值,让类型即时没有初始化也能正常使用。标准库里有很多例子,前面提到的 `sync.Mutex` 就是一个。让我们回看这个例子: + +```go +// 例子来源:https://gobyexample.com/mutexes +type Container struct { + mu sync.Mutex + counters map[string]int +} + +func (c *Container) inc(name string) { + c.mu.Lock() + defer c.mu.Unlock() + c.counters[name]++ +} +// ... +``` + +显然,`inc()` 方法没有初始化 `c.mu` 这个 `sync.Mutex` 类型的变量,`c.mu` 里面装的是默认的零值,但零值仍然可以被正常使用。我觉得这句谚语更准确的说法应该是:Make the zero value us**able**. + -假设有三个作业(A、B、C),C 作业依赖 A 和 B 的结果,而 B 依赖 A 的结果, [^1]: C 语言里还有用信号量(Semaphore)实现互斥的方法,不过似乎已经没有什么人用了。 |
