Fork me on GitHub

Go语言开发-过程式编程-switch语句-类型开关

5.2.2.2. 类型开关

之前我们讲解过类型断言,当使用interface{}类型的变量时,我们常常需要访问其底层数据。如果我们知道其类型,我们就可以使用类型断言,但是如果其类型可能是多种类型中的一种,我们就可以使用类型开关语句。

Go语言的类型开关语法如下:

可选声明与表达式开关和if语句是一样的,而case子句的工作方式与表达式开关一样,除了需要列出一个或多个分号分隔的类型。同时,可选的default子句和fallthrough语句的工作方式与表达式开关也是一样的,每个子句都由零个或多个语句组成。

类型开关守护是一个表达式其结果是一个类型。如果该表达式使用了:=操作符,则创建的变量的值为类型开关守护表达式中的值,其类型取决于case子句:如果在一个case子句中,类型列表中只有一种类型,则该变量的类型就为该类型;如果case子句含有不止一种类型,则变量的类型为类型开关守护表达式的类型。

对于面向对象的程序员来说,他们并不喜欢使用类型开关所支持的这种类型测试,反而更愿意依赖于多态。Go语言可以通过鸭子类型支持多态,但尽管如此,有时使用显式的类型测试显得更有意义。

下面的例子演示了如何调用一个简单类型的classifier函数及其结果输出:

classifier ()函数使用了一个简单的类型开关,它是一个可变参数的函数,也就是说,它可以接受数量可变的参数。因为参数类型是interface{},所以其参数可以是任意类型的。

这里使用的类型开关守护与类型断言中的设计是一样的,即variable.(Type),其中使用type关键字而不是使用实际类型可以用于表示任意类型。

有时我们可能希望同时访问一个interface(}的底层数据及其类型。接下来我们将会看到,这可以通过对类型开关守护进行赋值(使用:=操作符)来实现。

关于类型测试的一个常见用例是用于处理外部数据。例如,如果我们需要解析JSON(JavaScript Object Notation)编码的数据,我们就必须以某种方式将数据转换成相应的Go语言数据类型。这可以通过使用Go语言的json.Unmarsha()函数来做到这一点。如果我们指定一个指向成员变量与JSON数据相匹配的结构体的指针作为该函数的参数,该函数会将JSON数据的每一项转换为相对应的结构体成员变量类型的值,并将其填充到该结构体中。但是如果我们事先并不知道JSON数据的结构,我们就不能将结构体作为参数传给json.Unmarsha()函数。在这种情况下,我们可以传入一个指向interface{}的指针,这样json.lJnmarsha()函数会将其设置成指向map[string]interface{}的映射,其中该映射的键为JSON字段的名称,值为相应的的保存为interface{}的值。

下面的例子,演示了如何反序列化一个原始的未知结构的JSON对象及如何创建并以字符串的形式打印出JSON对象。

如果在反序列化时没有错误发生,interface{}类型的object变量就会指向一个map[string]interface{}类型的变量,其中键为该JSON对象的字段名称。jsonObjectAsString()函数接受一个这种类型的映射作为参数并返回相应的JSON字符串。这里我们使用了一个不安全的类型断言来将interface{}类型的对象转换成map[string]interface{}类型的jsonObject。

上面的函数将一个映射转换为一个JSON对象并返回相应的JSON格式表示的字符串。映射中的JSON列是以[]interface{}类型来表示的。关于JSON数组,该函数基于一个简单的假设:其元素为字符串类型的。

为了访问该映射中的数据,我们使用了一个for…range来遍历映射的键和值,同时使用了一个类型开关来处理每种不同类型的值。类型开关守护将一个值(interface(}类型)赋值给一个新的变量,该变量与相匹配的case子句具有相同的类型。这里使用影子变量是明智的(我们完全可以创建一个新的变量)。所以,如果interface{}的值的类型为bool,则value的类型也为bool,并会匹配第二个case子句,类似的,其它case子句的情况也是这样。

为了将数据写入到缓冲区,这里我们使用了fmt.Fprintf()函数,因为该函数比buffer.WriteString(fmt.Sprintf(…))函数更方便。fmt.Fprintf()函数可以将数据写入到一个io.writer中,其为该fmt.Fprintf()函数的第一个参数。虽然bytes.Buffer不是一个io.Writer,但是*bytes.Buffer是,这就是为什么我们传入的是buffer的地址。简而言之,io.writer是一个接口,可以被任何提供了相应Write()方法的值实现。bytes.Buffer.Write()方法接受一个指针接收器(即,一个*bytes.Buffer而不是bytes.Buffer类型的值)作为参数,所以只有*bytes.Buffer满足了接口,也就是说,我们必须将buffer的地址传递给fmt.Fprintf()函数,而不是buffer的值本身。

如果JSON对象包含JSON数组,我们可以使用for…range循环来遍历[]interface{}中的每个元素并使用安全类型断言,也就是说我们只有在这些元素确实为string类型时才将其添加到输出结果中。此外,我们又使用了影子变量(这次是string类型的s),这是因为我们需要的不是接口,而是该接口所引用的值。(类型断言请参见5.1.2节)

当然,如果我们事先知道原始JSON对象的结构,我们就可以大大的简化我们的代码。我们只需要使用一个结构体来保存数据并使用一个方法以字符串的形式输出这些数据。下面是在这种情况下反序列化并将其数据输出的代码示例。

这段代码看起来与之前的代码类似。然而,这里没必要使用jsonObjectAsString()函数,相反,我们需要定义一个State类型及相应的State.String()方法。

之前我们使用过类似的结构体。不过请注意每个成员变量必须以大写字母开头,这样该结构体就是可导出的(public),这是因为json.Unmarshal()函数只能填充可导出的成员变量。此外,尽管Go语言的encoding/json包并不区分数值类型,它会将所有的JSON数值视为float43类型的,但是json.Unmarshal()函数足够智能,必要时会自动填充其他数值类型的成员变量。

上面的方法返回JSON字符串表示的State值。

大部分Go程序应该不需要类型断言和类型开关;即使需要,也很少会使用。其中一个会使用的场景是当我们传递一个满足某个接口的值并需要检查其是否也满足另一个接口。(参见第6章)另一个使用场景是必须将来自外部的数据转换为Go语言的数据类型时。为了便于维护,最好将这些代码与程序的其余代码分离开来。这样可以使的程序能够完全按照Go语言的数据类型来工作,也意味着,可以将任何来自于外部来源的对于格式或类型的更改所导致的维护工作集中在较小的范围内。


目录


作者:Johnson
原创文章,版权所有,转载请保留原文链接。

发表评论

电子邮件地址不会被公开。 必填项已用*标注