Go笔记3-Go流程控制和语法

流程控制

Go语言也不会免俗的加入基本的流程控制语句。这都是我们在其他语言当中都见到过的。当然也有没见到的和似曾相识的。 - 条件语句 if,else, else if - 选择语句 switch, case和select - 循环语句 for,range - 跳转语句 goto - 其他控制关键字:break,continue,fallthrough ###条件语句 我们可以编写如下条件语句:

if a < 5 {
    return 0
} else {
    return 1
}

短短的几行代码里,我们可以看到语法和C系非常相似。只是少了圆括号。 实际上,Go的判断语句与C区别更多: - 不需要圆括号 - 花括号对 {}必须存在,无论有几行代码 - 左边花括号必须和if或else同一行,不可换另一行 - 在if之后,条件语句之前,可以添加变量初始化语句,使用;间隔;

注:还有一点,Go语言编程一书中是这么写的: - 在有返回值的函数中,不允许将“最终的” return语句包含在if…else…结构中,否则会编译失败,会报告找不到return语句错误。 编译失败的案例如下:

    func example(x int) int {
        if x == 0 {
            return 5
        } else {
            return x
        }
    }

但是在我使用Go 1.6.2实际编写用例时,这个问题没有出现。说明这个问题应该是被Go开发团队所修复,不再成为Go语言的一个特性。

###选择语句 选择语句是我们常见的swtich,case等语句。但是它又不同于我们在别的语言里的switch语句。让我们看一个例子:

package main

import (
    "fmt"
)

func main() {
    i := 20
    whatNum(i)
}

func whatNum(n int) {
    switch n {
    case 10:
        fmt.Println("10")
    case 20:
        fallthrough
    case 30:
        fmt.Println("20 or 30")
    case 40, 50, 60:
        fmt.Println("40,50 or 60")
    default:
        fmt.Println("Other")
    }
}

当i = 10时,执行结果是:10 当i = 20或者30时,执行结果是:20 or 30 当i = 40,50或者60时,执行结果是:40,50 or 60 当i = 其他数值时,执行结果是Other

而且,switch后面可以省略条件判定变量,因此上面的判定函数还可以 如此修改:

func whatNum(n int) {
    switch {
    case n == 10:
        fmt.Println("10")
    case n == 20:
        fallthrough //这里的代码执行完毕后会因为这个关键字继续下落到下一个case继续执行。
    case n == 30:
        fmt.Println("20 or 30")
    case n == 40 || n == 50 || n == 60:
        fmt.Println("40,50 or 60")
    default:
        fmt.Println("Other")
    }

}

所以我们可以看到switch可以不设定switch之后的条件表达式,在此种情况下,整个switch结构与多个if…else…的逻辑作用等同。

除此以外, - 左花括号还是要和关键字在同一行上 - 条件表达式可以不限制为常量或者整数 - 一个case可以有多个选项结果,可以保证多个结果落入同一个case中 - 与C系语言反其道而行之,在switch中不设立break来阻止语句执行完毕继续下落执行, 而是通过一个新的关键字fallthrough指示程序继续下落执行。

###循环语句 与多数语言不同,Go只为循环准备了for关键字结构的语句,而没有提供while或者do-while结构。for的最原始用法和C系语言相近。

i := 10
for i := 0; i < n; i++ {
    fmt.Println("loop via:", i)
}

或者我们有时需要在逻辑内部要控制退出,而不是for结构本身,那么我们可以直接使用这种方式写一个死循环块:

    i := 10
    sum := 0
    for {
        sum += i
        fmt.Println("sum: ", sum)
        if sum > 100 {
            break
        }
    }

当然,for语句结构中,也支持多重赋值。不过写起来麻烦,可读性差,所以不做演示。 总结循环: - 老生常谈,左花括号不能自己开新行,必须跟在关键字行行尾 - Go中for和C类似,允许定义和初始化变量,并且允许多重赋值多个变量。但是不支持以逗号分割的多个赋值语句。 - 同样支持continue和break来控制循环,但是break允许中断一个指定的循环。

跳转语句

Go语言编程一书中也对Goto这个关键字的存在表示了惊讶。不过好在Go语言中的goto关键字支持的是一个标签的跳转,而非行数的跳转。

func myfunc() {
    i := 0
HERE:
    fmt.Println(i)
    i++
    if i < 10 {
        goto HERE
    }
}

函数

Go中的函数基本组成为:关键字func、函数名、参数表、返回值类型、函数体和返回语句,用一个最全面的函数定义来看一下函数的语法:

func Add(a int, b int) (ret int, err error) {
    if a < 0 || b < 0 { // 假设这个函数只支持两个非负数字的加法
        err = errors.New("Should be non-negative numbers!")
        return
    }
    return a + b, nil // 支持多重返回值
}

如果有若干个相邻参数类型相同,例如上面这个例子,我们可以省略前面变量的类型声明, 如下修改:

