显式与隐式
  Go 语言社区对于显式的初始化、方法调用非常推崇,类似 Spring Boot 和 Rails 的框架其实都广泛地采纳了『约定优于配置』的中心思想,简化了开发者和工程师的工作量
init
  在这里先以一个非常常见的函数 init 为例,介绍 Go 语言社区对显式调用的推崇;相信很多人都在一些 package 中阅读过这样的代码:
| 1 | var grpcClient *grpc.Client | 
  这种代码虽然能够通过编译并且正常工作,然而这里的 init 函数其实隐式地初始化了 grpc 的连接资源,如果另一个 package 依赖了当前的包,那么引入这个依赖的工程师可能会在遇到错误时非常困惑,因为在 init 函数中做这种资源的初始化是非常耗时并且容易出现问题的
  一种更加合理的做法其实是这样的,首先定义一个新的 Client 结构体以及一个用于初始化结构的 NewClient 函数,这个函数接收了一个 grpc 连接作为入参返回一个用于获取 Post 资源的客户端,GetPost 成为了这个结构体的方法,每当我们调用 client.GetPost 时都会用到结构体中保存的 grpc 连接:
| 1 | // post/client.go | 
  初始化 grpc 连接的代码应该放到 main 函数或者 main 函数调用的其他函数中执行,如果我们在 main 函数中显式的初始化这种依赖,对于其他的工程师来说就非常易于理解,我们从 main 函数开始就能梳理出程序启动的整个过程:
| 1 | // cmd/grpc/main.go | 
各个模块之间会构成一种树形的结构和依赖关系,上层的模块会持有下层模块中的接口或者结构体,不会存在孤立的、不被引用的对象
  当然这并不是说我们一定不能使用 init 函数,作为 Go 语言赋予开发者的能力,因为它能在包被引入时隐式地执行了一些代码,所以我们更应该慎重地使用它们
  一些框架会在 init 中判断是否满足使用的前置条件,但是对于很多的 Web 或者 API 服务来说,大量使用 init 往往意味着代码质量的下降以及不合理的设计
| 1 | func init() { | 
  上述代码其实是 Effective Go 在介绍 init 方法使用是展示的实例代码,这是一个比较合理地 init 函数使用示例,我们不应该在 init 中做过重的初始化逻辑,而是做一些简单、轻量的前置条件判断
面向接口
接口的作用其实就是为不同层级的模块提供了一个定义好的中间层,上游不再需要依赖下游的具体实现,充分地对上下游进行了解耦
| 1 | package post | 
  上述代码其实就不是一个设计良好的代码,它不仅在 init 函数中隐式地初始化了 grpc 连接这种全局变量,而且没有将 ListPosts 通过接口的方式暴露出去,这会让依赖 ListPosts 的上层模块难以测试
我们可以使用下面的代码改写原有的逻辑,使得同样地逻辑变得更容易测试和维护:
| 1 | package post | 
- 通过接口 Service暴露对外的ListPosts方法
- 使用 NewService函数初始化Service接口的实现并通过私有的结构体service持有grpc连接
- ListPosts不再依赖全局变量,而是依赖结构体- service持有的连接
  当我们使用这种方式重构代码之后,就可以在 main 函数中显式的初始化 grpc 连接、创建 Service 接口的实现并调用 ListPosts 方法:
| 1 | package main | 
这种使用接口组织代码的方式在 Go 语言中非常常见,我们应该在代码中尽可能地使用这种思想和模式对外提供功能:
- 使用大写的 Service对外暴露方法
- 使用小写的 service实现接口中定义的方法
- 通过 NewService函数初始化Service接口
总结
- 显示与隐式:尽可能地消灭项目中的 init函数,保证显式地进行方法的调用以及错误的处理
- 面向接口:面向接口是 Go 语言鼓励的开发方式,也能够为我们写单元测试提供方便,我们应该遵循固定的模式对外提供功能
- 使用大写的 Service对外暴露方法
- 使用小写的 service实现接口中定义的方法
- 通过 func NewService(...) (Service, error)函数初始化Service接口