- Go语言的switch语句又分为
表达式switch语句
和类型switch语句
。每一个case可以携带一个表达式
或一个类型说明符
。所谓switch表达式
是指switch语句中要被判定的那个表达式
,其会依据该表达式的结果与各个case表达式的结果是否相同来决定执行哪个分支。可以有只包含一个字面量
或标识符的表达式
。它们是最简单的表达式,属于基本表达式的一种
。只要被发现其表达式与switch表达式的结果相同,该case语句就会被选中。它包含的那些语句就会被执行,而其余的case语句则会被忽略
。
- switch语句还可以包含
初始化子句
:例如:
names := []string{"Golang", "Java", "Rust", "C"}
switch name := names[0]; name {
case "Golang":
fmt.Println("A programming language from Google.")
case "Rust":
fmt.Println("A programming language from Mozilla.")
default:
fmt.Println("Unknown!")
}
- 类型switch语句
1、紧随case关键字的不是表达式
,而是类型说明符
。类型说明符由若干个类型字面量
组成,且多个类型字面量之间由英文逗号
分隔。
2、它的switch表达式不仅起到了类型断言
的作用,而且其表现形式很特殊,如:v.(type)
,其中v必须代表一个接口类型的值
。注意,该类表达式只能出现在类型switch语句
中,且只能充当switch表达式
。
v := 11
switch i := interface{}(v).(type) {
case int, int8, int16, int32, int64:
fmt.Printf("A signed integer: %d. The type is %T. \n", i, i)
// A signed integer: 11. The type is int.
case uint, uint8, uint16, uint32, uint64:
fmt.Printf("A unsigned integer: %d. The type is %T. \n", i, i)
default:
fmt.Println("Unknown!")
}
-
fallthrough
:它既是一个关键字
,又可以代表一条语句
。fallthrough语句
可被包含在表达式switch语句中的case语句中
。它的作用是使控制权流转到下一个case
。不过要注意,fallthrough语句仅能作为case语句中的最后一条语句出现
;并且包含它的case语句
不能是其所属switch语句的最后一条case语句
。
package main
import (
"fmt"
"math/rand"
)
func main() {
// 创建一个空接口数组,任何预定义的数据类型都是空接口的实现
ia := []interface{}{byte(6), 'a', uint(10), int32(-4)}
//类型switch语句,初始化子句
// 表达式rand.Intn(4)结果会是一个范围在[0,4)的随机数。
switch v := ia[rand.Intn(4) % 2]; interface{}(v).(type) {
case uint, rune :
fmt.Printf("Case A.")
case byte :
fmt.Printf("Case B.")
default:
fmt.Println("Unknown!")
}
// 输出的随机结果为:Case A.
}
- for语句代表着循环。一条语句通常由关键字
for
、初始化子句
、条件表达式
、后置子句
和以花括号包裹的代码块
组成,例如:
for i := 0; i < 10; i++ {
fmt.Print(i, " ")
}
- 其中,可以省略掉
初始化子句
、条件表达式
、后置子句
中的任何一个或多个
,不过起到分隔作用的分号
一般需要被保留下来
,除非在仅有条件表达式
或三者全被省略时分号才可以被一同省略
。
- range子句包含
一个或两个迭代变量
(用于与迭代出的值绑定)、特殊标记:=
或=
、关键字range
以及range表达式
。其中,range表达式的结果值的类型
应该是能够被迭代的,包括:字符串类型
、数组类型
、数组的指针类型
、切片类型
、字典类型
和通道类型
。例如:
for i, v := range "Go语言" {
fmt.Printf("%d: %c\n", i, v)
}
- 注意:一个中文字符在经过UTF-8编码之后会表现为
三个字节
。
- 对字典值上的迭代,Go语言是
不保证其顺序
的。
- 携带range子句的for语句还可以应用于一个
通道值
之上。其作用是不断地从该通道值中接收数据
,不过每次只会接收一个值
。注意,如果通道值中没有数据
,那么for语句的执行会处于阻塞状态
。无论怎样,这样的循环会一直进行下去。直至该通道值被关闭,for语句的执行才会结束。
- 字符串的底层是以
字节数组
的形式存储的。在Go语言中,字符串到字节数组的转换
是通过对其中的每个字符进行UTF-8编码来完成的
。
package main
import (
"fmt"
)
func main() {
map1 := map[int]string{1: "Golang", 2: "Java", 3: "Python", 4: "C"}
// for-each 循环
for index, value := range map1 {
fmt.Printf("%d: %s\n", index, value)
}
fmt.Println("===================")
// range表达式可以是数组类型
for _, v := range []int{1, 2, 3, 4} {
fmt.Printf("%d: %s\n", v, map1[v])
}
}
- select语句属于
条件分支流程控制方法
,不过它只能用于通道
。它可以包含若干条case语句
,并根据条件选择其中的一个执行。进一步说,select语句中的case关键字
只能后跟用于通道的发送操作的表达式
以及接收操作的表达式
或语句
。
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
// 省略若干条语句
select {
case e1 := <- ch1: // 从通道中取值
fmt.Printf("1th case is selected. e1=%v.\n", e1)
case e2 := <-ch2:
fmt.Printf("2th case is selected. e2=%v.\n", e2)
default:
fmt.Println("No data!")
}
- 如果该select语句被执行时
通道ch1和ch2中都没有任何数据
,那么肯定只有default case会被执行
。只要有一个通道
在当时有数据
就不会轮到default case
执行了。显然,对于包含通道接收操作的case
来讲,其执行条件就是通道中存在数据
(或者说通道未空
)。如果在当时有数据的通道多于一个
,那么Go语言会通过一种伪随机算法
来决定哪一个case将被执行。另一方面,对于包含通道发送操作的case
来讲,其执行条件就是通道中至少还能缓冲一个数据
(或者说通道未满
)。类似的,当有多个case中的通道未满
时,它们会被随机选择。
ch3 := make(chan int, 100)
// 省略若干条语句
// 外层为for循环,相当于用有限范围的随机整数集合去填满一个通道。
select {
case ch3 <- 1:
fmt.Printf("Sent %d\n", 1)
case ch3 <- 2:
fmt.Printf("Sent %d\n", 2)
default:
fmt.Println("Full channel!")
}
- 注意:若一条select语句中不存在
default case
, 并且在被执行时其中的所有case都不满足执行条件
,那么它的执行将会被阻塞
!当前流程的进行也会因此而停滞。直到其中一个case满足了执行条件,执行才会继续。未被初始化的通道
会使操作它的case永远满足不了执行条件
。对于针对它的发送操作
和接收操作
来说都是如此。
-
break语句
也可以被包含在select语句
中的case语句
中。它的作用是立即结束当前的select语句的执行
,不论其所属的case语句中是否还有未被执行的语句。
package main
import "fmt"
func main() {
ch4 := make(chan int, 1)
for i := 0; i < 4; i++ {
select {
case e, ok := <- ch4: // 从通道中取值, 第一次阻塞,默认执行default
fmt.Println("case上,当前通道实际容量为:", len(ch4))
if !ok { // ok表示通道的状态,第三次满足条件
fmt.Println("End.")
return // 退出程序
}
fmt.Println("case下,当前通道实际容量为:", len(ch4))
fmt.Println(e)
close(ch4) // 第二次循环关闭通道
default:
fmt.Println("default上,当前通道实际容量为:", len(ch4))
fmt.Println("No Data!")
ch4 <- 1 // 第一次向通道发送数据
fmt.Println("default下,当前通道实际容量为:", len(ch4))
}
}
}
// 通道测试
package main
import "fmt"
func main(){
ch1 := make(chan int, 3) // 声明并初始化了一个元素类型为 int、容量为 3 的通道 ch1
ch1 <- 2
ch1 <- 1
ch1 <- 3
fmt.Println("当前通道实际容量为:", len(ch1))
elem1 := <-ch1 // 从通道中取值
fmt.Printf("The first element received from channel ch1: %v\n", elem1)
fmt.Println("当前通道实际容量为:", len(ch1))
// 输出结果
// 当前通道实际容量为: 3
// The first element received from channel ch1: 2
// 当前通道实际容量为: 2
}
-
defer语句
仅能被放置在函数
或方法
中。它由关键字defer
和一个调用表达式
组成。注意:这里的调用表达式
所表示的既不能是对Go语言内建函数的调用
也不能是对Go语言标准库代码包unsafe中的那些函数的调用
。
- 注意:当一个函数中存在
多个defer语句
时,它们携带的表达式语句的执行顺序一定是它们的出现顺序的倒序
。
-
defer携带的表达式语句
代表的是对某个函数或方法的调用
。这个调用可能会有参数传入,比如:fmt.Print(i + 1)
。如果代表传入参数的是一个表达式
,此时表达式求值与被携带的表达式语句的执行时机是不同的。若defer携带的表达式语句
代表的是对匿名函数的调用
,那么我们就一定要非常警惕。
package main
import (
"fmt"
"io/ioutil"
"os"
)
// 读出指定文件或目录本身的内容并返回
func readFile(path string) ([]byte, error) {
file, err := os.Open(path) // os和ioutil代表的都是Go语言标准库中的代码包。
if err != nil { // 当有错误发生时立即向调用方报告。
return nil, err
}
defer file.Close()
return ioutil.ReadAll(file)
}
// 注意,当这条defer语句被执行的时候,其中的这条表达式语句并不会被立即执行。
// 它的确切的执行时机是在其所属的函数(这里是readFile)的执行即将结束的那个时刻。
// 也就是说,在readFile函数真正结束执行的前一刻,file.Close()才会被执行。
// 该语句可以保证在readFile函数将结果返回给调用方之前,那个文件或目录一定会被关闭。
// 其中的file.Close()都会在该函数即将退出那一刻被执行。这就更进一步地保证了资源的及时释放。
func deferIt1() { // 倒叙输出:4321
defer func() {
fmt.Print(1)
}()
defer func() {
fmt.Print(2)
}()
defer func() {
fmt.Print(3)
}()
fmt.Print(4)
}
func deferIt2() { // 倒叙输出:4321
for i := 1; i < 5; i++ {
defer fmt.Print(i)
}
}
func deferIt3() { // 输出:1 2 3 4 40 30 20 10 。有点像递归时的回溯过程
f := func(i int) int {
fmt.Printf("%d ",i)
return i * 10
}
for i := 1; i < 5; i++ {
defer fmt.Printf("%d ", f(i))
}
}
func deferIt4() {
for i := 1; i < 5; i++ {
defer func() {
fmt.Print(i)
}()
}
// 输出:5555,而不是4321
// 原因是defer语句携带的表达式语句中的那个匿名函数包含了对外部(确切地说,是该defer语句之外)的变量的使用。
// 注意,等到这个匿名函数要被执行(且会被执行4次)的时候,包含该defer语句的那条for语句已经执行完毕了。
// 此时的变量i的值已经变为了5。因此该匿名函数中的打印函数只会打印出5。
// 正确做法:把要使用的外部变量作为参数传入到匿名函数中。如deferIt5()
}
func deferIt5() {
for i := 1; i < 5; i++ {
defer func(n int) {
fmt.Print(n)
}(i)
}
// 输出:4321
}
func main(){
deferIt1() // 4321
fmt.Println()
deferIt2() // 4321
fmt.Println()
deferIt3()
fmt.Println()
deferIt4()
fmt.Println()
deferIt5()
/*
输出结果:
4321
4321
1 2 3 4 40 30 20 10
5555
4321
*/
}
package main
import (
"fmt"
)
func main() {
for i := 0; i < 10; i++ {
defer fmt.Printf("%d ", func(n int) int { // defer关键字模拟栈的过程,先push再pop
fmt.Printf("%d ", n)
return n // 注意要有返回值
}(fibonacci(i))) // 匿名函数传参调用
}
// 输出结果:0 1 1 2 3 5 8 13 21 34 34 21 13 8 5 3 2 1 1 0
}
func fibonacci(num int) int {
if num == 0 {
return 0
}
if num < 2 {
return 1
}
return fibonacci(num-1) + fibonacci(num-2)
}
- Go语言异常处理——error
- error是Go语言内置的一个
接口类型
。它的声明是这样的:
type error interface {
Error() string
}
- 显然,只要一个类型的
方法集合
包含了名为Error
、无参数声明
且仅声明了一个string类型的结果的方法
,就相当于实现了error接口
。
- 只需调用标准库代码包errors的New函数即可实现将将错误传递给上层程序。例如:
if path == "" {
return nil, errors.New("The parameter is invalid!")
}
// 在Go语言标准库的代码包中有很多由errors.New函数创建出来的错误值,
// 比如os.ErrPermission、io.EOF等变量的值。
-
io.EOF
信号用于通知数据读取方已无更多数据可读
。我们在得到这样一个错误的时候不应该把它看成一个真正的错误,而应该只去结束相应的读取操作
。
br := bufio.NewReader(file)
var buf bytes.Buffer
for { // 死循环,直到读出所有内容为止
// file代表的文件中的所有内容都读取到一个缓冲器(由变量buf代表)中。
ba, isPrefix, err := br.ReadLine()
if err != nil {
if err == io.EOF {
break
}
fmt.Printf("Error: %s\n", err)
break
}
buf.Write(ba)
if !isPrefix {
buf.WriteByte('\n')
}
}
package main
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"path/filepath"
)
func read(r io.Reader) ([]byte, error) {
br := bufio.NewReader(r)
var buf bytes.Buffer
for {
ba, isPrefix, err := br.ReadLine()
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
buf.Write(ba)
if !isPrefix {
buf.WriteByte('\n')
}
}
return buf.Bytes(), nil
}
func readFile(path string) ([]byte, error) {
parentPath, err := os.Getwd()
if err != nil {
return nil, err
}
fullPath := filepath.Join(parentPath, path)
file, err := os.Open(fullPath)
if err != nil {
return nil, err
}
defer file.Close()
return read(file)
}
func main() {
path := "test.txt" // 与main.go文件同目录
ba, err := readFile(path)
if err != nil {
fmt.Printf("Error: %s\n", err)
}
fmt.Printf("The content of '%s':\n%s\n", path, ba)
}
- Go语言异常处理——
panic
,只有在程序运行的时候才会被“抛出来”
。当有运行时恐慌发生时,它会被迅速地向调用栈的上层传递
。如果我们不显式地处理它的话,程序的运行瞬间就会被终止,即程序崩溃
。内建函数panic
可以让我们人为地产生一个运行时恐慌
。不过,这种致命错误
是可以被恢复的
。在Go语言中,内建函数recover
就可以做到这一点。前者用于产生运行时恐慌
,而后者用于“恢复”它
。
- 注意:
recover函数
必须要在defer语句中调用
才有效。因为一旦有运行时恐慌发生,当前函数以及在调用栈上的所有代码都是失去对流程的控制权
。只有defer语句携带的函数中的代码
才可能在运行时恐慌迅速向调用栈上层蔓延时“拦截到”它
。例如:
defer func() {
if p := recover(); p != nil {
fmt.Printf("Fatal error: %s\n", p)
}
}()
// recover函数会返回一个interface{}类型的值,
// 如果p不为nil,那么就说明当前确有运行时恐慌发生,
// 注意,一旦defer语句中的recover函数调用被执行了,运行时恐慌就会被恢复,
// 不论我们是否进行了后续处理。所以,我们一定不要只“拦截”不处理。
- 我们可以在调用
panic函数
时可以传入任何类型的值
,不过建议只传入error类型的值
。更重要的是,当我们调用recover函数来“恢复”由于调用panic函数而引发的运行时恐慌时,得到的值
正是调用后者时传给它的那个参数
。
package main
import (
"errors"
"fmt"
)
func innerFunc() {
fmt.Println("Enter innerFunc")
panic(errors.New("Occur a panic!"))
fmt.Println("Quit innerFunc")
}
func outerFunc() {
fmt.Println("Enter outerFunc")
innerFunc()
fmt.Println("Quit outerFunc")
}
func main() {
fmt.Println("Enter main")
// 根据defer的特性,调用defer语句时会将其地址压入栈中,
// 只要panic函数一抛出异常,恐慌迅速向调用栈上层蔓延时遇到并执行recover函数,
defer func() {
if p := recover(); p != nil {
fmt.Printf("Fatal error: %s\n", p)
}
}()
// 在panic函数抛出异常之前调用defer函数,因为一旦抛出异常,程序立即终止
outerFunc()
fmt.Println("Quit main")
}
Go语句
-
go语句
和通道类型
是Go语言的并发编程理念
的最终体现。与defer语句相同,go语句也可以携带一条表达式语句
。
注意:go语句的执行会很快结束,并不会对当前流程的进行造成阻塞
或明显的延迟
。例如:go fmt.Println("Go!")
- go语句的执行与其携带的表达式语句的执行
在时间上没有必然联系
。这里能够确定的仅仅是后者会在前者完成之后发生
。
- 在go语句被执行时,其携带的函数(也被称为
go函数
)以及要传给它的若干参数
(如果有的话)会被封装成一个实体
(即Goroutine
),并被放入到相应的待运行队列
中。Go语言的运行时系统会适时的从队列中取出待运行的Goroutine
并执行相应的函数调用操作。注意,对传递给这里的函数的那些参数的求值
会在go语句被执行时进行
。这一点也是与defer语句类似的。正是由于go函数的执行时间的不确定性
,所以Go语言提供了很多方法来帮助我们协调它们的执行。其中最简单粗暴的方法就是调用time.Sleep函数
。
package main
import (
"fmt"
)
func main() {
go fmt.Println("Go!")
}
- 以上的命令源码文件被运行时,
标准输出上不会有任何内容出现
。因为还没等Go语言运行时系统调度那个go函数执行
,主函数main就已经执行完毕了
。函数main的执行完毕意味着整个程序的执行的结束
。因此,这个go函数根本就没有执行的机会。
package main
import (
"fmt"
"time"
)
func main() {
go fmt.Println("Go!")
time.Sleep(100 * time.Millisecond)
}
- 语句
time.Sleep(100 * time.Millisecond)
会把main函数的执行结束时间向后延迟100毫秒
。100毫秒虽短暂,但足够go函数被调度执行的了。上述命令源码文件在被运行时会在标准输出上打印出Go!。
- 另一个比较绅士的做法是在main函数的最后调用
runtime.Gosched函数
,如下所示:
package main
import (
"fmt"
"runtime"
)
func main() {
go fmt.Println("Go!")
runtime.Gosched()
}
-
runtime.Gosched函数
的作用是让当前正在运行的Goroutine
(这里指运行main函数的那个Goroutine
)暂时“休息”一下
,而让Go运行时系统转去运行其它的Goroutine
(这里是与go fmt.Println("Go!")
对应并会封装fmt.Println("Go!")的那个Goroutine
)。如此一来,我们就更加精细地控制了对几个Goroutine的运行的调度
。
- 当然,我们还有其它方法可以满足上述需求。并且,如果我们需要去左右
更多的Goroutine的运行时机
的话,下面这种方法也许更合适一些。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
// sync.WaitGroup类型有三个方法可用:Add、Done和Wait。
// Add会使其所属值的一个内置整数得到相应增加,Done会使那个整数减1,
// 而Wait方法会使当前Goroutine(这里是运行main函数的那个Goroutine)阻塞直到那个整数为0。
wg.Add(3)
go func() {
fmt.Println("1、Go!")
wg.Done()
}()
go func() {
fmt.Println("2、Go!")
wg.Done()
}()
go func() {
fmt.Println("3、Go!")
wg.Done()
}()
wg.Wait()
// 输出是随机的:
// 1、Go!
// 3、Go!
// 2、Go!
// 我们在main函数中启用了三个Goroutine来封装三个go函数。
// 每个匿名函数的最后都调用了wg.Done方法,并以此表达当前的go函数会立即执行结束的情况。
// 当这三个go函数都调用过wg.Done函数之后,处于main函数最后的那条wg.Wait()语句的阻塞作用将会消失,main函数的执行将立即结束。
}
package main
import (
"fmt"
)
func main() {
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
ch3 := make(chan int, 3)
go func() {
fmt.Println("1")
ch1 <- 1 // 向通道1发送值
}()
go func() {
<- ch1 // 从通道1取值
fmt.Println("2")
ch2 <- 2 // 向通道2发送值
}()
go func() {
<- ch2 // 从通道2取值
fmt.Println("3")
ch3 <- 3 // 向通道3发送值
}()
<- ch3 // 从通道3取值
// 输出结果:
// 1
// 2
// 3
}