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来执行任务。
调度流程
- go func() 创建一个新的协程,G。
- G被放入P的本地队列(创建G的P)或者全局队列。
- 唤醒或者创建M以便执行G。
- 不断进行事件循环。
- 寻找可用状态下的G执行任务。
- 清除后,重新进入事件循环。
本地队列数量有限,不超过256。新建G的时候,优先选择本地队列,如果满了,将P本地队列一半的G移动到全局队列。
里面有一个steal,P执行完后,会从本地队列弹出G来执行,如果本地队列为空,就从其他P的本地队列中尝试窃取一半可运行的G到自己这。
限制
协程的运行中,真正干活的是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
}
集线器、交换机、路由器
集线器:物理层,广播

交换机:数据链路层,转发。会维护端口号和MAC地址的对应关系。
两种特殊情况
交换机发现目的端口和源端口一致:如
从一个集线器连到交换机上的情况。
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就可以很方便。(重复声明时需要有新变量)