显式与隐式
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
接口