GO语言学习笔记(5)| 青训营笔记

104 阅读5分钟

[ go 与 golang | 青训营笔记]

这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天, 在学习了go的相关基础知识以后,可以说是初步的了解了go,我在这过程中也记录了一些自己的学习经验以及心得,与大家分享一下。

19. 接口

定义

特殊的数据类型

方法定义的合集

方法名(形参类型)返回值类型

提高代码的复用率

package main

import "fmt"

// Animal 定义一个animal的接口,它有唱,跳,rap的方法
type Animal interface {
  sing()
  jump()
  rap()
}

// Chicken 需要全部实现这些接口
type Chicken struct {
  Name string
}

func (c Chicken) sing() {
  fmt.Println("chicken 唱")
}

func (c Chicken) jump() {
  fmt.Println("chicken 跳")
}
func (c Chicken) rap() {
  fmt.Println("chicken rap")
}

// 全部实现完之后,chicken就不再是一只普通的鸡了

func main() {
  var animal Animal

  animal = Chicken{"ik"}

  animal.sing()
  animal.jump()
  animal.rap()

}

接口本身不能绑定方法

接口是值类型,保存的是:值+原始类型

package main

import "fmt"

// Animal 定义一个animal的接口,它有唱,跳,rap的方法
type Animal interface {
  sing()
  jump()
  rap()
}

// Chicken 需要全部实现这些接口
type Chicken struct {
  Name string
}

func (c Chicken) sing() {
  fmt.Println("chicken 唱")
}

func (c Chicken) jump() {
  fmt.Println("chicken 跳")
}
func (c Chicken) rap() {
  fmt.Println("chicken rap")
}

// Cat 需要全部实现这些接口
type Cat struct {
  Name string
}

func (c Cat) sing() {
  fmt.Println("cat 唱")
}

func (c Cat) jump() {
  fmt.Println("cat 跳")
}
func (c Cat) rap() {
  fmt.Println("cat rap")
}

func sing(obj Animal) {
  obj.sing()
}

// 全部实现完之后,chicken就不再是一只普通的鸡了

func main() {
  chicken := Chicken{"ik"}
  cat := Cat{"阿狸"}
  sing(chicken)
  sing(cat)
}

实现接口:

一个类型实现了接口的所有方法

即实现了该接口

类型断言

还原为原始类型 interface.(Type)

如果接口没有保存类型,则会报错

可返回两个值

value,ok := interface.(Type)

空接口

interface{}

空接口可以保存任何类型

package main

import "fmt"

type data interface{}

type Dog struct {
  Name string
}

func Print(d data) {
  fmt.Println(d)
}

func main() {
  d := Dog{"小黑"}

  Print(d)

  Print(12)
  Print("123")
  Print(true)
  Print([]int{1, 2, 3})
  Print(make(map[string]string, 2))
}

nil

nil值:有类型没有值,接口本身并不是nil,可以处理

nil接口:既没有报错值,也没有保存类型,使用时会报错

20. 协程

Goroutine是Go运行时管理的轻量级线程

主线程结束时,协程会被中断,需要有效的阻塞机制

协程的使用

package main

import (
  "fmt"
  "time"
)

// SendCode 发送验证码
func SendCode() {
  fmt.Println("发送验证码开始")
  time.Sleep(3 * time.Second)
  fmt.Println("发送验证码完成!")
}

func main() {
  // 实现用户注册功能
  fmt.Println("用户注册校验完成")
  // 发送验证码
  //SendCode() // 会阻塞主线程
  go SendCode() // 会阻塞主线程
  fmt.Println("验证码已发送,请注意查收...")
}

问题:

  1. 主线程结束,协程也会结束
  2. 协程安全
  3. 如何获取协程函数的返回值,如何在协程中传递数据?

WaitGroup

前面我们通过让主程序延时的方式,可以成功让协程函数顺利结束

但是,延时多久没人能够知道

所以,睡眠这种方式肯定不靠谱

go 自带一个WaitGroup可以解决这个问题, 代码如下

package main

import (
    "sync"
)

var wg sync.WaitGroup

func say(s string) {
    for i := 0; i < 5; i++ {
        println(s)
    }
    wg.Done()
}

func main() {
    wg.Add(2)
    
    go say("Hello")
    go say("World")
    
    wg.Wait()
}

wg.add(2)是有2个goroutine需要执行

wg.Done 相当于 wg.Add(-1) 意思就是我这个协程执行完了。wg.Wait() 就是告诉主线程要等一下,等他们2个都执行完再退出

