翻译自<A theory of modern Go> by Peter Bourgon 2017/06/09

原文链接

全文论断:

全局状态会产生微小的副作用 ——> 须要防止包级别的变量和init函数

Part1

Go is easy to read

Go语言惟一最佳的属性是基本上没有什么魔法代码。除了极少数的例外外,间接浏览Go的源码不会产生诸如“定义”,“依赖关系”,“运行时行为”的歧义,而这让Go的可读性较好,从而使得Go代码较容易保护,这是工业化编程的最高境界。

Part2

Magic is bad

然而魔法代码依然有一些形式混入其中。可怜的是,十分广泛的一种形式是通过应用全局状态。包级别的全局对象能够对外部调用者暗藏状态和行为。调用这些全局变量的代码可能会产生意外的副作用,从而毁坏了读者了解和脑海中构建程序的能力。

函数(包含办法,在go中二者略有不同)基本上是Go用来构建形象的惟一机制。

思考以下函数定义:

func NewObject(n int) (*Object, error)

Part3 *

依照常规来讲,咱们心愿模式为NewXxx的函数是类型构造函数。而这个函数也的确是构造函数,因为咱们看到函数返回指向对象的指针和谬误。由此咱们能够推断出构造函数可能结构胜利也可能结构失败,如果结构失败,将收到error通知咱们起因。

该结构函数参数为单int,咱们假设该int参数管制了函数返回对象Object的生成。咱们假设对参数int n有一些束缚,如果不满足束缚将导致谬误。然而因为该函数不承受其余参数,因而咱们心愿它除了分配内存外应该没有其余副作用。

仅通过浏览函数签名,咱们就能够失去这些推论,脑海中大略就有此函数了。从main函数的第一行开始反复递归的利用这个过程,是咱们浏览和了解程序的形式。

假设这是NewObject函数的实现:

func NewObject(n int) (*Object, error) {    row := dbconn.QueryRow("SELECT ... FROM ... WHERE ...")    var id string    if err := row.Scan(&id); err != nil {        logger.Log("during row scan: %v", err)        id = "default"    }    resource, err := pool.Request(n)    if err != nil {        return nil, err    }    return &Object{        id:  id,        res: resource,    }, nil}

该函数调用了:

1.包级别的全局变量 database / sql.Conn,以对某些未指定的数据库进行查问;

2.包级别的全局记录器,用于将任意格局的字符串输入到某个地位;

3.以及包级别的某种类型的链接池对象,以申请某种类型的资源。

所有这些操作都有副作用,而这些副作用从函数签名则齐全不可见。调用者没有方法预测这些事件产生,除非通过浏览函数体并跳到所有全局变量的定义处查看。

思考另一种模式的签名函数:

func NewObject(db *sql.DB, pool *resource.Pool, n int, logger log.Logger) (*Object, error)

通过将每个全局依赖作为参数,咱们使读者能够精确地晓得函数的作用范畴和在函数体内可能产生的行为。调用者确切地晓得该函数须要什么参数,并能够提供这些参数。

如果咱们正在为此程序设计公共API,咱们甚至能够采取更无效的措施。

// RowQueryer models part of a database/sql.DB.type RowQueryer interface {    QueryRow(string, ...interface{}) *sql.Row}// Requestor models the requesting side of a resource.Pool.type Requestor interface {    Request(n int) (*resource.Value, error)}func NewObject(q RowQueryer, r Requestor, n int, logger log.Logger) (*Object, error) {    // ...}

通过将每个具体对象形象为接口,及仅捕捉函数中应用到的办法,咱们容许调用者本人去实现。这缩小了包之间的源码级耦合,并使咱们可能模仿测试中的具体依赖关系。如果对应用具体的包级别全局变量代码进行测试,咱们会发现这种做法是如许乏味且容易出错。

如果咱们所有的构造函数和其余函数都显式地承受了它们的依赖关系,那么全局变量就没有任何用途。相同,咱们能够在主函数中结构所有数据库连贯,日志记录,链接池,以便 未来的读者能够十分分明地绘制出组件图并应用。

而且,咱们能够十分明确地将这些依赖关系传递给应用它们的组件/函数,从而不会对全局变量感到困惑。另外值得注意的是,如果没有全局变量,那么也就不再须要应用init函数了,init函数的惟一目标是实例化或扭转包级别的全局状态。

Part4

Try to write go without global state

编写简直没有全局状态的Go程序不仅可能,而且非常容易。以我的教训来看,以这种形式编程不会比应用全局变量放大函数定义慢或乏味。

相同,当函数签名牢靠且残缺地形容了函数主体的作用范畴时,咱们能够更高效地进行代码推理,重构和保护。 Go kit从一开始就以这种格调编写,并因而受害。

Part5

Avoid two things

综上所述,咱们能够倒退出古代Go实践。依据 Dave Cheney所述,提出以下准则:

  • 防止包级别的变量
  • 防止初始化函数

当然也存在例外。