Go学习笔记(四)

  • 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))
        }
    }
}
Go学习笔记(四)_第1张图片
测试通道
// 通道测试
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
    */
}
  • defer小练习
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学习笔记(四)_第2张图片
读取文件内容
  • 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函数的执行将立即结束。
}
  • go语句小练习
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
}

你可能感兴趣的:(Go学习笔记(四))