翻译自<A theory of modern Go> by Peter Bourgon 2017/06/09html
原文连接git
全文结论:github
全局状态会产生巨大的反作用 ——> 须要避免包级别的变量和init函数sql
Go is easy to read数据库
Go语言惟一最佳的属性是基本上没有什么魔法代码。除了极少数的例外外,直接阅读Go的源码不会产生诸如“定义”,“依赖关系”,“运行时行为”的歧义,而这让Go的可读性较好,从而使得Go代码较容易维护,这是工业化编程的最高境界。编程
Magic is badide
可是魔法代码仍然有一些方式混入其中。不幸的是,很是广泛的一种方式是经过使用全局状态。包级别的全局对象能够对外部调用者隐藏状态和行为。调用这些全局变量的代码可能会产生意外的反作用,从而破坏了读者理解和脑海中构建程序的能力。函数
函数(包括方法,在go中两者略有不一样)基本上是Go用来构建抽象的惟一机制。测试
思考如下函数定义:翻译
func NewObject(n int) (*Object, error)
按照惯例来说,咱们但愿形式为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函数的惟一目的是实例化或改变包级别的全局状态。
Try to write go without global state
编写几乎没有全局状态的Go程序不只可能,并且很是容易。以个人经验来看,以这种方式编程不会比使用全局变量缩小函数定义慢或乏味。
相反,当函数签名可靠且完整地描述了函数主体的做用范围时,咱们能够更高效地进行代码推理,重构和维护。 Go kit从一开始就以这种风格编写,并所以受益。
Avoid two things
综上所述,咱们能够发展出现代Go理论。根据 Dave Cheney所述,提出如下准则:
固然也存在例外。