gopl-zh.github.com/ch7/ch7-13.md

5.4 KiB
Raw Blame History

7.13. 类型分支

接口被以两种不同的方式使用。在第一个方式中以io.Readerio.Writerfmt.Stringersort.Interfacehttp.Handler和error为典型一个接口的方法表达了实现这个接口的具体类型间的相似性但是隐藏了代码的细节和这些具体类型本身的操作。重点在于方法上而不是具体的类型上。

第二个方式是利用一个接口值可以持有各种具体类型值的能力将这个接口认为是这些类型的联合。类型断言用来动态地区别这些类型使得对每一种情况都不一样。在这个方式中重点在于具体的类型满足这个接口而不在于接口的方法如果它确实有一些的话并且没有任何的信息隐藏。我们将以这种方式使用的接口描述为discriminated unions可辨识联合

如果你熟悉面向对象编程你可能会将这两种方式当作是subtype polymorphism子类型多态和 ad hoc polymorphism非参数多态但是你不需要去记住这些术语。对于本章剩下的部分我们将会呈现一些第二种方式的例子。

和其它那些语言一样Go语言查询一个SQL数据库的API会干净地将查询中固定的部分和变化的部分分开。一个调用的例子可能看起来像这样

import "database/sql"

func listTracks(db sql.DB, artist string, minYear, maxYear int) {
	result, err := db.Exec(
		"SELECT * FROM tracks WHERE artist = ? AND ? <= year AND year <= ?",
		artist, minYear, maxYear)
	// ...
}

Exec方法使用SQL字面量替换在查询字符串中的每个'?'SQL字面量表示相应参数的值它有可能是一个布尔值一个数字一个字符串或者nil空值。用这种方式构造查询可以帮助避免SQL注入攻击这种攻击就是对手可以通过利用输入内容中不正确的引号来控制查询语句。在Exec函数内部我们可能会找到像下面这样的一个函数它会将每一个参数值转换成它的SQL字面量符号。

func sqlQuote(x interface{}) string {
	if x == nil {
		return "NULL"
	} else if _, ok := x.(int); ok {
		return fmt.Sprintf("%d", x)
	} else if _, ok := x.(uint); ok {
		return fmt.Sprintf("%d", x)
	} else if b, ok := x.(bool); ok {
		if b {
			return "TRUE"
		}
		return "FALSE"
	} else if s, ok := x.(string); ok {
		return sqlQuoteString(s) // (not shown)
	} else {
		panic(fmt.Sprintf("unexpected type %T: %v", x, x))
	}
}

switch语句可以简化if-else链如果这个if-else链对一连串值做相等测试。一个相似的type switch类型分支可以简化类型断言的if-else链。

在最简单的形式中一个类型分支像普通的switch语句一样它的运算对象是x.(type)——它使用了关键词字面量type——并且每个case有一到多个类型。一个类型分支基于这个接口值的动态类型使一个多路分支有效。这个nil的case和if x == nil匹配并且这个default的case和如果其它case都不匹配的情况匹配。一个对sqlQuote的类型分支可能会有这些case

switch x.(type) {
	case nil:       // ...
	case int, uint: // ...
	case bool:      // ...
	case string:    // ...
	default:        // ...
}

和(§1.8)中的普通switch语句一样每一个case会被顺序的进行考虑并且当一个匹配找到时这个case中的内容会被执行。当一个或多个case类型是接口时case的顺序就会变得很重要因为可能会有两个case同时匹配的情况。default case相对其它case的位置是无所谓的。它不会允许落空发生。

注意到在原来的函数中对于bool和string情况的逻辑需要通过类型断言访问提取的值。因为这个做法很典型类型分支语句有一个扩展的形式它可以将提取的值绑定到一个在每个case范围内都有效的新变量。

switch x := x.(type) { /* ... */ }

这里我们已经将新的变量也命名为x和类型断言一样重用变量名是很常见的。和一个switch语句相似地一个类型分支隐式的创建了一个词法块因此新变量x的定义不会和外面块中的x变量冲突。每一个case也会隐式的创建一个单独的词法块。

使用类型分支的扩展形式来重写sqlQuote函数会让这个函数更加的清晰

func sqlQuote(x interface{}) string {
	switch x := x.(type) {
	case nil:
		return "NULL"
	case int, uint:
		return fmt.Sprintf("%d", x) // x has type interface{} here.
	case bool:
		if x {
			return "TRUE"
		}
		return "FALSE"
	case string:
		return sqlQuoteString(x) // (not shown)
	default:
		panic(fmt.Sprintf("unexpected type %T: %v", x, x))
	}
}

在这个版本的函数中在每个单一类型的case内部变量x和这个case的类型相同。例如变量x在bool的case中是bool类型和string的case中是string类型。在所有其它的情况中变量x是switch运算对象的类型接口在这个例子中运算对象是一个interface{}。当多个case需要相同的操作时比如int和uint的情况类型分支可以很容易的合并这些情况。

尽管sqlQuote接受一个任意类型的参数但是这个函数只会在它的参数匹配类型分支中的一个case时运行到结束其它情况的它会panic出“unexpected type”消息。虽然x的类型是interface{}但是我们把它认为是一个intuintboolstring和nil值的discriminated union可识别联合