源码学啥子嘛?接口、组合
大家好,我叫谢伟,是一名程序员。
今天的主题:面向接口、组合编程。
作为程序员,都希望编写通用、可扩展的代码,通常这些知识靠的都是依靠设计模式进行指导开发。比如说面向对象的特性:封装、抽象、多态、继承。
要编写更通用的代码,一方面需要靠足够时间砸出来,一方面也需要自己实践摸索。编写代码过程中要时刻在脑中形成清单:
- 编写可读的代码
- 编写符合设计模式的代码
在 Go 中如何编写更通用的代码?
一是接口,二是组合。
Go 中没有继承的概念,摒除了”继承“可能导致层级过多的弊端,转而推荐使用组合的形式,达到”继承“的效果。
举个简单的示例:
type Languager interface { Can(string) string } type Someone struct { Language string } func (s Someone) Can(name string) string { return fmt.Sprintf("%s can program with %s", name, s.Language) } func Program(L Languager, name string) { log.Println(L.Can(name)) } func main() { b := Someone{Language: "go"} Program(b, "谢谢") } >>2019/12/26 11:10:55 谢谢 can program with go 复制代码
定义了一个接口:Languager 具备 Can 这个方法, Someone 结构体存在 Can 这个方法(参数、返回值一致),我们就说:Someone 实现了 Languager 接口。
接口是一系列“协议”的组合,描述其具备的抽象的能力,具体的实现依靠的是结构体具体的方法。
type OtherOne struct { Speaker string } func (o OtherOne) Can(name string) string { return fmt.Sprintf("%s can speake %s", name, o.Speaker) } func main(){ b := Someone{Language: "go"} Program(b, "谢谢") o := OtherOne{Speaker: "English"} Program(o, "不客气") } >>2019/12/26 11:24:39 谢谢 can program with go >>2019/12/26 11:24:39 不客气 can speake English 复制代码
Someone 真实的方法(Can)是描述在”编程”层面的,OtherOne 真实的方法(Can)是描述其在”语言”层面的。但都是一种能力的描述,两者都实现了 Languager 接口。
聚焦在“编程”层面的示例,编程语言有多种,那么你觉得是设计比较全而统一的接口好?还是设计职责单一的接口好?
选择职责单一的设计方法
有句话怎么说的来着?什么都想要,什么都得不到。
type Gopher interface { Program(string) string } type Student struct { Name string } func (S Student) Program(language string) string { return fmt.Sprintf("%s 会写 %s,叫他 Gopher。", S.Name, language) } func Go(body Gopher) { log.Println(body.Program("Go")) } type PHPer interface { Do(string) string } type Teacher struct { Name string } func (T Teacher) Do(language string) string { return fmt.Sprintf("%s 会教 %s,叫他 PHPer。", T.Name, language) } func Php(body PHPer) { log.Println(body.Do("Php")) } type Pythoner interface { Run(string) string } type Roommate struct { Name string } func (R Roommate) Run(language string) string { return fmt.Sprintf("%s 会学 %s,叫她 Pythoner。", R.Name, language) } func Python(body Pythoner) { log.Println(body.Run("Python")) } func main(){ s := Student{Name: "谢小路"} t := Teacher{Name: "谢小人"} r := Roommate{Name: "谢小甲"} Go(s) Php(t) Python(r) } >>2019/12/26 12:19:36 谢小路 会写 Go,叫他 Gopher。 >>2019/12/26 12:19:36 谢小人 会教 Php,叫他 PHPer。 >>2019/12/26 12:19:36 谢小甲 会学 Python,叫她 Pythoner。 复制代码
多种能力的组合:
type Gopher interface { Program(string) string } type Student struct { Name string } func (S Student) Program(language string) string { return fmt.Sprintf("%s 会写 %s,叫他 Gopher。", S.Name, language) } func (S Student) Run(language string) string { return fmt.Sprintf("%s 也会写 %s", S.Name, language) } func Go(body Gopher) { log.Println(body.Program("Go")) } type PHPer interface { Do(string) string } type Teacher struct { Name string } func (T Teacher) Do(language string) string { return fmt.Sprintf("%s 会教 %s,叫他 PHPer。", T.Name, language) } func Php(body PHPer) { log.Println(body.Do("Php")) } type Pythoner interface { Run(string) string } type Roommate struct { Name string } func (R Roommate) Run(language string) string { return fmt.Sprintf("%s 会学 %s,叫她 Pythoner。", R.Name, language) } func Python(body Pythoner) { log.Println(body.Run("Python")) } type AwesomeDeveloper interface { Gopher Pythoner } func Development(a AwesomeDeveloper) { log.Println(a.Program("go")) log.Println(a.Run("python")) } func main(){ s := Student{Name: "谢小路"} t := Teacher{Name: "谢小人"} r := Roommate{Name: "谢小甲"} Go(s) Php(t) Python(r) Development(s) } >>2019/12/26 12:24:31 谢小路 会写 Go,叫他 Gopher。 >>2019/12/26 12:24:31 谢小人 会教 Php,叫他 PHPer。 >>2019/12/26 12:24:31 谢小甲 会学 Python,叫她 Pythoner。 >>2019/12/26 12:24:31 谢小路 会写 go,叫他 Gopher。 >>2019/12/26 12:24:31 谢小路 也会写 python 复制代码
单一职责的设计方法,可以进行组合,创造出更多的“能力”,比如会两种及以上的编程语言,示例中 AwesomeDeveloper.
可以看出: 接口是一堆协议,描述其能力,不实现,接口可以被多个结构体实现,同一个结构体也可以实现多个接口。
内置库中可以看到诸多的使用接口的示例,比如 io
库,定义:Reader、Writer、Closer、Seeker…,具体的实现散布在各种库中。

