Go反模式之越俎代庖

反模式(anti-pattern或antipattern)又叫做反面模式,指的是在实践中经常出现但又低效或是有待优化的设计模式,是用来解决问题的带有共同性的不良方法。Andrew Koenig在1995年造了anti-pattern这个词,灵感来自于GoF的《设计模式》一书。
按《AntiPatterns》作者的说法,可以用至少两个关键因素来把反面模式和不良习惯、错误的实践或糟糕的想法区分开来:

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

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

这是第一篇,介绍 越俎代庖
反模式,或者叫做 画蛇添足
反模式,或者叫做 镀金
反模式( Gold plating
)。 意思是指项目已经达到了设计的最高价值,结果还添加额外的功能,反而使项目变得很差。
当然,本文以及后续文章中的实例可能会引起争议,欢迎在评论中讨论。你如果也发现了一些Go的反模式,也欢迎留言。

golang/glog
,这是Google开源的一个log库,可以实现多级的log日志输出。它实现了 google/glog
相同行为的日志输出。
项目介绍说这个项目的源代码master在Google内部。github上的目前处于不维护的状态,最新同步都是四年前了。
首先,我们说一下这个库的好处,简单好用,可以根据日志级别进行设置,而且带文件输出功能。
你可以写一个简单的程序测试一下:

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
试试:

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

这次终于看到日志了。

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

➜  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
显示版本号:

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失败:

➜  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的代码,这也会导致问题。