m

moox2020

V1

2022/09/19阅读:15主题:橙心

120 Go面试加强版之自虐篇

120 Go面试加强版之自虐篇

1、协程池、函数执行超时,nil判断

1. 限制协程执行的基本方法

  • 使用的概念,限制池的大小,写入
package main

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

var pool chan struct{} // 空结构体不占用内存空间

func job(index int) {
 time.Sleep(time.Second * 1)
 fmt.Printf("执行完毕,序号:%d\n", index)
}
func main() {
 maxNum := 10
 pool = make(chan struct{}, maxNum)
 wg := sync.WaitGroup{}
 for i := 0; i < 100; i++ {
  pool <- struct{}{} // 往pool中写入,到达最大长度时,阻塞
  wg.Add(1)
  go func(index int) {
   defer wg.Done()
   defer func() {
    <-pool // 从pool中取值
   }()
   job(index)
  }(i)
 }
 wg.Wait()
}

2. 函数执行超时控制

  • 假设程序执行时间很长,需要进行超时控制,如执行超过3s,就退出程序
  • 思路:函数执行时,不是等待函数执行完成再返回,而是直接返回channel,使用channel来控制
    • 把业务过程变成协程(因为业务没处理完,就可以返回,所以使用goroutine)
    • 把业务执行结果放入channel (因为执行结果是异步的,所以执行结果先放入channel)
    • 将耗时的过程放入channel管道
package main

import (
 "fmt"
 "time"
)

// 1.不需要等待执行完成就要返回时,使用goroutine
// 2.将业务执行结果写入管道,chan
func job() chan string {
 ret := make(chan string)
 go func() {
  time.Sleep(time.Second * 2// 模拟业务处理逻辑
  ret <- "success"
 }()
 return ret
}

// 3. 超时判断
func run() (interface{}, error) {
 c := job() // 执行业务逻辑
 // select持续判断
 select {
 // 取到值了,业务逻辑正常处理成功
 case r := <-c:
  return r, nil
 // 计时器,3秒的chan time
 case <-time.After(time.Second * 3):
  return nil, fmt.Errorf("time out")
 }
}
func main() {
 fmt.Println(run())
}

3. 明明是nil,却 !=nil 的问题

示例:

package main

import "fmt"

func main() {
 var f func()
 var a *struct{}
 fmt.Println(f) // nil
 fmt.Println(a) // nil

 // 放入interface切片中时,每一项都是interface{}
 // interface 包含类型和值,只有类型和值都一样时,才会相等
 list := []interface{}{f, a}
 for _, item := range list {
  if item == nil {
   fmt.Println("nil")
  } else {
   fmt.Println("not nil"// 打印了这个,说明item!=nil ????
  }

  fmt.Printf("%T,%v\n", item, item)
  /*
   类型:func(),  值:<nil>
   类型:*struct {}, 值:<nil>
  */

 }
}

原因:因为interface包含类型和值,只有类型和值都是nil时,才能与nil相等

  • 判断方式一:断言
func main() {
 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")
  }
 }
}
  • 判断方式二:反射
func main() {
 var f func()
 var a *struct{}

 list := []interface{}{f, a}
 for _, item := range list {
  if reflect.ValueOf(item).IsNil() {
   fmt.Println("nil")
  }
 }
}

2、变态的defer

4. defer 定义函数时的参数问题

题目:输出结果为1

func main() {
 a := 1
 defer fmt.Println(a)   // 1
 a++
}

原因:defer机制是:defer在定义时,如果defer后紧跟的语句中有参数,参数在定义的时候就确定了,所以定义时a=1

  • 如果defer后是匿名函数不带参数,则会在函数结束时执行
func main() {
 a := 1
 defer func() {
  fmt.Println(a)  // 2
 }()
 a++
}
  • 如果defer后是匿名函数带参数,则参数会在定义时确定下来
func main() {
 a := 1
 defer func(input int) {
  fmt.Println(input)   // 1
 }(a)
 a++
}

解决:如果函数中带有参数,又想使用defer,如何最后得到最终变量的值? ==> 传指针

func show(a *int) {
 fmt.Println(*a)
}
func main() {
 a := 1
 defer show(&a)   // 传指针,指针能取到最终变化后的值
 a++
}

5. defer 里使用链式调用、循环执行defer等

  • defer链式调用时,以最后一个调用作为defer的执行,其他前面的安装顺序执行
// 链式调用
func (t *Test) Do(i int) *Test {
 fmt.Println(i)
 return t
}

func main() {
 t := NewTest()
    defer t.Do(1).Do(2).Do(4).Do(5)   // 最后一个Do()做为defer的执行
 t.Do(3)
}
// 输出结果:1,2,4,3,5
  • 如果想要让defer最后执行,使用匿名函数包裹
func main() {
 t := NewTest()
 defer func() {
  t.Do(1).Do(2).Do(4).Do(5)  
 }()

 t.Do(3)   // 先执行
}
// 执行结果:3,1,2,4,5    

循环执行defer

  • defer属于栈结构,先defer,先压栈;后执行defer,先出栈;defer会保存变量的值
func main() {
 for i := 0; i < 3; i++ {
  defer func() {
   fmt.Println(i)
  }()
 }
}
// 输出333,因为0:defer func压栈i=1,1:defer func压栈1=2,2:defer func压栈i=3 ->
// 出栈2 (i=3)-> 出栈1 (i=3) -> 出栈0(i=3)
  • 输出210
func main() {
 for i := 0; i < 3; i++ {
  defer func(input int) {
   fmt.Println(input)
  }(i)
 }
}
// 或
func main() {
 for i := 0; i < 3; i++ {
  defer fmt.Println(i)
 }
}

6. defer和panic哪个先执行、嵌套panic

  • 先执行defer,后抛出异常
func main() {
 defer func() { fmt.Println("打印前") }()
 defer func() { fmt.Println("打印中") }()
 defer func() { fmt.Println("打印后") }()
 panic("触发异常1")
}
//打印后
//打印中
//打印前
//panic: 触发异常1
  • 变种1:先执行defer,再panic1
