限制协程执行数量的基本方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
"fmt"
"sync"
"time"
)

func job(index int) {
time.Sleep(time.Millisecond * 500)
fmt.Printf("执行完毕,序号:%d\n", index)
}

var pool chan struct{}

func main() {
maxNum := 10
pool = make(chan struct{}, maxNum)
wg := sync.WaitGroup{}
for i := 0; i < 100; i++ {
pool <- struct{}{}
wg.Add(1)
go func(index int) {
defer wg.Done()
defer func() {
<-pool
}()
job(index)
}(i)
}
wg.Wait()
}

函数执行超时的控制代码怎么写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"fmt"
"time"
)

// 1、业务过程放到协程
// 2、把业务结果塞入channel
// 3、控制一个time.After
func job() chan string {
ret := make(chan string)
go func() {
time.Sleep(time.Second * 5)
ret <- "success"
}()
return ret
}

func run() (interface{}, error) {
c := job()
select {
case r := <-c:
return r, nil
case <-time.After(time.Second * 3):
return nil, fmt.Errorf("time out")
}

}

func main() {
fmt.Println(run())
}

nil不等于nil的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
//fmt.Println(run())

var f func()
var a *struct{}

list := []interface{}{f, a}
for _, item := range list {
//if v, ok := item.(func()); ok &&v==nil {
// fmt.Println("nil func")
//}
//if v, ok := item.(*struct{}); ok &&v==nil {
// fmt.Println("nil struct")
//}
if reflect.ValueOf(item).IsNil() {
fmt.Println("nil")
}
}

}

Interface 有两部分组成:类型和值

defer定义函数时的参数问题

1
2
3
4
a := 1
defer fmt.Println(a) // 在定义是就确定了a是1,定义过程中如果有参数就会被确定下来
a++
// 结果是1

defer里链式调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

type test struct{}

func NewTest() *test {
return &test{}
}

func (this *test) do(i int) *test {
fmt.Println(i)
return this
}

func main() {
t := NewTest()
defer t.do(1).do(2).do(5)
t.do(3)
}
// 以最后一个执行单位为准放在最后执行, 结果是 1 2 3 5

循环执行defer

1
2
3
4
5
6
7
8
9
func main() {
for i := 0; i < 3; i++ {
defer func() {
fmt.Println(i)
}()
}
}

// 3 3 3

defer和panic哪个先执行

1
2
3
4
5
6
7
8
9
func main() {
defer func() {fmt.Println("打印前")}()
defer func() {fmt.Println("打印中")}()
defer func() {fmt.Println("打印后")}()

panic("触发异常")
}

// 先执行defer,再把异常往上抛
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
func() {
defer func() { fmt.Println("打印前") }()
defer func() { fmt.Println("打印中") }()
defer func() { fmt.Println("打印后") }()

panic("触发异常1")
}()

panic("触发异常2")

}

// 后 中 前 1,没有2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
defer func() {
defer func() { fmt.Println("打印前") }()
defer func() { fmt.Println("打印中") }()
defer func() { fmt.Println("打印后") }()

panic("触发异常1")
}()

panic("触发异常2")

}

// 后 中 前 2 1

n++不靠谱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func main() {
for i := 0; i < 1000000; i++ {
go func() {
n++
}()
}
fmt.Println(n)
}

// 答案是 1000000 毫无疑问的




func main() {
n := 0
wg := sync.WaitGroup{}
for i := 0; i < 1000000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
n++
}()
}
wg.Wait()
fmt.Println(n)
}

n++这个过程并不是原子的,有三个步骤:

  • 从内存读取n
  • 执行++
  • 再赋值结果

这个过程中,其他协程也会进来读取n的值,因此不是原子的

所谓的原子,执行过程中不会发生上下文(线程的切换)的切换

两种办法解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
n := 0
locker := sync.Mutex{}
wg := sync.WaitGroup{}
for i := 0; i < 1000000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
defer locker.Unlock()
locker.Lock()
n++
}()
}
wg.Wait()
fmt.Println(n)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
var n int32 = 0
wg := sync.WaitGroup{}
for i := 0; i < 1000000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
atomic.AddInt32(&n, 1)
}()
}
wg.Wait()
fmt.Println(n)
}

