Go语言中常见100问题-#8 any says nothing

2022-12-18 12:35:20 浏览数 (1)

不要过度使用any类型

在Go语言中,具有零个方法的接口类型称为空接口(interface {}). 从Go1.18版本开始,出现了一个新的关键字 any, 它是 interface{} 的别名。因此,所有 interface{} 出现的地方都可以替换为 any. 然而,在很多情况下,any被认为是过度概括。本文将讨论过度使用any存在的问题。

像下面的代码,any 类型的变量可以保存任何类型的值。

代码语言:javascript复制
func main() {
        var i any

        i = 42
        i = "foo"
        i = struct {
                s string
        }{
                s: "bar",
        }
        i = f

        _ = i
}

func f() {}

将任何类型的值赋值给 any 类型时,会丢失原来的类型信息, 这时需要类型断言才能从变量 i 中获取任何有用的信息。下面来看一个使用 any 不准确的具体例子,例子中将定义一个 Store 结构体 并且 它拥有 Get和Set 两个方法。用 Store 结构体存储 Customer 和 Contract 两种类型的对象,具体代码如下:

代码语言:javascript复制
package store

type Customer struct{
        // Some fields
}
type Contract struct{
        // Some fields
}

type Store struct{}

func (s *Store) Get(id string) (any, error) {
        // ...
}

func (s *Store) Set(id string, v any) error {
        // ...
}

尽管上述代码可以编译成功,但我们应该花点时间审视一下函数签名,可以发现它接收并返回 any 类型对象, 缺乏表现力,不知道它真正存储的是什么类型的值。如果将来开发人员使用 Store 结构,他们可能必须深入阅读接口文档或代码了解如何使用这些方法。因此,接收或返回 any 类型不能传达有意义的信息。此外,在编译时没有保护措施,没法阻止调用者使用任何数据类型(例如int)调用这些方法:

代码语言:javascript复制
s := store.Store{}
s.Set("foo", 42)

采用any类型,失去了Go作为静态类型语言的一些好处,我们应该避免使用 any 类型,尽可能使函数的签名明确。对于上面的例子,可能要为每种类型设置和获取各创建一个方法,示例代码如下。

代码语言:javascript复制
func (s *Store) GetContract(id string) (Contract, error) {
        // ...
}

func (s *Store) SetContract(id string, contract Contract) error {
        // ...
}

func (s *Store) GetCustomer(id string) (Customer, error) {
        // ...
}

func (s *Store) SetCustomer(id string, customer Customer) error {
        // ...
}

对比起来,上面这个新版本富有表现力,减少了函数名抽象不清晰的问题。虽然结构体Store的方法增加了,但这没有什么问题。因为客户端可以使用接口创建自己的抽象,例如,如果客户端只对Contract方法感兴趣,可以定义下面的接口:

代码语言:javascript复制
type ContractStorer interface {
        GetContract(id string) (store.Contract, error)
        SetContract(id string, contract store.Contract) error
}

那any类型在什么场景下是有用的呢?回到标准库中,来看看接收any参数的两个示例。第一个例子来至 encoding/json包,由于可以对任何类型进行序列化操作,所以Marshal函数接收的参数类型为any.

代码语言:javascript复制
func Marshal(v any) ([]byte, error) {
        // ...
}

第二个例子来至 database/sql 包,如果要执行参数化的查询,例如, SELECT * FROM FOO WHERE id = ?. 参数id可以是任何类型的数据,因此采用any作为接收参数。

代码语言:javascript复制
func (c *Conn) QueryContext(ctx context.Context, query string,
        args ...any) (*Rows, error) {
        // ...
}

总结,如果确实需要接收或返回任何可能的类型,采用any类型可能会有所帮助。例如,当涉及到序列化或语句格式化时。一般来说,我们应该极力避免过度概括编写的代码。如果能够增强代码的表达性或者带来其他收益,少量重复的代码有时是更好的选择。

0 人点赞