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数据类型(数据与程序的隔离方便程序的维护)。