go常见的并发模式

go的并发模式有哪些,几个点:

  • go有协程以及CSP调度模型,是进行并发运行的基础
  • 可以使用协程来完成 ”并发编程“
  • go并发编程哲学:不要通过共享内存来通信,而应通过通信来共享内存
  • 并发编程的重点在于:如何精准的控制 “共享数据”

基本:通过协程来执行并发任务,通过channel来通信

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 3; i++ {
wg.Add(1)
go func(input int) {
defer wg.Done()
fmt.Println(input * 2)
}(i)
}
wg.Wait()

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {

c := make(chan int, 3)
for i := 0; i < 3; i++ {

go func(input int) {
c <- input * 2
}(i)
}

for i := 0; i < cap(c); i++ {
fmt.Println(<-c)
}

}

生产者模式

第一种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func Producer(out chan int) {
defer close(out)
for i := 0; i < 5; i++ {
out <- i * 2
time.Sleep(time.Second * 1)
}
}

func Consumer(out chan int) {
for item := range out {
fmt.Println(item)
}
}

func main() {
c := make(chan int)
go Producer(c)
Consumer(c)
}

第二种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func Producer(out chan int) {
defer close(out)
for i := 0; i < 5; i++ {
out <- i * 2
time.Sleep(time.Second * 1)
}
}

func Consumer(out chan int, r chan struct{}) {
for item := range out {
fmt.Println(item)
}
r <- struct{}{}
}

func main() {
c := make(chan int)
r := make(chan struct{})
go Producer(c)
go Consumer(c, r)
<-r
}

推荐写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func Producer(out chan int) {
defer close(out)
for i := 0; i < 5; i++ {
out <- i * 2
time.Sleep(time.Second * 1)
}
}

func Consumer(out chan int) (r chan struct{}) {
r = make(chan struct{})
go func() {
defer func() {
r <- struct{}{}
}()
}()
for item := range out {
fmt.Println(item)
}
return r
}

func main() {
c := make(chan int)

go Producer(c)
r := Consumer(c)
<-r
}

优胜劣汰模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
"math/rand"
"time"
)

func job2() int {
rand.Seed(time.Now().Unix())
result := rand.Intn(5)
time.Sleep(time.Second * time.Duration(result)) // 模拟延迟
return result
}

func main() {
c := make(chan int)
for i := 0; i < 5; i++ {
go func() {
c <- job2()
}()
}
fmt.Println("最快用了:", <-c, "秒")
}

协程为什么总是先输出倒数第一个

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
runtime.GOMAXPROCS(1) // 单核限制
wg := sync.WaitGroup{}
wg.Add(5)
for i := 0; i < 5; i++ {
go func(i int) {
defer wg.Done()
fmt.Println(i)
}(i)
}
wg.Wait()
}
  • 首先 runtime.GOMAXPROCS,golang默认使用所以的cpu核,runtime.GOMAXPROCS(1) 就变成单核运行了
  • 单核情况下,所有goroutine运行在同一个线程(M)中,线程维护一个上下文(P)
  • 在上面程序中,我们循环创建了若干线程,且是单核
  • 在P就绪后,开始执行。默认先执行的是最后一个创建的协程
  • 然后再继续执行其他协程,此次是按照顺序来的

要想按照顺序输出,怎么解决: 再加一个协程就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
runtime.GOMAXPROCS(1) // 单核限制
wg := sync.WaitGroup{}
wg.Add(6)
for i := 0; i < 5; i++ {
go func(i int) {
defer wg.Done()
fmt.Println(i)
}(i)
}
go func() {
defer wg.Done()
fmt.Println("开始解决")
}()
wg.Wait()
}

写一个带过期机制的kv 获取map

原始的map不是线程安全的,要使用sync.map,使用time.After 实现简单的过期机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var kv sync.Map

func Set(key string, value interface{}, expire time.Duration) {
kv.Store(key, value)
time.AfterFunc(expire, func() {
kv.Delete(key)
})
}