...
func Add(a, b int) (ret int, err error) {
...

多返回值:返回类型如果是多个不同的类型,如上所示需要用圆括号括起来,用逗号分割,返回时也需要由相同的return顺序。当然,有多个相同类型返回值也可以省略前面的返回的类型声明。

与其他语言不同的是,返回值列表跟参数列表一样,都是有变量的,除了上述return a+b,nil的写法以外,还可以仅仅为ret和err这两个返回值量进行赋值,然后只写return来结束函数。如下写法和上面示例是等同的:

func Add(a int, b int) (ret int, err error) {
    if a < 0 || b < 0 { // 假设这个函数只支持两个非负数字的加法
        err = errors.New("Should be non-negative numbers!")
        return
    }
    ret = a + b
    err = nil
    return
}

在多个返回值时,返回值量不可省略。但是但返回值是返回值量可以省略,圆括号也可以省略。就可以修改成这样:

func Add(a int, b int) int {
 ...
}

这个思维更适用于从其他语言转过来的使用习惯。

###函数调用 函数的调用十分简易,只要事先导入了这个包,通过包名.函数名即可调用这个函数。

import "mymath"// 假设Add被放在一个叫mymath的包中
// ...
c := mymath.Add(1, 2)

Go语言节省了函数的可见性关键字,通过函数的首字母大小写来决定是否让其他包调用函数。如果你的函数名首字母是小写,那么将不可以被本包以外的代码调用,函数名首字母是大写,是可以被外部代码所调用的。本规则也适用于类型和变量的可见性。

##不定长度参数 要使用不定长参数特性,需要如下定义和使用:

func myArgsFunc(args ...int) {
    for _, args := range args {
        fmt.Println(args)
    }
}

省略号 … 作为语法糖,他如果在多类型参数表中,必须放在最后一个参数位置上,然而实际上他是一个数组切片可以通过for 和range关键字来对它进行遍历。

注:for range的语法是我比较看着不舒服的地方,因为这一块代码没有看出来是哪一
种特定的语法,抑或是range关键字特定的语法?这是我感觉比较让人吐槽的地方。

然而,如果没有这种语法糖,那么参数args…int就不得不写成args []int,传入时也必须是一个数组切片。

不定长参数的传递 假设有另一个变参函数叫做myfunc3(args …int),假设这个参数有可能是个尾递归调用函数,那么下面的例子演示了如何向其传递变参:

func myfunc(args ...int) {
    // 按原样传递
    myfunc3(args...)
    // 传递片段,实际上任意的int slice都可以传进去
    myfunc3(args[1:]...) //还记得吗?切片范围[M:N]
}

任意类型的不定参数 如果你希望传任意类型,可以指定类型为interface{}。用interface{}传递任意数据类型是Go语言的惯例用法。这个就要谈一谈interface的用法了,这个后面再说。Go语言标准库中fmt.Printf()的函数原型:

func Printf(format string, args ...interface{}) {
    // ...
}

可以用这么个例子来说明用法:

package main

import "fmt"

func MyPrintf(args ...interface{}) {
    for _, arg := range args {
        switch arg.(type) {
        case int:
            fmt.Println(arg, "is an int value.")
        case string:
            fmt.Println(arg, "is a string value.")
        case int64:
            fmt.Println(arg, "is an int64 value.")
        default:
            fmt.Println(arg, "is an unknown type.")
        }
    }
}
func main() {
    var v1 int = 1
    var v2 int64 = 234
    var v3 string = "hello"
    var v4 float32 = 1.234
    MyPrintf(v1, v2, v3, v4)
}

执行结果为:

1 is an int value.
234 is an int64 value.
hello is a string value.
1.234 is an unknown type.

##多返回值

其实前面已经有很多例子已经展示了Go语言当中多返回值的用法和形式等等。Go语言的函数或者成员的方法可以有多个返回值,这个特性能够使我们写出比其他语言更优雅、更简洁的代码,比如File.Read()函数就可以同时返回读取的字节数和错误信息。如果读取文件成功,则返回值中的n为读取的字节数, err为nil,否则err为具体的出错信息:

func (file *File) Read(b []byte) (n int, err Error)

同样,从上面的方法原型可以看到,我们还可以给返回值命名,就像函数的输入参数一样。返回值被命名之后,它们的值在函数开始的时候被自动初始化为空。在函数中执行不带任何参数的return语句时,会返回对应的返回值变量的值.当然,我们如果对某一个返回值不关心的话,只要用’_‘占据那个返回值的位置就可以了,比如:

n, _ := f.Read(buf)

##匿名函数

匿名函数,是函数式编程语言的一种语法结构,在很多语言上都有体现,比如C#,Python等。Java 8中也支持了这一特性。Go作为一个更加现代的语言,匿名函数自然也是不能缺少的。看一个简单的例子:

package main

import (
    "fmt"
)

func main() {
    //赋给f变量值的,实际上是一个匿名函数
    f := func(x, y int) int {
        return x * y
    }
    fmt.Println(f(2, 3))
}

我们可以从上例中看到,匿名函数本身可以赋值给变量,其可以作为定义在其它函数当的一个临时计算法则,在使用时,变量名后面可以直接跟括号使用这个匿名函数。当然另外的方式记忆这个规则:匿名函数是先定义了函数输入输出规则以后,再规定其命名的。一般的函数都是输入输出规则和命名一同定义好的。但是匿名函数在赋值给一个变量时,他就更像是一个实例,可以被当作参数来传递。如果一个函数需要在定义时直接被执行,只要在函数体结束时直接跟参数表,相当于直接调用这个定义好的参数。示例:


package main

import (
    "fmt"
)

func main() {
    f := func(x, y int) int {
        return x * y
    }(2, 3)
    fmt.Println(f)

}

如上代码所示,此时的f变量赋值的是函数的执行结果,而不是函数实例本身,这两段示例代码的执行结果都是

6

###闭包

Go的匿名函数本身就是一个闭包,然而因为面向对象语言中使用一个单函数委托或者单函数接口的时候,闭包、lambda表达式、匿名函数都被指做一样语法结构。基本上都被直接认作lambda表达式。我并不讨论这是对是错。实际上闭包实际上是匿名函数或者lambda表达式的一种性质体现,应该说是正是因为他是“匿名函数”或“lambda表达式”,所以它拥有“闭包”这样的性质,或者说闭包是匿名函数的一种“情况”。所以你无论使用的是Python,还是Clojure,Java 8,C#,F#,Groovy,Scala,Lisp,等,都应该有这种意识,甚至是C++11的使用者也是如此。当然,不去抠它们的字眼,只是为了弄明白这些概念之间的关系。

讨论了这么多废话,我们还是得稍微说说什么是闭包。闭包是可以包含/使用在当前闭包结构的作用域当中定义的变量的代码块。

看一个简单的例子:

package main

import (
    "fmt"
)

func main() {
    factor := 3
    f := func(x int) int {
        return x * factor   //我们可以在闭包函数内部使用这个函数所在的作用域中的变量factor。
    }(2)
    fmt.Println(f)
}

这个匿名函数就是典型的闭包,它的函数体内访问并使用了这个匿名函数所在的作用域的变量。这就是为什么我说闭包是匿名函数的一种“情况”。 我们看一个稍微复杂的例子。

package main

import (
    "fmt"
)

func main() {
    func() func() {
        return func() {
            fmt.Println("Go Closure!")
        }
    }()()
}

执行结果是:

Go Closure!

有没有一点Lisp和Clojure的感觉了? 有没有感觉快要被括号淹没了?

这就是匿名函数的魅力。

错误处理

error接口

与C++和Java的try-catch异常(或者叫例外)块不同,Go语言使用的是error接口来体现运行过程中出现的意外状况。 error接口的定义为:

type error interface {
    Error() string
}

一般用作函数返回值当中:

func Foo(param int)(n int,err error) {
    //...
}

调用函数返回值时可以如下使用处理错误情况: n,err := Foo(0) if err != nil { //进行错误处理 }else { //无错误处理时 } 有点类似于Java中的throws关键字,可以用于抛出非运行时异常。

defer关键字

defer关键字在一句话执行时返回错误,但是defer仍然可以保证执行。有点像java中的finally关键字的作用。defer后面可以直接跟上要执行的语句,或者使用一个匿名函数来执行多个动作。 比如:

defer dstFile.Close()

或者

defer func(){
    ...
    dstFile.Close()
}

需要注意:一个函数当中多个defer相当于将动作放入一个栈中,最后一个“入栈”的defer语句将会先执行。

panic()函数和recover()函数

出现预定的错误时,在函数总可以调用panic函数,正常的函数执行流程会终止,defer语句将正常执行,该函数会返回到调用函数,并且逐层向上panic流程,直到所属的goroutine所有正在执行的函数被终止。错误信息将会被报告出来,包括在调用panic时调用的参数。这个过程就是错误的处理流程。 函数原型为:

func panic(interface {})

传入的参数为任意类型。

作用类似于Java中的throw关键字,用于手动抛出运行时异常

例如可以在函数中出现问题时如下方式调用:

panic(404)
panic("Local file not found")
panic(Error("netword unavaiable"))

recovery()作用是用于完成错误处理流程的一个机制。类似于Java和C++中catch代码块,可以通过在defer中去调用recovery()函数去完成错误处理过程。可以使用折中方式完成:


defer func() {
    if r := recover(); r != nil {
        log.Printf("Runtime error caught: %v", r)
    }
}()
//无论foo中是否触发错误处理流程,defer修饰的匿名函数都会在函数退出时执行。
foo()

defer作为程序发生“恐慌”(panic)时进行“补救”(recovery)时的一个很有用的关键字,与panic函数和recovery函数组合起来实现了类似C++和Java的try-catch机制。但是它更加灵活,组合更加自由,相比Java的死板的异常处理系统和多重的大括号,要方便许多。但是也要防止对defer、panic和recover的滥用,因为它看起来并没有像Java那样”具体”。

到此为止,我们学习了Go语言当中的基础类型系统,流程控制,函数以及错误处理。后面将会了解一些更加高级的一些语法。

注:在《Go语言编程》一书中,还举了一个排序算法比较程序的小范例,
我打算学完本书的的大部分知识以后再回过头来看这一节。算是给这份笔
记留一个坑吧。