几年前,我就对 functional options
[7] 进行过讨论[6],使 API 更易用于默认用例。
本演讲的主旨是你应该为常见用例设计 API。 另一方面, API 不应要求调用者提供他们不在乎参数。
不鼓励使用 nil
作为参数
本章开始时我建议是不要强迫提供给 API 的调用者他们不在乎的参数。 这就是我要说的为默认用例设计 API。
这是 net/http
包中的一个例子
package http
// ListenAndServe listens on the TCP network address addr and then calls
// Serve with handler to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// The handler is typically nil, in which case the DefaultServeMux is used.
//
// ListenAndServe always returns a non-nil error.
func ListenAndServe(addr string, handler Handler) error {
ListenAndServe
有两个参数,一个用于监听传入连接的 TCP
地址,另一个用于处理 HTTP
请求的 http.Handler
。Serve
允许第二个参数为 nil
,需要注意的是调用者通常会传递 nil
,表示他们想要使用 http.DefaultServeMux
作为隐含参数。
现在,Serve
的调用者有两种方式可以做同样的事情。
http.ListenAndServe("0.0.0.0:8080", nil)
http.ListenAndServe("0.0.0.0:8080", http.DefaultServeMux)
两者完全相同。
这种 nil
行为是病毒式的。 http
包也有一个 http.Serve
帮助类,你可以合理地想象一下 ListenAndServe
是这样构建的
func ListenAndServe(addr string, handler Handler) error {
l, err := net.Listen("tcp", addr)
if err != nil {
return err
}
defer l.Close()
return Serve(l, handler)
}
因为 ListenAndServe
允许调用者为第二个参数传递 nil
,所以 http.Serve
也支持这种行为。 事实上,http.Serve
实现了如果 handler
是nil
,使用 DefaultServeMux
的逻辑。 参数可为 nil
可能会导致调用者认为他们可以为两个参数都使用 nil
。 像下面这样:
http.Serve(nil, nil)
会导致 panic
。
贴士:
不要在同一个函数签名中混合使用可为nil
和不能为nil
的参数。
http.ListenAndServe
的作者试图在常见情况下让使用 API 的用户更轻松些,但很可能会让该程序包更难以被安全地使用。
使用 DefaultServeMux
或使用 nil
没有什么区别。
const root = http.Dir("/htdocs")
http.Handle("/", http.FileServer(root))
http.ListenAndServe("0.0.0.0:8080", nil)
对比
const root = http.Dir("/htdocs")
http.Handle("/", http.FileServer(root))
http.ListenAndServe("0.0.0.0:8080", http.DefaultServeMux)
这种混乱值得拯救吗?
const root = http.Dir("/htdocs")
mux := http.NewServeMux()
http.Handle("/", http.FileServer(root))
http.ListenAndServe("0.0.0.0:8080", mux)
贴士: 认真考虑
helper
函数会节省不少时间。 清晰要比简洁好。
贴士:
避免公共 API 使用测试参数
避免在公开的 API 上使用仅在测试范围上不同的值。 相反,使用Public wrappers
隐藏这些参数,使用辅助方式来设置测试范围中的属性。
首选可变参数函数而非 []T
参数
编写一个带有切片参数的函数或方法是很常见的。
func ShutdownVMs(ids []string) error
这只是我编的一个例子,但它与我所写的很多代码相同。 这里的问题是他们假设他们会被调用于多个条目。 但是很多时候这些类型的函数只用一个参数调用,为了满足函数参数的要求,它必须打包到一个切片内。
另外,因为 ids
参数是切片,所以你可以将一个空切片或 nil
传递给该函数,编译也没什么错误。 但是这会增加额外的测试负载,因为你应该涵盖这些情况在测试中。
举一个这类 API 的例子,最近我重构了一条逻辑,要求我设置一些额外的字段,如果一组参数中至少有一个非零。 逻辑看起来像这样:
if svc.MaxConnections > 0 || svc.MaxPendingRequests > 0 || svc.MaxRequests > 0 || svc.MaxRetries > 0 {
// apply the non zero parameters
}
由于 if
语句变得很长,我想将签出的逻辑拉入其自己的函数中。 这就是我提出的:
// anyPostive indicates if any value is greater than zero.
func anyPositive(values ...int) bool {
for _, v := range values {
if v > 0 {
return true
}
}
return false
}
这就能够向读者明确内部块的执行条件:
if anyPositive(svc.MaxConnections, svc.MaxPendingRequests, svc.MaxRequests, svc.MaxRetries) {
// apply the non zero parameters
}
但是 anyPositive
还存在一个问题,有人可能会这样调用它:
if anyPositive() { ... }
在这种情况下,anyPositive
将返回 false
,因为它不会执行迭代而是立即返回 false
。对比起如果 anyPositive
在没有传递参数时返回 true
, 这还不算世界上最糟糕的事情。
然而,如果我们可以更改 anyPositive
的签名以强制调用者应该传递至少一个参数,那会更好。我们可以通过组合正常和可变参数来做到这一点,如下所示:
// anyPostive indicates if any value is greater than zero.
func anyPositive(first int, rest ...int) bool {
if first > 0 {
return true
}
for _, v := range rest {
if v > 0 {
return true
}
}
return false
}
现在不能使用少于一个参数来调用 anyPositive
。