func main() {
Set("id", 101, time.Second*5)
Set("name", "zhangsan", time.Second*8)
for {
fmt.Println(kv.Load("id"))
fmt.Println(kv.Load("name"))
time.Sleep(time.Second)
}
}

谈一谈Go的链表操作

谈一下双向链表和单向链表的区别

go自带一个双向链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"container/list"
"fmt"
)

func main() {
data := list.New()
e8 := data.PushBack(8)
data.PushBack(9)
data.PushBack(10)
data.PushFront(7)

data.InsertAfter(8.5, e8)

for e := data.Front(); e != nil; e = e.Next() {
fmt.Printf("%v ", e.Value)
}
}

如何使用golang定义枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import "fmt"

type UserType int

func (u UserType) String() string {
switch u {
case 0:
return "学生"
case 1:
return "老师"
default:
return "领导"
}
}

const (
Student UserType = iota
Teacher
Leader
)

func main() {
fmt.Println(Student, Teacher, Leader)
}

go的struct能不能比较

struct中包含了 map slice func 是不能比较的

结论:

  • 相同结构,只要成员类型都可以比较,就能比较
  • 不相同的结构,如果能互相转化,也能比较。前提是成员都是可以比较的

用go实现一个简单的set

目前go的标准库里没有set

所谓的set 简单来说就是一个集合,里面的内容不能重复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
"bytes"
"fmt"
)

type Empty struct{}

type Set map[interface{}]Empty

func (this Set) Add(vs ...interface{}) Set {
for _, v := range vs {
this[v] = Empty{}
}
return this
}

func (this Set) String() string {
var buf bytes.Buffer
for k, _ := range this {
if buf.Len() > 0 {
buf.WriteString(",")
}
buf.WriteString(fmt.Sprintf("%v", k))
}
return buf.String()
}

func NewSet() Set {
return make(map[interface{}]Empty)
}

func main() {
set := NewSet().Add(1, 2, 3, 4, 2, 1, 3)
fmt.Println(set)
}

go的切片浅拷贝和深拷贝的写法和区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
a := []int{1, 2, 3}
b := a
fmt.Println(a, b)
}
// 这种属于浅拷贝,共享同一层数组空间


func main() {
a := []int{1, 2, 3}
b := make([]int, len(a), cap(a))
copy(b, a)
a[1] = 100
b[1] = 90
fmt.Println(a, b)
}
// 深拷贝

说一说go的内存逃逸分析

逃逸分析:go在编译阶段确定内存是分配在栈上还是堆上的一种行为

  • 栈内存分配和释放非常快
  • 堆内存需要依靠Go垃圾回收(GC 占用CPU)
  • 通过逃逸分析,可以尽量把那些不需要分配到堆上的变量直接分配到栈上

Go的主要目的并不希望程序员关注分配,而是通过编译时的代码分析自动决定

go build -gcflags=-m main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 下面这种情况就会逃逸
func test() []int {
a := []int{1, 2, 3}
a[1] = 4
return a
}

// 局部变量原本应该在栈中分配,在栈中回收。由于返回时被外部引用




// 参数为interface类型时,比如 fmt.Println(a...interface{}), 编译期间很难确定其参数的具体类型,也能产生逃逸



// 经典案例:
package main

import "fmt"

type User struct {
id int
}

func NewUser() User {
return User{101}
}

func main() {
u := NewUser()
fmt.Println(u) // u escapes to heap
}

// 在上面的函数中,如果仅仅在main中一步搞定这个u,不需要将u传出去,user这个struct不需要使用指针

说一说Go的单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"fmt"
"sync"
)

type WebConfig struct {
Port int
}

var cc *WebConfig
var mu sync.Mutex

func GetConfig() *WebConfig {
mu.Lock()
defer mu.Unlock()
if cc == nil {
cc = &WebConfig{Port: 8080}
}
return cc
}

func main() {
c := GetConfig()
// 模拟很多模块都在调用这个函数
c2 := GetConfig()
c3 := GetConfig()
fmt.Println(c, c2, c3)
fmt.Println(c == c2, c2 == c3)
}

