Effictive Go(四)分号和控制结构

本文详细介绍了Go语言中的控制结构,包括if语句、for循环、switch语句及其使用方式。探讨了Go语言如何处理分号,以及如何利用range进行高效循环。此外,还深入分析了类型switch的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

分号

像C代码一样,Go的规范语法使用分号来断句,但和C不一样的是分号并不出现在源码中。词法分析器会在扫描的时候使用简单的规则自动插入分号,所以输入的文本大多不包含分号。

这个规则是,如果新行前的最后一个词元是一个标识符(包括int和float64这样的单词)一个基本的字符像一个数字或者字符串常量或者以下的词法单元之一

break continue fallthrough return ++ --

词法分析器总是在一个符号后插入分号。这可以总结为:“如果新行出现在一个可以结束一条语句的符号之后,则插入一个分号。”

在右大括号之前的分号也可以被省略掉,就像下面这条语句:

go func() { for { dst <- <-src } }()

不需要分号。地道的Go程序仅仅在for循环中才有分号,来区分初始化,条件和继续执行。分号也用于在一行中区分多条语句,这也是你应有的写代码的方式。

分号插入规则的一个结果就是,你无法将左大括号插入控制结构(if,for,switch,select)的下一行。如果你这样做了,一个分号将会插入到大括号之前,造成你不希望发生的结果。所以请这样写:

if i < f() {
    g()
}

而不是这样

if i < f()  // wrong!
{           // wrong!
    g()
}

控制结构

Go中的控制结构和C中的类似但也有重要的区别。其中没有do或者while循环,只有一个稍微广义的for循环;switch更加地灵活,if和switch像for循环一样接收一个可选的初始化语句。break和continue语句是作为一个可选项来决定中断的内容。这里还有新的控制结构包括类型switch和多路通信复用器,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语句并没有流向下一条语句,这是因为控制结构终结于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 := os.Open(name)

这条语句声明了两个变量,f和err。几行之后,调用f.Stat读入,

d, err := f.Stat()

这看起来好像声明了d和err。即使这样,注意err在两条语句中都出现了。这样的重复是合法的:err在第一条语句中被声明,但仅仅在第二条语句中被重赋值。这意味着调用f.Stat使用了之前声明的已存的err变量,并只是赋了新的值。

在”:=”声明中,变量v即使已经声明过也可以出现,前提是:

  • 作为已经存在的变量v的声明,声明在同一个域中(如果v已经声明在外围作用域,这次声明将会创建一个新的变量v)
  • 初始化中相应的值是可以赋给v的
  • 声明中至少有一个其他的变量被声明为新的变量
    这些不寻常的属性是纯粹的经验主义,例如在一个长的if-else链中使用单一的err值变得简单。你将会经常看到它的使用。
    在Go中值得一提的是函数参数和返回值的作用域和函数体的作用域是相同的,即使在词法上它们出现在包裹函数体的大括号之外。

For

Go中的for循环和C中的相似但又不完全相同。它结合了for和while但没有do-while。它有三种形式,但只有一种形式有分号。

// 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
}

如果你在数组、切片、字符串或者映射上进行循环,或者从管道中读取数据,range子句可以帮你管理循环。

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

如果你仅仅需要range中的第一项(键或索引),去掉第二项:

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

如果你仅仅需要range中的第二项(值),使用空白标志服——下划线,来丢弃第一个:

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

空白标识符有多种用法,就像之后的部分所描述的。

对于字符串来说,range会为你做更多的事情,通过解析UTF-8来拆分出单独的Unicode码点。错误的编码会消耗掉一个字节产生替换的符文(rune)U+FFFD(名字rune(与内建类型相关联的)是Go中用来指定单一编码点的术语。详情可查看the language specification获取更多细节)循环

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中使用多个变量,你应该使用并行赋值(即使会阻碍”++”和”–”。

// Reverse 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进行匹配。因此可以地道地将switch写成if-else-if-else链。

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可以由逗号分隔的列表表示。

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

就像其他类C的语言一样,break语句可以用来提前终结switch,但是却在Go语言中并不常见。有些时候,中断外围的循环而不是switch本身是很重要的。在Go中,可以通过放置一个标志在循环处并中断到该标志处来实现。这个例子同时展示了这两种用法。

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]<<shift)
        }
    }

当然,continue语句也接收一个可选标志,但它只能应用在循环中。

作为本章的结束,这里有一个用于比较字节切片的程序,使用了两个switch语句:

// 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
}

类型 switch

switch还可以用于获得一个接口变量的动态类型。这样的类型switch使用了在圆括号中的关键字type作为类型断言的语法。如果switch在表达式中声明了一个变量,这个变量将会在每一个子句中有相匹配的类型。在这样的case中复用名字是符合语法习惯的,事实上就是在每一个case中使用同样的名字声明一个新的变量,但使用的是不同的类型。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值