图难于其易,为大于其细

Posted on Fri 23 May 2025 in Journal

Abstract Journal on 2025-05-23
Authors Walter Fan
 Category    learning note  
Status v1.0
Updated 2025-05-23
License CC-BY-NC-ND 4.0

图难于其易,为大于其细

做软件开发的大家都知道,不要把系统做得太复杂,否则会很难维护,也会很难扩展。可是需求不断在加,业务逻辑不断在变,还有许多意想不到的特别用例(corner cases)、始料未及的异常情况。想保持简单太难了,无异于天方夜谭。

那怎么办呢?任由代码一天天地臃肿下去,软件一天天烂下去,逻辑一天天地复杂下去,补丁越来越多,bug 越来越密?

我以前写过一本书《微服务之道:度量驱动开发》,其中引用了老子的一段话:

老子曰:“图难于其易,为大于其细。天下难事必作于易,天下大事必作于细。是以圣人终不为大,故能成其大。”

圣人之言不谬也。任何一个复杂庞大的系统,莫不是由一个个简单微小的模块逐渐构建起来的。万丈高楼平地起。

为了不在复杂的大系统中迷失,我们不如不做单个的大系统,只做简单的微服务,然后由这些微服务共同协作,满足用户不同的需求。

所以我们所做的软件系统应该使用微服务,而我们认为的微服务之道如下:

  • 不要大,要小。
  • 不要复杂,要简单。
  • 不要松散,要紧凑。
  • 不要过于依赖别人,要独立自主。
  • 不要只为自己着想,要为他人服务。

虽然这番话原本是说微服务,其实也是整个软件工程中最朴素、最基本的原理。


从 Go Kata 到“写代码如呼吸”

有段时间我想学 Go 语言,找了一本书,囫囵吞枣地翻了一遍,又在 GitHub 上找了一个星星最多的项目研究了一番——说实话,那时候的我,看得懂语法,却写不出代码。你说不会吧?也不是;你说会吧?做个项目又没什么信心,还是愿意回去写熟悉的 C++ 或 Java。

直到有一天,我动手建立了一个叫 kata-go 的 GitHub 项目,专门练手,从最简单的例子开始,一步一步地写,把这些例子组织成一个个“套路库”(kata),才终于慢慢上手了,也可以做一些正式的 Go 项目开发了。

我的经验是:想掌握一样东西,最好的方式不是死啃厚书,而是把复杂问题拆成一个个简单问题,通过小例子构建信心,通过套路积累熟练度。

举个例子,下面是我写的一个最简单的 Go HTTP 服务的 kata:

package main

import (
    "fmt"
    "net/http"
)

func hello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Hello, world!")
}

func main() {
    http.HandleFunc("/", hello)
    http.ListenAndServe(":8080", nil)
}

就是这么简单。甚至不用框架、不用模板,直接裸奔也行。

关键在于:你能不能把“写服务”这件事,简化成一个你可以每天写两遍也不会烦的操作?


复杂,是从“差不多能跑”开始的

复杂不是一开始就有的,而是悄悄地、逐渐地长出来的。

最初你写了个 MVP,“差不多能跑”; 后来 PM 说:“我们这边要加个小逻辑,很简单啦~”; 再后来,“你这个逻辑如果能支持多租户就更好了”; 再后来,“这块业务做成通用组件吧,别的项目也要用”; …… 等你回过神来,已经是“你已经被安排得明明白白”的状态了。

代码变复杂,其实并不是谁的错,而是我们在每一个“可以不复杂”的时刻,都选择了“将就一下”

所以,保持简单不是一种技术能力,而是一种长期的“反人性”选择。


简单,不是少写代码,而是写好代码

很多人理解“简单”就是“代码少”,这其实是个误区。

简单的代码不一定短,但它一定清晰、一致、有意图。 反过来,晦涩的 one-liner,虽然“短小精悍”,其实常常是维护地狱。

比如:

// 可读性差
if err := doSomething(); err != nil { log.Fatal(err) }

// 可读性好
err := doSomething()
if err != nil {
    log.Fatalf("failed to do something: %v", err)
}

是的,后者更啰嗦一些,但也更有上下文,更容易阅读和扩展。

在追求“简单”的道路上,清晰和一致性永远排在代码行数前面。


有些“简单写法”,其实藏着大坑

Go 中还有个经典的例子,说明“看起来简单优雅”的写法,有时候可能是灾难的开始:

for _, item := range items {
    go func() {
        process(item)
    }()
}

很多人初学时都写过这样并发处理的代码,看起来干净利落:一行 goroutine,多么优雅!

但运行时你会发现,输出全是最后一个 item。

这是因为闭包捕获了 item 的地址,而 item 是循环变量,在 goroutine 真正执行时,早就变成别的值了。

正确的写法应该是:

for _, item := range items {
    item := item // 创建局部副本
    go func() {
        process(item)
    }()
}

或者传参:

for _, item := range items {
    go func(it string) {
        process(it)
    }(item)
}

真正简单的代码,不光是写起来爽,还要运行正确、易于理解、不踩坑。


写在最后:保持简单,是一种修行

“简单之美”听起来像句口号,做起来却像修行。

它意味着你要敢于说“不”:不为了“看起来高级”去堆砌设计模式,不为了“未来可能的扩展”去预支复杂度,不为了炫技而放弃可维护性。

它意味着你要不断问自己:

  • “这段代码,还有没有办法写得更直白一点?”
  • “这个模块,能不能再拆小一点?”
  • “这个问题,我们能不能通过流程优化而不是技术补丁来解决?”

就像健身不是为了炫腹肌,而是为了保持健康,写简单的代码,也不是为了让别人夸“优雅”,而是为了自己和团队能活得长久

愿我们都能在纷繁复杂的技术世界中,守住心中的“简单之美”。


如果你也曾在代码里迷路,不妨从明天起,每天写一个 kata,一行一行,写回初心。

写代码如写诗,删繁就简,字字有力。

愿我们都能在纷繁复杂的技术世界中,守住那一点点简单的初心。


本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。