func main() {
 func() {
  defer func() { fmt.Println("打印前") }()
  defer func() { fmt.Println("打印中") }()
  defer func() { fmt.Println("打印后") }()
  panic("触发异常1")
 }()
 panic("触发异常2")  // 不会执行
}
//打印后
//打印中
//打印前
//panic: 触发异常1
  • 变种2:先执行defer,再panic2,再panic1
func main() {
 defer func() {
  defer func() { fmt.Println("打印前") }()
  defer func() { fmt.Println("打印中") }()
  defer func() { fmt.Println("打印后") }()
  panic("触发异常1")
 }()
 panic("触发异常2")
}
//打印后
//打印中
//打印前
//panic: 触发异常2
//        panic: 触发异常1

7. n++原来也是不可靠的

  • n=1000000
func main() {
 n := 0
 for i := 0; i < 1000000; i++ {
  func() {
   n++
  }()
 }
 fmt.Println(n)  // 1000000
}
  • 变种:加入go routine
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++的过程不是原子的,

  1. 从内存读出n
  2. 执行++
  3. 再赋值结果

这个过程中,其他协程也会进入内存读取n,因此不是原子的。

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

  • 解决方法:1. 加锁
func main() {
 n := 0
 loker := sync.Mutex{}   // 互斥锁
 wg := sync.WaitGroup{}
 for i := 0; i < 1000000; i++ {
  wg.Add(1)
  go func() {
   defer wg.Done()
   defer loker.Unlock()   // 解锁
   loker.Lock()           // 计算前加锁
   n++
  }()
 }
 wg.Wait()
 fmt.Println(n)
}

  • 解决方法2:atomic,只支持int32和int64
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)    // AddInt32 atomically adds delta to *addr and returns the new value.
   // n++
  }()
 }
 wg.Wait()
 fmt.Println(n)
}

3、GO的并发模式

8. Go的常见并发模式(1)基础模式

go的并发模式有哪些:

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

最基本的并发编程:

  • 使用waitGroup控制协程并发
