Go反模式之越俎代庖

反模式(anti-pattern或antipattern)又叫做反面模式,指的是在实践中经常出现但又低效或是有待优化的设计模式,是用来解决问题的带有共同性的不良方法。Andrew Koenig在1995年造了anti-pattern这个词,灵感来自于GoF的《设计模式》一书。

按《AntiPatterns》作者的说法,可以用至少两个关键因素来把反面模式和不良习惯、错误的实践或糟糕的想法区分开来:

  • 行动、过程和结构中的一些重复出现的乍一看是有益的,但最终得不偿失的模式
  • 在实践中证明且可重复的清晰记录的重构方案

维基百科上列出了一些反模式列表: 反面模式, 我开了☝系列,用来记录Go语言开发中的一些反模式。

这是第一篇,介绍 越俎代庖 反模式,或者叫做画蛇添足反模式,或者叫做镀金反模式(Gold plating)。 意思是指项目已经达到了设计的最高价值,结果还添加额外的功能,反而使项目变得很差。

当然,本文以及后续文章中的实例可能会引起争议,欢迎在评论中讨论。你如果也发现了一些Go的反模式,也欢迎留言。

glog例子

golang/glog,这是Google开源的一个log库,可以实现多级的log日志输出。它实现了google/glog相同行为的日志输出。

项目介绍说这个项目的源代码master在Google内部。github上的目前处于不维护的状态,最新同步都是四年前了。

首先,我们说一下这个库的好处,简单好用,可以根据日志级别进行设置,而且带文件输出功能。

你可以写一个简单的程序测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import (
"flag"
"github.com/golang/glog"
)
func main() {
flag.Parse()
defer glog.Flush()
glog.Infof("hello %s", "glog")
}

然后运行go run main.go看看效果。

什么?没有任何日志输出,再尝试go run main.go -stderrthreshold INFO试试:

1
2
➜ go run main.go -stderrthreshold INFO
I0526 19:46:05.793886 11860 main.go:14] hello glog

这次终于看到日志了。

如果你运行go run main.go,你会看到你的程序莫名其妙的加了几个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜ abc go run main.go -h
-alsologtostderr
log to standard error as well as files
-log_backtrace_at value
when logging hits line file:N, emit a stack trace
-log_dir string
If non-empty, write log files in this directory
-logtostderr
log to standard error instead of files
-stderrthreshold value
logs at or above this threshold go to stderr
-v value
log level for V logs
-vmodule value
comma-separated list of pattern=N settings for file-filtered logging

这就是我们介绍的反模式。本来glog作为一个库提供给其它人使用,但是却额外偷偷的在命令行参数中注入了几个参数,这种强迫并且非显式的方式并不是作为库的好的行为。

这种方式并没有在库的使用者允许的情况下就注入额参数,一是污染了使用者的命令行解析方式,二是给使用者一个风险提示,这个库是否是安全的,会不会偷偷注入恶意代码?

更大的问题是命令行参数冲突。 假设你要为你的程序提供一个查看版本的功能,使用者可以使用main -v显示版本号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var (
v = flag.Bool("v", false, "show version")
)
func main() {
flag.Parse()
if *v {
fmt.Println("1.0.0")
}
defer glog.Flush()
glog.Infof("hello %s", "glog")
}

如果你运行上面的程序,会panic失败:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
➜ abc go run main.go
/var/folders/gq/jd9v5dd95p570hkztblb8ht40000gn/T/go-build692968448/b001/exe/main flag redefined: v
panic: /var/folders/gq/jd9v5dd95p570hkztblb8ht40000gn/T/go-build692968448/b001/exe/main flag redefined: v
goroutine 1 [running]:
flag.(*FlagSet).Var(0xc00005a180, 0x1113000, 0xc00001c11c, 0x10f15d8, 0x1, 0x10f28a3, 0xc)
/usr/local/go/src/flag/flag.go:851 +0x4b8
flag.(*FlagSet).BoolVar(...)
/usr/local/go/src/flag/flag.go:624
flag.(*FlagSet).Bool(0xc00005a180, 0x10f15d8, 0x1, 0x0, 0x10f28a3, 0xc, 0x6)
/usr/local/go/src/flag/flag.go:637 +0x8a
flag.Bool(0x10f15d8, 0x1, 0x11a6200, 0x10f28a3, 0xc, 0xe)
/usr/local/go/src/flag/flag.go:644 +0x5e
main.init()
/Users/abc/go/src/github.com/abc/abc/main.go:11 +0x50
exit status 2

原因在于glog定义了一个v参数,而你也定义了一个v参数,导致冲突。可是v是很通用的一个查看版本的参数,这也意味着你不得不改个参数名称。

很显然,glog库把一些本来使用者需要决定的事情给实现了,本来移除掉这些代码,或者单独抽取出独立的函数,或者使用者可以定制参数,都是比这种私自决定的方式好很多。

同样的,vitess也有同样的问题。

当然,vitess作为一个独立的工具,而不是库来说,问题不大,因为代码不会作为库使用。但是实际上很多项目也使用vitness的代码,这也会导致问题。

pprof

另一个非常常见的例子是对runtime/pprof的误用。

你已知道,go本身自带的pprof库非常的有用,如果你通过_ "net/http/pprof"引入了pporf,可以方便的跟踪Go程序性能和资源占用的问题。甚至你还可以自己定义开发自有的pprof,比如下面的文章中介绍的:

基本上,你需要实现类似的代码(摘自上面最后一个链接的例子):

1
2
3
4
5
6
7
8
9
10
var libProfile *pprof.Profile
func init() {
profName := “my_experiment_thing”
libProfile = pprof.Lookup(profName)
if libProfile == nil {
libProfile = pprof.NewProfile(profName)
}
}
// Warning: /vendor panic possibilities
var panicProfile = pprof.NewProfile(“this_unique_name”)

但是不幸的是,很多文章中并没有提及使用这个方式带来的副作用。

本来,如果你在你的程序中使用类似的代码并没有问题,但是如果你正在开发一个库,使用这段代码的话,相当于往profiles🀄️强制注册了一个Profile,不管别人用或不用。当别人使用_ "net/http/pprof"的时候,就会展示你的Profile。