go进阶笔记1


多Goroutine如何优雅处理错误

一般来讲,代码为:

func main() {
    var wg sync.WaitGroup
    wg.Add(2)

    go func() {
        fmt.Println("go routine 1")
        wg.Done()
    }()

    go func() {
    fmt.Println("go routine 2: 想要报错")
        wg.Done()
    }()
    wg.Wait()
}

记录到错误日志

比较乱,不直观,无法针对错误做特定的逻辑跳转。

利用channel传输

func main() {
    goerrors := make(chan error)
    wgDone := make(chan bool)

    var wg sync.WaitGroup
    wg.Add(2)

    // 普通的一个协程
    go func() {
        fmt.Println("go routine 1")
        wg.Done()
    }()

    // 抛出错误的一个协程
    go func() {
        err := returnError()
        if err != nil {
            goerrors <- err
        }
        wg.Done()
    }()

    // 等待执行两个协程执行完毕,给wgDone关闭
    go func() {
        wg.Wait()
        // x, ok := <-c,set ok to false
        close(wgDone)
    }()

    select {
        // select是任选一个执行
        // 如果先得到顺利执行完了,就结束
    case <-wgDone:
        break
        // 如果收到了异常,就打印
    case err := <-goerrors:
        close(goerrors)
        fmt.Println(err)
    }
    time.Sleep(time.Second)
}

func returnError() error {
    return errors.New("error报错了")
}

自己编写channel需要关心一些非业务的逻辑。

sync/errgroup

这种好处就是不用关心非业务的控制代码,直接Wait就可以了。

import (
    "fmt"
    "net/http"
    "golang.org/x/sync/errgroup"
)

func main() {
    g := new(errgroup.Group)
    var urls = []string{
        "http://www.google.hk",
        "https://orzlinux.cn",
        "https://google.com",
    }

    for _, url := range urls {
        link := url
        // 启动一个新协程
        g.Go(func() error {
            r, err := http.Get(link)
            if err == nil {
                r.Body.Close()
            }
            return err
        })
    }

    // wait 等待Go方法的所有函数调用都返回,然后返回第一个错误
    if err := g.Wait(); err == nil {
        fmt.Println("Success")
    } else {
        fmt.Printf("Errors: %+v\n", err)
        // Errors: Get "http://www.google.hk": ‘
        // dial tcp 142.250.157.199:80: i/o timeout
    }
}

Goroutine 数量控制在多少合适

操作系统无法感知到协程的存在,协程的操作和切换都是用户态的。

协程由特定的调度模式来控制,以多路复用的形式运行在操作系统为Go程序分配的几个系统线程上。

何为调度

需要有东西管理协程来运作,GMP模型。

G:Goroutine,每次go func就是生成了一个G。

P:Processor,处理器,一般就是处理器的核数,可以修改。

M:Machine,系统线程。

M需要和P绑定,然后不断在M上寻找可运行的G来执行任务。

调度流程

image-20220425151216094

  • go func() 创建一个新的协程,G。
  • G被放入P的本地队列(创建G的P)或者全局队列。
  • 唤醒或者创建M以便执行G。
  • 不断进行事件循环。
  • 寻找可用状态下的G执行任务。
  • 清除后,重新进入事件循环。

本地队列数量有限,不超过256。新建G的时候,优先选择本地队列,如果满了,将P本地队列一半的G移动到全局队列。

里面有一个steal,P执行完后,会从本地队列弹出G来执行,如果本地队列为空,就从其他P的本地队列中尝试窃取一半可运行的G到自己这。

image-20220425152413371

限制

协程的运行中,真正干活的是M(系统线程)。而Go里,M的默认数量限制是10000,超出会报错。

G的创建理论上没有限制,实际会受到内存的影响。

P的数量受到环境变量GOMAXPROCS的影响,基本受本机核数影响。而且,M是需要绑定P才能进行具体的任务执行。P的数量不影响协程的数量创建。

nil不是nil

v := reflect.ValueOf(nil)
fmt.Printf("v.IsNil(): %v\n", v.IsNil())
// panic: reflect: call of reflect.Value.IsNil on zero Value

return值命名问题

一段代码,输出啥:

func aaa() (done func(), err error) {
    return func() {
        print("aaa: done")
    }, nil
}

func bbb() (done func(), _ error) {
    done, err := aaa()
    return func() {
        print("bbb: surprise!")
        done()
    }, err
}

func main() {
    done, _ := bbb()
    done()
    // bbb: surprise!bbb: surprise!
    // 死循环
}

输出与想象中相悖,问题就出在bbb函数中,done不是一个新变量,而是bbb()的返回值,这就相当于bbb的return是个函数,里面又调用了自身。解决办法就是干脆return命名去掉就行了:

func aaa() (done func(), err error) {
    return func() {
        print("aaa: done")
    }, nil
}

func bbb() (func(), error) {
    done, err := aaa()
    return func() {
        print("bbb: surprise!")
        done()
    }, err
}

func main() {
    done, _ := bbb()
    done()
    // bbb: surprise!aaa: done
}

集线器、交换机、路由器

集线器:物理层,广播

image-20220425163153163

交换机:数据链路层,转发。会维护端口号和MAC地址的对应关系。

两种特殊情况

  • 交换机发现目的端口和源端口一致:如

    image-20220425163610436

    从一个集线器连到交换机上的情况。

  • MAC地址表找不到对应的MAC地址:

    广播。

网桥:可以理解为两个网线口的交换机。

路由器:网络层。交换机通过MAC地址判断转发目标,路由器根据IP头部判断。

Go变量声明为何有两种

  • 标准式声明

    var i int
    var m, n float64
    var k = 0
    var (
        i int
      u = 1.0
    )
    
  • 简短声明

    s := "hi"
    i, j := 0,10
    

区别:

  • 短变量声明不能全局变量。

  • 代码块的分组声明。

  • 标准声明可以只声明,但是简短声明必须有值。

  • 局部变量,区分作用域:

    for idx, value := range array {
        // Do something with index and value
    }
    
  • 短变量可以被重新声明,如:

    a := 1
    a, b := 1, 2
    

    这样err就可以很方便。(重复声明时需要有新变量)