为其默认用例设计 API
发布者:admin 发表于:438天前 阅读数:555 评论:0

几年前,我就对 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.HandlerServe 允许第二个参数为 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 实现了如果 handlernil,使用 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