这种做法有什么好处?分层(或者说是隔离)。
- 上游层和下游层通过接口进行关联,但两层之间没有相互依赖
- 上游层使用接口描述,稳定,不会轻易改动
- 下游层侧重实现,需求变更,更改对应的实现即可
这么说,有点抽象,找个具体的例子: go-elasticsearch
大家都知道 elasticsearch 是开源的搜索引擎,对外暴露的是丰富的 RESTful 接口,多丰富呢?上百个吧。那么如果要编写个客户端库,面对如此多的 RESTful 接口,一方面需要考虑的是如何进行组织,一方面考虑的是如何应对 elasticsearch 本身的不断迭代带来的 API 接口变动。
调用 RESTful API , 无外乎这么几个动作:
- 构造请求参数:比如 URL、HEADER、Method 等
- 发起网络请求:比如 http.Get
- 组织响应信息: Response
基于此,官方源代码在其中进行了接口设计:
// 描述其 Do 能力 type Request interface { Do(ctx context.Context, transport Transport) (*Response, error) } // 描述其 Perform 能力 type Transport interface { Perform(*http.Request) (*http.Response, error) } // 自定义的响应信息 type Response struct { StatusCode int Header http.Header Body io.ReadCloser } 复制代码
官方还划分为三层组织代码结构:
1. esapi API 接口层
这一层主要做的事是:组织所有 API 请求参数、响应等。但实际上并没有真实的发起网络请求,而只是借用了Transport 接口的能力。
抽取其中一个接口查看下源代码:
curl http://localhost:9200/_cat/health >>1577337625 05:20:25 es-clustername green 3 3 24 11 0 0 0 0 - 100.0% 复制代码
具体的源码: esapi/api.cat.health.go
type CatHealth func(o ...func(*CatHealthRequest)) (*Response, error) type CatHealthRequest struct { ... } func (r CatHealthRequest) Do(ctx context.Context, transport Transport) (*Response, error) { var ( method string path strings.Builder params map[string]string ) method = "GET" path.Grow(len("/_cat/health")) path.WriteString("/_cat/health") params = make(map[string]string) if r.Format != "" { params["format"] = r.Format } if len(r.H) > 0 { params["h"] = strings.Join(r.H, ",") } if r.Help != nil { params["help"] = strconv.FormatBool(*r.Help) } if len(r.S) > 0 { params["s"] = strings.Join(r.S, ",") } if r.Time != "" { params["time"] = r.Time } if r.Ts != nil { params["ts"] = strconv.FormatBool(*r.Ts) } if r.V != nil { params["v"] = strconv.FormatBool(*r.V) } if r.Pretty { params["pretty"] = "true" } if r.Human { params["human"] = "true" } if r.ErrorTrace { params["error_trace"] = "true" } if len(r.FilterPath) > 0 { params["filter_path"] = strings.Join(r.FilterPath, ",") } req, err := newRequest(method, path.String(), nil) if err != nil { return nil, err } if len(params) > 0 { q := req.URL.Query() for k, v := range params { q.Set(k, v) } req.URL.RawQuery = q.Encode() } if len(r.Header) > 0 { if len(req.Header) == 0 { req.Header = r.Header } else { for k, vv := range r.Header { for _, v := range vv { req.Header.Add(k, v) } } } } if ctx != nil { req = req.WithContext(ctx) } res, err := transport.Perform(req) if err != nil { return nil, err } response := Response{ StatusCode: res.StatusCode, Body: res.Body, Header: res.Header, } return &response, nil } 复制代码
其中 Do 方法看上去很长,其实只在做这三件事:
- 组织请求参数
- 发起请求
- 组织响应信息
其中发起请求步骤,只是借用了 Transport 的 Perform 能力,得出的 res, 进行重新组织成自定义的 Response。
那么肯定有地方要真实的实现 Transport 的 Perform 能力,才能真实的发起网络请求。
最后所有 RESTful 请求进行组合: esapi/api._.go
type API struct { Cat *Cat Cluster *Cluster Indices *Indices ... } type Cat struct { Aliases CatAliases Allocation CatAllocation Count CatCount Fielddata CatFielddata Health CatHealth ... } func New(t Transport) *API { return &API{ Bulk: newBulkFunc(t), ... } 复制代码
2. estransport 层
这层主要描述连接、传输的能力。即和 es 集群连接的设置和真实的发起网络请求的实现。
type Interface interface { Perform(*http.Request) (*http.Response, error) } type Client struct { ... transport http.RoundTripper ... } func (c *Client) Perform(req *http.Request) (*http.Response, error) { ... start := time.Now().UTC() res, err = c.transport.RoundTrip(req) dur := time.Since(start) ... } 复制代码
没错,真实的发起网络请求的靠的是 http.RoundTripper,实际上 http.RoundTripper 也是个接口。
type RoundTripper interface { RoundTrip(*Request) (*Response, error) } 复制代码
初始化 client 的时候,使用了默认的 http.RoundTripper 实现方案:http.DefaultTransport
func New(cfg Config) *Client { if cfg.Transport == nil { cfg.Transport = http.DefaultTransport } ... } 复制代码
这样 定义的 Client 既实现了 Interface 接口,又实现了 Transport 接口。虽然两者描述的能力一模一样。
那么这两层之间本身没什么依赖,那么如何交互呢?
func (r CatHealthRequest) Do(ctx context.Context, transport Transport) (*Response, error) 复制代码
每个请求的 Do 方法接受 Transport 参数,实例化 estransport 层的 client, 将实例化的 client 作为参数传给 Do 方法即可。但两者本身之间无耦合关系。
3. elasticsearch 层
定义上游 client 层。这层 esapi 层的 API 和 estransport 层的 Interface 组合起来。
type Client struct { *esapi.API // Embeds the API methods Transport estransport.Interface } func NewClient(cfg Config) (*Client, error) { ... tp := estransport.New(estransport.Config{ ... Transport: cfg.Transport, ... }) client := &Client{Transport: tp, API: esapi.New(tp)} } 复制代码
为什么这样啊?明明 esapi 层和 estransport 层就可以完成任务啊?
简单的说:esapi 和 estransport 配合使用的方式,最后的调用结果像这样:
req := esapi.IndexRequest{ Index: "test", DocumentID: strconv.Itoa(i + 1), Body: strings.NewReader(b.String()), Refresh: "true", } // Perform the request with the client. res, err := req.Do(context.Background(), es) 复制代码
而具有了elasticsearch 层之后,调用的方式像这样:
es, err := elasticsearch.NewDefaultClient() es.Cat.Health() 复制代码
简单的说:上游暴露给用户的信息更少,方便其使用,不让用户知道关于实现的更多细节,推荐使用第二种方式。
其实这种实现方式也简单:就是将 Resquest 的 Do 方法再封装一层,整成函数的类型.
type CatHealth func(o ...func(*CatHealthRequest)) (*Response, error) func newCatHealthFunc(t Transport) CatHealth { return func(o ...func(*CatHealthRequest)) (*Response, error) { var r = CatHealthRequest{} for _, f := range o { f(&r) } return r.Do(r.ctx, t) } } type Cat struct { ... Health CatHealth ... } 复制代码
基于此 elasticsearch 三层模型大概就是这样,其实内部还大量的使用了面向接口、组合的编程思想。读者可以根据源码去探讨研究。
看完就结束了吗?
不,我要借鉴相似的思想,自己实现一个,于是有了这个项目: cartooncharts ,js 的具体实现查看: chart.xkcd
下游层:侧重在细节实现层面
定义接口: charts.go
type ChartsInterface interface { Plot(t Transport) func(w http.ResponseWriter, r *http.Request) Save(string, Transport) bool Render(t Transport) func(w http.ResponseWriter, r *http.Request) } type Transport interface { Execute(w http.ResponseWriter, r *http.Request, v interface{}) Read(name string) ([]byte, error) } 复制代码
某种类型的图表实现:
type BarRequest struct { WithTitle WithXLabel WithYLabel WithDataCollection WithOption } func (bar BarRequest) Plot(t Transport) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { v := struct { Type string Interface BarRequest }{ Type: barStackedType, Interface: bar, } t.Execute(w, r, v) } } 复制代码
没有具体的实现,只是借用了 Transport 的 Execute 的能力。
传输层:侧重在模板渲染层面
type Template struct { Path string } func (T Template) Read(name string) ([]byte, error) { box := packr.New(name, T.Path) b, e := box.Find(name) if e != nil { log.Println("template read fail", e.Error()) return nil, e } return b, nil } func (T Template) Execute(w http.ResponseWriter, r *http.Request, v interface{}) { t := template.New("") text, e := T.Read("plot.html") if e != nil { log.Println("template read fail") return } tt, e := t.Parse(string(text)) if e != nil { log.Println("template parse fail") return } tt.Execute(w, v) } 复制代码
type Interface interface { Execute(w http.ResponseWriter, r *http.Request, v interface{}) Read(name string) ([]byte, error) } type ChartsTransport struct { Template Interface Charts *cartoon.Charts } func (C ChartsTransport) Execute(w http.ResponseWriter, r *http.Request, v interface{}) { C.Template.Execute(w, r, v) } func (C ChartsTransport) Read(name string) ([]byte, error) { return C.Template.Read(name) } func NewChartsTransport() *ChartsTransport { t := Template{Path: "./template"} return &ChartsTransport{ Template: t, Charts: cartoon.NewCharts(t), } } 复制代码
上游层:简洁的对外暴露层
type CartoonCharts struct { *cartoontransport.ChartsTransport } func NewCartoonCharts() *CartoonCharts { return &CartoonCharts{cartoontransport.NewChartsTransport()} } 复制代码
示例:
package main import ( "github.com/wuxiaoxiaoshen/cartooncharts" "log" "net/http" ) var charts *cartooncharts.CartoonCharts func init() { charts = cartooncharts.NewCartoonCharts() } func ExampleBar() { bar := charts.Charts.Bar("github stars VS patron number", charts.Charts.Bar.WithDataLabels([]interface{}{"github stars", "patrons"}), charts.Charts.Bar.WithDataDataSets("", []interface{}{100, 2}), charts.Charts.Bar.WithOptions("yTickCount", 2), ) http.HandleFunc("/bar", bar) } func ExampleXY() { type point struct { X interface{} `json:"x"` Y interface{} `json:"y"` } xy := charts.Charts.XY("Pokemon farms", charts.Charts.XY.WithXLabel("Coodinate"), charts.Charts.XY.WithYLabel("Count"), charts.Charts.XY.WithDataDataSets("Pikachu", []interface{}{point{3, 10}, point{4, 122}, point{10, 100}, point{1, 2}, point{2, 4}}), charts.Charts.XY.WithDataDataSets("Squirtle", []interface{}{point{3, 122}, point{4, 212}, point{-3, 100}, point{1, 1}, point{1.5, 12}}), charts.Charts.XY.WithOptions("xTickCount", 5), charts.Charts.XY.WithOptions("yTickCount", 5), charts.Charts.XY.WithOptions("legendPosition", "chartXkcd.config.positionType.upRight"), charts.Charts.XY.WithOptions("showLine", false), charts.Charts.XY.WithOptions("timeFormat", "undefined"), charts.Charts.XY.WithOptions("dotSize", 1), ) http.HandleFunc("/xy", xy) } func ExampleStackedBar() { stackedBar := charts.Charts.StackedBar("Issues and PR Submissions", charts.Charts.StackedBar.WithXLabel("Month"), charts.Charts.StackedBar.WithYLabel("Count"), charts.Charts.StackedBar.WithDataLabels([]interface{}{"Jan", "Feb", "Mar", "April", "May"}), charts.Charts.StackedBar.WithDataDataSets("Issues", []interface{}{12, 19, 11, 29, 17}), charts.Charts.StackedBar.WithDataDataSets("PRs", []interface{}{3, 5, 2, 4, 1}), charts.Charts.StackedBar.WithDataDataSets("Merges", []interface{}{2, 3, 0, 1, 1}), ) http.HandleFunc("/stackedBar", stackedBar) } func ExampleLine() { line := charts.Charts.Line("Monthly income of an indie developer", charts.Charts.Line.WithXLabel("Month"), charts.Charts.Line.WithYLabel("$ Dollars"), charts.Charts.Line.WithDataLabels([]interface{}{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}), charts.Charts.Line.WithDataDataSets("Plan", []interface{}{30, 70, 200, 300, 500, 800, 1500, 2900, 5000, 8000}), charts.Charts.Line.WithDataDataSets("Reality", []interface{}{0, 1, 30, 70, 80, 100, 50, 80, 40, 150}), charts.Charts.Line.WithOptions("yTickCount", 3), charts.Charts.Line.WithOptions("legendPosition", "chartXkcd.config.positionType.upLeft"), ) http.HandleFunc("/line", line) } func ExamplePie() { pie := charts.Charts.Pie("What Tim made of", charts.Charts.Pie.WithDataLabels([]interface{}{"a", "b", "e", "f", "g"}), charts.Charts.Pie.WithDataDataSets("", []interface{}{500, 200, 80, 90, 100}), charts.Charts.Pie.WithOptions("innerRadius", 0.5), charts.Charts.Pie.WithOptions("legendPosition", "chartXkcd.config.positionType.upRight"), ) http.HandleFunc("/pie", pie) } func ExampleRadar() { radar := charts.Charts.Radar("Letters in random words", charts.Charts.Radar.WithDataLabels([]interface{}{"c", "h", "a", "r", "t"}), charts.Charts.Radar.WithDataDataSets("ccharrrt", []interface{}{2, 1, 1, 3, 1}), charts.Charts.Radar.WithDataDataSets("chhaart", []interface{}{1, 2, 2, 1, 1}), charts.Charts.Radar.WithOptions("showLegend", true), charts.Charts.Radar.WithOptions("dotSize", 0.8), charts.Charts.Radar.WithOptions("showLabels", true), charts.Charts.Radar.WithOptions("legendPosition", "chartXkcd.config.positionType.upRight"), ) http.HandleFunc("/radar", radar) } func main() { ExampleBar() ExampleXY() ExampleStackedBar() ExampleLine() ExamplePie() ExampleRadar() log.Fatal(http.ListenAndServe(":9090", nil)) } 复制代码
维护了一致的风格。
结果:

下课!