事实上go提供了内置方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"fmt"
"sync"
)

type WebConfig struct {
Port int
}

var cc *WebConfig
var once sync.Once

func GetConfig() *WebConfig {
once.Do(func() {
cc = &WebConfig{Port: 8080}
})
return cc
}

func main() {
c := GetConfig()
// 模拟很多模块都在调用这个函数
c2 := GetConfig()
c3 := GetConfig()
fmt.Println(c, c2, c3)
fmt.Println(c == c2, c2 == c3)
}

说一说Go的简单工厂模式

可以将接口与具体实现分离,根据需要(根据参数)实例化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import "fmt"

type User interface {
GetRole() string
}

type Member struct{}

func (m *Member) GetRole() string {
return "会员用户"
}

type Admin struct{}

func (a *Admin) GetRole() string {
return "后台管理用户"
}

const (
Mem = iota
Adm
)

func CreateUser(t int) User {
switch t {
case Mem:
return new(Member)
case Adm:
return new(Admin)
default:
return new(Member)
}
}

func main() {
fmt.Println(CreateUser(Adm).GetRole())
}

Go的抽象工厂方法

接着上面的简单工厂模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package main

import "fmt"

type User interface {
GetRole() string
}

type Member struct{}

func (m *Member) GetRole() string {
return "会员用户"
}

type Admin struct{}

func (a *Admin) GetRole() string {
return "后台管理用户"
}

const (
Mem = iota
Adm
)

func CreateUser(t int) User {
switch t {
case Mem:
return new(Member)
case Adm:
return new(Admin)
default:
return new(Member)
}
}

type AbstractFactory interface {
CreateUser() User
}

type MemberFactory struct{}

func (m *MemberFactory) CreateUser() User {
return &Member{}
}

type AdminFactory struct{}

func (a *AdminFactory) CreateUser() User {
return &Admin{}
}

func main() {
var fact AbstractFactory = new(AdminFactory)
fmt.Println(fact.CreateUser().GetRole())
}

不动原来的代码,一旦增加了什么类别,只要增加如下代码:

1
2
3
4
5
6
7
type MemberFactory struct{}

func (m *MemberFactory) CreateUser() User {
return &Member{}
}

// 加一个工厂,以及一个抽象工厂的具体实现

请写一个Go的装饰器模式的例子

允许向一个现有对象添加新的功能,同时又不改变其结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "net/http"

func index(writer http.ResponseWriter, request *http.Request) {
writer.Write([]byte("index"))
}

func main() {

http.HandleFunc("/", index)
http.ListenAndServe(":8080", nil)
}

// 这里访问http://localhost:8080 输出index, 但是我想输出 this is index
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "net/http"

func Decorator(f http.HandlerFunc) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
writer.Write([]byte("this is "))
f(writer, request)
}
}

func index(writer http.ResponseWriter, request *http.Request) {
writer.Write([]byte("index"))
}

func main() {

http.HandleFunc("/", Decorator(index))
http.ListenAndServe(":8080", nil)
}

请简述go channel的底层机制

源码在 runtime/chan.go

image-20220527144940432

重点关注4个部分:

  • buf指向一个循环队列
  • sendx和recvx用于记录buf发送和接收的index
  • lock 互斥锁
  • sendq和recvq 等待(阻塞)列表

其他的:

  • qcount:队列剩余数
  • dataqsize:缓冲区大小
1
2
3
4
5
6
7
8
9
10
ch := make(chan int, 3)
// ch 本身是一个指针,指向堆中的hchan

// 一开始, sendx = 0。 revcx=0

// 插入值
// 执行前都要加锁
ch <-1 (send)
sendx = 1
recvx = 0

image-20220527152131672

一旦缓存满了……

回答的基本点:

  • chan创建在堆中,返回指针
  • 使用环形队列作为缓冲区
  • 每次操作都要加锁,并更新操作(send或recv的index)
  • 缓冲满了,进入等待队列,并让出M。等待被唤醒
  • 被唤醒后,重新加入G队列

读写关闭的channel是啥后果

简述Go协程调度机制

