Bootstrap

翻译: Effective Go (3)

控制结构

Go的控制结构和C类似,但还是有很多重点不一样。Go没有do或者while循环,只有一个更通用的for循环;switch相比也更灵活;if和switch可以像for循环一样,接受一个初始化语句;break和continue语句带有一个可选标签标志需要break或者continue的内容;新增了一个新的控制结构,包括一个类型选择和多路通信复用器——select;控制结构的语法相比也有细微的差别:没有小括号,并且主体必须用大括号分隔。

If

在Go中,一个简单的if应是这样:

if x > 0 {
    return y
}

强制使用大括号鼓励将简单的if语句分成多行编写,这是一个很好的风格,尤其当主体语句中包括一个控制语句(例如return或break)。

由于if和switch接受一个初始化语句,因此通常可以用来声明局部变量。

if err := file.Chmod(0664); err != nil {
    log.Print(err)
    return err
}

在Go的库中,你会发现if语句当不会执行下一条语句时——即if主体结束于break,continue,goto或return时,将会省略不必要的else。

f, err := os.Open(name)
if err != nil {
    return err
}
codeUsing(f)

上面的就是一个常见情况的示例,这种情况下,代码必须防范一系列错误情况,如果控制流继续运行到底,说明错误已经被全部处理。由于错误的情况通常以return返回,所以自然不需要else语句了。

f, err := os.Open(name)
if err != nil {
    return err
}
d, err := f.Stat()
if err != nil {
    f.Close()
    return err
}
codeUsing(f, d)

重复声明和重新赋值

上面的示例演示了 := 短变量声明的一些细节。调用os.Open的声明为:

这个语句声明了两个变量,f和err。几行后,f.Stat的调用:

似乎又声明了d和err两个变量。但是请注意,虽然err出现在了两个地方,但是这样的重复是合法的:err在第一个语句中声明,第二个语句只是重新赋值而已。也就是说,调用f.Stat只是使用了先前声明的,已存在的err变量,重新进行了赋值。

在 := 声明中,可以出现已经声明的变量v,条件是:

  • 本声明和v的声明在同一作用域。(如果v在另一个作用域声明,那么本次声明会创建一个新变量。注)

  • 两次声明的类型相同

  • 本次的声明至少创建了一个新的变量

这个特性纯粹是实用主义,你会发现在if-else长链,经常看见只要简单的使用一个err变量。

注:值得注意的是,Go的函数入参、返回值的作用域和函数体相同,即使它们按词法出现在包围主体的括号之外。

For

Go的for循环和C不完全一样。它统一了for和while循环,并且没有do-while循环。一共有三种for循环,只有一种有分号。

// Like a C for
for init; condition; post { }

// Like a C while
for condition { }

// Like a C for(;;)
for { }

短变量声明让索引变量声明变得轻松:

sum := 0
for i := 0; i < 10; i++ {
    sum += i
}

如果要循环数组(array),切片(slice),字符串(string),映射(map),或读取一个管道(channel),range可以更高效:

for key, value := range oldMap {
    newMap[key] = value
}

如果在range中只需要第一项,键(key)或者索引(index),只需要删除第二项:

for key := range m {
    if key.expired() {
        delete(m, key)
    }
}

如果在range中只需要第二项,则需要使用空白标识符(下划线)丢弃第一项:

sum := 0
for _, value := range array {
    sum += value
}

空白标示符有很多用法,可见.

至于string,range做了更多的工作。通过解析UTF-8来分解单个Unicode码点,错误的编码会占用一个字节,并产生替换rune U+FFFD。(内建类型rune用来表示一个Unicode码点。有关详细信息可以参考语言规范)。下面的循环:

for pos, char := range "日本\x80語" { // \x80 is an illegal UTF-8 encoding
    fmt.Printf("character %#U starts at byte position %d\n", char, pos)
}

会输出:

character U+65E5 '日' starts at byte position 0
character U+672C '本' starts at byte position 3
character U+FFFD '�' starts at byte position 6
character U+8A9E '語' starts at byte position 7

最后,Go没有逗号运算符,而 ++ 和 --为语句而不是表达式。因此,如果想在for循环中使用多个变量,你应该使用平行赋值。(尽管这排除了++和--)

// 反转 a
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
    a[i], a[j] = a[j], a[i]
}

Switch

Go的switch比C更为通用。其表达式不需要是常量或者整数,case会从上到下执行,直到有匹配,如果switch后面没有表达式,则匹配true,因此,可以将if-else-if-else长链写成一个switch,这也更符合Go的习惯。

func unhex(c byte) byte {
    switch {
    case '0' <= c && c <= '9':
        return c - '0'
    case 'a' <= c && c <= 'f':
        return c - 'a' + 10
    case 'A' <= c && c <= 'F':
        return c - 'A' + 10
    }
    return 0
}

switch不会自动执行所有case,相同条件的case可以通过逗号放在一起:

func shouldEscape(c byte) bool {
    switch c {
    case ' ', '?', '&', '=', '#', '+', '%':
        return true
    }
    return false
}

break语句可以用来提前结束switch,但不像其他类C语言中那么常用。有时候不仅需要中断switch,还需要中断外层的循环,在Go中就可以通过在循环前放置一个标签,然后break这个标签来完成。下面展示了这两种用法:

Loop:
	for n := 0; n < len(src); n += size {
		switch {
		case src[n] < sizeOne:
			if validateOnly {
				break
			}
			size = 1
			update(src[n])

		case src[n] < sizeTwo:
			if n+1 >= len(src) {
				err = errShortInput
				break Loop
			}
			if validateOnly {
				break
			}
			size = 2
			update(src[n] + src[n+1]<

当然,continue语句也可以接受一个可选的标签,不过仅用于循环。

本节的结束示例,是一个使用两个switch语句的字节切片(byte slices)的比较程序:

// Compare returns an integer comparing the two byte slices,
// lexicographically.
// The result will be 0 if a == b, -1 if a < b, and +1 if a > b
func Compare(a, b []byte) int {
    for i := 0; i < len(a) && i < len(b); i++ {
        switch {
        case a[i] > b[i]:
            return 1
        case a[i] < b[i]:
            return -1
        }
    }
    switch {
    case len(a) > len(b):
        return 1
    case len(a) < len(b):
        return -1
    }
    return 0
}

类型选择(Type switch)

switch也可以用于确认接口的动态类型。类型选择使用小括号中内建的关键字type进行类型断言。若switch后面的表达式声明了变量,则该变量应具备某个子句中相应的类型。这种情况下,通常会重用变量名,但实际上会声明一个具有相同名称但类型不同的新变量。

var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
    fmt.Printf("unexpected type %T\n", t)     // %T prints whatever type t has
case bool:
    fmt.Printf("boolean %t\n", t)             // t has type bool
case int:
    fmt.Printf("integer %d\n", t)             // t has type int
case *bool:
    fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
    fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}