Go 最被低估的设计:两个方法,统一了一切 人们谈论 Go总是先说 goroutine 和 channel。但真正让 Go 标准库如此简洁一致的是io包里两个只有一个方法的接口。文章目录同一个模式四种场景两个接口定义了一切数据流为什么这比别的语言更简洁隐式接口Go 最有争议也最巧妙的设计标准库即接口教材几点建议同一个模式四种场景先看四段代码——读取文件f,_:os.Open(data.txt)data,_:io.ReadAll(f)解析 HTTP 请求体varuser User json.NewDecoder(r.Body).Decode(user)接收 HTTP 响应resp,_:http.Get(https://api.example.com)body,_:io.ReadAll(resp.Body)读取 TCP 连接conn,_:net.Dial(tcp,localhost:9999)data,_:io.ReadAll(conn)文件、HTTP Body、TCP 连接——三种完全不同的数据源同一套读取方式。这不是巧合。两个接口定义了一切数据流io包的源码里两个核心接口各只有一个方法typeReaderinterface{Read(p[]byte)(nint,errerror)}typeWriterinterface{Write(p[]byte)(nint,errerror)}没有继承层级没有泛型约束没有抽象基类。就是两个方法签名。但 Go 标准库里几乎所有跟读有关的东西都实现了io.Reader所有跟写有关的东西都实现了io.Writerio.Reader ┌──────────┬──────┼──────┬──────────┐ │ │ │ │ │ os.File strings. bytes. net.Conn http. Reader Reader Response. Body io.Writer ┌──────────┬──────┼──────┬──────────┐ │ │ │ │ │ os.File strings. bytes. net.Conn http. Builder Buffer Response. Writer一旦理解了这两个接口你就可以写出这样的函数funcprocess(r io.Reader,w io.Writer)error{data,_:io.ReadAll(r)result:transform(data)_,err:w.Write(result)returnerr}这个函数能处理4×416 种输入输出组合——文件到文件、网络到内存、字符串到 HTTP 响应、TCP 到 JSON Encoder……不必修改一行代码因为所有数据源对process来说都是io.Reader所有数据目标都是io.Writer。为什么这比别的语言更简洁Python 靠鸭子类型——任何有read()方法的对象都能当作文件用。问题是你没法从类型签名上判断一个参数到底是什么。阅读代码时你只能靠经验和文档猜测。JavaScript 有ReadableStream和WritableStream概念上等价于 Go 的 Reader 和 Writer。但它的 API 要复杂得多——pipeTo、pipeThrough、getReader、controller——方法多意味着心智负担重。Go 选择了一条不同的路接口只有一个方法——实现成本极低任何类型加一个Read([]byte)就融入整个标准库体系隐式实现——不需要写implements Reader编译器自动识别全标准库遵守——学会一套模式到处都能用fmt.Fprintf就是一个好例子。它接受io.Writer作为第一个参数fmt.Fprintf(os.Stdout,hello)// 写到终端fmt.Fprintf(file,hello)// 写到文件fmt.Fprintf(buf,hello)// 写到内存 bufferfmt.Fprintf(httpResponse,hello)// 写到 HTTP 响应记住一次到处使用。隐式接口Go 最有争议也最巧妙的设计Java 程序员写接口要先定义interface再声明class X implements Y。实现方必须显式引用调用方定义的接口产生编译期耦合。Go 反其道而行接口由使用方定义实现方不需要知道接口的存在。// 实现方只管写自己的方法typeMyReaderstruct{}func(r MyReader)Read(p[]byte)(int,error){returncopy(p,hello),nil}// 使用方定义自己需要的接口typereaderinterface{Read([]byte)(int,error)}funcconsume(r reader){...}MyReader的作者根本不知道世界上有一个叫io.Reader的接口——他只是在 struct 上加了一个Read方法。然后这个类型就能被任何接受io.Reader的函数使用。这种解耦方式有一个优美的实践结果http.Handler接口只有一个ServeHTTP方法。你定义一个 struct写一个ServeHTTP方法不需要 import 任何东西它就是合法的 HTTP 处理器。框架不认识你但你满足它的接口它就接受你。这就是“Don’t design with interfaces, discover them”的含义——不为未来的抽象写接口在真正需要多态的地方才定义接口。标准库即接口教材换个角度看 Go 标准库——六个包六个领域同一套接口哲学包核心接口设计启示ioReader, Writer最好的接口只有一个方法fmtWriterFprint 系一个接口可以对接无数输出目标encoding/jsonReader, WriterDecoder/Encoder接口组合自然产生新能力net/httpHandlerServeHTTP隐式实现让框架和业务零耦合netConnReader Writer组合接口比类继承灵活太多bufioReader包装 Reader 加缓冲装饰器模式就是接口加组合标准库不是 API 参考手册是一本接口设计教材。学完一个包的接口风格下一个包的 API 你大概能猜到——好的设计会传染。几点建议先啃下io.Reader和io.Writer再学其他。它们是 Go 标准库的元接口。理解了它们后面学os、json、http、bufio都会有果然如此的感觉——不是在记新 API而是在看同一个模式的不同应用。写代码时故意换数据源。同一个处理函数先用文件测再用字符串测再用 HTTP Body 测。反复验证接口不关心具体类型直到它变成本能。别抱怨 Go 没有泛型方法、没有继承、没有 try-catch。当一段三百行的代码里反复出现的模式只有两个接口时你会意识到——少不是缺失是刻意。两个方法一个理念。Go 的简洁不是简陋是把正确的事情做对然后到此为止。

本月热点