https://medium.com/@ankur_anand/illustrated-tales-of-go-runtime-scheduler-74809ef6d19b

https://lessisbetter.site/2019/03/10/golang-scheduler-1-history/

https://zhuanlan.zhihu.com/p/27056944

https://www.kancloud.cn/aceld/golang/1958305

回到要点:

  • G、P、M是啥,基本关系
  • 如何找G
  • 阻塞过程
  • 自旋

Go的协程调度就是 P将G合理的分配给某个M的过程

简述Raft协议:选举机制

分布式集群:

  • 一旦单节点宕机,其他节点依然能提供服务
  • 关键的是:数据并不会丢失。其中一个重要过程就是数据一致性
  • 要实现一致性:一般来讲 节点中需要有个主节点来对数据日志进行统一管理(复制)

image-20220601122548765

image-20220601213259612

image-20220601213346193

正常情况下,A会赢得选举(大多数选票)

于是,它会立刻给所有节点发送心跳消息,避免其余节点触发新的选举

不正常情况,由于网络等原因,A和B也许会同时发起选举,

注意:在同一个任期内,C只能给A或B投一票。先来先得

假设A先到,此时A胜出。还是那样,A胜出后会发出心跳消息。B发现A的term比他大,则会自觉的改成follower,并更新term

image-20220601214401032

简述Raft协议:数据复制过程(初级)

让这个数据同步、可恢复,将这命令持久化到日志里面

基本过程:所有日志都必须首先提交至leader节点

  • leader加入本地日志
  • leader要求followers同步日志
  • leader等待多数节点的反馈(不是全部)
  • 上一步成功后leader确认操作ok,并修改本地状态和存储
  • 发出心跳要求 follower也提交并存储

最终一致性

框架中路由实现:手撸前缀树

特点:

  • 根节点不包含字符
  • 每个节点的所有子节点包含的字符都不相同
  • 一个节点的所有子节点都有相同的前缀
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package main

import "fmt"

type Trie struct {
root *Node
}

func NewTrie() *Trie {
return &Trie{root: NewNode()}
}

type Node struct {
isEnd bool // 是否是最后一个
children map[string]*Node
}

func NewNode() *Node {
return &Node{children: make(map[string]*Node)}
}

func (this *Trie) Insert(str string) {
current := this.root
for _, item := range []rune(str) {
if _, ok := current.children[string(item)]; !ok {
current.children[string(item)] = NewNode()
}
current = current.children[string(item)]
}
current.isEnd = true // 最后一个
}

func (this *Trie) Search(str string) bool {
current := this.root
for _, item := range []rune(str) {
if _, ok := current.children[string(item)]; !ok {
return false
}
current = current.children[string(item)]
}
return current.isEnd // 最后一个
}

func test() {
strs := []string{"go", "gin", "golang", "goapp", "guest"}
tree := NewTrie()
for _, s := range strs {
tree.Insert(s)
}
strs = append(strs, "gi", "gogo", "gia")
for _, s := range strs {
fmt.Println(tree.Search(s))
}
}

func main() {
test()
}

读取文件:如何统计文本的行数

这里用到的是bufio.scanner

好比是一个带缓冲区的迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"bufio"
"fmt"
"log"
"os"
)

func main() {
file, err := os.Open("pkg/mylog")
if err != nil {
log.Fatalln(err)
}
scanner := bufio.NewScanner(file)
count := 0
for scanner.Scan() {
count++
}
fmt.Println("一共有", count, "行")
}

Sync.Pool的基本用法

sync.pool :协程安全、可伸缩的、用于存放可重用对象的容器

原始目的是:

  • 存放已分配但暂时用不到的对象,需要时直接从pool中取,然后放回 以减少GC回收的压力
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"fmt"
"log"
"sync"
)

var p *sync.Pool

type User struct {
Name string
}

func main() {
p = &sync.Pool{
New: func() interface{}{
log.Println("create user")
return &User{Name: "zhangsan"}
},
}
u1 := p.Get().(*User)
fmt.Println(u1)
u1.Name = "lisi"
p.Put(u1)
u2 := p.Get()
fmt.Println(u2)
}