Branching
Go有三种分支语句:if, switch, select(后面章节讲解). 也可以通过map达到分支的效果,这个map的键用于选择分支,而值则为相应函数的调用(5.6.5会讲到)
If Statements
if语句有如下语法:
if optionalStatement1; booleanExpression1 {
block1
} else if optionalStatement2; booleanExpression2 {
block2
} else {
block3
}
if语句中的花括号必须要有,而分号只出现在有options statement. 从Go术语上来讲,options statement是一个简单的语句,可以是一个表达式,一个channel send(使用<-操作符), 自增或自减语句,赋值语句,或者一个短变量声明(:=). 哪果变量是在optional statement中创建的(e.g.,使用:=操作符), 它的作用域是从声明开始的地方到if语句的完成——所以它们存在于创建它们的if和else if中,以及之后的每个分支中,到整个if语句结束。
Boolean expression必须为bool类型。Go不会自动转换非布尔类型, 所以我们会一直使用比较操作符——比如,if i == 0.
我们在之前和之后才例子中都会使用到if语句。但我们还是先看两个小例子
// 标准的 ✓
if α := compute(); α < 0 {
fmt.Printf("(%d)\n", -α)
} else {
fmt.Println(α)
}
// 冗长的
{
α := compute()
if α < 0 {
fmt.Printf("(%d)\n", -α)
} else {
fmt.Println(α)
}
}
两段代码都是相同的效果。下面的代码必须使用一个额外的花括号用于限制变量α的作用域,而上面的代码会自动的限制变量的作域用在整个if语句中。
第二个例子是ArchiveFileList()函数,它是从archive_file_list(源文件archive_file_list/archiv_file_list.go)中摘录的。之后我们会使用这个函数的内容来比较if和swich语句
func ArchiveFileList(file string) ([]string, error) {
if suffix := Suffix(file); suffix == ".gz" {
return GzipFileList(file)
} else if suffix == ".tar" || suffix == ".tar.gz" || suffix == ".tgz" {
return TarFileList(file)
} else if suffix == ".zip" {
return ZipFileList(file)
}
return nil, errors.New("unrecognized archive")
}
这个程序读取命令行中指定的文件,可以处理(.gz, .tar, .tar.gz, .zip)后缀的档案文件,并打印它里面包含的文件。
需要注意的是suffix变量的作用域是从第一条if语句,到整个if ... else if ...语句的结尾。
func Suffix(file string) string {
file = strings.ToLower(filepath.Base(file))
if i := strings.LastIndex(file, "."); i > -1 {
if file[i:] == ".bz2" || file[i:] == ".gz" || file[i:] == ".xz" {
if j := strings.LastIndex(file[:i], ".");
j > -1 && strings.HasPrefix(file[j:], ".tar") {
return file[j:]
}
}
return file[i:]
}
return file
}
Suffix()函数接收一个文件名(也可能是路径), 并返回小写的文件扩展名。
Switch Statements
这里有两种switch语名:expression switchs and type switches. Expression switch跟C,C++和Java类似,而type switches是Go的特性。两种switch在语句构成是类似的,但跟C, C++和Java不同的时,Go的switch语句不会贯穿(即不需要在每个case后面添加break); 如果需要贯穿,则可以使用fallthrough语句。
Expression Switches
Go的Expression Switch语句的语法如下
switch optionalStatement; optionalExpression {
case expressionList1: block1
...
case expressionListN: blockN
default: blockD
}
只要有optional statement,而不管是否有optional expression,都必须要有分号。
如果switch没有option expression,编译器会假设表达式为true. optional statement跟if一样。如果变量是在optional statement(使用:=操作符)创建,则它的作用域从声明的地方到整个switch语句的结束——所以它们存在于每个case和default中。
为了使整个switch更高效,我们需要对cases按最有可能到最不可能进行排序(在有大量cases的情况或者swich被重复执行)。由于每个cases不会自动到下一个(fall through), 所以我们不需要使用到break. 如果我们需要到下一个,可以使用 fallthrough语句。default是可选的,并且可以存在任何地方。 如果没有匹配case, 而又存在default,则执行default.
每一个case并需有一个或者多个逗号分隔的表达式,它们的类型必须跟switch语句中的optional expression的类型一样。如果在swich后面没有optional expression,则编译器设置它为true, 因此每一个case子句中的表达式必须求值(evaluate)为bool类型.
如果一个case或default子句有break语句,整个switch语句将即退出,控制权交给跟在switch语句之后的语句,或者——如果break指定了标签——可以到for, switch 或者select最里面的语句(标签)
以下这个例子没有option statement 和 optional expression
func BoundedInt(minimum, value, maximum int) int {
switch {
case value < minimum:
return minimum
case value > maximum:
return maximum
}
return value
}
由于这里option expression,编译器设置switch的表达式为true, 所以每个case子句的表达式必须求值为一个bool.
switch {
case value < minimum:
return minimum
case value > maximum:
return maximum
default:
return value
}
panic("unreachable")
这是另一个BoundedInt()函数的函数体。switch语句会覆盖每一种可能。所以这个函数的控制权不会肯定不会到达函数的结尾。但是, Go认为有返回值的函数应当以return或者panic()结尾,所以我们使用了后者,更好的表达函数的语义.
在上一节中,我们展示了ArchiveFileList()函数. 以下switch的版本
switch suffix := Suffix(file); suffix { // Naïve and noncanonical!
case ".gz":
return GzipFileList(file)
case ".tar":
fallthrough
case ".tar.gz":
fallthrough
case ".tgz":
return TarFileList(file)
case ".zip":
return ZipFileList(file)
}
以下是更简洁的一个版本
switch Suffix(file) { // Canonical ✓
case ".gz":
return GzipFileList(file)
case ".tar", ".tar.gz", ".tgz":
return TarFileList(file)
case ".zip":
return ZipFileList(file)
}
Type Switches
我们在上一节type assertions中提到过,当我们的变量是interface{}类型时,我们经常要访问它潜在的值。如果我们知道这个类型,我们可以直接使用类型断言(type assertion), 但如果这个类型是若干可能类型中的一个,则我们需要使用type switch语句。
type switch语句有如下语法:
switch optionalStatement; typeSwitchGuard {
case typeList1: block1
...
case typeListN: blockN
default: blockD
}
optional statement跟expression switches和if语句是一样的。case子句也跟expression switches一样,但不同的是它们列出一个或者多个以逗号分隔的类型。 default子句是可选的, fallthrough语句跟expression switches一样。
type switch语法中的typeSwitchGuard是一个表达式,它的结果是一个类型value.(type)。如果这个表达式通过:=操作符,赋值给一个变量。则这个变量保存的是在typeSwitchGuard表达式的值(valut.(type)中的value), 但这个变量的类型依赖于case子句:如果匹配的case子句类型列表只有一个类型, 这个变量的类型为caes子句的类型,如果case子句类型列表为两个或者多个,则变量的类型是 type switch guard表达式。
对于面向对像的程序员来说,肯定不会喜欢用type switch语句来做类型测试,而更愿意依赖多态性。 Go是通过鸭子类型支持多态,但不管怎么样,有时明确的类型测试是有意义的。
下面的例子展示了如何调用一个简单的类型分类(classifier)函数,并且查看它的输出
classifier(5, -17.9, "ZIP", nil, true, complex(1, 1))
/*
param #0 is an int
param #1 is a float64
param #2 is a string
param #3 is nil
param #4 is a bool
param #5's type is unknown
*/
classifier()函数使用了一个简单的type switch. 它是一个可变参数的函数。 由于它的参数类型是interface{},所以参数是可以是任何类型(函数,可变函数以及省略号...,将在5.6中讲解)
func classifier(items ...interface{}) {
for i, x := range items {
switch x.(type) {
case bool:
fmt.Printf("param #%d is a bool\n", i)
case float64:
fmt.Printf("param #%d is a float64\n", i)
case int, int8, int16, int32, int64:
fmt.Printf("param #%d is an int\n", i)
case uint, uint8, uint16, uint32, uint64:
fmt.Printf("param #%d is an unsigned int\n", i)
case nil:
fmt.Printf("param #%d is nil\n", i)
case string:
fmt.Printf("param #%d is a string\n", i)
default:
fmt.Printf("param #%d's type is unknown\n", i)
}
}
}
type switch guard ( x.(type) )跟类型断言的用法一样,variable.(Type), 但它用的是关键字 type替换实际的类型。
有时我们需要访问interface{}真实的值以及它的类型,这可以通过将type switch guard赋值给一个变量(使用:=操作符), 我们将在后面看到它的使用。
使用case做类型测试的常见情况是当我们在处理外部数据时。比如我们在解析JSON数据时,我们必以某种法方将数据转换为相应的Go数据类型。这也可以通过使用Go的json.Unmarshal()函数。如果我们给json.Unmarshal()函数传递了一个struct的指针参数,这个struct的字段跟一定要跟JSON的数据匹配,则这个函数将JSON数据中的每一项填充到struct相应数据类型的字段中。但是如果我们事先不知道JSON数据的结构,则我们不能给json.Unmarshal()函数传递一个struct。这种情况下我们可以给这个函数传递一个interface{}类型的指针,json.Unmarshal()将设置这个指针指向一个map[string]interface{}引用,这个map的键是JSON字段的名字,它的值是相应的JSON的值,但类型为interface{};
以下这个例子将向我们展示如何反编列一个原始的JSON对像(结构未知),以及如何创建一个字符串表示的JSON对像,并打印这个字符串。
MA := []byte(`{"name": "Massachusetts", "area": 27336,
"water": 25.7, "senators": ["John Kerry", "Scott Brown"]}`)
var object interface{}
if err := json.Unmarshal(MA, &object); err != nil {
fmt.Println(err)
} else {
jsonObject := object.(map[string]interface{}) ➊
fmt.Println(jsonObjectAsString(jsonObject))
}
/*
{"senators": ["John Kerry", "Scott Brown"], "name": "Massachusetts",
"water": 25.700000, "area": 27336.000000}
*/
如果反编列的过程中出现错误,interface{}类型的变量object将引用一个map[string]interface{}类型的变量,它的键为JSON对应的字段名。 jsonObjectAsString()函数接受一个map[string]interface{}的参数,并返回相应的JSON字符串。我们在➊中使用了一个unchecked type assertion,将一个interface类型的对像转换为map[string]interface{}类型的变量jsonObject.
func jsonObjectAsString(jsonObject map[string]interface{}) string {
var buffer bytes.Buffer
buffer.WriteString("{")
comma := ""
for key, value := range jsonObject {
buffer.WriteString(comma)
switch value := value.(type) { // shadow variable
case nil:
fmt.Fprintf(&buffer, "%q: null", key)
case bool:
fmt.Fprintf(&buffer, "%q: %t", key, value)
case float64:
fmt.Fprintf(&buffer, "%q: %f", key, value)
case string:
fmt.Fprintf(&buffer, "%q: %q", key, value)
case []interface{}:
fmt.Fprintf(&buffer, "%q: [", key)
innerComma := ""
for _, s := range value {
if s, ok := s.(string); ok { // shadow variable
fmt.Fprintf(&buffer, "%s%q", innerComma, s)
innerComma = ", "
}
}
buffer.WriteString("]")
}
comma = ", "
}
buffer.WriteString("}")
return buffer.String()
}
这个函数将一个map表示的JSON对应转换为字符串表示。JSON的数组在map中是以[]interface{}表示。这里只是简单的假设JSON数组只包含字符串元素。
为了访问数据,我们使用for .. range循环遍历map的key和它的值,并使用type switch来访问这些值以及类型。这个switch语句的type switch guard将value(interface{}类型)赋值给一个新的变量,这个新的变量也称为value, 它的类型为相匹配到的case. 所以如果value(interface{})是一个布尔类型,则内部的value将是一个bool类型,将且匹配第二个分支。
为了将值写入到buffer, 我们使用了fmt.Fprintf()函数,这对比于buffer.WriteString(fmt.Sprintf(...))来说更方便。fmt.Fprintf()函数的第一个参数是io.Writer类型。 一个bytes.Buffer不是一个io.Writer——但是*bytes.Buffer是,这也是什么我们要传递buffer地址的原因。更多相关的细节将在第6章讲到,在这只是简单说下,io.Writer是一个接口,任何值只要有相应的Writer()方法,它就实现了io.Writer接口。 bytes.Buffer.Write()方法接受的接收者receiver是一个指针(i.e., *bytes.Buffer, 而不是bytes.Buffer value), 所以只有*bytes.Buffer满足接口。所以我们只能传递buffer的地址给fmt.Fprintf()函数,而不能是buffer value自身。
如果一个JSON对像包含了数组,我们使用内部for ... range循环来遍历[]interface{}的元素,并且使用checked type assertion,这样添加到输出的只能是字符串。
当然,如果我们知道原始的JSON对像的结构,我们可以简化代码。我们需要一个struct来保存数据和一个方法用来输出它的字符串形式。如下所示
var state State
if err := json.Unmarshal(MA, &state); err != nil {
fmt.Println(err)
}
fmt.Println(state)
/*
{"name": "Massachusetts", "area": 27336, "water": 25.700000,
"senators": ["John Kerry", "Scott Brown"]}
*/
这段代码跟之前的很想似。但是,这里不需要使用到jsonObjectAsString()函数,而是定义一个State类型和相应的State.Stirng()方法。
type State struct {
Name string
Senators []string
Water float64
Area int
}
这个struct跟之前的很相似,但需要注意的是,每一个字段首字母必须大写,使它可以导出(public)。这是因为json.Unmarshal()函数只会填充public字段。此外,虽然Go的encoding/json包不能区分json中的数字类型——所有的JSON数字以float64对待——json.Unmarshal()函数却可以在有必要时,智能的填充其它数字类型的字段。
func (state State) String() string {
var senators []string
for _, senator := range state.Senators {
senators = append(senators, fmt.Sprintf("%q", senator))
}
return fmt.Sprintf(
`{"name": %q, "area": %d, "water": %f, "senators": [%s]}`,
state.Name, state.Area, state.Water, strings.Join(senators, ", "))
}
这个方法返回JSON数据的字符串表示。
许多GO程序都不会使用到type assertions 和 type switches; 一种使用情况是当我们传递值满足某一个接口,而我们又要确认它是否符合另一个接口时(原书第6章,§6.5.2, ➤ 289.). 另一种情况是数据来源于外部。而我们又要将这些数据转换为Go数据类型(数据与程序的隔离方便程序的维护)。