func main() {
 wg := sync.WaitGroup{}
 for i := 0; i < 5; i++ {
  wg.Add(1)
  // 协程是并发的,无序的
  go func(i int) {
   defer wg.Done()
   fmt.Println(i * 2// 这里是业务处理逻辑,这里的数据如何拿出来=> channel
  }(i)
 }
 wg.Wait()
}
  • 使用协程做并发,使用channel通信,可以不使用锁
func main() {
 c := make(chan int5// 创建channel,容器时5
 for i := 0; i < 5; i++ {
  // 协程是并发的,无序的
  go func(i int) {
   c <- (i * 2// 将业务逻辑处理结果保存到channel中
  }(i)
 }
 for i := 0; i < cap(c); i++ {
  fmt.Println(<-c)
 }
}

9. Go的常见并发模式(2)生产者模式(队列模式)、多种写法

  • 生产者(往外抛) -> channel -> 消费者(往里读)
  • 生产者和消费者共用一个channel,类似于MQ

生产者和消费者:第一种写法

func Producer(out chan int) {
 defer close(out) // 写完后关闭管道
 for i := 0; i < 5; i++ {
  out <- i * 2 // 生产者往channel中写入数据
  time.Sleep(time.Second * 1)
 }
}

func Comsumer(out chan int) {
 for item := range out {
  fmt.Println(item)
 }
}
func main() {
 c := make(chan int// 为生产者和消费者创建共用的channel
 go Producer(c) // 1个协程生产数据
 Comsumer(c)    // 消费数据,没有数据时等待
}
// 输出 0 2 4 6 8

// 如果消费者使用协程,因为c容量为0,主进程结束,消费者和生产者协程可能还没执行就结束了
// 没有数据输出
func main() {
 c := make(chan int// 0 容量的channel
 go Producer(c)      // 1个协程生产数据
 go Comsumer(c)      // 1个协程消费数据,因为消费时没有数据,主进程结束了,所以结束了。
}
  • 第二种写法:使用开关当做消费者入参
func Producer(out chan int) {
 defer close(out)
 for i := 0; i < 5; i++ {
  out <- i * 2 // 生产者往channel中写入数据
  time.Sleep(time.Second * 1)
 }
}

func Comsumer(out chan int, r chan struct{}) {
 for item := range out {
  fmt.Println(item)
 }
 // 保证遍历channel out完成后,往channel r中写入空结构体
 r <- struct{}{}
}
func main() {
 c := make(chan int)      // 0 容量的channel
 r := make(chan struct{}) // 创建一个空结构体管道作为开关
 go Producer(c)           // 1个协程生产数据
 go Comsumer(c, r)        // 1个协程消费数据,因为消费时没有数据,所以结束了。
 <-r                      // 这里会读取channel,没有数据时等待
}

  • 第三种写法:消费完成返回开关信息,推荐使用
func Producer(out chan int) {
 defer close(out)
 for i := 0; i < 5; i++ {
  out <- i * 2 // 生产者往channel中写入数据
  time.Sleep(time.Second * 1)
 }
}

func Comsumer(out chan int) chan struct{} {
 r := make(chan struct{})
 go func() {
  // 保证遍历channel out完成后,往channel r中写入空结构体
  defer func() {
   r <- struct{}{}
  }()
  for item := range out {
   fmt.Println(item)
  }
 }()
 return r
}
func main() {
 c := make(chan int// 0 容量的channel
 go Producer(c)      // 1个协程生产数据
 r := Comsumer(c)
 <-r
}

10. Go的常见并发模式(3)优胜劣汰模式

  • 多个协程,每个协程都执行相同的job,将执行的结果保存到channel
  • 消费端只消费channel一次,也就是最快进入channel的,谁先到就消费谁,优胜劣汰模式
// 模拟延时,每次延迟时间不一样
func job() int {
 rand.Seed(time.Now().Unix())
 result := rand.Intn(5)
 time.Sleep(time.Second * time.Duration(result))
 return result
}

func main() {
 c := make(chan int5)   // chan 中最多保存5个int
 for i := 0; i < 5; i++ { // 同时有5个协程执行job
  go func() {
   c <- job() // 写入chan
  }()
 }
    // 只消费一次: 
 fmt.Printf("最快用了:%d 秒\n", <-c) // 等待写入chan,只要消费到chan,就结束,
    // 如果是全部消费:
    // for item := range c{...}
}

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

  • 协程默认是在多核上运行,不能保证哪一个协程先执行
func main() {
 wg := sync.WaitGroup{}
 for i := 0; i < 5; i++ {
  wg.Add(1)
  go func(i int) {
   defer wg.Done()
   fmt.Println(i)
  }(i)
 }
 wg.Wait()
}
// 无序输出
  • 设置为单核执行时
func main() {
 runtime.GOMAXPROCS(1// 单核,单线程
 wg := sync.WaitGroup{}
 for i := 0; i < 5; i++ {
  wg.Add(1)
  go func(i int) {
   defer wg.Done()
   fmt.Println(i)
  }(i)
 }
 wg.Wait()
}
// 输出:4,0,1,2,3, 为什么是这样的顺序?

先扯一个废话:

  • runtime.GOMAXPROCS() ,GOLANG默认使用所有的核执行
  • runtime.GOMAXPROCS(1),就变成单核运行
    • 单核情况下,所有goroutine运行在同一个线程M中,线程维护一个上线文P
    • MPG
      • M: 工作线程
      • P: processor(上下文)或cpu
      • G:goroutine,协程只是一个调度机制,真正干活的是线程

为什么输出是4,0,1,2,3的原因:

  • 程序中,我们循环创建了若干个协程,且是单核的,只有一个线程在干所有的活
  • 等待P(processor/cpu)上下文就绪后,才开始执行。默认先执行的是最后一个创建的协程,所以输出4
  • 然后才按顺序执行协程,输出0,1,2,3

如果要解决最后创建的协程第一个执行的问题:让最后创建出的协程单独出来

func main() {
 runtime.GOMAXPROCS(1// 单核,单线程
 wg := sync.WaitGroup{}
 for i := 0; i < 5; i++ {
  wg.Add(1)
  go func(i int) {
   defer wg.Done()
   fmt.Println(i) // 单核情况下业务逻辑顺序执行
  }(i)
 }
 go func() {
  defer wg.Done()
  fmt.Println("我要开始执行了..."// 单核情况下最后创建的协程第一个执行
 }()
 wg.Wait()
}

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

map问题:

  1. 原始的map不是线程安全的
  2. 应该使用线程安全的sync.map或自己加锁实现安全

过期机制:time.AfterFun()

package main

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

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 * 1)
 }
}

13. 谈一谈GO的链表操作

  • go自带一个双向链表
  • 单向链表,查询时间不如双向链表,但空间占用小
  • 双向链表,查询时间优于单向链表,但占用空间大
package main

import (
 "container/list" // Go自带的双向链表包list
 "fmt"
)

func main() {

 data := list.New()     // 初始化一个双向链表
 e8 := data.PushBack(8// 队尾插入
 e9 := data.PushBack(9// 队尾插入
 data.PushBack(10)      // 队尾插入
 data.PushFront(7)      // 队首插入

 e85 := data.InsertAfter(8.5, e8) // 在e8元素之后插入8.5,成为一个新元素
 data.MoveAfter(e85, e9)          // 将元素e85移动到元素e9的后面

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

14. 如何使用Golang定义枚举

  • 首先,go里面没有枚举关键字enum
  • 替代方法是使用iota,
  • iota是go语言的常量计数器,只能在常量的表达式中使用(const)
  • iota在const关键字出现时将被重置为0(const内部的第一行之前),
  • const中每新增一行常量声明,将使用iota计数一次
package main

import (
 "fmt"
)

type UserType int

// 重写了String方法,fmt调用时会执行这个方法
func (u UserType) String() string {
 switch u {
 case 0:
  return "学生"
 case 1:
  return "教师"
 case 3:
  return "领导"
 case 7:
  return "工作者"
 default:
  return "教职工"
 }
}

const (
 student UserType = iota // 0
 teacher                 // 1
 _                       // 2
 leader                  // 3
 master  = "a"           // a ,可以设置为自定义值,但占用了数值4
 master2                 // a ,与上一个值相同,但占用了数值5
 worker  = iota          // 6 ,继续iota值
 worker2                 // 7
)

func main() {
 // 强制转换一下类型
 fmt.Println(UserType(student),
  UserType(teacher),
  UserType(leader),
  master,
  master2,
  UserType(worker),
  UserType(worker2))
}
// 学生 教师 领导 a a 教职工 工作者

4、高频面试题

15. Go的Struct能不能比较

答案:可以比较,也可以不能比较

  • 相同结构,只要成员类型都可以比较,则就能比较

    • 结构体不包含map,slice,func时,可以比较
    • 结构体包含map,slice,func时不能比较
  • 不相同的结构,如果能互相转换,也能比较。前提是成员是可以比较的


示例:

  • 同一struct不包含map
type User struct {
 id int
}

func main() {
 u1 := User{101}
 u2 := User{101}
 u3 := &User{101}
 fmt.Println(u1 == u2, &u1 == u3) // 能比较,true,false
}
  • 同一struct包含map,不能被比较
type User struct {
 id int
 m  map[string]string
}

func main() {
 u1 := User{id: 101}
 u2 := User{id: 101}
 fmt.Println(u1 == u2) // u1 == u2 (struct containing map[string]string cannot be compared)
}
  • 不相同的结构,如果能互相转换,也能比较
type User1 struct {
 id int
}
type User2 struct {
 id int
}

func main() {
 u1 := User1{id: 101}
 u2 := User2{id: 101}
 fmt.Println(u1 == u2)        // cannot compare u1 == u2 (mismatched types User1 and User2)
 fmt.Println(u1 == User1(u2)) // 不相同的结构,如果能互相转换,也能比较
}
  • 不相同的结构,不能互相转换,不能比较
type User1 struct {
 id1 int  // 成员不同,不能相互转换
}
type User2 struct {
 id2 int // 成员不同,不能相互转换
}

func main() {
 u1 := User1{id1: 101}
 u2 := User2{id2: 101}
 fmt.Println(u1 == u2)        // cannot compare u1 == u2 (mismatched types User1 and User2)
 fmt.Println(u1 == User1(u2)) // cannot convert u2 (variable of type User2) to type User1
}

16. 请用Go实现一个简单的set

  • 目前go标准库中没有set
  • 所谓set,简单来说就是一个集合,集合中的内容不能重复
  • 类比于切片,[]int{1,2,3,3,4},但是切片可以重复
  • 思考:可以使用map实现,因为map的key是不能重复的,基于这一特性,可以来设置Set
  • type Set map[interface{}]struct{};这里的key类型为interface{},value不重要,所以类型为struct{}空结构体,不占用空间

示例:

  • 使用Map实现的简单Set是无序的,如果需要有序Set,可以考虑使用ASCII排序
  • 如果考虑多线程安全,这里的Map可以使用sync.Map
package main

import (
 "bytes"
 "fmt"
)

type Empty struct{}
type Set map[interface{}]Empty

func NewSet() Set {
 return make(Set)
}

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

// 重写了String(),fmt在传入Set时,会调用这里的String
func (s Set) String() string {
 var buf bytes.Buffer
 for k, _ := range s {
  // 第一个字符不写入
  if buf.Len() > 0 {
   buf.WriteString(",")
  }
  buf.WriteString(fmt.Sprintf("%v", k))
 }
 return buf.String()
}

func main() {
 s := NewSet().Add(123"a""a"23)
 fmt.Println(s) // 这里的Set是无序的map
}

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

  • 浅拷贝:共享同一底层数组空间

    • 不同的切片指针指向同一个底层数组空间,其中一个修改,其余也会被迫修改了
  • 深拷贝:指针指向不同的底层数组空间

示例:

package main

import "fmt"

func main() {
 // 浅拷贝,a,b指针指向同一个底层数组空间
 a := []int{123}
 b := a
 a[1] = 100
 fmt.Println(a, b) // [1 100 3] [1 100 3]   

 // 深拷贝,c,d指针指向不同的底层数组
 c := []int{112131}
 d := make([]intlen(a), cap(a))
 copy(d, c)
 c[1] = 200
 fmt.Println(c, d) // [11 200 31] [11 21 31]
}

18. go的内存逃逸分析

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

几个底层知识点:

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

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

  • Go在编译时可以设置参数去分析那些变量发生逃逸
  • 逃逸就是本身应该被分配到栈上的变量,实际被分配到了堆上

逃逸场景:

    1. 局部变量原本应该在栈中分配,在栈中回收,但由于返回时被外部引用,所以被分配到堆中 如何分析:分析代码是否真实的外部引用,如果没有引用,就不需要返回
    1. 参数为interface类型,如fmt.Println(a ...interface{}),编译期间很难决定其参数的具体类型,也能产生逃逸
    1. 结构体使用时,使用指针的不同情况可能发生逃逸

示例:

  • 没有发生逃逸
package main

func test() {
 a := []int{122}
 a[1] = 4
}
func main() {
 test()
}

// 使用go build -gcflags=-m src/main.go
/*
go build -gcflags=-m src/main.go
# command-line-arguments
src\main.go:3:6: can inline test
src\main.go:7:6: can inline main
src\main.go:8:6: inlining call to test
src\main.go:4:12: []int{...} does not escape  // 说明没有发生逃逸
src\main.go:8:6: []int{...} does not escape   // 说明没有发生逃逸
*/


  • 局部变量返回,被外部引用,发生逃逸
package main

func test() []int {
 // 局部变量原本应该在栈中分配,在栈中回收
 // 但由于返回时被外部引用,所以被分配到堆中。
 // 如何分析:分析代码是否真实的外部引用,如果没有引用,就不需要返回
 a := []int{122}
 a[1] = 4
 return a
}
func main() {
 test()
}

// 使用go build -gcflags=-m src/main.go
/*
# command-line-arguments
src\main.go:3:6: can inline test
src\main.go:8:6: can inline main
src\main.go:9:6: inlining call to test
src\main.go:4:12: []int{...} escapes to heap  // 发生了逃逸,第4行:a := []int{1, 2, 2}
src\main.go:9:6: []int{...} does not escape
*/


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

import "fmt"

func test() {
 a := []int{122}
 a[1] = 4
 // []int{...} escapes to heap,因为fmt.Println(a ...any),any是interface{}
 fmt.Println(a)
}
func main() {
 test()
}

/*
go build -gcflags=-m src/main.go
# command-line-arguments
src\main.go:11:13: inlining call to fmt.Println
src\main.go:13:6: can inline main
src\main.go:9:12: []int{...} escapes to heap
src\main.go:11:13: ... argument does not escape
src\main.go:11:13: a escapes to heap
*/


  • struct场景可能发生逃逸
package main

import "fmt"

type User struct {
 Id int
}

func newUser() *User {
 return &User{Id: 1// 10:9: &User{...} escapes to heap
}

func main() {
 u := newUser() // 13:14: &User{...} escapes to heap
 fmt.Println(u) // 14:13: ... argument does not escape
}

// 为什么会发生逃逸:因为局部变量被外部引用索引发生逃逸
// 但u只是在main()中获取并打印,并没有给到其他的函数调用,所以这种逃逸是可以避免的
// 如何避免:返回不使用指针即可

/*
go build -gcflags=-m src/main.go
# command-line-arguments
src\main.go:9:6: can inline newUser
src\main.go:13:14: inlining call to newUser
src\main.go:14:13: inlining call to fmt.Println
src\main.go:10:9: &User{...} escapes to heap
src\main.go:13:14: &User{...} escapes to heap
src\main.go:14:13: ... argument does not escape
*/


  • struct避免发生逃逸,u只是在main()中获取并打印,并没有给到其他的函数调用,所以这种逃逸是可以避免的
func newUser() User {
 return User{Id: 1// ... argument does not escape
}

func main() {
 u := newUser() // ... argument does not escape
    // 只有一步调用的,没必要使用指针,导致发生逃逸
 fmt.Println(u) // 14:13: ... argument does not escape
}

5、设计模式

19. Go的单例模式

在类似配置文件只有一份,但是可能有多处调用时,使用单例模式。

面试回到:1. 如果自己实现,使用最基本的方法加锁;2. 使用go内置的sync.Once

  1. 最基本的方法:sync.Mutex 加锁,性能低
  2. go提供的内置方法:sync.Once,性能高
  • 最基本的方法 sync.Mutex 加锁
package main

import (
 "fmt"
 "sync"
)

type WebConfig struct {
 Port int
}

var cc *WebConfig // 默认为cc==nil

// 多线程调用时,需要加锁
var mu sync.Mutex

func GetConfig() *WebConfig {
 mu.Lock()         // 调用GetConfig()时加锁
 defer mu.Unlock() // 调用完成时解锁
 if cc == nil {
  cc = &WebConfig{Port: 8080}
 }
 return cc
}

func main() {
 c := GetConfig()
 c1 := GetConfig()
 c1.Port = 80
 fmt.Println(c, c == c1)
}
  • Go提供的内置方法:sync.Once
package main

import (
 "fmt"
 "sync"
)

type WebConfig struct {
 Port int
}

var cc *WebConfig // 默认为cc==nil

var once sync.Once

func GetConfig() *WebConfig {
 // 这里的func语句只执行一次,后面就不再执行了
 once.Do(func() {
  cc = &WebConfig{Port: 8080}
 })
 return cc
}

func main() {
 c := GetConfig()
 c1 := GetConfig()
 c1.Port = 80
 fmt.Println(c, c == c1)
}


20. Go的简单工厂模式

关于逼格

  • 将接口与具体实现分离,根据需要(根据参数)实例化
  • 一般用于不同的角色调用具有相同的方法,这时候将这个相同的方法抽象出来作为接口方法

写法:

  • User(接口): GetRole string
    • Member(前台用户)实现:GetRole() string
    • Admin(后台管理用户)实现:GetRole() string
  • Factory(工厂)
    • CreateUser(type int)
    • 缺点:一旦新增用户类别,就要修改Factory代码
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
)

// 简单工厂模式Factory
// 使用方法实现,通过判断参数值,返回不同的struct实例
// 缺点:一旦新增用户类别,就要修改const和CreateUser方法的代码
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(Mem).GetRole()) // 会员用户
 fmt.Println(CreateUser(Adm).GetRole()) // 后台管理用户
}


21. Go的抽象工厂模式

简单工厂模式的缺点:一旦新增用户类别,就要修改Factory代码

抽象工厂的目标就是,新增用户类别,不需要修改factory代码。如何实现?

实现:

  1. 将工厂类或工厂方法抽象出来成为接口,也就是新增一个抽象工厂,包含简单工厂实现用户创建接口
  2. 为每个类创建工厂,每个实体类成为一个工厂来实现抽象工厂
  3. 新增类时,只需要新增一个工厂来实现抽象工厂即可
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
}

// MemberFactory
type MemberFactory struct{}

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

// MemberFactory
type AdminFactory struct{}

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

func main() {
 var fact AbstractFactory = new(MemberFactory) // 会员用户
 fmt.Println(fact.CreateUser().GetRole())

 var fact1 AbstractFactory = new(AdminFactory) // 台管理用户
 fmt.Println(fact1.CreateUser().GetRole())
}


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

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

经典的实现案例:借助http制作和使用一个装饰器

package main

import "net/http"

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

// 装饰器: 输出 this is 前缀
func Decorator(f http.HandlerFunc) http.HandlerFunc {
 return func(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte("this is "))
  f(w, r)  // 传入的HandlerFunc
 }
}

// 装饰器:CheckLogin 判断 
func CheckLogin(f http.HandlerFunc) http.HandlerFunc {
 return func(w http.ResponseWriter, r *http.Request) {
  if r.URL.Query().Get("token") == "" {
   w.Write([]byte("token err"))
  } else {
   w.Write([]byte("this is "))
   f(w, r) // 传入的HandlerFunc
  }
 }
}

func main() {
 http.HandleFunc("/", index)                        // 直接访问 输出 index
 http.HandleFunc("/index", Decorator(index))        // 使用装饰器 输出 this is index
 http.HandleFunc("/checklogin/", CheckLogin(index)) // 先检查token
 http.ListenAndServe(":8080"nil)
}

/*
访问测试:
http://localhost:8080/         ==> index
http://localhost:8080/index      ==> this is index
http://localhost:8080/checklogin/?token=123     ==> this is index
http://localhost:8080/checklogin/?aa=123     ==> index
*/


6、各种原理

23. 请简述 go channel 的底层机制(初级原理)

只能是说原理,如果让你去手撸代码,那你就微笑着夺门而出吧,从此家里没有了门。 大概源码约在:runtime/chan.go

type hchan struct {
    qcount   uint           // buffer 中已放入的元素个数
    dataqsiz uint           // 用户构造 channel 时指定的 buf 大小,也就是底层循环数组的长度
    buf      unsafe.Pointer // 指向底层循环数组的指针 只针对有缓冲的 channel
    elemsize uint16         // buffer 中每个元素的大小
    closed   uint32         // channel 是否关闭,== 0 代表未 closed
    elemtype *_type         // channel 元素的类型信息
    sendx    uint           // 已发送元素在循环数组中的索引
    recvx    uint           // 已接收元素在循环数组中的索引
    recvq    waitq          // 等待接收的 goroutine  list of recv waiters
    sendq    waitq          // 等待发送的 goroutine list of send waiters

    lock mutex              // 保护 hchan 中所有字段
}

重点关注4个部分:

  • buf指向一个循环队列。【如ch:=make(chan int,2)就会创建出一个循环队列,buf指向这个队列】
  • sendx和recvx用于记录buf发送和接收的index。【ch<-2写入chan就是sendx;<-ch读取chan就是recvx】
  • lock 互斥锁。【sendx和recvx都需要加锁】
  • sendq 和recvq 等待(阻塞)列表

其他:

  • qcount:队列剩余数
  • dataqsize:缓冲区大小

过程: (1)第一步;ch := make(chan int,3) ch本身是一个指针,指向堆中的hchan 循环队列,所以index = [0,1,2] (2)第二步;看buf 初始时索引,sendx = 0;recvx = 0 ; (3)第三步;插入或获取值; ch<- 100; sendx = 1; recvx = 0 ch <- 200; sendx = 2; recvx = 0 <-ch ; sendx = 2; recvx = 1 ... ch<-x 把数据copy到buf指向的循环队列(写);<-ch 把数据从buf指向的循环队列中copy出来(读)

缓冲区满了,(如执行多个 ch<- xxx):

  • PMG调度机制:P(cpu),M(worker线程),G(协程)多个。协程是一个调度概念,真正干活的是线程

  • P(cpu)以最优的方式将G(协程)取出来 ,匹配给M(worker线程)去执行。

  • 如果一个chan循环队列满了,P核会将执行该操作的协程G拿开,切换新的协程做其他操作

  • 被拿开的协程G会被sudog指针指向它,放入sendq队列,进入等待或阻塞状态

唤醒(比如执行<-ch )

  • 数据被取出之后,chan循环队列变成可写的G队列,会再次进入可写的队列中,等待被调度。

综述:

  1. chan创建在堆中,返回指针;
  2. 使用循环队列作为缓冲区;
  3. 每次操作都要加锁,并更新操作(sendx和recvx的index);
  4. 缓冲满了,进入等待队列,并让出M(工作线程)。等待被唤醒;
  5. 被唤醒后,重新加入G队列(可被调度的协程)

24. 读写关闭的channel是啥后果

总结:

  1. 关闭后的channel,如果还有数据,可以正常读,返回值和true;如果没有数据了,读取时返回零值和false。
  2. 关闭后的channel,写channel会panic,不能忘已关闭的channel send。报错: panic: send on closed channel
  • 关闭channel后正常读
func main() {
 ch := make(chan int3)
 ch <- 1
 ch <- 2
 ch <- 3
 close(ch) // 关闭channel
 for item := range ch {
  fmt.Println(item) // 正常读取channel的值
 }
}
  • 关闭channel后正常读,读完channel,返回零值和false
func main() {
 ch := make(chan int3)
 ch <- 1
 ch <- 2
 ch <- 3
 close(ch) // 关闭channel后依然可以正常读取
 item1 := <-ch
 item2 := <-ch
 item3, state := <-ch                    // 3 true
 fmt.Println(item1, item2, item3, state) // 1 2 3 true
 item4, state := <-ch                    // 0 false
 fmt.Println(item4, state)               // 0 false,channel 读取完成之后,返回0值,和false
}
// 也可以使用死循环读取channel
func main() {
 ch := make(chan int3)
 ch <- 1
 ch <- 2
 ch <- 3
 close(ch) // 关闭channel后依然可以正常读取
 for {
  item, state := <-ch   // channel没有数据时,state为false
  if !state {
   break
  }
  fmt.Println(item)
 }
}
  • 关闭后的channel,写会panic
func main() {
 ch := make(chan int3)
 ch <- 1
 ch <- 2
 ch <- 3
 close(ch) // 关闭channel后依然可以正常读取
 ch <- 4   // panic: send on closed channel
}


25. 简述Go协程调度机制

参考文章:

  • https://lessisbetter.site/2019/03/10/golang-scheduler-1-history/ (这个不错)
  • https://zhuanlan.zhihu.com/p/27056944 (知乎的一篇文章)

首先要知道:

  • M,真正干活的是线程,OS提供的线程,M(Work Thread),代表工作线程
  • G,协程是由“用户”创建的,G(Goroutine) ,代表协程
    • 协程G必须按照一定的机制绑定M,才能被执行,协程自身不会执行
  • P(Processor),代表处理器(cpu核),又称上下文。不超过GOMAZPROCS

让MPG三者之间发生绑定,就叫做协程调度。

绑定关系 :

  • P和M存在绑定关系
    • 运行的M都必须绑定一个P,而P保存着一个协程G的队列
    • 调度器还拥有一个全局的G队列
    • P从队列中提取G交给M,并执行

找G的顺序:

  • 1/61的几率是在全局队列中找G,60/61的几率是在本地队列找G;
    • 如果全局队列中找不到G,从P的本地队列中找G;
      • 如果从P的本地队列中找不到,从其他P的本地队列中窃取G;
        • 如果还找不到,则从全局队列中取一部分G到本地队列。
        • 这里拿取的一部分满足公式:n = min(len(GQ)/GOMAXPROCS+1,len(GQ/2))

阻塞过程:

  • 假设G1阻塞,从而造成M1阻塞
    • P1第一件事就是和M1解绑
    • 然后M1和G1依然保持着“藕断丝连”的关系
    • 阻塞结束,M1寻找P来接手G1
      • 如果找到了P,将G1放到P的本地队列汇总;
      • 如果找不到P,就将G1扔给全局队列

自旋:

  • 如果M在本地队列和全局队列中都找不到G ,则会进入自旋(有条件,如2M<繁忙的P)
  • 自旋:可以理解为循环执行某个代码块。从而不进入休眠,当然会消耗一些CPU
    • 但是比起反复启动新M性能要高

一句话总结:

  • GO的协程调度就是:P(CPU核) 将 G(协程)合理的分配给某个M(工作线程) 的过程

26. 简述Raft协议:选举机制

图解:http://thesecretlivesofdata.com/raft/

  • 如果单机 -- 一焚俱焚

  • 分布式集群

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

    • 角色:
      • 领导者:leader(主节点)
      • 跟随者:follower
      • 候选者:candidate
    • 三个节点(假设记为:A,B,C):
      • 初始状态都是follower,都有个term值等于0,term代表任期。候选者和跟随者也有任期
      • 并且随机分配一个随机时间给三个节点,谁的时间最短,谁就开始发起选举
  • 选举:

    • 假设A节点先“苏醒”,它要做三件事

      • (1)A设置任期 term=1,并把自己的角色改为candidate1
      • (2)给自己投一票
      • (3)给其他两个节点发送Msg(带term值)。等待回复
    • 以B节点为例,收到A发来的投票Msg,并获取term值。发现B.term < A.term,则:

      • (1)持续保持follower角色
      • (2)把自己的term修改为1表示同意;term=1
      • (3)向A节点回执Msg
    • 同样C也会收到A的Msg,C为follower,C.term = 1

  • 正常情况下:

    • A会赢得选举(大多数选票)
    • 于是,A会立刻给所有节点发送心跳消息,避免其余节点触发新的选举
  • 不正常情况:

    • 由于网络等原因,A和B也许会同时发起选举
    • 注意:在同一任期类,C只能给A或B投一票,先到先得。
    • 假设A先到,此时还是A胜出
      • A胜出后,继续向其他节点发送心跳,这时候如果B收到A的心跳,A.term>B.term,B会将自己修改为follower,并更新term

为什么必须由leader节点的分布式集群要设置奇数个节点:

  • 因为如果有不止一个节点发起选举时,可能会发生平票,就得等待超时后重新发起投票,从而延长了系统选举的时间;
  • 没有产生leader,集群就不能提供服务

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

图解:http://thesecretlivesofdata.com/raft/

比如:当etcd发生写操作时,首先会给到leader节点去做处理,通过日志记录方式复制给其他节点

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

  1. leader加入本地日志。假设日志为(term=1; command='set x = 1';uncommitted)
  2. leader要求followers同步日志 (term=1; command='set x = 1';uncommitted)
  3. leader等待多数节点的反馈(不是全部,超过半数即可)==> OK
  4. leader确认步骤3 操作OK,并修改本地状态和存储(term=1; command='set x = 1';committed -> apply -> applied)
  5. 发出心跳要求followers也提交并存储 (term=1; command='set x = 1';committed -> apply -> applied)

状态:uncommitted -> committed -> apply -> applied

  • commit 代表日志被复制到了大多数节点的状态
  • apply 将日志命令应用到状态机(写数据,执行command)

最终一致性:

  • 完全一致性是说,必须等待全部节点都复制了日志,才能给客户端响应。==> 严重影响性能
  • 最终一致性:大多数节点完成数据复制,即可认为是一致,完成数据复制了,可以给客户端响应。

7、杂项

28. 框架中路由实现--手撸前缀树

用过框架的都知道,有个很重要的功能:路由

比如,/user/index -> 对应 UserController里面的index方法

如果是静态路由,简单做法,弄个hashmap就行,如

map[string]interface{
    "/user/index":xxxxx,   // 正好是key,则返回xxxxx
}

但是如果是动态路由:/user/:id这种形式就不方便使用

前缀树/字典树

  • 一种hash树的变种,典型应用是用于统计,排序和保存大量的字符串(但不限于字符串),所以经常被搜索引擎系统用于文本词频统计。
  • 优点是:利用字符串的公共前缀来减少查询时间,最大限度的减少无畏的字符串比较,查询效率比哈希树高

特征:

  • 根节点不包含字符
  • 每个节点的所有子节点包含的字符都不相同
  • 一个节点的所有子节点都有相同的前缀

代码实现:

package main

import (
 "fmt"
)

type Node struct {
 isend    bool             // 最后一个节点(最后一个字符)标记
 Children map[string]*Node // 这一步,用map就省的遍历了;如果使用[]*Node,需要遍历
}

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

type Trie struct {
 root *Node
}

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

func (n *Trie) Insert(str string) {
 current := n.root // root没有值,是空节点
 for _, item := range str {
  // 节点不存在,则插入
  if _, ok := current.Children[string(item)]; !ok {
   // 从root的第一个子节点开始判断
   current.Children[string(item)] = NewNode()
  }
  current = current.Children[string(item)] // 指向下一个节点
 }
 // 遍历完成,将str的最后一个字符标记为true,表示一个完整的str
 current.isend = true
}

func (n *Trie) Search(str string) bool {
 current := n.root
 for _, item := range str {
  if _, ok := current.Children[string(item)]; !ok {
   return false // 没有找到
  }
  current = current.Children[string(item)]
 }
 return current.isend // str最后一个字符是true,才算查找成功,精准查找
}

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

}

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

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

这里用到的是bufio.scanner,好比是一个带缓冲区的迭代器,可以按行或自定义的分隔符去除数据

  1. 可以扫描文件内容
  2. 可以扫描字符串内容
  3. 可以新写分隔方法
  • Scanner方法
    • NewScanner 创建Scanner ==> scanner := bufio.NewScanner(file)
    • Scanner.Split ,设置处理函数(分隔方法,默认按行分割) ==> scanner.Split(bufio.ScanLines)
    • Scanner.Scan,获取当前token,扫描一下token
    • Scanner.Bytes,将token以bytes的形式返回
    • Scanner.Text,将token以string的形式返回
    • Scanner.Err,获取处理方法返回的错误
  • Scanner 内置的split分隔处理方法:
    • ScanBytes,将token处理为单一字节
    • ScanRunes,将token处理为utf-8编码的Unicode码
    • ScanWords,以空格分割
    • ScanLines,以换行符分割,"\n"
  • 写一个按照自己的分隔符的函数
    • 以ScanLines为例,找出 func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {...}
    • 重写func MySplit(data []byte, atEOF bool) (advance int, token []byte, err error) {...}
    • 改写方法体(修改换行为:):bytes.IndexByte(data, '\n'); ==> bytes.IndexByte(data, ':');

scanner方法的好处,使用缓冲区,不会一次将文件内容加载到内存

package main

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

// 分隔方法,来源于bufio.ScanLines()方法改写
func MySplit(data []byte, atEOF bool) (advance int, token []byte, err error) {
 if atEOF && len(data) == 0 {
  return 0nilnil
 }
 // 改写,将"\n",改为自己的分隔符。如":"
 // if i := bytes.IndexByte(data, '\n'); i >= 0 {
 if i := bytes.IndexByte(data, ':'); i >= 0 {
  // We have a full newline-terminated line.
  return i + 1, dropCR(data[0:i]), nil
 }
 // If we're at EOF, we have a final, non-terminated line. Return it.
 if atEOF {
  return len(data), dropCR(data), nil
 }
 // Request more data.
 return 0nilnil
}

// 直接复制
func dropCR(data []byte) []byte {
 if len(data) > 0 && data[len(data)-1] == '\r' {
  return data[0 : len(data)-1]
 }
 return data
}
func main() {
 file, err := os.Open("src/test.log")
 if err != nil {
  log.Fatal(err, file.Name())

 }
 // 扫描文件内容
 // scanner := bufio.NewScanner(file)

 // 扫描字符串内容
 reader := strings.NewReader("aaa:bbb:ccc:ddd")
 scanner := bufio.NewScanner(reader)

 // scanner.Split(bufio.ScanLines) // 默认的按行分隔
 // scanner.Split(bufio.ScanWords) // 按空格分隔
 scanner.Split(MySplit)

 count := 0
 for scanner.Scan() {
  fmt.Println(scanner.Text())
  count++
 }
 fmt.Println("一共有", count, "部分")
}

30. sync.Pool(1):基本用法

sync.Pool 概念:

  • 协程安全,可伸缩的,用于存放可重用对象的容器。

  • 原始的目的是:存放已分配但是暂时不用的对象,需要时直接从pool中取,然后放回,以减少GC回收的压力

  • GC压力是啥:很简单,对象多了,GC要回收自然压力大(内存标记了解下)

注意:sync.Pool 不是持久的保存,而是临时的暂存。所以可以理解为sync.Cache

使用示例:

package main

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

var p *sync.Pool

type User struct {
 Name string
}

func main() {
 // 初始化
 p = &sync.Pool{
  // 取Pool时,如果是空的,会调用New创建一个新的对象,返回any;type any = interface{}
  New: func() any {
   log.Println("create user ...")
   return &User{Name: "zhangsan"}
  },
 }
 u1 := p.Get()
 fmt.Println(u1) // pool空,执行:create user ... &{zhangsan}

 u2 := p.Get().(*User)
 fmt.Println(u2) // pool空,执行:create user ... &{zhangsan}

 u2.Name = "lisi"       // u2断言,设置值
 p.Put(u2)              // 将u2放入pool
 u3 := p.Get()          // 从poo汇中取
 fmt.Println("u3:", u3) // &{lisi}

 p.Put(u2)              // 将u2放入pool
 runtime.GC()           // GC后,pool被清空,说明sync.Pool是临时存储
 u4 := p.Get()          // 从poo汇中取
 fmt.Println("u4:", u4) // pool空,执行:create user ... &{zhangsan}
}

31. sync.Pool(2):web开发中的简单使用场景

场景:在web中,每次请求都会返回一个Result对象。因此需要在内存中反复创建Result,产出频繁GC。这种情况就可以使用sync.Pool

  • sync.Pool 在数据中开辟的临时存储,GC后会被清除

  • 压测工具:hey,可以在github上找到

    • hey -n 1000 -c 500 http://localhost:8080

代码示例:

package main

import (
 "fmt"
 "sync"
 "time"

 "github.com/gin-gonic/gin"
)

var retPool *sync.Pool

type Result struct {
 Message string      `json:"message"`
 Status  string      `json:"status"`
 Logger  interface{} `json:"-"`
}

func (r *Result) Success(ctx *gin.Context, msg string) {
 r.Status = "success"
 r.Message = msg
 ctx.JSON(200, r)
}

func (r *Result) Error(ctx *gin.Context, msg string) {
 r.Status = "error"
 r.Message = msg
 ctx.JSON(400, r)
}

func main() {
 retPool = &sync.Pool{
  New: func() any {
   fmt.Println("create result...")
   return &Result{}
  },
 }
 r := gin.New() // 典型的使用sync.Pool的代表
 // 加一个中间件
 r.Use(func(ctx *gin.Context) {
  defer func() {
   if e := recover(); e != nil {
    // ret := &Result{}
    // ret.Error(ctx, e.(string))
    // 改写为sync.Pool
    ret := retPool.Get().(*Result)
    ret.Error(ctx, e.(string))
    ctx.Abort()
   }
  }()
  ctx.Next()
 })
 r.Handle("GET""/"func(ctx *gin.Context) {
  // r := &Result{}
  if time.Now().Unix()%2 == 0 {
   ret := retPool.Get().(*Result)
   defer func() {
    retPool.Put(ret)
   }()
   ret.Success(ctx, "index")
   // r.Success(ctx, "index")
  } else {
   panic("it's an error.")
  }
 })
 r.Run(":8080")
}

压测:hey -n 1000 -c 500 http://localhost:8080

可以看到,1000次调用,并发500,但很少有create result...,说明内存中 New Result很少。sync.Pool 复用了Result。


分类:

后端

标签:

后端

作者介绍

m
moox2020
V1