洞察探讨小游戏SDK接入的最佳实践以及对企业跨平台开发的优势
691
2022-08-31
我们应该怎么样编写 Go 语言库,有哪些标准可以参考?(我们要怎样去做才能写好)
不要对 HTTP 客户端硬编码
首先,我们很多人都读过这篇讲述 http.DefaultClient 不能自定义超时时间的文章《Don’t use Go’s default HTTP client (in production)[1]》,当你没法保证你的HTTP 请求一定会完成(或者至少要等一个完全无法预估时间的响应)时,你的程序可能会遇到奇怪的 goroutine 泄漏和一些无法预知的行为。在我看来,这会使每一个对 http.DefaultClient 采用硬编码的库不可用。
其次,网络需要一些额外的配置。有时候需要用到代理,有时候需要对 URL 进行一丢丢的改写,甚至可能 http.Transport 需要被一个定制的接口替换。当一个程序员在你的库里用他们自己的 http.Client 实例时,以上这些都很容易被实现。
在你的库中处理 http.Client 的推荐方式是使用提供的客户端,但是如果需要的话,有一个默认的备选:
func CreateLibrary(client *http.Client) *Library {
if client == nil {
client = http.DefaultClient
}
...
}
或者如果你想从工厂函数中移除参数,请在你的 struct 中定义一个辅助方法,并且让用户在需要时设置其属性:
type Library struct {
Client *http.Client
}
func (l *Library) getClient() *http.Client {
if l.Client == nil {
return http.DefaultClient
}
return l.Client
}
另外,如果一些全局的特性对于每个请求来讲都是必须的,人们经常感觉到需要用他们自己的实例来替换 http.Client。这是一个错误的方法 — 如果你需要在你的请求中设置一些额外的 headers,或者在你的客户端引入某类公共的特性,你只需要简单为每个请求进行设置或者用组装定制客户端的方式来代替完全替换它。
不要引入全局变量
另一个反面模式是允许用户在一个库中设置全局变量。举个例子,在你的库中允许用户设置一个全局的 http.Client 并被所有的 HTTP 调用执行:
var libraryClient *http.Client = http.DefaultClient
func SetHttpClient(client *http.Client) {
libraryClient = client
}
通常在一个库中不应该存在一堆全局变量。当你写代码的时候,你应该想想用户在他们的程序中多次使用你的这个库会发生什么。全局变量会使不同的参数没有办法被使用。而且,在你的代码中引入全局变量会引起测试上的问题并造成代码上不必要的复杂度。使用全局变量可能会导致在你程序的不同模块有不必要的依赖。在写你的库的时候,避免全局状态是格外重要的。
返回 structs,而不是 interfaces
这是一个普遍的问题(实际上我在这一点上也犯过错)。很多库都有下面这类函数:
func New() LibraryInterface {
...
}
在上面的 case 中,返回一个 interface 使 struct 的特性在库里被隐藏了。实际上应该这么写:
func New() *LibraryStruct {
...
}
在库里不应该存在接口的声明,除非它被用在某个函数参数中。如果出现上面的 case,你就应该想想你在写这个库的时候的约定。当返回一个 interface 时,你基本上得声明一系列可用的方法。如果有人想用这个接口来实现他们自己的功能(比如说为了测试),他得打乱他们的代码来添加更多的方法。这意味着尽管在 struct 里添加方法是安全的,但在 interface 里不是。这个想法在这篇文章中被总结得很好《Accept Interfaces Return Struct in Go[2]》。这个方案也能解决配置的问题。你想修改库中的一些特性,你可以简单的修改 struct 中一些公开的字段。但是如果你的库只提供给用户一个 interface,这就玩不转了。
详情请参见 Go http.Client[3]。
使用配置结构体来避免修改你的APIs
另一种配置方法是在你的工厂函数中接收一个配置结构体,而不是直接传配置参数。你可以很随意的添加新的参数而不用破坏现有的 API。你只需要做一件事情,在Config结构体中添加一个新的字段,并且确保不会影响它原本的特性。
func New(config Config) *LibraryStruct {
...
}
下面是一种添加结构体字段的正确的场景,如果一个用户初始化结构体的时候忘了添加字段名,这是一种我认为修改他们的代码能得到原谅的场景。为了维护兼容性,你应该在你的代码中用 person{name: "Alice", age: 30} 而不是 person{"Alice", 30}。
你能在 golang.org/x/crypto[4] 包里看到对上面的补充。总之,对配置来说,我认为允许用户在返回的结构体里设置不同的参数是一个更好的方法,并且只在编写复杂方法时才使用这种特定方法。
总结
根据经验来讲,在写一个库的时候,你应该总是允许用户指定他们自己的 http.Client 来执行 HTTP 调用。而且考虑到未来迭代修改带来的影响,你可以尝试用可扩展的方式编写代码。避免全局变量,库不能存储全局状态。如果你有任何疑问-参考标准库是怎么写的。
我认为有一个很好的想法,在你的程序中用你的库来测试并问自己一些问题:
如果你尝试多次引入库会发生什么?
你的库有没有单元测试?
在不破坏原有代码的前提下,有没有一种非侵入式的方式来扩展你的库?
在不破坏原有代码的前提下,是否可以添加额外配置参数?
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~