协程安全

非常经典的例子,两个协程函数,分别对同一个全局变量进行操作

按照我们预期的结果,应该是200万,但是多运行几次,会发现结果各不相同

这就是协程安全问题

package main

import (
  "fmt"
  "sync"
)

var w = sync.WaitGroup{}
var num = 0

func AddNum() {
  for i := 0; i < 1000000; i++ {
    num++
  }
  w.Done()
}

func main() {
  w.Add(2)
  go AddNum()
  go AddNum()
  w.Wait()
  fmt.Println(num)

}

这种情况我们可以通过加锁进行解决,go语言中给我们通过了这个方法

package main

import (
  "fmt"
  "sync"
)

var lock = sync.Mutex{}
var w = sync.WaitGroup{}
var num = 0

func AddNum() {
  lock.Lock()  // 上锁
  for i := 0; i < 1000000; i++ {
    num++
  }
  lock.Unlock() // 解锁
  w.Done() 
}

func main() {
  w.Add(2)
  go AddNum()
  go AddNum()
  w.Wait()
  fmt.Println(num)

}

21. channel

定义

channel,是一种带有类型的管道引用类型

使用前需要make(Type, (缓冲容量))

不带缓冲区的管道必须结合协程使用

可以查看长度len和容量cap

初始化

package main

import "fmt"

func main() {
  // 声明一个string信道,容量为2
  var ch chan string = make(chan string, 2)

  ch <- "枫枫" // 写入数据到信道中
  ch <- "知道"

  s := <-ch // 从信道读取数据
  fmt.Println(s)
  ss, ok := <-ch
  fmt.Println(ss, ok)

  close(ch)

}

存入:channel <- value

取出:value, (ok) <- channel

丢弃:<- channel

先进先出,自动阻塞

数据需要保持流动,否则会阻死报错

搭配协程使用

package main

import "fmt"

func pushNum(c chan int) {
  for i := 0; i < 100; i++ {
    c <- i
  }
  close(c) // 写完必须要关闭,不然会死锁
}

func main() {
  var c1 chan int = make(chan int, 2) // 2表示缓冲区大小
  go pushNum(c1)

  for value := range c1 {
    fmt.Println(value)
  }
}

多个协程函数,close就不能写在协程函数里了

package main

import (
  "fmt"
  "sync"
)

var ch chan int = make(chan int, 10)
var wg = sync.WaitGroup{}

func pushNum() {

  for i := 0; i < 5; i++ {
    ch <- i
  }
  wg.Done()
}

func main() {

  wg.Add(2)
  go pushNum()
  go pushNum()
  wg.Wait()
  close(ch)
  for {
    res, ok := <-ch
    if !ok {
      break

    }
    fmt.Println(res)
  }

}

close

使用close之后就不能在继续写入了,但是还可以继续从缓冲区读取

  1. close之后,读取的chan是数据类型的默认值
  2. close之后,不能再往chan里面写入数据
  3. for range之前必须要close

可读可写

package main

import "fmt"

func main() {
  var ch chan int = make(chan int, 2)
  // 可读chan
  var readCh <-chan int = ch
  // 可写chan
  var writeCh chan<- int = ch

  writeCh <- 1
  writeCh <- 2

  fmt.Println(<-readCh)
  fmt.Println(<-readCh)

}

select case

适用于无法确认合适关闭信道的情况

通常结合for循环使用

select ... case会阻塞到某个分支可以继续执行时执行该分支,当没有可执行的分支是执行default分支

package main

import "fmt"

func main() {
  var ch1 chan int = make(chan int, 2)
  var ch2 chan int = make(chan int, 2)
  var ch3 chan int = make(chan int, 2)
  ch1 <- 1
  ch2 <- 2
  ch3 <- 3

  select {
  // 监听多个chan的情况,是随机执行
  case v := <-ch1:
    fmt.Println(v)
  case v := <-ch2:
    fmt.Println(v)
  case v := <-ch3:
    fmt.Println(v)
  default:
    fmt.Println("没有数据")
  }

}
package main

import "fmt"

func PrimeNum(n int, c chan int) {
  for i := 2; i < n; i++ {
    if n%i == 0 {
      return
    }

  }
  c <- n
}

func main() {
  c := make(chan int)
  for i := 2; i < 100001; i++ {
    go PrimeNum(i, c)
  }
Print:
  for {
    select {
    case v := <-c:
      fmt.Printf("%v\t", v)
    default:
      fmt.Printf("所有素数都已被找到")
      break Print
    }
  }

}