[ 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("验证码已发送,请注意查收...")
}
问题:
- 主线程结束,协程也会结束
- 协程安全
- 如何获取协程函数的返回值,如何在协程中传递数据?
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之后就不能在继续写入了,但是还可以继续从缓冲区读取
- close之后,读取的chan是数据类型的默认值
- close之后,不能再往chan里面写入数据
- 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
}
}
}