• Go学习笔记


    1. 简介

    Go的优点

    • 编译速度快

    • 天生支持并发

      编写并发程序非常简便

    • 高效GC回收

    • runtime系统调度机制

    • 语法简洁

    • 面向对象语言

    • 目前大厂钟爱

    Go的缺点

    • 包管理还不完善:大部分包私人托管在github上
    • 没有泛型
    • 争议:将所有Exception都用Error处理

    Go适合做什么

    • 云计算基础设施:docker, k8s, cloudflare CDN
    • 基础后端软件: tidb, influxdb
    • 微服务
    • 互联网基础设施: 以太坊, hyperledger

    2. 基础语法

    hello world

    package main
    
    import "fmt"
    
    func main(){
    	fmt. Println("hello world.")
    }
    

    特点

    • 行末可不加分号,编译器会自动加

      因此有一些换行要求,如:

      • 函数起始大括号 { 一定要和函数名同一行
    • 主程序包名为 main

    类型

    - bool
    - 数值类型
    	- int8, int16, int32, int64, int
    	- uint8, uint16, uint32, uint64, uint
    	- float32, float64
    	- complex64, complex128
    	- byte
    	- rune
    - string
    
    • 默认就用int,32位机器就是int32

    • complex是复数, complex64是实部虚部都是float32的复数,complex128都是float64

      // 几种初始化复数变量方法
      f := 10 + 5i
      f := complex(10, 5)
      
    • byte是uint8的别名

    • rune是int32的别名

    类型转换

    Go是强类型语言,不会自动转换类型。

    比如: c = a+b 如果a和b的类型不同,就不能相加,而c中可以,int+float会转为float。

    和c、java一样,通过 类型(变量)强制转换类型。int(b)

    变量和常量

    声明变量

    如果变量未被赋值,Go 会自动地将其初始化,赋值该变量类型的零值

    1. var a int // 使用var起始表示变量,先写变量名,再写类型
    2. var b int = 100 // 给定初始化值,默认为0
    3. var c = 100 // var可以根据初始化值推测类型
    4. d := 100  // 使用 := 是最常用的,会根据值推断类型
    

    方法4最常用,但是不支持声明全局变量

    //多个变量
    
    var {
        a int = 100
        b string = "abc"
    }
    
    var a, b int = 100, 200
    var a, b = 100, "abc"
    

    常量

    const pi float32 = 3.14
    
    const {
        A = 100
        B = 200
        C = 300
    }
    
    // 枚举
    // iota会从 0 “逐行”累加,下面即 A=0,B=1,C=2
    const {
        A = iota
        B
        C
    }
    // iota还可以进行一些运算
    const {
        A = iota * 2
        B
        C
        D = iota + 2
        F
    }
    // 以上 A=0 B=2 C=4 D=5 F=6
    

    打印

    引入 fmt 包

    fmt.Println("hello")
    -----
    var a int = 100
    fmt.Println("a = ", a)
    -----
    var a int = 100
    fmt.Ptintf("a = %d", a)
    fmt.Ptintf("type of a is %T", a)  // %T打印变量类型 
    

    函数

    函数结构

    // 无返回值 无形参
    func funcA {
        ...
    }
    // 单返回值 单形参
    func funcA(a int) int {
    	return a + 1
    }
    //单返回值 多形参
    func funcA(a int, b int) int {
        return a + b
    }
    // 多返回值
    func funcA(a int, b int) (int, int) {
        return a+1, b*2
    }
    // 指定返回值名称
    func funcA(a int, b int) (r1 int, r2 int) {
        r1 = a + 1
        r2 = b * 2
        return  // 之前赋值过了,可以直接return
    }
    

    init函数

    go程序执行过程:

    image-20211111215430818

    每个包都可以有自己的init函数,不定义就没有。init函数用于执行一些资源初始化,不能显示调用,也没有参数和返回值。

    defer

    延迟函数defer。含有 defer 语句的函数,会在该函数将要返回之前,调用另一个函数。类似AOP的功能。通常用于释放资源,错误处理。

    匿名函数

    函数可以没有名字,匿名函数可以直接调用。

    func main() {  
        func() {
            fmt.Println("hello world first class function")
        }()
    }
    
    func main() {  
        func(n string) {  // 还可以接收参数
            fmt.Println("Welcome", n)
        }("Gophers")
    }
    

    闭包

    闭包是由函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)。

    当一个匿名函数所访问的变量定义在函数体的外部时,就产生了闭包:

    func main(){
      a := 5
      func() {
        fmt.Println("a=",a)
      }()
    }
    

    匿名函数中并没有定义a,a是调用它的环境中定义的,它只是引用环境变量。

    func main() {
        a := 5
        f1 := func() {
        	a++
        	fmt.Println("a=",a)
        }
        f1()
        f1()
    }
    

    闭包所引用的环境变量会在堆中新建一份,而不是使用栈中的那份。

    func adda() func() {
    	a := 5
    	c := func() {
    		a++
    		fmt.Println("a=",a)
    	}
    	return c
    }
    
    func main() {
        f1 := adda()
        f2 := adda()
        f1()
        f2()  // f1、f2产生了闭包, 在堆中会生成属于该闭包的环境变量a
        adda()()  // 直接调用没产生闭包,a使用的依然是栈中的
        fmt.Println()
        f1()
        f2()
        adda()()
    }
    

    上述程序输出:

    a= 6
    a= 6
    a= 6
    
    a= 7
    a= 7
    a= 6  
    

    头等函数

    go是支持头等函数特性的语言。头等函数特性是指,函数可以赋值给变量,可以作为其他函数的参数,作为函数的返回值。这个特性的意思是 函数是类似类型一样的“头等公民”。

    头等函数的特性使得函数式编程变得非常灵活。

    看看将一个匿名函数赋值给变量,然后调用:

    func main() {  
        a := func() {
            fmt.Println("hello world first class function")
        }
        a()
        fmt.Printf("%T", a)
    }
    

    函数也类似一种类型,我们也可以通过 type 定义自己的函数类型。

    type add func(a int, b int) int  // add 是该函数类型的名称
    
    func main() {  
      // 变量a是add类型,并赋值了一个符合add类型签名的函数
        var a add = func(a int, b int) int { 
            return a + b
        }
        s := a(5, 6)
        fmt.Println("Sum", s)
    }
    

    函数作为参数传递给其他函数:

    func simple(a func(a, b int) int) {  
        fmt.Println(a(60, 7))
    }
    
    func main() {  
        f := func(a, b int) int {
            return a + b
        }
        simple(f)
    }
    

    函数作为返回值:

    func simple() func(a, b int) int {  
        f := func(a, b int) int {
            return a + b
        }
        return f
    }
    
    func main() {  
        s := simple()
        fmt.Println(s(60, 7))
    }
    

    使用示例1:

    // 过滤数据
    type student struct {  
        firstName string
        lastName  string
        grade     string
        country   string
    }
    
    // 传入过滤函数就可以过滤
    func filter(s []student, f func(student) bool) []student {  
        var r []student
        for _, v := range s {
            if f(v) == true {
                r = append(r, v)
            }
        }
        return r
    }
    
    func main() {  
        s1 := student{ "Naveen","Ramanathan", "A","India",}
        s2 := student{ "Samuel","Johnson","B","USA",}
        s := []student{s1, s2}
        f := filter(s, func(s student) bool { // 调用时传入过滤函数
            if s.grade == "B" {
                return true
            }
            return false
        })
        fmt.Println(f)
    }
    

    使用示例2:

    // 将切片的每个元素扩大5倍
    func iMap(s []int, f func(int) int) []int {  
        var r []int
        for _, v := range s {
            r = append(r, f(v))
        }
        return r
    }
    func main() {  
        a := []int{5, 6, 7, 8, 9}
        r := iMap(a, func(n int) int {
            return n * 5
        })
        fmt.Println(r)
    }
    

    Go程序执行的入口是main包下的main函数

    一般来说,属于某一个包的源文件都应该放置于一个和包同名的文件夹里。

    • 空白标识符

      导入包却不使用在Go中非法,编译会报错,这是为了极致压缩编译时间。

      有时只需要某个包的init,而不用到它。可以这样做避免报错

      import _ packagename
      

      "_"是空白标识符,用于屏蔽一些错误,避免报错。

    if-else

    if a > 0 {
        ...
    } else if a < 0{
        ...
    } else {
        ...
    }
    

    if 还有另外一种形式,它包含一个 statement 可选语句部分,它在条件判断之前运行。

    if a = 1; a > 0 {
        ...
    } else {
        ...
    }
    

    注意 else 和 if 的结束大括号在同一行,不在同一行会报错,因为 go 中的分号是自动添加的,不在同一行if结束就会自动加上分号。

    循环

    和c一样,分号和逗号一样的用法,不过没有括号而已

    for init; condition; post {
        
    }
    ---
    for i := 0; i < 10; i++ {
        ...
    }
    ---
    // 可以作while用
    i := 0
    for i < 10 {
        ...
        i++
    }
    ---
    // 死循环
    for {
        ...
    }
    

    switch

    	finger := 4
        switch finger {
        case 1:
            fmt.Println("Thumb")
        case 2:
            fmt.Println("Index")
        case 3:
            fmt.Println("Middle")
        case 4:
            fmt.Println("Ring")
        case 5:
            fmt.Println("Pinky")
        default:
            fmt.Println("incorrect finger number")
    

    case 可以有多个匹配项,用逗号分隔。

    3. 常用类型

    数组

    // 定义数组
    var arr [10]int  
    arr := [10]int
    arr := [3]int{1,2,3} 
    arr := [...]int{1,2,3,4,5}	// 使用...将自动计算数组长度
    
    len(arr) // 获取数组长度
    
    // 遍历
    // range方法
    for i, v := range arr {
      fmt.Printf(v)
    }
    // 如果不需要索引
    for _, v := range arr {
      ...
    }
    

    go中数组的大小是类型的一部分,也就是说 int[5] 和 int[10] 是两种数据类型,在设置形参的时候可以指定数组大小,传入的数组就必须和指定的大小相等。这样使用起来会比较麻烦,所以go还有切片slice。

    此外与java不同的是,go中的数组是值类型而不是引用类型。也就是说数组变量传递是拷贝一份数组,java中数组是引用对象,拷贝的是对象引用地址。

    切片 slice

    切片只是对数组的引用。

    // 创建
    a := [5]int{1,2,3,4,5}
    var s []int = a[1:3]	//创建一个切片,引用数组a的a[1]-a[3]
    
    var s []int{1,2,3} // 创建一个数组,并且返回一个对该数组引用的切片
    
    var s []int = make([]int,3) //使用make创建切片,长度和容量为3
    
    len(s) // 获取长度
    cap(s) // 获取容量
    

    切片有长度和容量两个属性。长度是切片包含的元素个数,容量是当前能容纳的元素个数,切片可以自动扩容。

    • 切片追加元素

      使用 append 可以追加新的元素,并且超过容量后会进行扩容,扩大2倍。扩容是将原来的元素复制到一个更大的数组中。

      // 追加元素
      s := []int{1,2,3}
      s = append(s, 4)
      
      // 追加一个切片 "..."
      s1 := []int{1,2,3}
      s2 := []int{4,5}
      s1 = append(s1, s2...)
      

    map

    make(map[type of key]type of value) 是创建 map 的语法。

    map必须初始化后才能使用,未初始化之前它是零值 nil,通常使用make初始化。

    map也是引用类型。

    var mymap map[string]int	// 未初始化
    mymap = make(map[string]int) // 初始化
    
    // 或者
    mymap := make(map[string]int)
    
    // 或者创建时指定元素来初始化,那么也就不需要make
    mymap := map[string]int {
      "a": 1,
      "b": 2,		// 注意最后的","也是必须的
    }
    

    map用起来和数组一样。

    mymap := make(map[string]int)
    // 给map添加元素
    mymap["a"] = 1
    mymap["c"] = 3、
    
    // 获取元素
    v := mymap["a"]
    // 获取不存在的元素返回改类型的零值
    v, has := map["a"]	// 第二个返回值是bool类型,map中存在该元素则为true
    
    // 遍历
    for k,v := range mymap {
      ...
    }
    
    // 删除元素
    delete(mymap, "a")
    

    string

    go中的string是一个字节切片。(go中采用utf-8编码)

    所以我们可以通过 [i] 访问每个字节。虽然utf-8大部分常用字符都是一个字节,不过也有很多字符编码成两个或三个字节。go中提供了 rune (意:符文)类型,rune代表一个代码点,无论多少个字节的字符都可以用一个rune表示。

    // 可以将字符串转为rune类型,然后遍历
    s := "hello 你好"
    runes := []rune(s)  // go中这些转换还是很方便
    for i,v := range runes{
      ...
    }
    
    // 或者直接使用字符串的range遍历,它是通过字符而非字节遍历
    for i,v := range s{
      ...
    }
    

    与java一样,string也是不可修改的。

    指针

    和c一样,*T代表指向T类型的指针,使用&取变量地址。不一样的是go不支持指针运算,例如 p++ 非法。

    指针的零值是 nil,引用变量的零值都是 nil,相当于 null 。

    结构体

    // 定义
    type Person struct {
      name string
      age int
    }
    
    // 创建
    p1 := Person{"Peng",24}
    // 创建时若未初始化字段,则为字段类型的零值
    

    相同类型的字段可以写在同一行,更加紧凑。

    还可以使用匿名结构体,可以即时的创建一个匿名结构体变量来使用。

    // 提前没有定义
    
    p2 := struct {
      name string
      age int
    }{
      "Peng",24
    }
    ...
    

    和其他语言一样,使用 . 访问结构体字段。

    4. 面向对象

    go中似乎是采用结构体取代类。

    方法

    go不是一个纯面向对象的语言,而且没有class,为了达到与class相似的效果,go新增了一个特性叫做“方法”。

    type Person struct {
      name string
      age int
    }
    
    func (p Person) show() {
      fmt.Printf("%s is %d", p.name, p.age)
    }
    
    func main(){
      p1 := Person {"Peng", 24}
      p1.show()
    }
    

    如上,在show方法中 func 和 方法名之间加入了一个特殊的接收器类型,那么我们可以像调用对象的方法一样,这样写起来的代码就很有面向对象的感觉了。

    加上了接收器,我们就可以称它为该接收器类型的一个方法,而不是普通的函数了。

    相同名称的方法可以定义在不同类型上,而函数不允许重名。

    结构体定义和方法定义必须在一个包中。

    指针接收器

    上述的接收器是值接收器,也就是说方法的调用者是将接收器复制一份传给方法,在方法内部对接收器字段的任何改变都不影响调用者结构。

    有时候我们需要使用引用传递而非值传递,这就要使用指针接收器。

    func (p *Person) changeAge(newAge int){
      p.age = newAge
    }
    

    如果是指针接收器,一般来说我们需要通过接收器指针来调用方法,如写成&p.changeAge(18),不过go种提供了语法糖让我们可以省略 &。

    匿名字段的方法

    type Person struct {
      name string
      age int
      Child   // Child 也是一个结构体,假设其有一个方法 play()
    }
    
    // 匿名字段的方法可以直接这样调用
    p.play()
    

    在非结构体上的方法

    非结构体类型即是语言自带的类型,如int,string。

    // 给 int 类型新增一个方法
    func (a int) add(b int) {
    }
    
    func main() {
    }
    
    // 这样无法通过编译,因为int类型的定义和方法定义不在一个包中
    

    可以这样解决:

    type myInt int
    
    func (a myInt) add(b myInt) myInt {
        return a + b
    }
    

    接口

    go中也有接口,这样定义:

    type TestInterface interface {
      // 接口方法签名
      // ...
    }
    

    go中的接口实现是隐式的,不需要 implement 这样的关键字,一个类型定义了接口中的所有方法就说明它实现了这个接口。

    空接口

    没有方法的接口,表示为 interface{},所有类都实现了空接口。因此可以使用空接口作为形参,表示函数可以接收任何类型。

    func test(i interface{}) {
      ...
    }
    

    类型断言

    i.(T),断言接口 i 的具体类型是不是 T。

    func test(i interface{}) {
      v := i.(int)	// 若传入类型不是int,则会报错
    }
    
    // 不希望报错的用法
    v, ok := i.(int)  // 如果不是int,则v赋值为int类型的零值,ok为fasle
    

    断言还可以结合 switch 使用:

    func findType(i interface{}) {  
        switch i.(type) {
        case string:
            fmt.Printf("I am a string and my value is %s\n", i.(string))
        case int:
            fmt.Printf("I am an int and my value is %d\n", i.(int))
        case MyInterface:
            fmt.Printf("I am an class, and I implement MyInterface interface)
        default:
            fmt.Printf("Unknown type\n")
        }
    } // 这很有意思
    

    嵌套接口

    type SalaryCalculator interface {  
        DisplaySalary()
    }
    
    type LeaveCalculator interface {  
        CalculateLeavesLeft() int
    }
    
    type EmployeeOperations interface {  
        SalaryCalculator
        LeaveCalculator
    }
    

    结构体取代类

    go中使用结构体来实现类的功能。面向对象语言中类是通过构造器新建的,go中也可以实现类似的做法。

    如果你想创建一个类,那么单独把这个结构体放到一个包中。

    如果将结构体的名称首字母小写,那么该结构体将不可引用,也就是说我们不能 p := person.Person {...} (person包中的Person结构体)这样来得到一个结构体变量。类似构造器,go中的习惯是在该结构体的包中定义一个名称为 New 的函数来模拟构造器。

    package person
    
    type person struct {
      name string
      age int
    }
    
    func New(name string, age int) person {
      p := person {name, age}
      return p
    }
    
    package main
    
    import "person"
    
    func main() {
      p := person.New("Peng", 24)  // 通过 New 新建“类对象”
    }
    

    组合取代继承

    go使用组合来代替继承。结构体可以随意嵌套,这就是go的组合。

    如果结构体A内嵌套了结构体B字段,那我们可以在A中直接访问B的字段,好像这些字段属于外部结构体一样。好像B就是A的子类。

    多态

    使用接口就可以实现多态。

    5. 并发

    - 并行不一定会加快运行速度,因为并行运行的组件之间可能需要相互通信。在我们浏览器的例子里,当文件下载完成后,应当对用户进行提醒,比如弹出一个窗口。于是,在负责下载的组件和负责渲染用户界面的组件之间,就产生了通信。在并发系统上,这种通信开销很小。但在多核的并行系统上,组件间的通信开销就很高了。所以,并行不一定会加快运行速度.
    

    go 原生支持并发,使用Go 协程(Goroutine) 和信道(Channel)来处理并发。

    go协程

    Go 协程可以看作是轻量级线程。

    • 相比线程而言,Go 协程的成本极低。堆栈大小只有若干 kb,并且可以根据应用的需求进行增减。而线程必须指定堆栈的大小,其堆栈是固定不变的。

    • Go 协程会复用OS 线程。即使程序有数以千计的 Go 协程,也可能只有一个线程。如果该线程中的某一 Go 协程发生了阻塞(比如说等待用户输入),那么系统会再创建一个 OS 线程,并把其余 Go 协程都移动到这个新的 OS 线程。好消息是程序员不需要关心这些细节。

    • Go 协程使用信道来进行通信。信道用于防止多个协程访问共享内存时发生竞态条件(Race Condition)。

    启动协程

    在调用函数或者方法时,在前面加上 go,则会创建一个协程并发运行。

    main 函数运行在一个特有的协程上,称为 Go 主协程。主协程结束,程序也即结束。

    func hello() {  
        fmt.Println("Hello world goroutine")
    }
    func main() {  
        go hello()
        time.Sleep(1 * time.Second) // 等待上面协程运行
        fmt.Println("main function")
    }
    

    信道

    类似linux中的管道,用于协程间通信。

    无缓冲信道

    信道 chan 创建需要关联一种类型,然后该信道就只能传输该类型数据。

    // 创建信道
    a := make(chan int)
    
    // 读取信道
    data := <- a
    // 写入信道
    a <- data
    // 注意,读写默认都是阻塞的。
    

    使用信道:

    func hello(done chan bool) {
      ...
      done <- true
    }
    func main() {
      done := make(chan bool)
      go hello(done)
      <-done			// 没有接受值也是可以的
    }
    

    以上程序利用信道实现了通信,协程hello在运行完后向信道写入信号,主协程则阻塞等待直到信道信号的到来。

    再看一个使用信道的例子:

    // 程序目的是计算一个数中每一位的平方和与立方和。设置了两个协程,一个计算平方和,一个计算立方和,主协程计算它俩的和。
    func calcSquares(number int, squareop chan int) {  
        sum := 0
        for number != 0 {
            digit := number % 10
            sum += digit * digit
            number /= 10
        }
        squareop <- sum
    }
    
    func calcCubes(number int, cubeop chan int) {  
        sum := 0 
        for number != 0 {
            digit := number % 10
            sum += digit * digit * digit
            number /= 10
        }
        cubeop <- sum
    } 
    
    func main() {  
        number := 589
        sqrch := make(chan int)
        cubech := make(chan int)
        go calcSquares(number, sqrch)
        go calcCubes(number, cubech)
        squares, cubes := <-sqrch, <-cubech
        fmt.Println("Final output", squares + cubes)
    }
    

    以上,可以看到可以通过信道将结果返回,非常高效方便。

    关闭信道:

    通常由发送方关闭信道,告知接收方不再发送数据。从信道接收数据时,可以多使用一个变量检查信道是否已经关闭:

    v,ok := <- ch
    

    遍历信道:

    func producer(chnl chan int) {  
        for i := 0; i < 10; i++ {
            chnl <- i
        }
        close(chnl)
    }
    func main() {  
        ch := make(chan int)
        go producer(ch)
        for v := range ch {  // 循环会直到信道关闭才结束
            fmt.Println("Received ",v)
        }
    }
    
    • 死锁

      上面说到无缓冲信道读写都是阻塞的,有写必须要有读。如果只有写入,没有协程读取,程序就会 出现死锁。

    单向信道

    上述所说都是双向信道,go还提供单向信道,即只能写或只能读。

    通常我们不会直接创建一个单向信道,而是将双向信道转换为单向传入函数,这样在函数中使用信道就只能读或写,用于保护信道信息安全。

    // 使用 chan<- 定义只能写入的信道, <-chan 定义只能读的信道
    func sendData(sendch chan<- int) {  
        sendch <- 10
    }
    
    func main() {  
        cha1 := make(chan int)
        go sendData(cha1)
        fmt.Println(<-cha1)
    }
    

    缓冲信道

    上述所说的无缓冲信道中不能缓冲数据,因此读写必须成对出现。此外还有缓冲信道,信道可以指定缓冲容量,无缓冲信道容量默认为0.

    ch := make(chan type, capacity)
    

    缓冲信道容量未满之前写入不会阻塞,数据未被及时读取导致容量满了仍然会阻塞。

    WaitGroup

    WaitGroup是一个结构体类型,用于等待一批Go协程执行结束,类似于java中的circlebarrier?

    func process(i int, wg *sync.WaitGroup) {  
        fmt.Println("started Goroutine ", i)
        time.Sleep(2 * time.Second)
        fmt.Printf("Goroutine %d ended\n", i)
        wg.Done()
    }
    
    func main() {  
        no := 3
        var wg sync.WaitGroup
        for i := 0; i < no; i++ {
            wg.Add(1)
            go process(i, &wg)
        }
        wg.Wait()
        fmt.Println("All go routines finished executing")
    }
    

    waitgroup使用计数器工作,Add方法增加计数器,Done将计数器减一,Wait方法则阻塞直到计数器为0。

    注意,将waitgroup作为参数传给协程时,应该传递地址,否则协程得到的是一个拷贝。

    使用缓冲信道实现工作池

    go中的worker pool其实也就是线程池,不过go中是协程。

    看一个例子,这个例子中工作是计算一些随机数的每个位之和。

    package main
    
    import (  
        "fmt"
        "math/rand"
        "sync"
        "time"
    )
    
    type Job struct {  
        id       int
        randomno int
    }
    type Result struct {  
        job         Job
        sumofdigits int
    }
    
    var jobs = make(chan Job, 10)  
    var results = make(chan Result, 10)
    
    func digits(number int) int {  
        sum := 0
        no := number
        for no != 0 {
            digit := no % 10
            sum += digit
            no /= 10
        }
        time.Sleep(2 * time.Second)
        return sum
    }
    func worker(wg *sync.WaitGroup) {  
        for job := range jobs {
            output := Result{job, digits(job.randomno)}
            results <- output
        }
        wg.Done()
    }
    func createWorkerPool(noOfWorkers int) {  
        var wg sync.WaitGroup
        for i := 0; i < noOfWorkers; i++ {
            wg.Add(1)
            go worker(&wg)
        }
        wg.Wait()
        close(results)
    }
    func allocate(noOfJobs int) {  
        for i := 0; i < noOfJobs; i++ {
            randomno := rand.Intn(999)
            job := Job{i, randomno}
            jobs <- job
        }
        close(jobs)
    }
    func result(done chan bool) {  
        for result := range results {
            fmt.Printf("Job id %d, input random no %d , sum of digits %d\n", result.job.id, result.job.randomno, result.sumofdigits)
        }
        done <- true
    }
    func main() {  
        startTime := time.Now()
        noOfJobs := 100
        go allocate(noOfJobs)
        done := make(chan bool)
        go result(done)
        noOfWorkers := 10
        createWorkerPool(noOfWorkers)
        <-done
        endTime := time.Now()
        diff := endTime.Sub(startTime)
        fmt.Println("total time taken ", diff.Seconds(), "seconds")
    }
    

    select

    用于在多个信道中做选择。select 会一直阻塞,直到发送/接收操作准备就绪,如果有多个操作同时准备好,那么会随机执行其中一个。语法类似switch。

    func server1(ch chan string) {  
        time.Sleep(6 * time.Second)
        ch <- "from server1"
    }
    func server2(ch chan string) {  
        time.Sleep(3 * time.Second)
        ch <- "from server2"
    }
    func main() {  
        output1 := make(chan string)
        output2 := make(chan string)
        go server1(output1)
        go server2(output2)
        select {
        case s1 := <-output1:
            fmt.Println(s1)
        case s2 := <-output2:
            fmt.Println(s2)
        }
    }
    

    上述例子中,明显server2更快发送数据,因此select会执行case2。

    select还可以有default,没有case满足时,执行default。

    Mutex

    就是锁。提供两个方法 Lock 和 Unlock。

    看个使用的例子:

    var x  = 0  
    func increment(wg *sync.WaitGroup, m *sync.Mutex) {  
        m.Lock()
        x = x + 1
        m.Unlock()
        wg.Done()   
    }
    func main() {  
        var w sync.WaitGroup
        var m sync.Mutex
        for i := 0; i < 1000; i++ {
            w.Add(1)        
            go increment(&w, &m)
        }
        w.Wait()
        fmt.Println("final value of x", x)
    }
    

    其实使用缓冲信道也可以实现锁。

    var x  = 0  
    func increment(wg *sync.WaitGroup, ch chan bool) {  
        ch <- true
        x = x + 1
        <- ch
        wg.Done()   
    }
    func main() {  
        var w sync.WaitGroup
        ch := make(chan bool, 1)
        for i := 0; i < 1000; i++ {
            w.Add(1)        
            go increment(&w, ch)
        }
        w.Wait()
        fmt.Println("final value of x", x)
    }
    

    容量为1的缓冲信道,第一个协程向信道写入数据后,必须要执行完x=x+1,并将数据消耗掉,其他协程才能继续往信道中写入,否则在写入处阻塞。

    6. 错误处理

    go中所有的异常、错误统称为错误 error 。

    // 打开文件
    f, err := os.Open("/test.txt")
    if err != nil {
      fmt.Println(err)
      return
    }
    fmt.Println(f.Name(), "opened successfully")
    

    err 接收一个 error 类型,如果没有出错,err 则为 nil。

    error 是一个接口类型:

    type error interface {  
        Error() string
    }
    

    可能出错的类型都可以实现它。fmt.Println(err) 在打印错误时,会在内部调用 Error() string 方法来得到该错误的描述。

    判断错误类型

    看看常见的error使用方法:

    1. 断言

      看看 Open 函数返回的错误类型:

      type PathError struct {  
          Op   string
          Path string
          Err  error
      }
      
      func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
      

      它对 error 进行了包装,是一个PathError。

      f, err := os.Open("/test.txt")
      // 可以通过断言看是不是PathError类型,然后就可以提取出错的路径
      if err, ok := err.(*os.PathError); ok {  
        fmt.Println("File at path", err.Path, "failed to open")
        return
      }
      
    2. 直接比较

      error可以直接用 == 比较。看个例子:

      filepath包中的Glob用于返回所有满足glob模式的文件名,如果模式格式不对,则会返回一个 ErrBadPattern 错误,该错误定义如下:

      var ErrBadPattern = errors.New("syntax error in pattern")
      
      // 使用的时候
      files, error := filepath.Glob("[")  // 此处模式提供格式错误
      if error != nil && error == filepath.ErrBadPattern {
        fmt.Println(error)
        return
      }
      

    不能忽略错误

    此前学过 _ 符号可以忽略参数,同样也可以用它忽略错误。在实际工程项目中不建议这样做,所有的错误都应该被及时发现及时处理。

    自定义错误

    有哪些方法创建自定义错误?

    1. 使用 errors.New

      使用New可以创建自定义错误,如上面的 ErrBadPattern 错误就是使用 New定义的。

    2. 使用 fmt 的 Errorf

      Errorf 能格式化输出错误以提供不止于字符串的错误信息。

      和 Printf 用法一样,不过它会返回一个 error 类型。

    3. 使用结构体类型

      和之前的PathError例子一样,我们可以对错误进行结构体包装。

    Panic

    上面所说的错误其实在java中都是异常,真正的错误是类似虚拟机故障,内存不足,死锁之类,通常这时程序已经无法恢复需要停止,go中把这些称为 panic (恐慌)。不过go中的panic可以使用recover重新获取对程序的控制。

    panic用于系统错误,程序员使用时一般只需要 error 即可。

    发生 panic

    发生 panic 时, 函数会停止运行,但 defer 函数仍会被执行,然后将控制权返回函数调用方,直到当前协程的所有函数都退出。然后控制台会打印 panic 信息,接着打印堆栈跟踪信息,然后程序终止。

    看一个例子:

    func fullName(firstName *string, lastName *string) {  
        defer fmt.Println("deferred call in fullName")
        if firstName == nil {
            panic("runtime error: first name cannot be nil")
        }
        if lastName == nil {  // 产生panic
            panic("runtime error: last name cannot be nil")
        }
        fmt.Printf("%s %s\n", *firstName, *lastName)
        fmt.Println("returned normally from fullName")
    }
    
    func main() {  
        defer fmt.Println("deferred call in main")
        firstName := "Elon"
        fullName(&firstName, nil)  // 传入的lastname为nil
        fmt.Println("returned normally from main")
    }
    
    - 该程序会打印:
    
    deferred call in fullName  
    deferred call in main  
    panic: runtime error: last name cannot be nil
    
    goroutine 1 [running]:  
    main.fullName(0x1042bf90, 0x0)  
        /tmp/sandbox060731990/main.go:13 +0x280
    main.main()  
        /tmp/sandbox060731990/main.go:22 +0xc0
    

    recover panic

    通过在defer函数内部调用 recover,panic可以被恢复,恢复后的panic将继续执行剩下的程序。

    依然是上面名字的例子:

    func recoverName() {  
        if r := recover(); r!= nil {
            fmt.Println("recovered from ", r) // r 即 panic 的传参
        }
    }
    // 
    func fullName(firstName *string, lastName *string) {  
        defer recoverName()
        ...
    }
    

    注意,recover 只能恢复同一个协程发出的 panic。

    运行时 panic

    类似 java 的运行时异常,比如数组越界访问,这种情况会产生 panic。这种panic同样可以恢复。

    恢复后获得堆栈跟踪

    panic恢复后即不会打印堆栈跟踪信息,如果仍然想获取这些信息,可以使用 Debug 包中的 PrintStack 函数。通常我们在defer函数中recover后调用。

    7. 文件处理

    文件操作完记得 close

    读文件

    读取整个文件

    使用 ioutil 包中的 ReadFile 函数。

    data, err := ioutil.ReadFile("test.txt")
    

    如果 err 为 nil,则读取成功了,该函数返回的数据是一个字节切片。

    import (
        "fmt"
        "io/ioutil"
    )
    
    func main() {
        data, err := ioutil.ReadFile("test.txt")
        if err != nil {
            fmt.Println("File reading error", err)
            return
        }
        fmt.Println("Contents of file:", string(data))
    }
    

    上述程序读取test.txt 文件,并打印。test.txt文件应该放在运行go install的目录下,否则将找不到文件报错。因此文件可以放在任意位置,取决于你在哪个地方编译程序。还可以使用下面三种方法解决路径问题。

    • 使用绝对路径
    • 使用命令行标记来传递文件路径
    • 将文件绑定在二进制文件中

    分块读取文件

    文件很大时可能需要分块读取,bufio 包提供了这样的功能。bufio的NewReader

    逐行读取文件

    bufio还提供了逐行读取的功能,bufio的NewScanner

    写文件

    写字符串

    f, err := os.Create("test.txt")  // 创建文件
    
    l, err := f.WriteString("Hello World") // 写入字符串
    

    写字节

    Write方法,可传入字节切片

  • 相关阅读:
    设计模式
    Linux 使用 script 分享
    动态代理中的 UndeclaredThrowableException 以及其他异常
    浅析 Spring 异常处理
    SLAM中的优化理论(二)- 非线性最小二乘
    SLAM中的优化理论(一)—— 线性最小二乘
    卡尔曼滤波器推导与解析
    Python学习(一) —— matplotlib绘制三维轨迹图
    ZED 相机 && ORB-SLAM2安装环境配置与ROS下的调试
    [转载]如何使用USSD命令设置呼叫转移
  • 原文地址:https://www.cnblogs.com/cpcpp/p/15579041.html
Copyright © 2020-2023  润新知